auth: Refactor home page
This commit is contained in:
parent
2d5b92f464
commit
b8f1281f78
14 changed files with 222 additions and 129 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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">`,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -47,4 +47,20 @@ 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%;
|
||||||
}
|
}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue