auth: Refactor home page

This commit is contained in:
Tom Wiesing 2023-01-02 14:23:10 +01:00
parent 2d5b92f464
commit b8f1281f78
No known key found for this signature in database
14 changed files with 222 additions and 129 deletions

File diff suppressed because one or more lines are too long

View file

@ -2,16 +2,76 @@ package auth
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
) )
// Permission represents a permission for a user // Permission represents a permission granted to a user.
// //
// The nil permission represents any authenticated user. // The nil permission represents any authenticated user.
type Permission func(user *AuthUser, r *http.Request) (ok bool, err error) type Permission func(user *AuthUser, r *http.Request) (ok Grant, err error)
// Grant represents an object that either grants or denies access for a certain permission
type Grant interface {
isGranted()
// Granted returns a boolean indicating if permission to the resource in question
// has been granted
Granted() bool
// Denied returns a string containing an error message to display to the user when permission is denied.
// When Granted() returns true, the behaviour is undefined.
Denied() string
}
// Bool2Grant returns a new grant that returns granted for the given boolean, and message as the denied message.
func Bool2Grant(granted bool, message string) Grant {
if granted {
return grantAllow{}
}
return grantDeny(message)
}
type grantAllow struct{}
func (grantAllow) isGranted() {}
func (grantAllow) Granted() bool { return true }
func (grantAllow) Denied() string { return "" }
type grantDeny string
func (grantDeny) isGranted() {}
func (g grantDeny) Granted() bool { return false }
func (g grantDeny) Denied() string {
if g == "" {
return "Forbidden"
}
return string(g)
}
var errPermissionPanic = errors.New("permission: panic()")
// Permit checks if the given user has this permission.
func (perm Permission) Permit(user *AuthUser, r *http.Request) (ok Grant, err error) {
// if there is no permission, then we just check if there is some user
if perm == nil {
return Bool2Grant(user != nil, ""), nil
}
// recover any panic()ed permission call
// to prevent the handler from panic()ing
defer func() {
if p := recover(); p != nil {
ok = Bool2Grant(false, "unknown error")
err = errPermissionPanic
}
}()
return perm(user, r)
}
// Protect returns a new handler which requires a user to be logged in and pass the perm function. // Protect returns a new handler which requires a user to be logged in and pass the perm function.
// //
@ -20,14 +80,15 @@ type Permission func(user *AuthUser, r *http.Request) (ok bool, err error)
// If an authenticated calls the endpoint, and they have the given permissions, the original handler is called. // If an authenticated calls the endpoint, and they have the given permissions, the original handler is called.
func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler { func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var grant Grant
// load the user in the session // load the user in the session
user, err := auth.UserOf(r) user, err := auth.UserOf(r)
if err != nil { if err != nil {
goto err goto err
} }
// if there is no user in the session // if there is no user in the session, they need to login first!
// we need to login the user
if user == nil { if user == nil {
// we can't redirect anything other than GET // we can't redirect anything other than GET
// (because it might be a form) // (because it might be a form)
@ -42,31 +103,41 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
return return
} }
// if we have a permission check, we need to call it {
// to find out if the user is actually allowed to access the page var err error
if perm != nil { // call the permission check
ok, err := perm(user, r) grant, err = perm.Permit(user, r)
if err != nil { if err != nil {
goto err goto err
} }
if !ok { if !grant.Granted() {
goto forbidden goto forbidden
} }
} }
// store the user into the session // store the user into the session, and then return the new session
r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user)) r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user))
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
return return
forbidden: forbidden:
httpx.HTMLInterceptor.Intercept(w, r, httpx.ErrForbidden) {
return message := "Forbidden"
if grant != nil {
message = grant.Denied()
}
httpx.Response{
ContentType: "text/plain",
StatusCode: http.StatusForbidden,
Body: []byte(message),
}.ServeHTTP(w, r)
return
}
err: err:
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) httpx.TextInterceptor.Fallback.ServeHTTP(w, r)
}) })
} }
// Admin represents a permission that checks if a user is an administrator and has totp enabled. // 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 bool, err error) { var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) {
return user != nil && user.Admin && user.TOTPEnabled, nil return Bool2Grant(user != nil && user.Admin && user.TOTPEnabled, "user needs to have admin permissions and TOTP enabled"), nil
} }

View file

@ -4,7 +4,6 @@ import (
"context" "context"
_ "embed" _ "embed"
"errors" "errors"
"html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -29,11 +28,6 @@ func (auth *Auth) authHome(ctx context.Context) http.Handler {
var passwordHTMLString string var passwordHTMLString string
var passwordTemplate = static.AssetsAuthLogin.MustParseShared("password.html", passwordHTMLString) var passwordTemplate = static.AssetsAuthLogin.MustParseShared("password.html", passwordHTMLString)
type authpasswordContext struct {
Message string
Form template.HTML
}
var ( var (
errPasswordsNotIdentical = errors.New("passwords are not identical") errPasswordsNotIdentical = errors.New("passwords are not identical")
errPasswordIsEmpty = errors.New("password is empty") errPasswordIsEmpty = errors.New("password is empty")
@ -55,16 +49,7 @@ func (auth *Auth) authPassword(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil), CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) { RenderTemplate: passwordTemplate,
ctx := authpasswordContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, passwordTemplate, "", w, r)
},
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
old, passcode, new, new2 := values["old"], values["passcode"], values["new"], values["new2"] old, passcode, new, new2 := values["old"], values["passcode"], values["new"], values["new2"]

View file

@ -2,7 +2,7 @@ package auth
import ( import (
"context" "context"
"html/template" "errors"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -110,10 +110,7 @@ var loginResponse = httpx.Response{
Body: []byte("user is signed in"), Body: []byte("user is signed in"),
} }
type authloginContext struct { var errLoginFailed = errors.New("Login failed")
Message string
Form template.HTML
}
// authLogin implements a view to login a user // authLogin implements a view to login a user
func (auth *Auth) authLogin(ctx context.Context) http.Handler { func (auth *Auth) authLogin(ctx context.Context) http.Handler {
@ -127,15 +124,11 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil), CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) { RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
ctx := authloginContext{ if context.Err != nil {
Message: "", context.Err = errLoginFailed
Form: template,
} }
if err != nil { httpx.WriteHTML(context, nil, loginTemplate, "", w, r)
ctx.Message = "Login Failed"
}
httpx.WriteHTML(ctx, nil, loginTemplate, "", w, r)
}, },
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {

View file

@ -10,10 +10,33 @@
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<div> Welcome {{ .User.User }}!
Welcome {{ .User.User }}! <a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
<hr /> <hr />
</div>
<div class="pure-u-1">
<p>
{{ if .User.Admin }}
You are an administrator.
{{ else }}
You are a regular user.
{{ end }}
{{ if .User.TOTPEnabled }}
You have TOTP enabled.
{{ else }}
You do not have TOTP enabled.
{{ end }}
</p>
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/password/">Change Password</a>
{{ if .User.TOTPEnabled }}
<a class="pure-button" href="/auth/totp/disable/">Disable TOTP</a>
{{ else }}
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
{{ end }}
</div> </div>
<hr />
</div> </div>
{{ if .User.Admin }} {{ if .User.Admin }}
@ -31,21 +54,8 @@
{{ end }} {{ end }}
<div class="pure-u-1"> <div class="pure-u-1">
<div class="pure-button-group" role="group" role="Actions"> There will be a list of WissKIs you have access to here.
<a class="pure-button" href="/auth/password/">Change Password</a>
{{ if .User.TOTPEnabled }}
<a class="pure-button" href="/auth/totp/disable/">Disable TOTP</a>
{{ else }}
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
{{ end }}
</div>
<hr />
</div> </div>
<div class="pure-u-1">
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/logout/">Logout</a>
</div>
</div>
{{ end }} {{ end }}

View file

@ -13,7 +13,7 @@
<img src="{{ .TOTPImage }}" alt="TOTP Enrollment Image"> <img src="{{ .TOTPImage }}" alt="TOTP Enrollment Image">
</a> </a>
<ul> <ul>
<li>scan the token above using a <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP</a>app on your phone</li> <li>scan the token above using a <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP</a> app on your phone</li>
<li>enter your current password and the now generated token to confirm</li> <li>enter your current password and the now generated token to confirm</li>
</ul> </ul>
</div> </div>

View file

@ -11,11 +11,6 @@ import (
_ "embed" _ "embed"
) )
type totpContext struct {
Message string
Form template.HTML
}
//go:embed "templates/totp_enable.html" //go:embed "templates/totp_enable.html"
var totpEnableStr string var totpEnableStr string
var totpEnableTemplate = static.AssetsAuthLogin.MustParseShared("totp_enable.html", totpEnableStr) var totpEnableTemplate = static.AssetsAuthLogin.MustParseShared("totp_enable.html", totpEnableStr)
@ -30,19 +25,10 @@ func (auth *Auth) authTOTPEnable(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil), CSRF: auth.csrf.Get(nil),
SkipForm: func(r *http.Request) (data struct{}, skip bool) { SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, _ := auth.UserOf(r) user, err := auth.UserOf(r)
return struct{}{}, user != nil && user.TOTPEnabled return struct{}{}, err == nil && user != nil && user.TOTPEnabled
},
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := totpContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpEnableTemplate, "", w, r)
}, },
RenderTemplate: totpEnableTemplate,
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password := values["password"] password := values["password"]
@ -80,7 +66,7 @@ var totpEnrollStr string
var totpEnrollTemplate = static.AssetsAuthLogin.MustParseShared("totp_enroll.html", totpEnrollStr) var totpEnrollTemplate = static.AssetsAuthLogin.MustParseShared("totp_enroll.html", totpEnrollStr)
type totpEnrollContext struct { type totpEnrollContext struct {
totpContext httpx.FormContext
TOTPImage template.URL TOTPImage template.URL
TOTPURL template.URL TOTPURL template.URL
} }
@ -99,12 +85,9 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler {
user, _ := auth.UserOf(r) user, _ := auth.UserOf(r)
return struct{}{}, user != nil && user.TOTPEnabled return struct{}{}, user != nil && user.TOTPEnabled
}, },
RenderForm: func(tpl template.HTML, err error, w http.ResponseWriter, r *http.Request) { RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
ctx := totpEnrollContext{ ctx := totpEnrollContext{
totpContext: totpContext{ FormContext: context,
Message: "",
Form: tpl,
},
} }
if user, err := auth.UserOf(r); err == nil && user != nil { if user, err := auth.UserOf(r); err == nil && user != nil {
@ -116,9 +99,6 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler {
ctx.TOTPURL = template.URL(secret.URL()) ctx.TOTPURL = template.URL(secret.URL())
} }
} }
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpEnrollTemplate, "", w, r) httpx.WriteHTML(ctx, nil, totpEnrollTemplate, "", w, r)
}, },
@ -171,16 +151,7 @@ func (auth *Auth) authTOTPDisable(ctx context.Context) http.Handler {
user, _ := auth.UserOf(r) user, _ := auth.UserOf(r)
return struct{}{}, user != nil && !user.TOTPEnabled return struct{}{}, user != nil && !user.TOTPEnabled
}, },
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) { RenderTemplate: totpDisableTemplate,
ctx := totpContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpDisableTemplate, "", w, r)
},
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, passcode := values["password"], values["passcode"] password, passcode := values["password"], values["passcode"]

View file

@ -5,47 +5,47 @@ package static
// AssetsHomeHome contains assets for the 'HomeHome' entrypoint. // AssetsHomeHome contains assets for the 'HomeHome' entrypoint.
var AssetsHomeHome = Assets{ var AssetsHomeHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/HomeHome.2353e048.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/HomeHome.2353e048.css">`,
} }
// AssetsComponentsIndex contains assets for the 'ComponentsIndex' entrypoint. // AssetsComponentsIndex contains assets for the 'ComponentsIndex' entrypoint.
var AssetsComponentsIndex = Assets{ var AssetsComponentsIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ComponentsIndex.38d394c2.js"></script><script src="/static/ComponentsIndex.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ComponentsIndex.38d394c2.js"></script><script src="/static/ComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ComponentsIndex.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ComponentsIndex.38d394c2.css">`,
} }
// AssetsControlIndex contains assets for the 'ControlIndex' entrypoint. // AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
var AssetsControlIndex = Assets{ var AssetsControlIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script src="/static/ControlIndex.75d2a312.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script src="/static/ControlIndex.75d2a312.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
} }
// AssetsControlInstance contains assets for the 'ControlInstance' entrypoint. // AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
var AssetsControlInstance = Assets{ var AssetsControlInstance = Assets{
Scripts: `<script nomodule="" defer src="/static/ControlIndex.75d2a312.js"></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/static/ControlIndex.75d2a312.js"></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
} }
// AssetsInstanceComponentsIndex contains assets for the 'InstanceComponentsIndex' entrypoint. // AssetsInstanceComponentsIndex contains assets for the 'InstanceComponentsIndex' entrypoint.
var AssetsInstanceComponentsIndex = Assets{ var AssetsInstanceComponentsIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/InstanceComponentsIndex.38d394c2.js"></script><script src="/static/InstanceComponentsIndex.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/InstanceComponentsIndex.38d394c2.js"></script><script src="/static/InstanceComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`,
} }
// AssetsAuthLogin contains assets for the 'AuthLogin' entrypoint. // AssetsAuthLogin contains assets for the 'AuthLogin' entrypoint.
var AssetsAuthLogin = Assets{ var AssetsAuthLogin = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthLogin.38d394c2.js"></script><script src="/static/AuthLogin.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthLogin.38d394c2.js"></script><script src="/static/AuthLogin.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`,
} }
// AssetsAuthHome contains assets for the 'AuthHome' entrypoint. // AssetsAuthHome contains assets for the 'AuthHome' entrypoint.
var AssetsAuthHome = Assets{ var AssetsAuthHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthHome.38d394c2.js"></script><script src="/static/AuthHome.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthHome.38d394c2.js"></script><script src="/static/AuthHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthHome.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthHome.38d394c2.css">`,
} }
// AssetsAuthTOTP contains assets for the 'AuthTOTP' entrypoint. // AssetsAuthTOTP contains assets for the 'AuthTOTP' entrypoint.
var AssetsAuthTOTP = Assets{ var AssetsAuthTOTP = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthTOTP.38d394c2.js"></script><script src="/static/AuthTOTP.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthTOTP.38d394c2.js"></script><script src="/static/AuthTOTP.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthTOTP.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthTOTP.38d394c2.css">`,
} }

View file

@ -48,3 +48,19 @@ footer {
.pure-button-success { .pure-button-success {
background-color: rgb(28, 184, 65) !important; background-color: rgb(28, 184, 65) !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%;
}

View file

@ -12,9 +12,10 @@
<div class="pure-u-1"> <div class="pure-u-1">
{{ block "form/message" . }} {{ block "form/message" . }}
{{ if .Message }} {{ $E := .Error }}
{{ if not (eq $E "") }}
<div> <div>
{{ .Message }} {{ $E }}
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}

View file

@ -26,7 +26,7 @@ type Storage struct {
// Get retrieves metadata with the provided key and deserializes the first one into target. // Get retrieves metadata with the provided key and deserializes the first one into target.
// If no metadatum exists, returns [ErrMetadatumNotSet]. // If no metadatum exists, returns [ErrMetadatumNotSet].
func (s Storage) Get(ctx context.Context, key Key, target any) error { func (s Storage) Get(ctx context.Context, key Key, target any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }
@ -55,7 +55,7 @@ func (s Storage) Get(ctx context.Context, key Key, target any) error {
// //
// When no metadatum exists, targets is not called, and nil error is returned. // When no metadatum exists, targets is not called, and nil error is returned.
func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total int) any) error { func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total int) any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +82,7 @@ func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total i
// Delete deletes all metadata with the provided key. // Delete deletes all metadata with the provided key.
func (s Storage) Delete(ctx context.Context, key Key) error { func (s Storage) Delete(ctx context.Context, key Key) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }
@ -98,7 +98,7 @@ func (s Storage) Delete(ctx context.Context, key Key) error {
// Set serializes value and stores it with the provided key. // Set serializes value and stores it with the provided key.
// Any other metadata with the same key is deleted. // Any other metadata with the same key is deleted.
func (s Storage) Set(ctx context.Context, key Key, value any) error { func (s Storage) Set(ctx context.Context, key Key, value any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }
@ -133,7 +133,7 @@ func (s Storage) Set(ctx context.Context, key Key, value any) error {
// Set serializes values and stores them with the provided key. // Set serializes values and stores them with the provided key.
// Any other metadata with the same key is deleted. // Any other metadata with the same key is deleted.
func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error { func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }
@ -167,7 +167,7 @@ func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error {
// Purge removes all metadata, regardless of key. // Purge removes all metadata, regardless of key.
func (s Storage) Purge(ctx context.Context) error { func (s Storage) Purge(ctx context.Context) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable) table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil { if err != nil {
return err return err
} }

View file

@ -91,7 +91,7 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
{ {
"metadata", "metadata",
&models.Metadatum{}, &models.Metadatum{},
models.MetadataTable, models.AccessTable,
}, },
{ {
"snapshot", "snapshot",

View file

@ -15,12 +15,14 @@ var DefaultFieldTemplate = template.Must(template.New("").Parse(`<input type="{{
var PureCSSFieldTemplate = template.Must(template.New("").Parse(` var PureCSSFieldTemplate = template.Must(template.New("").Parse(`
<div class="pure-control-group"><label for="{{.Name}}">{{.Label}}</label><input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"></div>`)) <div class="pure-control-group"><label for="{{.Name}}">{{.Label}}</label><input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"></div>`))
// Form implements a user-submittable form // Form provides a form that a user can submit via a http POST method call.
// It implements [http.Handler].
type Form[D any] struct { type Form[D any] struct {
// Fields are the fields this form consists of.
Fields []Field Fields []Field
// FieldTemplate is executed for each field. // FieldTemplate is an optional template to be executed for each field.
// Defaults to a [DefaultFieldTemplate] // FieldTemplate may be nil; in which case [DefaultFieldTemplate] is used.
FieldTemplate *template.Template FieldTemplate *template.Template
// CSRF holds an optional reference to a CSRF.Protect call. // CSRF holds an optional reference to a CSRF.Protect call.
@ -33,11 +35,17 @@ type Form[D any] struct {
SkipForm func(r *http.Request) (data D, skip bool) SkipForm func(r *http.Request) (data D, skip bool)
// RenderForm handles rendering a form into a request. // RenderForm handles rendering a form into a request.
// If RenderForm is nil, RenderTemplate is invoked with an appropriate [FormContext] instance.
// Either RenderForm or RenderTemplate must be non-nil.
// //
// template holds pre-rendered html fields. // template holds pre-rendered html fields.
// err is a non-nil error returned from Validate, or the r.ParseForm() method. // err is a non-nil error returned from Validate, or the r.ParseForm() method.
// It is nil on the initial render. // It is nil on the initial render.
RenderForm func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) RenderForm func(context FormContext, w http.ResponseWriter, r *http.Request)
// RenderTemplate represents an optional form to display to the user when RenderForm is nil
// It is passed a [FormContext] instance.
RenderTemplate *template.Template
// Validate, if non-nil, validates the given submitted values. // Validate, if non-nil, validates the given submitted values.
// There is no guarantee that the values are set. // There is no guarantee that the values are set.
@ -88,6 +96,8 @@ func (form *Form[D]) Values(r *http.Request) (v map[string]string, d D, err erro
return values, d, nil return values, d, nil
} }
// ServeHTTP implements [http.Handler].
// If the form contains a csrf reference, then this is invoked also.
func (form *Form[D]) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (form *Form[D]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := form.csrf.Get(func() (handler http.Handler) { handler := form.csrf.Get(func() (handler http.Handler) {
handler = http.HandlerFunc(form.serveHTTP) handler = http.HandlerFunc(form.serveHTTP)
@ -122,14 +132,48 @@ func (form *Form[D]) serveHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
// renderForm renders the form into a request
func (form *Form[D]) renderForm(err error, values map[string]string, w http.ResponseWriter, r *http.Request) { func (form *Form[D]) renderForm(err error, values map[string]string, w http.ResponseWriter, r *http.Request) {
template := form.Template(values, err != nil) template := form.Template(values, err != nil)
if form.CSRF != nil { if form.CSRF != nil {
template += csrf.TemplateField(r) template += csrf.TemplateField(r)
} }
form.RenderForm(template, err, w, r)
ctx := FormContext{Err: err, Form: template}
if form.RenderForm != nil {
form.RenderForm(ctx, w, r)
return
}
// must have a form or a RenderForm
if form.RenderTemplate == nil {
panic("form.RenderForm and form.Form are nil")
}
// render the form
WriteHTML(ctx, nil, form.RenderTemplate, "", w, r)
} }
// FormContext is passed to Form.Form when used
type FormContext struct {
// Error is the underlying error (if any)
Err error
// Template is the underlying template rendered as html
Form template.HTML
}
// Error returns the underlying error string
func (fc FormContext) Error() string {
if fc.Err == nil {
return ""
}
return fc.Err.Error()
}
// renderSuccess renders a successfull pass of the form
// if an error occurs during rendering, renderForm is called instead
func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.ResponseWriter, r *http.Request) { func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.ResponseWriter, r *http.Request) {
err := form.RenderSuccess(data, values, w, r) err := form.RenderSuccess(data, values, w, r)
if err == nil { if err == nil {
@ -138,17 +182,18 @@ func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.Resp
form.renderForm(err, values, w, r) form.renderForm(err, values, w, r)
} }
// Field represents a field // Field represents a field inside a form.
type Field struct { type Field struct {
Name string Name string // Name is the name of the field
Type InputType Type InputType // Type is the type of the field. It corresponds to the "name" attribute in html.
Placeholder string // Optional placeholder Placeholder string // Value for the "placeholder" attribute
Label string // Label for the template. Not used by the default template. Label string // (External) Label for the field. Not used by the default template.
EmptyOnError bool // indicates if the field should be reset on error EmptyOnError bool // indicates if the field should be reset on error
} }
// fieldContext is passed to the template context
type fieldContext struct { type fieldContext struct {
Field Field
Value string Value string