feat: Login, OP Support and Auth Queries (#177)

* fix: change oidc config

* fix: change oidc config secret

* begin models

* begin repo

* fix: implement grpc app funcs

* fix: add application requests

* fix: converter

* fix: converter

* fix: converter and generate clientid

* fix: tests

* feat: project grant aggregate

* feat: project grant

* fix: project grant check if role existing

* fix: project grant requests

* fix: project grant fixes

* fix: project grant member model

* fix: project grant member aggregate

* fix: project grant member eventstore

* fix: project grant member requests

* feat: user model

* begin repo

* repo models and more

* feat: user command side

* lots of functions

* user command side

* profile requests

* commit before rebase on user

* save

* local config with gopass and more

* begin new auth command (user centric)

* Update internal/user/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/user_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/eventstore_mock_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* changes from mr review

* save files into basedir

* changes from mr review

* changes from mr review

* move to auth request

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* changes requested on mr

* fix generate codes

* fix return if no events

* password code

* email verification step

* more steps

* lot of mfa

* begin tests

* more next steps

* auth api

* auth api (user)

* auth api (user)

* auth api (user)

* differ requests

* merge

* tests

* fix compilation error

* mock for id generator

* Update internal/user/repository/eventsourcing/model/password.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* requests of mr

* check email

* begin separation of command and query

* otp

* change packages

* some cleanup and fixes

* tests for auth request / next steps

* add VerificationLifetimes to config and make it run

* tests

* fix code challenge validation

* cleanup

* fix merge

* begin view

* repackaging tests and configs

* fix startup config for auth

* add migration

* add PromptSelectAccount

* fix copy / paste

* remove user_agent files

* fixes

* fix sequences in user_session

* token commands

* token queries and signout

* fix

* fix set password test

* add token handler and table

* handle session init

* add session state

* add user view test cases

* change VerifyMyMfaOTP

* some fixes

* fix user repo in auth api

* cleanup

* add user session view test

* fix merge

* begin oidc

* user agent and more

* config

* keys

* key command and query

* add login statics

* key handler

* start login

* login handlers

* lot of fixes

* merge oidc

* add missing exports

* add missing exports

* fix some bugs

* authrequestid in htmls

* getrequest

* update auth request

* fix userid check

* add username to authrequest

* fix user session and auth request handling

* fix UserSessionsByAgentID

* fix auth request tests

* fix user session on UserPasswordChanged and MfaOtpRemoved

* fix MfaTypesSetupPossible

* handle mfa

* fill username

* auth request query checks new events

* fix userSessionByIDs

* fix tokens

* fix userSessionByIDs test

* add user selection

* init code

* user code creation date

* add init user step

* add verification failed types

* add verification failures

* verify init code

* user init code handle

* user init code handle

* fix userSessionByIDs

* update logging

* user agent cookie

* browserinfo from request

* add DeleteAuthRequest

* add static login files to binary

* add login statik to build

* move generate to separate file and remove statik.go files

* remove static dirs from startup.yaml

* generate into separate namespaces

* merge master

* auth request code

* auth request type mapping

* fix keys

* improve tokens

* improve register and basic styling

* fix ailerons font

* improve password reset

* add audience to token

* all oidc apps as audience

* fix test nextStep

* fix email texts

* remove "not set"

* lot of style changes

* improve copy to clipboard

* fix footer

* add cookie handler

* remove placeholders

* fix compilation after merge

* fix auth config

* remove comments

* typo

* use new secrets store

* change default pws to match default policy

* fixes

* add todo

* enable login

* fix db name

* Auth queries (#179)

* my usersession

* org structure/ auth handlers

* working user grant spooler

* auth internal user grants

* search my project orgs

* remove permissions file

* my zitadel permissions

* my zitadel permissions

* remove unused code

* authz

* app searches in view

* token verification

* fix user grant load

* fix tests

* fix tests

* read configs

* remove unused const

* remove todos

* env variables

* app_name

* working authz

* search projects

* global resourceowner

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* model2 rename

* at least it works

* check token expiry

* search my user grants

* remove token table from authz

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix test

* fix ports and enable console

Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Livio Amstutz
2020-06-05 07:50:04 +02:00
committed by GitHub
parent 46b60a6968
commit 8a5badddf6
293 changed files with 14189 additions and 3176 deletions

View File

@@ -1,4 +0,0 @@
package login
type Config struct {
}

View File

@@ -0,0 +1,27 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
queryAuthRequestID = "authRequestID"
)
func (l *Login) getAuthRequest(r *http.Request) (*model.AuthRequest, error) {
authRequestID := r.FormValue(queryAuthRequestID)
if authRequestID == "" {
return nil, nil
}
return l.authRepo.AuthRequestByID(r.Context(), authRequestID)
}
func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*model.AuthRequest, error) {
authReq, err := l.getAuthRequest(r)
if err != nil {
return nil, err
}
err = l.parser.Parse(r, data)
return authReq, err
}

View File

@@ -0,0 +1,11 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
callback := l.oidcAuthCallbackURL + authReq.ID
http.Redirect(w, r, callback, http.StatusFound)
}

View File

@@ -0,0 +1,52 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplChangePassword = "changepassword"
tmplChangePasswordDone = "changepassworddone"
)
type changePasswordData struct {
OldPassword string `schema:"old_password"`
NewPassword string `schema:"new_password"`
}
func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
data := new(changePasswordData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.OldPassword, data.NewPassword)
if err != nil {
l.renderChangePassword(w, r, authReq, err)
return
}
l.renderChangePasswordDone(w, r, authReq)
}
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := userData{
baseData: l.getBaseData(r, authReq, "Change Password", errType, errMessage),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangePassword], data, nil)
}
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
var errType, errMessage string
data := userData{
baseData: l.getBaseData(r, authReq, "Password Change Done", errType, errMessage),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangePasswordDone], data, nil)
}

View File

@@ -0,0 +1,18 @@
package handler
import (
"net/http"
)
func (l *Login) handleHealthz(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
func (l *Login) handleReadiness(w http.ResponseWriter, r *http.Request) {
err := l.authRepo.Health(r.Context())
if err != nil {
http.Error(w, "not ready", http.StatusInternalServerError)
return
}
w.Write([]byte("OK"))
}

View File

@@ -0,0 +1,97 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/errors"
"net/http"
)
const (
queryInitPWCode = "code"
queryInitPWUserID = "userID"
tmplInitPassword = "initpassword"
tmplInitPasswordDone = "initpassworddone"
)
type initPasswordFormData struct {
Code string `schema:"code"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
}
type initPasswordData struct {
baseData
Code string
UserID string
}
func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryInitPWUserID)
code := r.FormValue(queryInitPWCode)
l.renderInitPassword(w, r, nil, userID, code, nil)
}
func (l *Login) handleInitPasswordCheck(w http.ResponseWriter, r *http.Request) {
data := new(initPasswordFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.Resend {
l.resendPasswordSet(w, r, authReq)
return
}
l.checkPWCode(w, r, authReq, data, nil)
}
func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *initPasswordFormData, err error) {
if data.Password != data.PasswordConfirm {
err := errors.ThrowInvalidArgument(nil, "VIEW-KaGue", "passwords dont match")
l.renderInitPassword(w, r, authReq, data.UserID, data.Code, err)
return
}
userOrg := login
if authReq != nil {
userOrg = authReq.UserOrgID
}
err = l.authRepo.SetPassword(setContext(r.Context(), userOrg), data.UserID, data.Code, data.Password)
if err != nil {
l.renderInitPassword(w, r, authReq, data.UserID, "", err)
return
}
l.renderInitPasswordDone(w, r, authReq)
}
func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
err := l.authRepo.RequestPasswordReset(r.Context(), authReq.UserName)
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
}
func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID, code string, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
if userID == "" && authReq != nil {
userID = authReq.UserID
}
data := initPasswordData{
baseData: l.getBaseData(r, authReq, "Init Password", errType, errMessage),
UserID: userID,
Code: code,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitPassword], data, nil)
}
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
data := userData{
baseData: l.getBaseData(r, authReq, "Password Init Done", "", ""),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitPasswordDone], data, nil)
}

View File

@@ -0,0 +1,105 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
caos_errs "github.com/caos/zitadel/internal/errors"
"net/http"
)
const (
queryInitUserCode = "code"
queryInitUserUserID = "userID"
tmplInitUser = "inituser"
tmplInitUserDone = "inituserdone"
)
type initUserFormData struct {
Code string `schema:"code"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
}
type initUserData struct {
baseData
Code string
UserID string
}
func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryInitUserUserID)
code := r.FormValue(queryInitUserCode)
l.renderInitUser(w, r, nil, userID, code, nil)
}
func (l *Login) handleInitUserCheck(w http.ResponseWriter, r *http.Request) {
data := new(initUserFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
if data.Resend {
l.resendUserInit(w, r, authReq, data.UserID)
return
}
l.checkUserInitCode(w, r, authReq, data, nil)
}
func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *initUserFormData, err error) {
if data.Password != data.PasswordConfirm {
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-fsdfd", "passwords dont match")
l.renderInitUser(w, r, nil, data.UserID, data.Code, err)
return
}
userOrgID := login
if authReq != nil {
userOrgID = authReq.UserOrgID
}
err = l.authRepo.VerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, data.Code, data.Password)
if err != nil {
l.renderInitUser(w, r, nil, data.UserID, "", err)
return
}
l.renderInitUserDone(w, r, nil)
}
func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID string) {
userOrgID := login
if authReq != nil {
userOrgID = authReq.UserOrgID
}
err := l.authRepo.ResendInitVerificationMail(setContext(r.Context(), userOrgID), userID)
l.renderInitUser(w, r, authReq, userID, "", err)
}
func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID, code string, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
if authReq != nil {
userID = authReq.UserID
}
data := initUserData{
baseData: l.getBaseData(r, nil, "Init User", errType, errMessage),
UserID: userID,
Code: code,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitUser], data, nil)
}
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
var errType, errMessage, userName string
if authReq != nil {
userName = authReq.UserName
}
data := userData{
baseData: l.getBaseData(r, authReq, "User Init Done", errType, errMessage),
UserName: userName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitUserDone], data, nil)
}

View File

@@ -0,0 +1,92 @@
package handler
import (
"context"
"net"
"net/http"
"github.com/caos/logging"
"github.com/gorilla/mux"
"github.com/rakyll/statik/fs"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/form"
_ "github.com/caos/zitadel/internal/login/statik"
)
type Login struct {
endpoint string
router *mux.Router
renderer *Renderer
parser *form.Parser
authRepo *eventsourcing.EsRepository
zitadelURL string
oidcAuthCallbackURL string
}
type Config struct {
Port string
OidcAuthCallbackURL string
ZitadelURL string
LanguageCookieName string
DefaultLanguage language.Tag
}
const (
login = "LOGIN"
)
func StartLogin(ctx context.Context, config Config, authRepo *eventsourcing.EsRepository) {
login := &Login{
endpoint: config.Port,
oidcAuthCallbackURL: config.OidcAuthCallbackURL,
zitadelURL: config.ZitadelURL,
authRepo: authRepo,
}
statikFS, err := fs.NewWithNamespace("login")
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start listener")
login.router = CreateRouter(login, statikFS)
login.renderer = CreateRenderer(statikFS, config.LanguageCookieName, config.DefaultLanguage)
login.parser = form.NewParser()
login.Listen(ctx)
}
func (l *Login) Listen(ctx context.Context) {
if l.endpoint == "" {
l.endpoint = ":80"
} else {
l.endpoint = ":" + l.endpoint
}
defer logging.LogWithFields("APP-xUZof", "port", l.endpoint).Info("html is listening")
httpListener, err := net.Listen("tcp", l.endpoint)
logging.Log("CONFI-W5q2O").OnError(err).Panic("unable to start listener")
httpServer := &http.Server{
Handler: l.router,
}
go func() {
<-ctx.Done()
if err = httpServer.Shutdown(ctx); err != nil {
logging.Log("APP-mJKTv").WithError(err)
}
}()
go func() {
err := httpServer.Serve(httpListener)
logging.Log("APP-oSklt").OnError(err).Panic("unable to start listener")
}()
}
func setContext(ctx context.Context, resourceOwner string) context.Context {
data := auth.CtxData{
UserID: login,
OrgID: resourceOwner,
}
return auth.SetCtxData(ctx, data)
}

View File

@@ -0,0 +1,63 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplLogin = "login"
)
type loginData struct {
UserName string `schema:"username"`
}
func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) handleUsername(w http.ResponseWriter, r *http.Request) {
authSession, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
l.renderLogin(w, r, authSession, nil)
}
func (l *Login) handleUsernameCheck(w http.ResponseWriter, r *http.Request) {
data := new(loginData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.CheckUsername(r.Context(), authReq.ID, data.UserName)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := userData{
baseData: l.getBaseData(r, authReq, "Login", errType, errMessage),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplLogin], data, nil)
}

View File

@@ -0,0 +1,20 @@
package handler
import (
"net/http"
)
const (
tmplLogoutDone = "logoutdone"
)
func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
l.renderLogoutDone(w, r)
}
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
data := userData{
baseData: l.getBaseData(r, nil, "Logout Done", "", ""),
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplLogoutDone], data, nil)
}

View File

@@ -0,0 +1,90 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
queryCode = "code"
queryUserID = "userID"
tmplMailVerification = "mail_verification"
tmplMailVerified = "mail_verified"
)
type mailVerificationFormData struct {
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
}
type mailVerificationData struct {
baseData
UserID string
}
func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryUserID)
code := r.FormValue(queryCode)
if code != "" {
l.checkMailCode(w, r, nil, userID, code)
return
}
l.renderMailVerification(w, r, nil, userID, nil)
}
func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Request) {
data := new(mailVerificationFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if !data.Resend {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
return
}
userOrg := login
if authReq != nil {
userOrg = authReq.UserOrgID
}
err = l.authRepo.ResendEmailVerificationMail(setContext(r.Context(), userOrg), data.UserID)
l.renderMailVerification(w, r, authReq, data.UserID, err)
}
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID, code string) {
userOrg := login
if authReq != nil {
userID = authReq.UserID
userOrg = authReq.UserOrgID
}
err := l.authRepo.VerifyEmail(setContext(r.Context(), userOrg), userID, code)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
return
}
l.renderMailVerified(w, r, authReq)
}
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID string, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
if userID == "" {
userID = authReq.UserID
}
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "Mail Verification", errType, errMessage),
UserID: userID,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMailVerification], data, nil)
}
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "Mail Verified", "", ""),
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMailVerified], data, nil)
}

View File

@@ -0,0 +1,20 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplMfaInitDone = "mfainitdone"
)
type mfaInitDoneData struct {
}
func (l *Login) renderMfaInitDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaDoneData) {
var errType, errMessage string
data.baseData = l.getBaseData(r, authReq, "Mfa Init Done", errType, errMessage)
data.UserName = authReq.UserName
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaInitDone], data, nil)
}

View File

@@ -0,0 +1,95 @@
package handler
import (
"bytes"
"net/http"
"github.com/aaronarduino/goqrsvg"
svg "github.com/ajstarks/svgo"
"github.com/boombuler/barcode/qr"
"github.com/caos/zitadel/internal/auth_request/model"
)
const (
tmplMfaInitVerify = "mfainitverify"
)
type mfaInitVerifyData struct {
MfaType model.MfaType `schema:"mfaType"`
Code string `schema:"code"`
URL string `schema:"url"`
Secret string `schema:"secret"`
}
func (l *Login) handleMfaInitVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaInitVerifyData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
var verifyData *mfaVerifyData
switch data.MfaType {
case model.MfaTypeOTP:
verifyData = l.handleOtpVerify(w, r, authReq, data)
}
if verifyData != nil {
l.renderMfaInitVerify(w, r, authReq, verifyData, err)
return
}
done := &mfaDoneData{
MfaType: data.MfaType,
}
l.renderMfaInitDone(w, r, authReq, done)
}
func (l *Login) handleOtpVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData {
err := l.authRepo.VerifyMfaOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code)
if err == nil {
return nil
}
mfadata := &mfaVerifyData{
MfaType: data.MfaType,
otpData: otpData{
Secret: data.Secret,
Url: data.URL,
},
}
return mfadata
}
func (l *Login) renderMfaInitVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data.baseData = l.getBaseData(r, authReq, "Mfa Init Verify", errType, errMessage)
data.UserName = authReq.UserName
if data.MfaType == model.MfaTypeOTP {
code, err := generateQrCode(data.otpData.Url)
if err == nil {
data.otpData.QrCode = code
}
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaInitVerify], data, nil)
}
func generateQrCode(url string) (string, error) {
var b bytes.Buffer
s := svg.New(&b)
qrCode, err := qr.Encode(url, qr.M, qr.Auto)
if err != nil {
return "", err
}
qs := goqrsvg.NewQrSVG(qrCode, 5)
qs.StartQrSVG(s)
qs.WriteQrSVG(s)
s.End()
return string(b.Bytes()), nil
}

View File

@@ -0,0 +1,88 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
caos_errs "github.com/caos/zitadel/internal/errors"
"net/http"
)
const (
tmplMfaPrompt = "mfaprompt"
)
type mfaPromptData struct {
MfaProvider model.MfaType `schema:"provider"`
Skip bool `schema:"skip"`
}
func (l *Login) handleMfaPrompt(w http.ResponseWriter, r *http.Request) {
data := new(mfaPromptData)
authSession, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
if !data.Skip {
mfaVerifyData := new(mfaVerifyData)
mfaVerifyData.MfaType = data.MfaProvider
l.handleMfaCreation(w, r, authSession, mfaVerifyData)
return
}
err = l.authRepo.SkipMfaInit(setContext(r.Context(), authSession.UserOrgID), authSession.UserID)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
l.handleLogin(w, r)
}
func (l *Login) renderMfaPrompt(w http.ResponseWriter, r *http.Request, authSession *model.AuthRequest, mfaPromptData *model.MfaPromptStep, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := mfaData{
baseData: l.getBaseData(r, authSession, "Mfa Prompt", errType, errMessage),
UserName: authSession.UserName,
}
if mfaPromptData == nil {
l.renderError(w, r, authSession, caos_errs.ThrowPreconditionFailed(nil, "APP-XU0tj", "No available mfa providers"))
return
}
data.MfaProviders = mfaPromptData.MfaProviders
data.MfaRequired = mfaPromptData.Required
if len(mfaPromptData.MfaProviders) == 1 && mfaPromptData.Required {
data := &mfaVerifyData{
MfaType: mfaPromptData.MfaProviders[0],
}
l.handleMfaCreation(w, r, authSession, data)
return
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaPrompt], data, nil)
}
func (l *Login) handleMfaCreation(w http.ResponseWriter, r *http.Request, authSession *model.AuthRequest, data *mfaVerifyData) {
switch data.MfaType {
case model.MfaTypeOTP:
l.handleOtpCreation(w, r, authSession, data)
return
}
l.renderError(w, r, authSession, caos_errs.ThrowPreconditionFailed(nil, "APP-Or3HO", "No available mfa providers"))
}
func (l *Login) handleOtpCreation(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *mfaVerifyData) {
otp, err := l.authRepo.AddMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
data.otpData = otpData{
Secret: otp.SecretString,
Url: otp.Url,
}
l.renderMfaInitVerify(w, r, authReq, data, nil)
}

View File

@@ -0,0 +1,49 @@
package handler
import (
"net/http"
"github.com/caos/zitadel/internal/auth_request/model"
)
const (
tmplMfaVerify = "mfaverify"
)
type mfaVerifyFormData struct {
MfaType model.MfaType `schema:"mfaType"`
Code string `schema:"code"`
}
func (l *Login) handleMfaVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaVerifyFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.MfaType == model.MfaTypeOTP {
err = l.authRepo.VerifyMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Code, model.BrowserInfoFromRequest(r))
}
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderMfaVerify(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, verificationStep *model.MfaVerificationStep, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := userData{
baseData: l.getBaseData(r, authReq, "Mfa Verify", errType, errMessage),
UserName: authReq.UserName,
}
if verificationStep != nil {
data.MfaProviders = verificationStep.MfaProviders
data.SelectedMfaProvider = verificationStep.MfaProviders[0]
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplMfaVerify], data, nil)
}

View File

@@ -0,0 +1,42 @@
package handler
import (
"net/http"
"github.com/caos/zitadel/internal/auth_request/model"
)
const (
tmplPassword = "password"
)
type passwordData struct {
Password string `schema:"password"`
}
func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := userData{
baseData: l.getBaseData(r, authReq, "Password", errType, errMessage),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplPassword], data, nil)
}
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
data := new(passwordData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Password, model.BrowserInfoFromRequest(r))
if err != nil {
l.renderPassword(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@@ -0,0 +1,32 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplPasswordResetDone = "passwordresetdone"
)
func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.RequestPasswordReset(setContext(r.Context(), authReq.UserOrgID), authReq.UserName)
l.renderPasswordResetDone(w, r, authReq, err)
}
func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
data := userData{
baseData: l.getBaseData(r, authReq, "Password Reset Done", errType, errMessage),
UserName: authReq.UserName,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplPasswordResetDone], data, nil)
}

View File

@@ -0,0 +1,119 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
caos_errs "github.com/caos/zitadel/internal/errors"
usr_model "github.com/caos/zitadel/internal/user/model"
"golang.org/x/text/language"
"net/http"
)
const (
tmplRegister = "register"
globalRO = "GlobalResourceOwner"
)
type registerFormData struct {
Email string `schema:"email"`
Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"`
Language string `schema:"language"`
Gender int32 `schema:"gender"`
Password string `schema:"password"`
Password2 string `schema:"password2"`
}
type registerData struct {
baseData
registerFormData
}
func (l *Login) handleRegister(w http.ResponseWriter, r *http.Request) {
data := new(registerFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
l.renderRegister(w, r, authRequest, data, nil)
}
func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
data := new(registerFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
if data.Password != data.Password2 {
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-KaGue", "passwords dont match")
l.renderRegister(w, r, authRequest, data, err)
return
}
iam, err := l.authRepo.GetIam(r.Context())
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
user, err := l.authRepo.Register(setContext(r.Context(), iam.GlobalOrgID), data.toUserModel(), iam.GlobalOrgID)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
if authRequest == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
authRequest.UserName = user.UserName
l.renderNextStep(w, r, authRequest)
}
func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authRequest *model.AuthRequest, formData *registerFormData, err error) {
var errType, errMessage string
if err != nil {
errMessage = err.Error()
}
if formData == nil {
formData = new(registerFormData)
}
if formData.Language == "" {
formData.Language = l.renderer.Lang(r).String()
}
data := registerData{
baseData: l.getBaseData(r, authRequest, "Register", errType, errMessage),
registerFormData: *formData,
}
funcs := map[string]interface{}{
"selectedLanguage": func(l string) bool {
if formData == nil {
return false
}
return formData.Language == l
},
"selectedGender": func(g int32) bool {
if formData == nil {
return false
}
return formData.Gender == g
},
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplRegister], data, funcs)
}
func (d registerFormData) toUserModel() *usr_model.User {
return &usr_model.User{
Profile: &usr_model.Profile{
FirstName: d.Firstname,
LastName: d.Lastname,
PreferredLanguage: language.Make(d.Language),
Gender: usr_model.Gender(d.Gender),
},
Password: &usr_model.Password{
SecretString: d.Password,
},
Email: &usr_model.Email{
EmailAddress: d.Email,
},
}
}

View File

@@ -0,0 +1,260 @@
package handler
import (
"fmt"
"github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/renderer"
"net/http"
"path"
"github.com/caos/logging"
"golang.org/x/text/language"
)
const (
tmplError = "error"
)
type Renderer struct {
*renderer.Renderer
}
func CreateRenderer(staticDir http.FileSystem, cookieName string, defaultLanguage language.Tag) *Renderer {
r := new(Renderer)
tmplMapping := map[string]string{
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplMfaVerify: "mfa_verify.html",
tmplMfaPrompt: "mfa_prompt.html",
tmplMfaInitVerify: "mfa_init_verify.html",
tmplMfaInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
tmplRegister: "register.html",
tmplLogoutDone: "logout_done.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {
return path.Join(EndpointResources, file)
},
"resourceThemeUrl": func(file, theme string) string {
return path.Join(EndpointResources, "themes", theme, file)
},
"loginUrl": func() string {
return EndpointLogin
},
"registerUrl": func(id string) string {
return fmt.Sprintf("%s?%s=%s", EndpointRegister, queryAuthRequestID, id)
},
"usernameUrl": func() string {
return EndpointUsername
},
"usernameChangeUrl": func(id string) string {
return fmt.Sprintf("%s?%s=%s", EndpointUsername, queryAuthRequestID, id)
},
"userSelectionUrl": func() string {
return EndpointUserSelection
},
"passwordResetUrl": func(id string) string {
return fmt.Sprintf("%s?%s=%s", EndpointPasswordReset, queryAuthRequestID, id)
},
"passwordUrl": func() string {
return EndpointPassword
},
"mfaVerifyUrl": func() string {
return EndpointMfaVerify
},
"mfaPromptUrl": func() string {
return EndpointMfaPrompt
},
"mfaInitVerifyUrl": func() string {
return EndpointMfaInitVerify
},
"mailVerificationUrl": func() string {
return EndpointMailVerification
},
"initPasswordUrl": func() string {
return EndpointInitPassword
},
"initUserUrl": func() string {
return EndpointInitUser
},
"changePasswordUrl": func() string {
return EndpointChangePassword
},
"registrationUrl": func() string {
return EndpointRegister
},
"selectedLanguage": func(l string) bool {
return false
},
"selectedGender": func(g int32) bool {
return false
},
}
var err error
r.Renderer, err = renderer.NewRenderer(
staticDir,
tmplMapping, funcs,
i18n.TranslatorConfig{DefaultLanguage: defaultLanguage, CookieName: cookieName},
)
logging.Log("APP-40tSoJ").OnError(err).WithError(err).Panic("error creating renderer")
return r
}
func (l *Login) renderNextStep(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
authReq, err := l.authRepo.AuthRequestByID(r.Context(), authReq.ID)
if err != nil {
l.renderInternalError(w, r, authReq, errors.ThrowInternal(nil, "APP-sio0W", "could not get authreq"))
}
if len(authReq.PossibleSteps) == 0 {
l.renderInternalError(w, r, authReq, errors.ThrowInternal(nil, "APP-9sdp4", "no possible steps"))
return
}
l.chooseNextStep(w, r, authReq, 0, nil)
}
func (l *Login) renderError(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
if authReq == nil || len(authReq.PossibleSteps) == 0 {
l.renderInternalError(w, r, authReq, errors.ThrowInternal(err, "APP-OVOiT", "no possible steps"))
return
}
l.chooseNextStep(w, r, authReq, 0, err)
}
func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, stepNumber int, err error) {
switch step := authReq.PossibleSteps[stepNumber].(type) {
case *model.LoginStep:
if len(authReq.PossibleSteps) > 1 {
l.chooseNextStep(w, r, authReq, 1, err)
return
}
l.renderLogin(w, r, authReq, err)
case *model.SelectUserStep:
l.renderUserSelection(w, r, authReq, step)
case *model.InitPasswordStep:
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
case *model.PasswordStep:
l.renderPassword(w, r, authReq, nil)
case *model.MfaVerificationStep:
l.renderMfaVerify(w, r, authReq, step, err)
case *model.RedirectToCallbackStep:
if len(authReq.PossibleSteps) > 1 {
l.chooseNextStep(w, r, authReq, 1, err)
return
}
l.redirectToCallback(w, r, authReq)
case *model.ChangePasswordStep:
l.renderChangePassword(w, r, authReq, err)
case *model.VerifyEMailStep:
l.renderMailVerification(w, r, authReq, "", err)
case *model.MfaPromptStep:
l.renderMfaPrompt(w, r, authReq, step, err)
case *model.InitUserStep:
l.renderInitUser(w, r, authReq, "", "", nil)
default:
l.renderInternalError(w, r, authReq, errors.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
}
}
func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var msg string
if err != nil {
msg = err.Error()
}
data := l.getBaseData(r, authReq, "Error", "Internal", msg)
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplError], data, nil)
}
func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title string, errType, errMessage string) baseData {
return baseData{
errorData: errorData{
ErrType: errType,
ErrMessage: errMessage,
},
Lang: l.renderer.Lang(r).String(),
Title: title,
Theme: l.getTheme(r),
ThemeMode: l.getThemeMode(r),
AuthReqID: getRequestID(authReq, r),
}
}
func (l *Login) getTheme(r *http.Request) string {
return "zitadel" //TODO: impl
}
func (l *Login) getThemeMode(r *http.Request) string {
return "" //TODO: impl
}
func getRequestID(authReq *model.AuthRequest, r *http.Request) string {
if authReq != nil {
return authReq.ID
}
return r.FormValue(queryAuthRequestID)
}
type baseData struct {
errorData
Lang string
Title string
Theme string
ThemeMode string
AuthReqID string
}
type errorData struct {
ErrType string
ErrMessage string
}
type userData struct {
baseData
UserName string
PasswordChecked string
MfaProviders []model.MfaType
SelectedMfaProvider model.MfaType
}
type userSelectionData struct {
baseData
Users []model.UserSelection
}
type mfaData struct {
baseData
UserName string
MfaProviders []model.MfaType
MfaRequired bool
}
type mfaVerifyData struct {
baseData
UserName string
MfaType model.MfaType
otpData
}
type mfaDoneData struct {
baseData
UserName string
MfaType model.MfaType
}
type otpData struct {
Url string
Secret string
QrCode string
}

View File

@@ -0,0 +1,9 @@
package handler
import (
"net/http"
)
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler {
return http.FileServer(staticDir)
}

View File

@@ -0,0 +1,58 @@
package handler
import (
"net/http"
"github.com/gorilla/mux"
)
const (
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointUsername = "/username"
EndpointUserSelection = "/userselection"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMfaVerify = "/mfa/verify"
EndpointMfaPrompt = "/mfa/prompt"
EndpointMfaInitVerify = "/mfa/init/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegister = "/register"
EndpointLogoutDone = "/logout/done"
EndpointResources = "/resources"
)
func CreateRouter(login *Login, staticDir http.FileSystem) *mux.Router {
router := mux.NewRouter()
router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointHealthz, login.handleHealthz).Methods(http.MethodGet)
router.HandleFunc(EndpointReadiness, login.handleReadiness).Methods(http.MethodGet)
router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointUsername, login.handleUsername).Methods(http.MethodGet)
router.HandleFunc(EndpointUsername, login.handleUsernameCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost)
router.HandleFunc(EndpointPassword, login.handlePasswordCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointInitPassword, login.handleInitPassword).Methods(http.MethodGet)
router.HandleFunc(EndpointInitPassword, login.handleInitPasswordCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordReset, login.handlePasswordReset).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaVerify, login.handleMfaVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaPrompt, login.handleMfaPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointMfaInitVerify, login.handleMfaInitVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet)
router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost)
router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet)
router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet)
return router
}

View File

@@ -0,0 +1,42 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplUserSelection = "userselection"
)
type userSelectionFormData struct {
UserID string `schema:"userID"`
}
func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, selectionData *model.SelectUserStep) {
var errType, errMessage string
data := userSelectionData{
baseData: l.getBaseData(r, authReq, "Select User", errType, errMessage),
Users: selectionData.Users,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplUserSelection], data, nil)
}
func (l *Login) handleSelectUser(w http.ResponseWriter, r *http.Request) {
data := new(userSelectionFormData)
authSession, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
if data.UserID == "0" {
l.renderLogin(w, r, authSession, nil)
return
}
err = l.authRepo.SelectUser(r.Context(), authSession.ID, data.UserID)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
l.renderNextStep(w, r, authSession)
}

17
internal/login/login.go Normal file
View File

@@ -0,0 +1,17 @@
package login
import (
"context"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/login/handler"
)
type Config struct {
Handler handler.Config
}
func Start(ctx context.Context, config Config, systemDefaults sd.SystemDefaults, authRepo *eventsourcing.EsRepository) {
handler.StartLogin(ctx, config.Handler, authRepo)
}

View File

@@ -0,0 +1,119 @@
Password:
Title: Passwort
Description: Gib deine Benutzerdaten ein.
Password: Passwort
Login:
Title: Anmeldung
Description: Gib deine Benutzerdaten ein.
Username: Benutzername
UserSelection:
Title: Account auswählen
Description: Wähle deinen Account aus.
OtherUser: Anderer Benutzer
SessionState0: aktiv
SessionState1: inaktiv
MfaVerify:
Title: Multifaktor verifizieren
Description: Verifiziere deinen Multifaktor
OTP: OTP
Code: Code
InitPassword:
Title: Passwort setzen
Description: Du hast einen Code erhalten, welcher im untenstehenden Formular eingegeben werden muss um ein neues Passwort zu setzen.
Code: Code
NewPassword: Neues Passwort
NewPasswordConfirm: Passwort bestätigen
InitPasswordDone:
Title: Passwort gesetzt
Description: Passwort erfolgreich gesetzt
InitUser:
Title: User aktivieren
Description: Du hast einen Code erhalten, welcher im untenstehenden Formular eingegeben werden muss um deine EMail zu verifizieren und ein neues Passwort zu setzen.
Code: Code
NewPassword: Neues Passwort
NewPasswordConfirm: Passwort bestätigen
InitUserDone:
Title: User aktiviert
Description: EMail verifiziert und Passwort erfolgreich gesetzt
MfaPrompt:
Title: Multifaktor hinzufügen
Description: Möchtest du einen Mulitfaktor hinzufügen?
Provider0: OTP
Provider1: SMS
MfaInitVerify:
Title: Multifaktor Verifizierung
Description: Verifiziere deinen Multifaktor
OtpDescription: Scanne den Code mit einem Authentifizierungs-App (z.B Google Authentificator) oder kopiere das Secret und gib anschliessend den Code ein.
Secret: Secret
Code: Code
MfaInitDone:
Title: Multifaktor Verifizierung erstellt
Description: Multifikator Verifizierung erfolgreich abgeschlossen. Der Multifaktor muss bei jeder Anmeldung eingegeben werden, dies beinhaltet auch den aktuellen Authentifizierungs Prozess.
PasswordChange:
Title: Passwort ändern
Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst.
OldPassword: Altes Passwort
NewPassword: Neues Passwort
PasswordChangeDone:
Title: Passwort ändern
Description: Das Passwort wurde erfolgreich geändert.
PasswordResetDone:
Title: Resetlink versendet
Description: Prüfe dein E-Mail Postfach, um ein neues Passwort zu setzen.
PasswordSetDone:
Title: Passwort gesetzt
Description: Das Passwort wurde erfolgreich gesetzt.
EmailVerification:
Title: E-Mail Verifizierung
Description: Du hast ein E-Mail zur Verifizierung deiner E-Mail Adresse bekommen. Gib den Code im untenstehenden Formular ein. Mit erneut versenden, wird dir ein neues E-Mail zugestellt.
Code: Code
EmailVerificationDone:
Title: E-Mail Verifizierung
Description: Deine E-Mail Adresse wurde erfolgreich verifiziert.
Registration:
Title: Registration
Description: Gib deine Benutzerangaben an. Die E-Mail Adresse wird als Benutzernamen verwendet.
Email: E-Mail
Firstname: Vorname
Lastname: Nachname
Language: Sprache
German: Deutsch
English: English
Gender: Geschlecht
Female: weiblich
Male: männlich
Diverse: diverse
Password: Passwort
Password2: Passwort wiederholen
LogoutDone:
Title: Ausgeloggt
Description: Du wurdest erfolgreich ausgeloggt.
Actions:
Login: anmelden
Next: weiter
Back: zurück
Resend: erneut senden
Skip: überspringen
Register: registrieren
ForgotPassword: Password zurücksetzen
optional: (optional)

View File

@@ -0,0 +1,119 @@
Login:
Title: Login
Description: Enter your logindata.
Username: Username
UserSelection:
Title: Select account
Description: Select your account.
OtherUser: Other User
SessionState0: active
SessionState1: inactive
Password:
Title: Password
Description: Enter your logindata.
Password: Password
MfaVerify:
Title: Verify Multificator
Description: Verify your multifactor
OTP: OTP
Code: Code
InitPassword:
Title: Set Password
Description: You have received a code, which you have to enter in the form below, to set your new password.
Code: Code
NewPassword: New Password
NewPasswordConfirm: Confirm Password
InitPasswordDone:
Title: Passwortd set
Description: Password successfully set
InitUser:
Title: Activate User
Description: You have received a code, which you have to enter in the form below, to verify your email and set your new password.
Code: Code
NewPassword: New Password
NewPasswordConfirm: Confirm Password
InitUserDone:
Title: User activated
Description: Email verified and Password successfully set
MfaPrompt:
Title: Multifactor Setup
Description: Would you like to setup multifactor authentication?
Provider0: OTP
Provider1: SMS
MfaInitVerify:
Title: Multifactor Verification
Description: Verify your multifactor.
OtpDescription: Scan the code with your authenticator app (e.g Google-Authenticator) or copy the secret and insert the generated code below.
Secret: Secret
Code: Code
MfaInitDone:
Title: Multifcator Verification done
Description: Multifactor verification successfully done. The multifactor has to be entered on each login, even in the actual authentification process.
PasswordChange:
Title: Change Password
Description: Change your password. Enter your old and new password.
OldPassword: Old Password
NewPassword: New Password
PasswordChangeDone:
Title: Change Password
Description: Your password was changed successfully.
PasswordResetDone:
Title: Reset link set
Description: Check your email to to reset your password.
PasswordSetDone:
Title: Password set
Description: Your password was set successfully.
EmailVerification:
Title: E-Mail Verification
Description: We have sent you an email to verify your address. Please enter the code in the form below.
Code: Code
EmailVerificationDone:
Title: E-Mail Verification
Description: Your email address has been successfully verified.
Registration:
Title: Registration
Description: Enter your Userdata. Your email address will be used as username.
Email: E-Mail
Firstname: Firstname
Lastname: Lastname
Language: Language
German: Deutsch
English: English
Gender: Gender
Female: Female
Male: Male
Diverse: diverse / X
Password: Password
Password2: Password confirmation
LogoutDone:
Title: Logged out
Description: You have logged out successfully.
Actions:
Login: login
Next: next
Back: back
Resend: resend
Skip: skip
Register: register
ForgotPassword: Reset password
optional: (optional)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,7 @@
package resources
// scss
//go:generate sass themes/scss/zitadel/dark.scss themes/zitadel/css/dark.css
//go:generate sass themes/scss/zitadel/light.scss themes/zitadel/css/light.css
//go:generate sass themes/scss/caos/dark.scss themes/caos/css/dark.css
//go:generate sass themes/scss/caos/light.scss themes/caos/css/light.css

View File

@@ -0,0 +1,257 @@
@font-face {
font-family: Aileron;
src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
font-style: normal;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
font-style: italic;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
font-style: italic;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
font-style: normal;
font-weight: 800;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
font-style: italic;
font-weight: 800;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../../../fonts/material/MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local("Material Icons"), local("MaterialIcons-Regular"), url(../../../fonts/material/MaterialIcons-Regular.woff2) format("woff2"), url(../../../fonts/material/MaterialIcons-Regular.woff) format("woff"), url(../../../fonts/material/MaterialIcons-Regular.ttf) format("truetype");
}
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-weight: 400;
}
body {
margin: 0;
}
html {
background-color: #282828;
color: white;
}
h1 {
color: white;
font-family: Aileron;
text-transform: uppercase;
text-align: center;
font-size: 40px;
}
p {
font-width: 300;
}
header {
padding: 8px;
}
header .logo {
background-image: url("../logo-dark.png");
background-repeat: no-repeat;
background-size: contain;
height: 80px;
margin: 30px;
}
.content {
margin: auto;
padding: 20px;
width: 100%;
max-width: 500px;
}
a {
color: #760038;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
}
a:hover {
color: #f60075;
}
button {
text-transform: uppercase;
background-color: #282828;
color: #760038;
border: 2px solid #760038;
border-radius: 5px;
width: 100%;
max-width: 600px;
height: 50px;
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
}
button:hover {
background-color: #f60075;
border: 2px solid #f60075;
}
button.primary {
background-color: #760038;
color: white;
border: none;
}
button.primary:hover {
background-color: #f60075;
}
button > .sessionstate {
text-transform: lowercase;
}
input:not([type=radio]), select {
background-color: #252525;
color: white;
height: 50px;
border: 2px solid #595959;
border-radius: 5px;
padding-left: 15px;
}
form .field {
display: grid;
padding: 10px 0;
}
form .field.radio-button {
display: flex;
}
form .field.radio-button input[type=radio] {
height: 20px;
vertical-align: middle;
}
form .field.radio-button label {
height: 20px;
display: inline-block;
padding: 3px 0 0 15px;
width: 100%;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
margin-bottom: 3px;
}
form label span.optional {
font-style: italic;
text-transform: none;
}
form .actions {
padding: 20px 0;
}
form .actions .right {
float: right;
}
form .actions button, form .actions a {
margin: 10px 0;
}
#copy-secret {
visibility: hidden;
position: absolute;
}
#qrcode {
text-align: center;
}
#qrcode svg rect[style*="fill:white"] {
fill: #282828 !important;
}
#qrcode svg rect[style*="fill:black"] {
fill: white !important;
}
#secret .copy {
float: right;
cursor: pointer;
}
footer {
background-image: url("../gradientdeco-full.svg");
width: 100%;
background-size: cover;
height: 44vw;
position: fixed;
bottom: 0;
z-index: -1;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: "liga";
}
/*# sourceMappingURL=dark.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCGW;EDFX;EACA;;;AAGJ;EACI;;;AAGJ;EACI,kBCDc;EDEd,OCDQ;;;ADIZ;EACI,OCLQ;EDMR,aCZS;EDaT;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCnCW;EDoCX;EACA;EACA;;AAEA;EACI,OCxCY;;;AD4CpB;EACI;EACA,kBCjDc;EDkDd,OChDW;EDiDX;EACA;EACA;EACA;EACA,QE/DU;EFgEV;EACA;EACA;;AACA;EACI,kBCzDY;ED0DZ;;AAGJ;EACI,kBC/DO;EDgEP,OCjEI;EDkEJ;;AACA;EACI,kBClEQ;;ADsEhB;EACI;;;AAIR;EACI,kBE7EmB;EF8EnB,OC/EQ;EDgFR,QEzFU;EF0FV;EACA;EACA;;;AAIA;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI,OE9GK;EF+GL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKJ;EACI;EACA;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA","file":"dark.css"}

View File

@@ -0,0 +1,299 @@
@font-face {
font-family: Aileron;
src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
font-style: normal;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
font-style: italic;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
font-style: italic;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
font-style: normal;
font-weight: 800;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
font-style: italic;
font-weight: 800;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../../../fonts/material/MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local("Material Icons"), local("MaterialIcons-Regular"), url(../../../fonts/material/MaterialIcons-Regular.woff2) format("woff2"), url(../../../fonts/material/MaterialIcons-Regular.woff) format("woff"), url(../../../fonts/material/MaterialIcons-Regular.ttf) format("truetype");
}
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-weight: 400;
}
body {
margin: 0;
}
html {
background-color: #282828;
color: white;
}
h1 {
color: white;
font-family: Aileron;
text-transform: uppercase;
text-align: center;
font-size: 40px;
}
p {
font-width: 300;
}
header {
padding: 8px;
}
header .logo {
background-image: url("../logo-dark.png");
background-repeat: no-repeat;
background-size: contain;
height: 80px;
margin: 30px;
}
.content {
margin: auto;
padding: 20px;
width: 100%;
max-width: 500px;
}
a {
color: #760038;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
}
a:hover {
color: #f60075;
}
button {
text-transform: uppercase;
background-color: #282828;
color: #760038;
border: 2px solid #760038;
border-radius: 5px;
width: 100%;
max-width: 600px;
height: 50px;
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
}
button:hover {
background-color: #f60075;
border: 2px solid #f60075;
}
button.primary {
background-color: #760038;
color: white;
border: none;
}
button.primary:hover {
background-color: #f60075;
}
button > .sessionstate {
text-transform: lowercase;
}
input:not([type=radio]), select {
background-color: #252525;
color: white;
height: 50px;
border: 2px solid #595959;
border-radius: 5px;
padding-left: 15px;
}
form .field {
display: grid;
padding: 10px 0;
}
form .field.radio-button {
display: flex;
}
form .field.radio-button input[type=radio] {
height: 20px;
vertical-align: middle;
}
form .field.radio-button label {
height: 20px;
display: inline-block;
padding: 3px 0 0 15px;
width: 100%;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
margin-bottom: 3px;
}
form label span.optional {
font-style: italic;
text-transform: none;
}
form .actions {
padding: 20px 0;
}
form .actions .right {
float: right;
}
form .actions button, form .actions a {
margin: 10px 0;
}
#copy-secret {
visibility: hidden;
position: absolute;
}
#qrcode {
text-align: center;
}
#qrcode svg rect[style*="fill:white"] {
fill: #282828 !important;
}
#qrcode svg rect[style*="fill:black"] {
fill: white !important;
}
#secret .copy {
float: right;
cursor: pointer;
}
footer {
background-image: url("../gradientdeco-full.svg");
width: 100%;
background-size: cover;
height: 44vw;
position: fixed;
bottom: 0;
z-index: -1;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: "liga";
}
html {
background-color: white;
color: #282828;
}
html header .logo {
background-image: url("../logo-light.png");
}
html h1 {
color: #282828;
}
html button {
background-color: white;
color: #760038;
border: 2px solid #760038;
}
html button:hover {
background-color: #f60075;
border: 2px solid #f60075;
}
html button.primary {
background-color: #760038;
color: white;
border: none;
box-shadow: 0px 10px 30px #760038;
}
html button.primary:hover {
background-color: #f60075;
}
html input {
background-color: white;
color: #282828;
}
html #qrcode svg rect[style*="fill:white"] {
fill: white !important;
}
html #qrcode svg rect[style*="fill:black"] {
fill: #282828 !important;
}
html footer {
background-image: url("../gradientdeco-full.svg");
}
/*# sourceMappingURL=light.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCGW;EDFX;EACA;;;AAGJ;EACI;;;AAGJ;EACI,kBCDc;EDEd,OCDQ;;;ADIZ;EACI,OCLQ;EDMR,aCZS;EDaT;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCnCW;EDoCX;EACA;EACA;;AAEA;EACI,OCxCY;;;AD4CpB;EACI;EACA,kBCjDc;EDkDd,OChDW;EDiDX;EACA;EACA;EACA;EACA,QE/DU;EFgEV;EACA;EACA;;AACA;EACI,kBCzDY;ED0DZ;;AAGJ;EACI,kBC/DO;EDgEP,OCjEI;EDkEJ;;AACA;EACI,kBClEQ;;ADsEhB;EACI;;;AAIR;EACI,kBE7EmB;EF8EnB,OC/EQ;EDgFR,QEzFU;EF0FV;EACA;EACA;;;AAIA;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI,OE9GK;EF+GL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKJ;EACI;EACA;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AG1MJ;EACI,kBFYQ;EEXR,OFUc;;AERd;EACI;;AAGJ;EACI,OFGU;;AEAd;EACI;EACA;EACA;;AAEA;EACI,kBFIa;EEHb;;AAGJ;EACI,kBFTG;EEUH,OFXA;EEYA;EACA;;AACA;EACI,kBFbI;;AEkBhB;EACI,kBFrBI;EEsBJ,OFvBU;;AE2BV;EACI;;AAGJ;EACI;;AAIR;EACI","file":"light.css"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,3 @@
@import "../variables.scss";
@import "./variables.scss";
@import "../main.scss";

View File

@@ -0,0 +1,4 @@
@import "../variables.scss";
@import "./variables.scss";
@import "../main.scss";
@import "../light.scss";

View File

@@ -0,0 +1,24 @@
$logoImgDark: "../logo-dark.png";
$logoImgLight: "../logo-light.png";
$footerimgDark: "../gradientdeco-full.svg";
$footerimgLight: "../gradientdeco-full.svg";
// ----- FONTS ------------
$standardFont: Lato;
$headerFont: Aileron;
// ----- COLORS ------------
// ------ DARK-THEME -------
$backgroundColor: #282828;
$fontColor: white;
$primaryColor: #760038;
$primaryColorHover: lighten($primaryColor, 25%);
// ------ LIGHT-THEME -------
$backgroundColorLight: $fontColor;
$fontColorLight: $backgroundColor;
$primaryColorLight: $primaryColor;
$primaryColorHoverLight: lighten($primaryColorLight, 25%);

View File

@@ -0,0 +1,84 @@
//Aileron
@font-face {
font-family: Aileron;
src: url(../../../fonts/ailerons/ailerons.otf ) format('opentype');
}
//Lato
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Thin.ttf ) format('truetype');
font-style: normal;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-ThinItalic.ttf ) format('truetype');
font-style: italic;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Light.ttf ) format('truetype');
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-LightItalic.ttf ) format('truetype');
font-style: italic;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Regular.ttf ) format('truetype');
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Italic.ttf ) format('truetype');
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Bold.ttf ) format('truetype');
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BoldItalic.ttf ) format('truetype');
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Black.ttf ) format('truetype');
font-style: normal;
font-weight: 800;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BlackItalic.ttf ) format('truetype');
font-style: italic;
font-weight: 800;
}
//Material Icons
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(../../../fonts/material/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../../../fonts/material/MaterialIcons-Regular.woff2) format('woff2'),
url(../../../fonts/material/MaterialIcons-Regular.woff) format('woff'),
url(../../../fonts/material/MaterialIcons-Regular.ttf) format('truetype');
}

View File

@@ -0,0 +1,53 @@
// ---- LIGHT-THEME-------
html {
background-color: $backgroundColorLight;
color: $fontColorLight;
header .logo {
background-image: url($logoImgLight);
}
h1 {
color: $fontColorLight;
}
button {
background-color: $backgroundColorLight;
color: $primaryColorLight;
border: 2px solid $primaryColorLight;
&:hover {
background-color: $primaryColorHoverLight;
border: 2px solid $primaryColorHoverLight;
}
&.primary {
background-color: $primaryColor;
color: $fontColor;
border: none;
box-shadow: 0px 10px 30px $primaryColor;
&:hover {
background-color: $primaryColorHover;
}
}
}
input {
background-color: $backgroundColorLight;
color: $fontColorLight;
}
#qrcode {
svg rect[style*="fill:white"] {
fill: $backgroundColorLight !important;
}
svg rect[style*="fill:black"] {
fill: $fontColorLight !important;
}
}
footer {
background-image: url($footerimgLight);
}
}

View File

@@ -0,0 +1,205 @@
@import "fonts";
*, *::before, *::after {
box-sizing: border-box;
font-family: $standardFont;
font-size: 18px;
font-weight: 400;
}
body {
margin: 0;
}
html {
background-color: $backgroundColor;
color: $fontColor;
}
h1 {
color: $fontColor;
font-family: $headerFont;
text-transform: uppercase;
text-align: center;
font-size: 40px;
}
p {
font-width: 300;
}
header {
padding: 8px;
.logo {
background-image: url($logoImgDark);
background-repeat: no-repeat;
background-size: contain;
height: 80px;
margin: 30px;
}
}
.content {
margin: auto;
padding: 20px;
width: 100%;
max-width: 500px;
}
a {
color: $primaryColor;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
&:hover {
color: $primaryColorHover;
}
}
button {
text-transform: uppercase;
background-color: $backgroundColor;
color: $primaryColor;
border: 2px solid $primaryColor;
border-radius: 5px;
width: 100%;
max-width: 600px;
height: $inputHeight;
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
&:hover {
background-color: $primaryColorHover;
border: 2px solid $primaryColorHover;
}
&.primary {
background-color: $primaryColor;
color: $fontColor;
border: none;
&:hover {
background-color: $primaryColorHover;
}
}
& > .sessionstate {
text-transform: lowercase;
}
}
input:not([type='radio']), select {
background-color: $inputBackgroundColor;
color: $fontColor;
height: $inputHeight;
border: 2px solid $inputBorderColor;
border-radius: 5px;
padding-left: 15px;
}
form {
.field {
display: grid;
padding: 10px 0;
}
.field.radio-button {
display: flex;
input[type='radio'] {
height: 20px;
vertical-align: middle;
}
& label {
height: 20px;
display: inline-block;
padding: 3px 0 0 15px;
width: 100%;
}
}
label {
color: $labelColor;
text-transform: uppercase;
font-size: 0.9rem;
margin-bottom: 3px;
span.optional {
font-style: italic;
text-transform: none;
}
}
.actions {
padding: 20px 0;
.right {
float: right;
}
button, a {
margin: 10px 0;
}
}
}
#copy-secret {
visibility: hidden;
position: absolute;
}
#qrcode {
text-align: center;
svg rect[style*="fill:white"] {
fill: $backgroundColor !important;
}
svg rect[style*="fill:black"] {
fill: $fontColor !important;
}
}
#secret {
.copy {
float: right;
cursor: pointer;
}
}
footer {
background-image: url($footerimgDark);
width: 100%;
background-size: cover;
height: 44vw;
position: fixed;
bottom: 0;
z-index: -1;
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

View File

@@ -0,0 +1,23 @@
// ----- FONTS ------------
$standardFont: Lato;
$headerFont: Aileron;
// ----- LAYOUT ------------
$inputHeight: 50px;
// ----- DARK-THEME --------
$backgroundColor: #282828;
$fontColor: #FFFFFF;
$primaryColor: #364DF6;
$primaryColorHover: lighten($primaryColor, 10%);
$labelColor: #898989;
$inputBorderColor: #595959;
$inputBackgroundColor: #252525;
// ----- LIGHT-THEME --------
$backgroundColorLight: $fontColor;
$fontColorLight: $backgroundColor;
$primaryColorLight: $primaryColor;
$primaryColorHoverLight: lighten($primaryColorLight, 10%);

View File

@@ -0,0 +1,3 @@
@import "../variables.scss";
@import "./variables.scss";
@import "../main.scss";

View File

@@ -0,0 +1,4 @@
@import "../variables.scss";
@import "./variables.scss";
@import "../main.scss";
@import "../light.scss";

View File

@@ -0,0 +1,5 @@
$logoImgDark: "../logo-dark.png";
$logoImgLight: "../logo-light.png";
$footerimgDark: "../gradientdeco-full.svg";
$footerimgLight: "../gradientdeco-full.svg";

View File

@@ -0,0 +1,257 @@
@font-face {
font-family: Aileron;
src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
font-style: normal;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
font-style: italic;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
font-style: italic;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
font-style: normal;
font-weight: 800;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
font-style: italic;
font-weight: 800;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../../../fonts/material/MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local("Material Icons"), local("MaterialIcons-Regular"), url(../../../fonts/material/MaterialIcons-Regular.woff2) format("woff2"), url(../../../fonts/material/MaterialIcons-Regular.woff) format("woff"), url(../../../fonts/material/MaterialIcons-Regular.ttf) format("truetype");
}
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-weight: 400;
}
body {
margin: 0;
}
html {
background-color: #282828;
color: #FFFFFF;
}
h1 {
color: #FFFFFF;
font-family: Aileron;
text-transform: uppercase;
text-align: center;
font-size: 40px;
}
p {
font-width: 300;
}
header {
padding: 8px;
}
header .logo {
background-image: url("../logo-dark.png");
background-repeat: no-repeat;
background-size: contain;
height: 80px;
margin: 30px;
}
.content {
margin: auto;
padding: 20px;
width: 100%;
max-width: 500px;
}
a {
color: #364DF6;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
}
a:hover {
color: #6778f8;
}
button {
text-transform: uppercase;
background-color: #282828;
color: #364DF6;
border: 2px solid #364DF6;
border-radius: 5px;
width: 100%;
max-width: 600px;
height: 50px;
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
}
button:hover {
background-color: #6778f8;
border: 2px solid #6778f8;
}
button.primary {
background-color: #364DF6;
color: #FFFFFF;
border: none;
}
button.primary:hover {
background-color: #6778f8;
}
button > .sessionstate {
text-transform: lowercase;
}
input:not([type=radio]), select {
background-color: #252525;
color: #FFFFFF;
height: 50px;
border: 2px solid #595959;
border-radius: 5px;
padding-left: 15px;
}
form .field {
display: grid;
padding: 10px 0;
}
form .field.radio-button {
display: flex;
}
form .field.radio-button input[type=radio] {
height: 20px;
vertical-align: middle;
}
form .field.radio-button label {
height: 20px;
display: inline-block;
padding: 3px 0 0 15px;
width: 100%;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
margin-bottom: 3px;
}
form label span.optional {
font-style: italic;
text-transform: none;
}
form .actions {
padding: 20px 0;
}
form .actions .right {
float: right;
}
form .actions button, form .actions a {
margin: 10px 0;
}
#copy-secret {
visibility: hidden;
position: absolute;
}
#qrcode {
text-align: center;
}
#qrcode svg rect[style*="fill:white"] {
fill: #282828 !important;
}
#qrcode svg rect[style*="fill:black"] {
fill: #FFFFFF !important;
}
#secret .copy {
float: right;
cursor: pointer;
}
footer {
background-image: url("../gradientdeco-full.svg");
width: 100%;
background-size: cover;
height: 44vw;
position: fixed;
bottom: 0;
z-index: -1;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: "liga";
}
/*# sourceMappingURL=dark.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI,kBCLc;EDMd,OCLQ;;;ADQZ;EACI,OCTQ;EDUR,aClBS;EDmBT;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvCW;EDwCX;EACA;EACA;;AAEA;EACI,OC5CY;;;ADgDpB;EACI;EACA,kBCrDc;EDsDd,OCpDW;EDqDX;EACA;EACA;EACA;EACA,QC/DU;EDgEV;EACA;EACA;;AACA;EACI,kBC7DY;ED8DZ;;AAGJ;EACI,kBCnEO;EDoEP,OCrEI;EDsEJ;;AACA;EACI,kBCtEQ;;AD0EhB;EACI;;;AAIR;EACI,kBC7EmB;ED8EnB,OCnFQ;EDoFR,QCzFU;ED0FV;EACA;EACA;;;AAIA;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI,OC9GK;ED+GL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKJ;EACI;EACA;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA","file":"dark.css"}

View File

@@ -0,0 +1,299 @@
@font-face {
font-family: Aileron;
src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
font-style: normal;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
font-style: italic;
font-weight: 100;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
font-style: italic;
font-weight: 200;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
font-style: italic;
font-weight: 400;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
font-style: italic;
font-weight: 700;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
font-style: normal;
font-weight: 800;
}
@font-face {
font-family: Lato;
src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
font-style: italic;
font-weight: 800;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../../../fonts/material/MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local("Material Icons"), local("MaterialIcons-Regular"), url(../../../fonts/material/MaterialIcons-Regular.woff2) format("woff2"), url(../../../fonts/material/MaterialIcons-Regular.woff) format("woff"), url(../../../fonts/material/MaterialIcons-Regular.ttf) format("truetype");
}
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-weight: 400;
}
body {
margin: 0;
}
html {
background-color: #282828;
color: #FFFFFF;
}
h1 {
color: #FFFFFF;
font-family: Aileron;
text-transform: uppercase;
text-align: center;
font-size: 40px;
}
p {
font-width: 300;
}
header {
padding: 8px;
}
header .logo {
background-image: url("../logo-dark.png");
background-repeat: no-repeat;
background-size: contain;
height: 80px;
margin: 30px;
}
.content {
margin: auto;
padding: 20px;
width: 100%;
max-width: 500px;
}
a {
color: #364DF6;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
}
a:hover {
color: #6778f8;
}
button {
text-transform: uppercase;
background-color: #282828;
color: #364DF6;
border: 2px solid #364DF6;
border-radius: 5px;
width: 100%;
max-width: 600px;
height: 50px;
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
}
button:hover {
background-color: #6778f8;
border: 2px solid #6778f8;
}
button.primary {
background-color: #364DF6;
color: #FFFFFF;
border: none;
}
button.primary:hover {
background-color: #6778f8;
}
button > .sessionstate {
text-transform: lowercase;
}
input:not([type=radio]), select {
background-color: #252525;
color: #FFFFFF;
height: 50px;
border: 2px solid #595959;
border-radius: 5px;
padding-left: 15px;
}
form .field {
display: grid;
padding: 10px 0;
}
form .field.radio-button {
display: flex;
}
form .field.radio-button input[type=radio] {
height: 20px;
vertical-align: middle;
}
form .field.radio-button label {
height: 20px;
display: inline-block;
padding: 3px 0 0 15px;
width: 100%;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
margin-bottom: 3px;
}
form label span.optional {
font-style: italic;
text-transform: none;
}
form .actions {
padding: 20px 0;
}
form .actions .right {
float: right;
}
form .actions button, form .actions a {
margin: 10px 0;
}
#copy-secret {
visibility: hidden;
position: absolute;
}
#qrcode {
text-align: center;
}
#qrcode svg rect[style*="fill:white"] {
fill: #282828 !important;
}
#qrcode svg rect[style*="fill:black"] {
fill: #FFFFFF !important;
}
#secret .copy {
float: right;
cursor: pointer;
}
footer {
background-image: url("../gradientdeco-full.svg");
width: 100%;
background-size: cover;
height: 44vw;
position: fixed;
bottom: 0;
z-index: -1;
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: "liga";
}
html {
background-color: #FFFFFF;
color: #282828;
}
html header .logo {
background-image: url("../logo-light.png");
}
html h1 {
color: #282828;
}
html button {
background-color: #FFFFFF;
color: #364DF6;
border: 2px solid #364DF6;
}
html button:hover {
background-color: #6778f8;
border: 2px solid #6778f8;
}
html button.primary {
background-color: #364DF6;
color: #FFFFFF;
border: none;
box-shadow: 0px 10px 30px #364DF6;
}
html button.primary:hover {
background-color: #6778f8;
}
html input {
background-color: #FFFFFF;
color: #282828;
}
html #qrcode svg rect[style*="fill:white"] {
fill: #FFFFFF !important;
}
html #qrcode svg rect[style*="fill:black"] {
fill: #282828 !important;
}
html footer {
background-image: url("../gradientdeco-full.svg");
}
/*# sourceMappingURL=light.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI,kBCLc;EDMd,OCLQ;;;ADQZ;EACI,OCTQ;EDUR,aClBS;EDmBT;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvCW;EDwCX;EACA;EACA;;AAEA;EACI,OC5CY;;;ADgDpB;EACI;EACA,kBCrDc;EDsDd,OCpDW;EDqDX;EACA;EACA;EACA;EACA,QC/DU;EDgEV;EACA;EACA;;AACA;EACI,kBC7DY;ED8DZ;;AAGJ;EACI,kBCnEO;EDoEP,OCrEI;EDsEJ;;AACA;EACI,kBCtEQ;;AD0EhB;EACI;;;AAIR;EACI,kBC7EmB;ED8EnB,OCnFQ;EDoFR,QCzFU;ED0FV;EACA;EACA;;;AAIA;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI,OC9GK;ED+GL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;;AAGJ;EACI;;;AAKJ;EACI;EACA;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AE1MJ;EACI,kBDQQ;ECPR,ODMc;;ACJd;EACI;;AAGJ;EACI,ODDU;;ACId;EACI,kBDJI;ECKJ,ODJO;ECKP;;AAEA;EACI,kBDGa;ECFb;;AAGJ;EACI,kBDbG;ECcH,ODfA;ECgBA;EACA;;AACA;EACI,kBDjBI;;ACsBhB;EACI,kBDzBI;EC0BJ,OD3BU;;AC+BV;EACI;;AAGJ;EACI;;AAIR;EACI","file":"light.css"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,32 @@
{{template "main-top" .}}
<h1>{{t "PasswordChange.Title"}}</h1>
<p>{{t "PasswordChange.Description"}}</p>
<form action="{{ changePasswordUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="fields">
<div class="field">
<label class="label" for="old_password">{{t "PasswordChange.OldPassword"}}</label>
<input class="input" type="password" id="old_password" name="old_password" autocomplete="current-password" autofocus required>
</div>
<div class="field">
<label class="label" for="new_password">{{t "PasswordChange.NewPassword"}}</label>
<input class="input" type="password" id="new-password" name="new_password" autocomplete="new-password" required>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button type="submit" name="resend" value="false" class="primary right" >{{t "Actions.Next"}}</buttontype="submit">
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,19 @@
{{template "main-top" .}}
<h1>{{t "PasswordChangeDone.Title"}}</h1>
<p>{{t "PasswordChangeDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,9 @@
{{ define "error-message" }}
{{if .ErrMessage }}
<div class="field">
<div class="error">
{{ if .ErrType }}{{ .ErrType }} - {{end}}{{ .ErrMessage }}
</div>
</div>
{{end}}
{{ end }}

View File

@@ -0,0 +1,9 @@
{{template "main-top" .}}
<div>
{{ .ErrType }}
{{ .ErrMessage }}
</div>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,3 @@
{{define "footer"}}
{{end}}

View File

@@ -0,0 +1,3 @@
{{define "header"}}
<div class="logo"></div>
{{end}}selec

View File

@@ -0,0 +1,37 @@
{{template "main-top" .}}
<h1>{{t "InitPassword.Title" }}</h1>
<p>{{t "InitPassword.Description" }}</p>
<form action="{{ initPasswordUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
<div class="fields">
<div class="field">
<label class="label" for="code">{{t "InitPassword.Code"}}</label>
<input class="input" type="text" id="code" name="code" value="{{.Code}}" autocomplete="off" autofocus required>
</div>
<div class="field">
<label class="label" for="password">{{t "InitPassword.NewPassword"}}</label>
<input class="input" type="password" id="password" name="password" autocomplete="new-password" autofocus required>
</div>
<div class="field">
<label class="label" for="passwordconfirm">{{t "InitPassword.NewPasswordConfirm"}}</label>
<input class="input" type="password" id="passwordconfirm" name="passwordconfirm" autocomplete="new-password" autofocus required>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button type="submit" name="resend" value="false" class="primary right" >{{t "Actions.Next"}}</buttontype="submit">
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend" }}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,17 @@
{{template "main-top" .}}
<h1>{{t "PasswordSetDone.Title"}}</h1>
<p>{{t "PasswordSetDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,37 @@
{{template "main-top" .}}
<h1>{{t "InitUser.Title" }}</h1>
<p>{{t "InitUser.Description" }}</p>
<form action="{{ initUserUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
<div class="fields">
<div class="field">
<label class="label" for="code">{{t "InitUser.Code"}}</label>
<input class="input" type="text" id="code" name="code" value="{{.Code}}" autocomplete="off" autofocus required>
</div>
<div class="field">
<label class="label" for="password">{{t "InitUser.NewPassword"}}</label>
<input class="input" type="password" id="password" name="password" autocomplete="new-password" autofocus required>
</div>
<div class="field">
<label class="label" for="passwordconfirm">{{t "InitUser.NewPasswordConfirm"}}</label>
<input class="input" type="password" id="passwordconfirm" name="passwordconfirm" autocomplete="new-password" autofocus required>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button type="submit" name="resend" value="false" class="primary right" >{{t "Actions.Next"}}</buttontype="submit">
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend" }}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,17 @@
{{template "main-top" .}}
<h1>{{t "InitUserDone.Title"}}</h1>
<p>{{t "InitUserDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,27 @@
{{template "main-top" .}}
<h1>{{t "Login.Title"}}</h1>
<p>{{t "Login.Description"}}</p>
<form action="{{ usernameUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="fields">
<div class="field">
<label class="label" for="username">{{t "Login.Username"}}</label>
<input class="input" type="text" id="username" name="username" value="{{ .UserName }}" autocomplete="username" autofocus required>
</div>
</div>
{{template "error-message" .}}
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a class="default right" href="{{ registerUrl .AuthReqID }}" >{{t "Actions.Register"}}</a>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,15 @@
{{template "main-top" .}}
<h1>{{t "LogoutDone.Title"}}</h1>
<p>{{t "LogoutDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Login"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,31 @@
{{template "main-top" .}}
<h1>{{t "EmailVerification.Title"}}</h1>
<p>{{t "EmailVerification.Description"}}</p>
<form action="{{ mailVerificationUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
<div class="fields">
<div class="field">
<label class="label" for="code">{{t "EmailVerification.Code"}}</label>
<input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button type="submit" name="resend" value="false" class="primary right" >{{t "Actions.Next"}}</buttontype="submit">
{{ if .UserID }}
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend"}}</button>
{{ end }}
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,19 @@
{{template "main-top" .}}
<h1>{{t "EmailVerificationDone.Title"}}</h1>
<p>{{t "EmailVerificationDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{if .AuthReqID }}{{t "Actions.Next"}}{{else}}{{t "Actions.Login"}}{{end}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,34 @@
{{define "main-top"}}
<!DOCTYPE html>
<html lang="{{ .Lang }}" class="{{.ThemeMode}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{{if .ThemeMode}}
<link rel="stylesheet" href="{{ resourceThemeUrl (printf "css/%s.css" .ThemeMode) .Theme }}" type="text/css" media="all">
{{else}}
<link rel="stylesheet" href="{{ resourceThemeUrl "css/dark.css" .Theme }}" type="text/css" media="(prefers-color-scheme: dark), (prefers-color-scheme: no-preference)">
<link rel="stylesheet" href="{{ resourceThemeUrl "css/light.css" .Theme }}" type="text/css" media="(prefers-color-scheme: light)">
{{end}}
<link rel="icon" type="image/x-icon" href="{{ resourceThemeUrl "favicon.ico" .Theme }}">
<title>{{ .Title }}</title>
</head>
<body>
<header>
{{template "header" .}}
</header>
<div class="content">
{{end}}
<!-- here goes the content -->
{{define "main-bottom"}}
</div>
</body>
<footer>
{{template "footer" .}}
</footer>
{{end}}

View File

@@ -0,0 +1,19 @@
{{template "main-top" .}}
<h1>{{t "MfaInitDone.Title"}}</h1>
<p>{{t "MfaInitDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .MfaType }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,47 @@
{{template "main-top" .}}
<h1>{{t "MfaInitVerify.Title"}}</h1>
<p>{{t "MfaInitVerify.Description"}}</p>
<form action="{{ mfaInitVerifyUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .MfaType }}" />
<input type="hidden" name="url" value="{{ .Url }}" />
<input type="hidden" name="secret" value="{{ .Secret }}" />
{{if (eq .MfaType 0) }}
<p>{{t "MfaInitVerify.OtpDescription"}}</p>
<div id="qrcode">
{{.QrCode}}
</div>
<div class="fields">
<div class="field">
<span class="label" for="secret">{{t "MfaInitVerify.Secret"}}</span>
<span class="input" id="secret">
{{.Secret}}
<span class="copy material-icons" onclick="copyToClipboard('{{ .Secret }}')">content_copy</span>
</span>
</div>
<div class="field">
<label class="label" for="code">{{t "MfaInitVerify.Code"}}</label>
<input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div>
</div>
{{end}}
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
<script>
const copyToClipboard = str => {
navigator.clipboard.writeText(str);
}
</script>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,30 @@
{{template "main-top" .}}
<h1>{{t "MfaPrompt.Title"}}</h1>
<form action="{{ mfaPromptUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="fields">
{{ range $provider := .MfaProviders}}
{{ $providerName := (t (printf "MfaPrompt.Provider%v" $provider)) }}
<div class="field radio-button">
<input id="{{ $provider }}" type="radio" name="provider" value="{{ $provider }}">
<label for="{{ $provider }}">{{ $providerName }}</label>
</div>
{{ end }}
</div>
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
{{if not .MfaRequired}}
<button class="default right" name="skip" value="true" type="submit" formnovalidate>{{t "Actions.Skip"}}</button>
{{end}}
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,28 @@
{{template "main-top" .}}
<h1>{{t "MfaVerify.Title"}}</h1>
<p>{{t "MfaVerify.Description"}}</p>
<form action="{{ mfaVerifyUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .SelectedMfaProvider }}" />
<div class="fields">
<div class="field">
<label class="label" for="code">{{t "MfaVerify.Code"}}</label>
<input class="input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div>
</div>
{{ template "error-message" .}}
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,33 @@
{{template "main-top" .}}
<h1>{{t "Password.Title"}}</h1>
<form action="{{ passwordUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="username" value="{{ .UserName }}" />
<div class="fields">
<div class="field">
<label class="label" for="password">{{t "Password.Password"}}</label>
<input class="input" type="password" id="password" name="password" autocomplete="current-password" autofocus required>
</div>
</div>
{{template "error-message" .}}
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ usernameChangeUrl .AuthReqID }}">
<button class="secondary" type="button">{{t "Actions.Back"}}</button>
</a>
<a href="{{ passwordResetUrl .AuthReqID }}">
<button class="secondary" type="button">{{t "Actions.ForgotPassword"}}</button>
</a>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,17 @@
{{template "main-top" .}}
<h1>{{t "PasswordResetDone.Title"}}</h1>
<p>{{t "PasswordResetDone.Description"}}</p>
<form action="{{ loginUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,61 @@
{{template "main-top" .}}
<h1>{{t "Registration.Title"}}</h1>
<p>{{t "Registration.Description"}}</p>
<form action="{{ registrationUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="fields">
<div class="field">
<label class="label" for="email">{{t "Registration.Email"}}</label>
<input class="input" type="text" id="email" name="email" autocomplete="email" value="{{ .Email }}" autofocus required>
</div>
<div class="field">
<label class="label" for="firstname">{{t "Registration.Firstname"}}</label>
<input class="input" type="text" id="firstname" name="firstname" autocomplete="given-name" value="{{ .Firstname }}" required>
</div>
<div class="field">
<label class="label" for="lastname">{{t "Registration.Lastname"}}</label>
<input class="input" type="text" id="lastname" name="lastname" autocomplete="family-name" value="{{ .Lastname }}" required>
</div>
<div class="field">
<label class="label" for="languages">{{t "Registration.Language"}}</label>
<select id="languages" name="language">
<option value=""></option>
<option value="de" id="de" {{if (selectedLanguage "de")}} selected {{end}}>{{t "Registration.German"}}</option>
<option value="en" id="en" {{if (selectedLanguage "en")}} selected {{end}}>{{t "Registration.English"}}</option>
</select>
</div>
<div class="field">
<label class="label" for="genders">
{{t "Registration.Gender"}}
<span class="optional">{{t "optional"}}</span>
</label>
<select id="genders" name="gender">
<option value=""></option>
<option value="1" id="female" {{if (selectedGender 1)}} selected {{end}}>{{t "Registration.Female"}}</option>
<option value="2" id="male" {{if (selectedGender 2)}} selected {{end}}>{{t "Registration.Male"}}</option>
<option value="3" id="diverse" {{if (selectedGender 3)}} selected {{end}}>{{t "Registration.Diverse"}}</option>
</select>
</div>
<div class="field">
<label class="label" for="password">{{t "Registration.Password"}}</label>
<input class="input" type="password" id="password" name="password" autocomplete="new-password" required>
</div>
<div class="field">
<label class="label" for="password2">{{t "Registration.Password2"}}</label>
<input class="input" type="password" id="password2" name="password2" autocomplete="new-password" required>
</div>
</div>
{{template "error-message" .}}
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,25 @@
{{template "main-top" .}}
<h1>{{t "UserSelection.Title"}}</h1>
<p>{{t "UserSelection.Description"}}</p>
<form action="{{ userSelectionUrl }}" method="POST">
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
{{ range $user := .Users }}
{{ $sessionState := (t (printf "UserSelection.SessionState%v" $user.UserSessionState)) }}
<button type="submit" name="userID" value="{{$user.UserID}}" class="primary">
<span class="username">{{$user.UserName}}</span>
<span class="sessionstate">({{$sessionState}})</span>
</button>
{{ end }}
<button type="submit" name="userID" value="0" class="primary">{{t "UserSelection.OtherUser"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,3 @@
package statik
//go:generate statik -src=../static -dest=.. -ns=login