diff --git a/cmd/dis_user.go b/cmd/dis_user.go index e051218..c1536c1 100644 --- a/cmd/dis_user.go +++ b/cmd/dis_user.go @@ -192,8 +192,7 @@ func (du disUser) runCheckPassword(context wisski_distillery.Context) error { context.Println() var passcode string - if user.TOTPEnabled { - context.Printf("Enter passcode for %s:", du.Positionals.User) + if user.IsTOTPEnabled() { passcode, err = context.IOStream.ReadPassword() if err != nil { @@ -261,9 +260,7 @@ func (du disUser) runMakeAdmin(context wisski_distillery.Context) error { if err != nil { return err } - - user.Admin = true - return user.Save(context.Context) + return user.MakeAdmin(context.Context) } func (du disUser) runRemoveAdmin(context wisski_distillery.Context) error { @@ -272,6 +269,5 @@ func (du disUser) runRemoveAdmin(context wisski_distillery.Context) error { return err } - user.Admin = false - return user.Save(context.Context) + return user.MakeRegular(context.Context) } diff --git a/internal/cli/cli_notices.go b/internal/cli/cli_notices.go index ef55f30..719b306 100755 --- a/internal/cli/cli_notices.go +++ b/internal/cli/cli_notices.go @@ -1,7 +1,7 @@ package cli // =========================================================================================================== -// This file was generated automatically at 04-01-2023 11:57:49 using gogenlicense. +// This file was generated automatically at 04-01-2023 14:54:26 using gogenlicense. // Do not edit manually, as changes may be overwritten. // =========================================================================================================== @@ -2189,7 +2189,7 @@ package cli // # Generation // // This variable and the associated documentation have been automatically generated using the 'gogenlicense' tool. -// It was last updated at 04-01-2023 11:57:49. +// It was last updated at 04-01-2023 14:54:26. var LegalNotices string func init() { diff --git a/internal/dis/component/auth/auth.go b/internal/dis/component/auth/auth.go index 0a7315c..a705d49 100644 --- a/internal/dis/component/auth/auth.go +++ b/internal/dis/component/auth/auth.go @@ -32,17 +32,7 @@ func (auth *Auth) Routes() []string { return []string{"/user/"} } func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) { router := httprouter.New() - // setup the csrf handler (if needed) - auth.csrf.Get(func() func(http.Handler) http.Handler { - var opts []csrf.Option - if !auth.Config.HTTPSEnabled() { - opts = append(opts, csrf.Secure(false)) - } - opts = append(opts, csrf.Path(route)) - return csrf.Protect(auth.Config.CSRFSecret(), opts...) - }) - - router.Handler(http.MethodGet, route, auth.authHome(ctx)) + router.Handler(http.MethodGet, route, auth.authUser(ctx)) { login := auth.authLogin(ctx) @@ -78,3 +68,14 @@ func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, return router, nil } + +func (auth *Auth) CSRF() func(http.Handler) http.Handler { + // setup the csrf handler (if needed) + return auth.csrf.Get(func() func(http.Handler) http.Handler { + var opts []csrf.Option + if !auth.Config.HTTPSEnabled() { + opts = append(opts, csrf.Secure(false)) + } + return csrf.Protect(auth.Config.CSRFSecret(), opts...) + }) +} diff --git a/internal/dis/component/auth/protect.go b/internal/dis/component/auth/protect.go index 104ec44..fc138b9 100644 --- a/internal/dis/component/auth/protect.go +++ b/internal/dis/component/auth/protect.go @@ -74,5 +74,5 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler { // Admin represents a permission that checks if a user is an administrator and has totp enabled. var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) { - return Bool2Grant(user != nil && user.Admin && user.TOTPEnabled, "user needs to have admin permissions and TOTP enabled"), nil + return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and TOTP enabled"), nil } diff --git a/internal/dis/component/auth/routes.go b/internal/dis/component/auth/routes.go index 7c640f1..6f3e9e7 100644 --- a/internal/dis/component/auth/routes.go +++ b/internal/dis/component/auth/routes.go @@ -10,17 +10,17 @@ import ( "github.com/FAU-CDI/wisski-distillery/pkg/httpx" ) -//go:embed "templates/home.html" -var homeHTMLStr string -var homeTemplate = static.AssetsHome.MustParseShared( - "home.html", - homeHTMLStr, +//go:embed "templates/user.html" +var userHTMLStr string +var userTemplate = static.AssetsUser.MustParseShared( + "user.html", + userHTMLStr, ) -func (auth *Auth) authHome(ctx context.Context) http.Handler { +func (auth *Auth) authUser(ctx context.Context) http.Handler { return auth.Protect(&httpx.HTMLHandler[*AuthUser]{ Handler: auth.UserOf, - Template: homeTemplate, + Template: userTemplate, }, nil) } @@ -47,7 +47,7 @@ func (auth *Auth) authPassword(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.csrf.Get(nil), + CSRF: auth.CSRF(), RenderTemplate: passwordTemplate, RenderTemplateContext: auth.UserFormContext, diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index ea5e45d..cb23ff7 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -49,7 +49,7 @@ func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) { } // user isn't enabled - if !user.Enabled { + if !user.IsEnabled() { return nil, nil } @@ -122,7 +122,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.csrf.Get(nil), + CSRF: auth.CSRF(), RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { if context.Err != nil { diff --git a/internal/dis/component/auth/templates/home.html b/internal/dis/component/auth/templates/user.html similarity index 88% rename from internal/dis/component/auth/templates/home.html rename to internal/dis/component/auth/templates/user.html index b85b662..acb210e 100644 --- a/internal/dis/component/auth/templates/home.html +++ b/internal/dis/component/auth/templates/user.html @@ -13,12 +13,12 @@ {{ define "content" }}

- {{ if .User.Admin }} + {{ if .User.IsAdmin }} You are an administrator. {{ else }} You are a regular user. {{ end }} - {{ if .User.TOTPEnabled }} + {{ if .User.IsTOTPEnabled }} You have TOTP enabled. {{ else }} You do not have TOTP enabled. @@ -26,7 +26,7 @@

Change Password - {{ if .User.TOTPEnabled }} + {{ if .User.IsTOTPEnabled }} Disable TOTP {{ else }} Enable TOTP @@ -35,9 +35,9 @@
-{{ if .User.Admin }} +{{ if .User.IsAdmin }}
- {{ if (not .User.TOTPEnabled) }} + {{ if (not .User.IsTOTPEnabled) }}

TOTP is required to access these. diff --git a/internal/dis/component/auth/totp.go b/internal/dis/component/auth/totp.go index 0706bf9..285ded7 100644 --- a/internal/dis/component/auth/totp.go +++ b/internal/dis/component/auth/totp.go @@ -22,11 +22,11 @@ func (auth *Auth) authTOTPEnable(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.csrf.Get(nil), + CSRF: auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { user, err := auth.UserOf(r) - return struct{}{}, err == nil && user != nil && user.TOTPEnabled + return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, RenderTemplate: totpEnableTemplate, @@ -81,11 +81,11 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.csrf.Get(nil), + CSRF: auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { - user, _ := auth.UserOf(r) - return struct{}{}, user != nil && user.TOTPEnabled + user, err := auth.UserOf(r) + return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { user, err := auth.UserOf(r) @@ -152,11 +152,11 @@ func (auth *Auth) authTOTPDisable(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.csrf.Get(nil), + CSRF: auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { - user, _ := auth.UserOf(r) - return struct{}{}, user != nil && !user.TOTPEnabled + user, err := auth.UserOf(r) + return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled() }, RenderTemplate: totpDisableTemplate, RenderTemplateContext: auth.UserFormContext, diff --git a/internal/dis/component/auth/user.go b/internal/dis/component/auth/user.go index 30db63f..314faae 100644 --- a/internal/dis/component/auth/user.go +++ b/internal/dis/component/auth/user.go @@ -4,13 +4,13 @@ import ( "bytes" "context" "encoding/base64" - "errors" "fmt" "image/png" "net/http" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" @@ -90,15 +90,17 @@ func (auth *Auth) CreateUser(ctx context.Context, name string) (user *AuthUser, user = &AuthUser{ User: models.User{ - User: name, - Enabled: false, + User: name, }, } + user.SetAdmin(false) + user.SetEnabled(false) + user.SetTOTPEnabled(false) // do the create statement - err = table.Create(&user.User).Error + err = table.Select("*").Create(&user.User).Error if err != nil { - return nil, err + return nil, errors.Wrapf(err, "Create") } user.auth = auth @@ -116,7 +118,7 @@ func (au *AuthUser) String() string { return "User{nil}" } hasPassword := len(au.PasswordHash) > 0 - return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.Enabled, hasPassword, au.User.Admin) + return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.IsEnabled(), hasPassword, au.User.IsAdmin()) } var ( @@ -140,7 +142,7 @@ func (au *AuthUser) CheckTOTP(passcode string) error { return err } - if au.TOTPEnabled && !totp.Validate(passcode, secret.Secret()) { + if au.IsTOTPEnabled() && !totp.Validate(passcode, secret.Secret()) { return ErrTOTPFailed } return nil @@ -148,7 +150,7 @@ func (au *AuthUser) CheckTOTP(passcode string) error { // NewTOTP generates a new TOTP secret, returning a totp key. func (au *AuthUser) NewTOTP(ctx context.Context) (*otp.Key, error) { - if au.User.TOTPEnabled { + if au.User.IsTOTPEnabled() { return nil, ErrTOTPEnabled } @@ -191,14 +193,14 @@ func (au *AuthUser) EnableTOTP(ctx context.Context, passcode string) error { return ErrTOTPFailed } - au.User.TOTPEnabled = true + au.User.SetTOTPEnabled(true) return au.Save(ctx) } // DisableTOTP disables totp for the given user func (au *AuthUser) DisableTOTP(ctx context.Context) (err error) { - au.User.TOTPEnabled = false + au.User.SetTOTPEnabled(false) au.User.TOTPURL = "" return au.Save(ctx) } @@ -209,14 +211,14 @@ func (au *AuthUser) SetPassword(ctx context.Context, password []byte) (err error if err != nil { return err } - au.User.Enabled = true + au.User.SetEnabled(true) return au.Save(ctx) } // UnsetPassword removes the password from this user, and disables them func (au *AuthUser) UnsetPassword(ctx context.Context) error { au.User.PasswordHash = nil - au.User.Enabled = false + au.User.SetEnabled(false) return au.Save(ctx) } @@ -230,7 +232,7 @@ func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error { if au == nil { return ErrNoUser } - if !au.User.Enabled { + if !au.User.IsEnabled() { return ErrUserDisabled } @@ -254,14 +256,14 @@ func (au *AuthUser) CheckCredentials(ctx context.Context, password []byte, passc // MakeAdmin makes this user an admin, and saves the update in the database. // If the user is already an admin, does not return an error. func (au *AuthUser) MakeAdmin(ctx context.Context) error { - au.User.Admin = true + au.User.SetAdmin(true) return au.Save(ctx) } // MakeRegular removes admin rights from this user. // If this user is not an dmin, does not return an error. func (au *AuthUser) MakeRegular(ctx context.Context) error { - au.User.Admin = true + au.User.SetAdmin(false) return au.Save(ctx) } @@ -271,7 +273,7 @@ func (au *AuthUser) Save(ctx context.Context) error { if err != nil { return err } - return table.Save(&au.User).Error + return table.Select("*").Updates(&au.User).Error } // Delete deletes the user from the database diff --git a/internal/dis/component/control/admin/admin.go b/internal/dis/component/control/admin/admin.go index b6bd97b..35c8bb2 100644 --- a/internal/dis/component/control/admin/admin.go +++ b/internal/dis/component/control/admin/admin.go @@ -9,6 +9,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger" "github.com/julienschmidt/httprouter" + "github.com/rs/zerolog" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" @@ -48,6 +49,7 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http Handler: admin.serveSocket, } handler = admin.Dependencies.Auth.Protect(socket, auth.Admin) + handler = admin.Dependencies.Auth.CSRF()(handler) } // handle everything @@ -61,6 +63,26 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http Template: indexTemplate, }) + // add a handler for the user page + router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{ + Handler: admin.users, + Template: userTemplate, + }) + + // add a user create form + { + create := admin.createUser(ctx) + router.Handler(http.MethodGet, route+"users/create", create) + router.Handler(http.MethodPost, route+"users/create", create) + } + + // add all the admin actions + router.Handler(http.MethodPost, route+"users/delete", admin.usersDeleteHandler(ctx)) + router.Handler(http.MethodPost, route+"users/disable", admin.usersDisableHandler(ctx)) + router.Handler(http.MethodPost, route+"users/disabletotp", admin.usersDisableTOTPHandler(ctx)) + router.Handler(http.MethodPost, route+"users/password", admin.usersPasswordHandler(ctx)) + router.Handler(http.MethodPost, route+"users/toggleadmin", admin.usersToggleAdmin(ctx)) + // add a handler for the component page router.Handler(http.MethodGet, route+"components", httpx.HTMLHandler[componentContext]{ Handler: admin.components, @@ -79,9 +101,19 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http Template: instanceTemplate, }) - router.Handler(http.MethodPost, route+"api/login", httpx.RedirectHandler(func(r *http.Request) (string, int, error) { + // add a router for the login page + router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx)) + + return +} + +func (admin *Admin) loginHandler(ctx context.Context) http.Handler { + logger := zerolog.Ctx(ctx) + + return httpx.RedirectHandler(func(r *http.Request) (string, int, error) { // parse the form if err := r.ParseForm(); err != nil { + logger.Err(err).Msg("failed to parse admin login") return "", 0, err } @@ -93,10 +125,9 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http target, err := instance.Users().Login(r.Context(), nil, r.PostFormValue("user")) if err != nil { + logger.Err(err).Msg("failed to admin login") return "", 0, err } return target.String(), http.StatusSeeOther, err - })) - - return + }) } diff --git a/internal/dis/component/control/admin/html/index.html b/internal/dis/component/control/admin/html/index.html index 4909062..8472c44 100644 --- a/internal/dis/component/control/admin/html/index.html +++ b/internal/dis/component/control/admin/html/index.html @@ -6,7 +6,10 @@ Admin

- Components +

{{ end }} diff --git a/internal/dis/component/control/admin/html/instance.html b/internal/dis/component/control/admin/html/instance.html index 4fed81f..167120a 100644 --- a/internal/dis/component/control/admin/html/instance.html +++ b/internal/dis/component/control/admin/html/instance.html @@ -7,7 +7,9 @@ Instance

- Ingredients +

{{ end }} @@ -216,6 +218,7 @@ {{ $slug := .Instance.Slug }} + {{ $csrf := .CSRF }} {{ range $index, $user := .Info.Users }} @@ -247,10 +250,11 @@ {{ $user.Login.Time.Format "2006-01-02T15:04:05Z07:00" }} -
+ + {{ $csrf }}
diff --git a/internal/dis/component/control/admin/html/user_create.html b/internal/dis/component/control/admin/html/user_create.html new file mode 100644 index 0000000..91b8fb6 --- /dev/null +++ b/internal/dis/component/control/admin/html/user_create.html @@ -0,0 +1,11 @@ +{{ template "_form.html" . }} +{{ define "form/title" }}Distillery Admin - Create User{{ end }} +{{ define "form/button" }}Create{{ end }} + +{{ define "header"}} +

+ Control > + Users > + Create +

+{{ end }} diff --git a/internal/dis/component/control/admin/html/users.html b/internal/dis/component/control/admin/html/users.html new file mode 100644 index 0000000..0957103 --- /dev/null +++ b/internal/dis/component/control/admin/html/users.html @@ -0,0 +1,95 @@ +{{ template "_base.html" . }} +{{ define "title" }}Distillery Admin - Users{{ end }} + +{{ define "header"}} +

+ Control > + Users +

+

+

+

+{{ end }} + +{{ define "content" }} +
+
+
+ + + + + + + + + + + + {{ $csrf := .CSRF }} + {{ range .Users }} + + + + + + + + {{ end }} + +
+ Username + + Enabled + + Admin + + Passcode (TOTP) + + Actions +
+ {{ .User.User }} + + {{ .User.IsEnabled }} + + + {{ .User.IsAdmin }} + + {{ .User.IsTOTPEnabled }} + +
+
+ + + {{ $csrf }} +
+
+ +   + + {{ $csrf }} +
+
+ + + {{ $csrf }} +
+
+ + + {{ $csrf }} +
+
+ + + {{ $csrf }} +
+
+
+
+
+
+ +{{ end }} \ No newline at end of file diff --git a/internal/dis/component/control/admin/instance.go b/internal/dis/component/control/admin/instance.go index 4a3a98c..cb2d4b8 100644 --- a/internal/dis/component/control/admin/instance.go +++ b/internal/dis/component/control/admin/instance.go @@ -2,6 +2,7 @@ package admin import ( _ "embed" + "html/template" "net/http" "time" @@ -10,6 +11,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/gorilla/csrf" "github.com/gorilla/mux" ) @@ -23,11 +25,14 @@ var instanceTemplate = static.AssetsAdmin.MustParseShared( type instanceContext struct { Time time.Time + CSRF template.HTML Instance models.Instance Info status.WissKI } func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) { + is.CSRF = csrf.TemplateField(r) + // find the instance itself! instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["slug"]) if err == instances.ErrWissKINotFound { diff --git a/internal/dis/component/control/admin/users.go b/internal/dis/component/control/admin/users.go new file mode 100644 index 0000000..68ce422 --- /dev/null +++ b/internal/dis/component/control/admin/users.go @@ -0,0 +1,190 @@ +package admin + +import ( + "context" + "errors" + "html/template" + "net/http" + "time" + + _ "embed" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/gorilla/csrf" + "github.com/rs/zerolog" +) + +//go:embed "html/users.html" +var userTemplateString string +var userTemplate = static.AssetsAdmin.MustParseShared( + "users.html", + userTemplateString, +) + +type userContext struct { + Time time.Time + + CSRF template.HTML + Users []*auth.AuthUser +} + +func (admin *Admin) users(r *http.Request) (uc userContext, err error) { + uc.CSRF = csrf.TemplateField(r) + uc.Time = time.Now() + uc.Users, err = admin.Dependencies.Auth.Users(r.Context()) + return +} + +//go:embed "html/user_create.html" +var userCreateTemplateString string +var userCreateTemplate = static.AssetsAdmin.MustParseShared( + "user_create.html", + userCreateTemplateString, +) + +var ( + errCreateInvalidUsername = errors.New("invalid username") + errCreateInvalidPassword = errors.New("invalid password") +) + +type createUserResult struct { + User string + Passsword string + Admin bool +} + +func (admin *Admin) createUser(ctx context.Context) http.Handler { + return &httpx.Form[createUserResult]{ + Fields: []httpx.Field{ + {Name: "username", Type: httpx.TextField, Label: "Username"}, + {Name: "password", Type: httpx.PasswordField, Label: "Password"}, + {Name: "admin", Type: httpx.CheckboxField, Label: "Distillery Administrator"}, + }, + FieldTemplate: httpx.PureCSSFieldTemplate, + + CSRF: admin.Dependencies.Auth.CSRF(), + + RenderTemplate: userCreateTemplate, + + Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) { + cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == "on" + + if cu.User == "" { + return cu, errCreateInvalidUsername + } + if cu.Passsword == "" { + return cu, errCreateInvalidPassword + } + + return cu, nil + }, + + RenderSuccess: func(cu createUserResult, values map[string]string, w http.ResponseWriter, r *http.Request) error { + // create the user + user, err := admin.Dependencies.Auth.CreateUser(r.Context(), cu.User) + if err != nil { + return err + } + + // disable the user and setup the admin flag + user.SetAdmin(cu.Admin) + if err := user.Save(r.Context()); err != nil { + return err + } + + // set the password! + err = user.SetPassword(r.Context(), []byte(cu.Passsword)) + if err != nil { + return err + } + + // everything went fine, redirect the user back to the user page! + http.Redirect(w, r, "/admin/users/", http.StatusSeeOther) + return nil + }, + } +} + +var errNotCurrentUser = httpx.Response{ + Body: []byte("attempt to modify current user"), + StatusCode: http.StatusBadRequest, +} + +func (admin *Admin) useraction(ctx context.Context, name string, action func(r *http.Request, user *auth.AuthUser) error) http.Handler { + logger := zerolog.Ctx(ctx) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + logger.Err(err).Str("action", name).Msg("failed to parse form") + httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) + return + } + + username := r.PostFormValue("user") + user, err := admin.Dependencies.Auth.User(r.Context(), username) + if err != nil { + logger.Err(err).Str("action", name).Msg("failed to get user") + httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) + return + } + + me, err := admin.Dependencies.Auth.UserOf(r) + if err != nil { + logger.Err(err).Str("action", name).Msg("failed to get current user") + httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) + return + } + + // don't allow the current user + if me.User.User == user.User.User { + errNotCurrentUser.ServeHTTP(w, r) + return + } + + if err := action(r, user); err != nil { + logger.Err(err).Str("action", name).Msg("failed to act on user") + httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) + return + } + + http.Redirect(w, r, "/admin/users/", http.StatusSeeOther) + }) +} + +func (admin *Admin) usersDeleteHandler(ctx context.Context) http.Handler { + return admin.useraction(ctx, "delete user", func(r *http.Request, user *auth.AuthUser) error { + return user.Delete(r.Context()) + }) +} + +func (admin *Admin) usersDisableHandler(ctx context.Context) http.Handler { + return admin.useraction(ctx, "disable user", func(r *http.Request, user *auth.AuthUser) error { + return user.UnsetPassword(r.Context()) + }) +} + +func (admin *Admin) usersDisableTOTPHandler(ctx context.Context) http.Handler { + return admin.useraction(ctx, "disable user totp", func(r *http.Request, user *auth.AuthUser) error { + return user.DisableTOTP(r.Context()) + }) +} + +func (admin *Admin) usersToggleAdmin(ctx context.Context) http.Handler { + return admin.useraction(ctx, "toggle admin", func(r *http.Request, user *auth.AuthUser) error { + if user.IsAdmin() { + return user.MakeRegular(r.Context()) + } + return user.MakeAdmin(r.Context()) + }) +} + +func (admin *Admin) usersPasswordHandler(ctx context.Context) http.Handler { + return admin.useraction(ctx, "set password", func(r *http.Request, user *auth.AuthUser) error { + password := r.PostFormValue("password") + if password == "" { + return httpx.ErrBadRequest + } + return user.SetPassword(r.Context(), []byte(password)) + }) +} diff --git a/internal/dis/component/control/static/assets_dist.go b/internal/dis/component/control/static/assets_dist.go index af6c995..7949dbc 100644 --- a/internal/dis/component/control/static/assets_dist.go +++ b/internal/dis/component/control/static/assets_dist.go @@ -5,17 +5,17 @@ package static // AssetsHome contains assets for the 'Home' entrypoint. var AssetsHome = Assets{ Scripts: ``, - Styles: ``, + Styles: ``, } // AssetsUser contains assets for the 'User' entrypoint. var AssetsUser = Assets{ Scripts: ``, - Styles: ``, + Styles: ``, } // AssetsAdmin contains assets for the 'Admin' entrypoint. var AssetsAdmin = Assets{ Scripts: ``, - Styles: ``, + Styles: ``, } diff --git a/internal/dis/component/control/static/dist/Home.4ec77c43.css b/internal/dis/component/control/static/dist/Home.9f00501f.css similarity index 98% rename from internal/dis/component/control/static/dist/Home.4ec77c43.css rename to internal/dis/component/control/static/dist/Home.9f00501f.css index 38eb875..4a8baf4 100644 --- a/internal/dis/component/control/static/dist/Home.4ec77c43.css +++ b/internal/dis/component/control/static/dist/Home.9f00501f.css @@ -1 +1 @@ -html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{text-decoration:underline;border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;max-width:100%;white-space:normal;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template,[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;flex-flow:wrap;align-content:flex-start;font-family:FreeSans,Arimo,Droid Sans,Helvetica,Arial,sans-serif;display:flex}@media (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-5-12,.pure-u-10-24{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-7-12,.pure-u-14-24{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-5-8,.pure-u-15-24{width:62.5%}.pure-u-2-3,.pure-u-16-24{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-3-4,.pure-u-18-24{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-5-6,.pure-u-20-24{width:83.3333%}.pure-u-7-8,.pure-u-21-24{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box;line-height:normal;display:inline-block}.pure-button::-moz-focus-inner{border:0;padding:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{color:#000c;background-color:#e6e6e6;border:#0000;border-radius:2px;padding:.5em 1em;font-family:inherit;font-size:100%;text-decoration:none}.pure-button-hover,.pure-button:hover,.pure-button:focus{background-image:linear-gradient(#0000,#0000000d 40%,#0000001a)}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{border-color:#000;box-shadow:inset 0 0 0 1px #00000026,inset 0 0 6px #0003}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none;background-image:none;border:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{color:#fff;background-color:#0078e7}.pure-button-group .pure-button{border-right:1px solid #0003;border-radius:0;margin:0}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-right:none;border-top-right-radius:2px;border-bottom-right-radius:2px}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{vertical-align:middle;box-sizing:border-box;border:1px solid #ccc;border-radius:4px;padding:.5em .6em;display:inline-block;box-shadow:inset 0 1px 3px #ddd}.pure-form input:not([type]){box-sizing:border-box;border:1px solid #ccc;border-radius:4px;padding:.5em .6em;display:inline-block;box-shadow:inset 0 1px 3px #ddd}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus,.pure-form input:not([type]):focus{border-color:#129fea;outline:0}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled],.pure-form input:not([type])[disabled]{cursor:not-allowed;color:#cad2d3;background-color:#eaeded}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{color:#777;background-color:#eee;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;background-color:#fff;border:1px solid #ccc}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{border:0;margin:0;padding:.35em 0 .75em}.pure-form legend{width:100%;color:#333;border-bottom:1px solid #e5e5e5;margin-bottom:.3em;padding:.3em 0;display:block}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea,.pure-form-stacked input:not([type]){margin:.25em 0;display:block}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-message-inline{vertical-align:middle;display:inline-block}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;vertical-align:middle;width:10em;margin:0 1em 0 0;display:inline-block}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{border-radius:0;margin:0 0 -1px;padding:10px;display:block;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{border-radius:4px 4px 0 0;margin:0;top:1px}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{border-radius:4px;margin:0;top:1px}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{border-radius:0 0 4px 4px;margin:0;top:-2px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{color:#666;vertical-align:middle;padding-left:.3em;font-size:.875em;display:inline-block}.pure-form-message{color:#666;font-size:.875em;display:block}@media only screen and (max-width:480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{text-align:left;width:100%;margin-bottom:.3em;display:block}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form-message-inline,.pure-form-message{padding:.2em 0 .8em;font-size:.75em;display:block}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{z-index:3;position:fixed;top:0;left:0}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{margin:0;padding:0;list-style:none}.pure-menu-item{height:100%;margin:0;padding:0}.pure-menu-link,.pure-menu-heading{white-space:nowrap;text-decoration:none;display:block}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{vertical-align:middle;display:inline-block}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{z-index:3;margin:0;padding:0;display:none;position:absolute;top:0;left:100%}.pure-menu-horizontal .pure-menu-children{width:inherit;top:auto;left:0}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{content:"▸";padding-left:.5em;font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"▾"}.pure-menu-scrollable{overflow-x:hidden;overflow-y:scroll}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;padding:.5em 0;overflow-x:auto;overflow-y:hidden}.pure-menu-separator,.pure-menu-horizontal .pure-menu-children .pure-menu-separator{height:1px;background-color:#ccc;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{width:auto;display:block}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{cursor:default;background-color:#0000}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;text-align:center;padding:1em 0;font:italic 85%/1 arial,sans-serif}.pure-table td,.pure-table th{font-size:inherit;border-width:0 0 0 1px;border-left-style:solid;border-left-color:#cbcbcb;margin:0;padding:.5em 1em;overflow:visible}.pure-table thead{color:#000;text-align:left;vertical-align:bottom;background-color:#e0e0e0}.pure-table td{background-color:#0000}.pure-table-odd td,.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom-style:solid;border-bottom-color:#cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}@media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}}@media screen and (min-width:120em){.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-1-2,.pure-u-xxl-1-3,.pure-u-xxl-2-3,.pure-u-xxl-1-4,.pure-u-xxl-3-4,.pure-u-xxl-1-5,.pure-u-xxl-2-5,.pure-u-xxl-3-5,.pure-u-xxl-4-5,.pure-u-xxl-5-5,.pure-u-xxl-1-6,.pure-u-xxl-5-6,.pure-u-xxl-1-8,.pure-u-xxl-3-8,.pure-u-xxl-5-8,.pure-u-xxl-7-8,.pure-u-xxl-1-12,.pure-u-xxl-5-12,.pure-u-xxl-7-12,.pure-u-xxl-11-12,.pure-u-xxl-1-24,.pure-u-xxl-2-24,.pure-u-xxl-3-24,.pure-u-xxl-4-24,.pure-u-xxl-5-24,.pure-u-xxl-6-24,.pure-u-xxl-7-24,.pure-u-xxl-8-24,.pure-u-xxl-9-24,.pure-u-xxl-10-24,.pure-u-xxl-11-24,.pure-u-xxl-12-24,.pure-u-xxl-13-24,.pure-u-xxl-14-24,.pure-u-xxl-15-24,.pure-u-xxl-16-24,.pure-u-xxl-17-24,.pure-u-xxl-18-24,.pure-u-xxl-19-24,.pure-u-xxl-20-24,.pure-u-xxl-21-24,.pure-u-xxl-22-24,.pure-u-xxl-23-24,.pure-u-xxl-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-xxl-1-24{width:4.1667%}.pure-u-xxl-1-12,.pure-u-xxl-2-24{width:8.3333%}.pure-u-xxl-1-8,.pure-u-xxl-3-24{width:12.5%}.pure-u-xxl-1-6,.pure-u-xxl-4-24{width:16.6667%}.pure-u-xxl-1-5{width:20%}.pure-u-xxl-5-24{width:20.8333%}.pure-u-xxl-1-4,.pure-u-xxl-6-24{width:25%}.pure-u-xxl-7-24{width:29.1667%}.pure-u-xxl-1-3,.pure-u-xxl-8-24{width:33.3333%}.pure-u-xxl-3-8,.pure-u-xxl-9-24{width:37.5%}.pure-u-xxl-2-5{width:40%}.pure-u-xxl-5-12,.pure-u-xxl-10-24{width:41.6667%}.pure-u-xxl-11-24{width:45.8333%}.pure-u-xxl-1-2,.pure-u-xxl-12-24{width:50%}.pure-u-xxl-13-24{width:54.1667%}.pure-u-xxl-7-12,.pure-u-xxl-14-24{width:58.3333%}.pure-u-xxl-3-5{width:60%}.pure-u-xxl-5-8,.pure-u-xxl-15-24{width:62.5%}.pure-u-xxl-2-3,.pure-u-xxl-16-24{width:66.6667%}.pure-u-xxl-17-24{width:70.8333%}.pure-u-xxl-3-4,.pure-u-xxl-18-24{width:75%}.pure-u-xxl-19-24{width:79.1667%}.pure-u-xxl-4-5{width:80%}.pure-u-xxl-5-6,.pure-u-xxl-20-24{width:83.3333%}.pure-u-xxl-7-8,.pure-u-xxl-21-24{width:87.5%}.pure-u-xxl-11-12,.pure-u-xxl-22-24{width:91.6667%}.pure-u-xxl-23-24{width:95.8333%}.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-5-5,.pure-u-xxl-24-24{width:100%}}body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}header,main,footer{margin:2em}.padding{padding:1em}.overflow{overflow-x:auto}.overflow table{width:100%}.overflow table td,.overflow table th{padding:.5em}.overflow table td:not(:last-child),.overflow table th:not(:last-child){width:1px;text-align:left;white-space:nowrap}.overflow table td:last-child,.overflow table th:last-child{white-space:nowrap}.hspace{height:1em;display:block}.pure-button-action{background-color:#42b8dd!important}.pure-button-success{background-color:#1cb841!important}.pure-button-xsmall{font-size:70%}.pure-button-small{font-size:85%}.pure-button-large{font-size:110%}.pure-button-xlarge{font-size:125%}.error-message{color:red;background-color:pink;border:1px solid red;padding:2px} \ No newline at end of file +html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{text-decoration:underline;border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;max-width:100%;white-space:normal;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template,[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;flex-flow:wrap;align-content:flex-start;font-family:FreeSans,Arimo,Droid Sans,Helvetica,Arial,sans-serif;display:flex}@media (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-5-12,.pure-u-10-24{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-7-12,.pure-u-14-24{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-5-8,.pure-u-15-24{width:62.5%}.pure-u-2-3,.pure-u-16-24{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-3-4,.pure-u-18-24{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-5-6,.pure-u-20-24{width:83.3333%}.pure-u-7-8,.pure-u-21-24{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box;line-height:normal;display:inline-block}.pure-button::-moz-focus-inner{border:0;padding:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{color:#000c;background-color:#e6e6e6;border:#0000;border-radius:2px;padding:.5em 1em;font-family:inherit;font-size:100%;text-decoration:none}.pure-button-hover,.pure-button:hover,.pure-button:focus{background-image:linear-gradient(#0000,#0000000d 40%,#0000001a)}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{border-color:#000;box-shadow:inset 0 0 0 1px #00000026,inset 0 0 6px #0003}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none;background-image:none;border:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{color:#fff;background-color:#0078e7}.pure-button-group .pure-button{border-right:1px solid #0003;border-radius:0;margin:0}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-right:none;border-top-right-radius:2px;border-bottom-right-radius:2px}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{vertical-align:middle;box-sizing:border-box;border:1px solid #ccc;border-radius:4px;padding:.5em .6em;display:inline-block;box-shadow:inset 0 1px 3px #ddd}.pure-form input:not([type]){box-sizing:border-box;border:1px solid #ccc;border-radius:4px;padding:.5em .6em;display:inline-block;box-shadow:inset 0 1px 3px #ddd}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus,.pure-form input:not([type]):focus{border-color:#129fea;outline:0}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled],.pure-form input:not([type])[disabled]{cursor:not-allowed;color:#cad2d3;background-color:#eaeded}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{color:#777;background-color:#eee;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;background-color:#fff;border:1px solid #ccc}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{border:0;margin:0;padding:.35em 0 .75em}.pure-form legend{width:100%;color:#333;border-bottom:1px solid #e5e5e5;margin-bottom:.3em;padding:.3em 0;display:block}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea,.pure-form-stacked input:not([type]){margin:.25em 0;display:block}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-message-inline{vertical-align:middle;display:inline-block}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;vertical-align:middle;width:10em;margin:0 1em 0 0;display:inline-block}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{border-radius:0;margin:0 0 -1px;padding:10px;display:block;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{border-radius:4px 4px 0 0;margin:0;top:1px}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{border-radius:4px;margin:0;top:1px}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{border-radius:0 0 4px 4px;margin:0;top:-2px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{color:#666;vertical-align:middle;padding-left:.3em;font-size:.875em;display:inline-block}.pure-form-message{color:#666;font-size:.875em;display:block}@media only screen and (max-width:480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{text-align:left;width:100%;margin-bottom:.3em;display:block}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form-message-inline,.pure-form-message{padding:.2em 0 .8em;font-size:.75em;display:block}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{z-index:3;position:fixed;top:0;left:0}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{margin:0;padding:0;list-style:none}.pure-menu-item{height:100%;margin:0;padding:0}.pure-menu-link,.pure-menu-heading{white-space:nowrap;text-decoration:none;display:block}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{vertical-align:middle;display:inline-block}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{z-index:3;margin:0;padding:0;display:none;position:absolute;top:0;left:100%}.pure-menu-horizontal .pure-menu-children{width:inherit;top:auto;left:0}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{content:"▸";padding-left:.5em;font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"▾"}.pure-menu-scrollable{overflow-x:hidden;overflow-y:scroll}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;padding:.5em 0;overflow-x:auto;overflow-y:hidden}.pure-menu-separator,.pure-menu-horizontal .pure-menu-children .pure-menu-separator{height:1px;background-color:#ccc;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{width:auto;display:block}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{cursor:default;background-color:#0000}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;text-align:center;padding:1em 0;font:italic 85%/1 arial,sans-serif}.pure-table td,.pure-table th{font-size:inherit;border-width:0 0 0 1px;border-left-style:solid;border-left-color:#cbcbcb;margin:0;padding:.5em 1em;overflow:visible}.pure-table thead{color:#000;text-align:left;vertical-align:bottom;background-color:#e0e0e0}.pure-table td{background-color:#0000}.pure-table-odd td,.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom-style:solid;border-bottom-color:#cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}@media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}}@media screen and (min-width:120em){.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-1-2,.pure-u-xxl-1-3,.pure-u-xxl-2-3,.pure-u-xxl-1-4,.pure-u-xxl-3-4,.pure-u-xxl-1-5,.pure-u-xxl-2-5,.pure-u-xxl-3-5,.pure-u-xxl-4-5,.pure-u-xxl-5-5,.pure-u-xxl-1-6,.pure-u-xxl-5-6,.pure-u-xxl-1-8,.pure-u-xxl-3-8,.pure-u-xxl-5-8,.pure-u-xxl-7-8,.pure-u-xxl-1-12,.pure-u-xxl-5-12,.pure-u-xxl-7-12,.pure-u-xxl-11-12,.pure-u-xxl-1-24,.pure-u-xxl-2-24,.pure-u-xxl-3-24,.pure-u-xxl-4-24,.pure-u-xxl-5-24,.pure-u-xxl-6-24,.pure-u-xxl-7-24,.pure-u-xxl-8-24,.pure-u-xxl-9-24,.pure-u-xxl-10-24,.pure-u-xxl-11-24,.pure-u-xxl-12-24,.pure-u-xxl-13-24,.pure-u-xxl-14-24,.pure-u-xxl-15-24,.pure-u-xxl-16-24,.pure-u-xxl-17-24,.pure-u-xxl-18-24,.pure-u-xxl-19-24,.pure-u-xxl-20-24,.pure-u-xxl-21-24,.pure-u-xxl-22-24,.pure-u-xxl-23-24,.pure-u-xxl-24-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block}.pure-u-xxl-1-24{width:4.1667%}.pure-u-xxl-1-12,.pure-u-xxl-2-24{width:8.3333%}.pure-u-xxl-1-8,.pure-u-xxl-3-24{width:12.5%}.pure-u-xxl-1-6,.pure-u-xxl-4-24{width:16.6667%}.pure-u-xxl-1-5{width:20%}.pure-u-xxl-5-24{width:20.8333%}.pure-u-xxl-1-4,.pure-u-xxl-6-24{width:25%}.pure-u-xxl-7-24{width:29.1667%}.pure-u-xxl-1-3,.pure-u-xxl-8-24{width:33.3333%}.pure-u-xxl-3-8,.pure-u-xxl-9-24{width:37.5%}.pure-u-xxl-2-5{width:40%}.pure-u-xxl-5-12,.pure-u-xxl-10-24{width:41.6667%}.pure-u-xxl-11-24{width:45.8333%}.pure-u-xxl-1-2,.pure-u-xxl-12-24{width:50%}.pure-u-xxl-13-24{width:54.1667%}.pure-u-xxl-7-12,.pure-u-xxl-14-24{width:58.3333%}.pure-u-xxl-3-5{width:60%}.pure-u-xxl-5-8,.pure-u-xxl-15-24{width:62.5%}.pure-u-xxl-2-3,.pure-u-xxl-16-24{width:66.6667%}.pure-u-xxl-17-24{width:70.8333%}.pure-u-xxl-3-4,.pure-u-xxl-18-24{width:75%}.pure-u-xxl-19-24{width:79.1667%}.pure-u-xxl-4-5{width:80%}.pure-u-xxl-5-6,.pure-u-xxl-20-24{width:83.3333%}.pure-u-xxl-7-8,.pure-u-xxl-21-24{width:87.5%}.pure-u-xxl-11-12,.pure-u-xxl-22-24{width:91.6667%}.pure-u-xxl-23-24{width:95.8333%}.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-5-5,.pure-u-xxl-24-24{width:100%}}body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}header,main,footer{margin:2em}.padding{padding:1em}.overflow{overflow-x:auto}.overflow table{width:100%}.overflow table td,.overflow table th{padding:.5em}.overflow table td:not(:last-child),.overflow table th:not(:last-child){width:1px;text-align:left;white-space:nowrap}.overflow table td:last-child,.overflow table th:last-child{white-space:nowrap}.hspace{height:1em;display:block}.pure-form-group{display:inline}.pure-button-action{background-color:#42b8dd!important}.pure-button-success{background-color:#1cb841!important}.pure-button-danger{background:#ca3c3c!important}.pure-button-warning{background:#df7514!important}.pure-button-xsmall{font-size:70%}.pure-button-small{font-size:85%}.pure-button-large{font-size:110%}.pure-button-xlarge{font-size:125%}.error-message{color:red;background-color:pink;border:1px solid red;padding:2px} \ No newline at end of file diff --git a/internal/dis/component/control/static/src/base/index.css b/internal/dis/component/control/static/src/base/index.css index f5660ce..73361a0 100644 --- a/internal/dis/component/control/static/src/base/index.css +++ b/internal/dis/component/control/static/src/base/index.css @@ -21,7 +21,7 @@ footer { } .overflow table td, -.overflow table th{ +.overflow table th { padding: .5em .5em; } @@ -41,14 +41,26 @@ footer { display: block; height: 1em; } +.pure-form-group { + display: inline; +} .pure-button-action { background-color: rgb(66, 184, 221) !important; } + .pure-button-success { background-color: rgb(28, 184, 65) !important; } +.pure-button-danger { + background: rgb(202, 60, 60) !important; +} + +.pure-button-warning { + background: rgb(223, 117, 20) !important; +} + .pure-button-xsmall { font-size: 70%; } @@ -70,4 +82,4 @@ footer { border: 1px solid red; padding: 2px; color: red; -} +} \ No newline at end of file diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..8d2fe58 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,2 @@ +// Package contains all database models +package models diff --git a/internal/models/user.go b/internal/models/user.go index 7973fd5..8c80215 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -10,9 +10,33 @@ type User struct { User string `gorm:"column:user;not null;unique"` // name of the user PasswordHash []byte `gorm:"column:password"` // password of the user, hashed - TOTPEnabled bool `gorm:"column:totpenabled"` // is totp enabled for the user + TOTPEnabled *bool `gorm:"column:totpenabled"` // is totp enabled for the user TOTPURL string `gorm:"column:totp"` // the totp of the user - Enabled bool `gorm:"enabled;not null"` - Admin bool `gorm:"column:admin;not null"` + Enabled *bool `gorm:"enabled;not null"` + Admin *bool `gorm:"column:admin;not null"` +} + +func (user *User) IsAdmin() bool { + return user.Admin != nil && *user.Admin +} + +func (user *User) SetAdmin(v bool) { + user.Admin = &v +} + +func (user *User) IsEnabled() bool { + return user.Enabled != nil && *user.Enabled +} + +func (user *User) SetEnabled(v bool) { + user.Enabled = &v +} + +func (user *User) IsTOTPEnabled() bool { + return user.TOTPEnabled != nil && *user.TOTPEnabled +} + +func (user *User) SetTOTPEnabled(v bool) { + user.TOTPEnabled = &v } diff --git a/internal/wisski/ingredient/bookkeeping/bookkeeping.go b/internal/wisski/ingredient/bookkeeping/bookkeeping.go index 2cb30b3..3e380b8 100644 --- a/internal/wisski/ingredient/bookkeeping/bookkeeping.go +++ b/internal/wisski/ingredient/bookkeeping/bookkeeping.go @@ -25,7 +25,7 @@ func (bk *Bookkeeping) Save(ctx context.Context) error { } // Update based on the primary key! - return sdb.Where("pk = ?", bk.Instance.Pk).Updates(&bk.Instance).Error + return sdb.Select("*").Save(&bk.Instance).Error } // Delete deletes this instance from the bookkeeping table diff --git a/pkg/httpx/errors.go b/pkg/httpx/errors.go index 9c3e33c..61c7048 100644 --- a/pkg/httpx/errors.go +++ b/pkg/httpx/errors.go @@ -49,6 +49,7 @@ func StatusInterceptor(contentType string, body func(code int, text string) ([]b return ErrInterceptor{ Errors: map[error]Response{ + ErrBadRequest: makeResponse(http.StatusBadRequest), ErrNotFound: makeResponse(http.StatusNotFound), ErrForbidden: makeResponse(http.StatusForbidden), ErrMethodNotAllowed: makeResponse(http.StatusMethodNotAllowed), @@ -59,6 +60,7 @@ func StatusInterceptor(contentType string, body func(code int, text string) ([]b // Common errors accepted by all httpx handlers var ( + ErrBadRequest = errors.New("httpx: Bad Request") ErrNotFound = errors.New("httpx: Not Found") ErrForbidden = errors.New("httpx: Forbidden") ErrMethodNotAllowed = errors.New("httpx: Method Not Allowed") diff --git a/pkg/httpx/form.go b/pkg/httpx/form.go index c6269b9..a90ff4a 100644 --- a/pkg/httpx/form.go +++ b/pkg/httpx/form.go @@ -224,4 +224,5 @@ type InputType string const ( TextField InputType = "text" PasswordField InputType = "password" + CheckboxField InputType = "checkbox" )