templates: Add a proper menu and navigation
This commit is contained in:
parent
0bb7f99fa3
commit
a00195be16
76 changed files with 336 additions and 233 deletions
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,7 @@ func (dis *Distillery) init() {
|
|||
lazy.RegisterPoolGroup[component.Cronable](&dis.pool)
|
||||
lazy.RegisterPoolGroup[component.UserDeleteHook](&dis.pool)
|
||||
lazy.RegisterPoolGroup[component.Table](&dis.pool)
|
||||
lazy.RegisterPoolGroup[component.Menuable](&dis.pool)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type Auth struct {
|
|||
|
||||
var (
|
||||
_ component.Routeable = (*Auth)(nil)
|
||||
_ component.Menuable = (*Auth)(nil)
|
||||
_ component.Table = (*Auth)(nil)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ func (panel *UserPanel) Routes() component.Routes {
|
|||
Prefix: "/user/",
|
||||
CSRF: true,
|
||||
Decorator: panel.Dependencies.Auth.Require(nil),
|
||||
|
||||
MenuPriority: component.MenuUser,
|
||||
MenuTitle: "User",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +84,20 @@ type userFormContext struct {
|
|||
User *models.User
|
||||
}
|
||||
|
||||
func (panel *UserPanel) UserFormContext(ctx httpx.FormContext, r *http.Request) any {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
|
||||
uctx := userFormContext{FormContext: ctx}
|
||||
panel.Dependencies.Custom.Update(&uctx, r)
|
||||
if err == nil {
|
||||
uctx.User = &user.User
|
||||
func (panel *UserPanel) UserFormContext(last component.MenuItem) func(ctx httpx.FormContext, r *http.Request) any {
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "User", Path: "/user/"},
|
||||
last,
|
||||
}
|
||||
return uctx
|
||||
return func(ctx httpx.FormContext, r *http.Request) any {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
|
||||
uctx := userFormContext{FormContext: ctx}
|
||||
panel.Dependencies.Custom.Update(&uctx, r, crumbs)
|
||||
if err == nil {
|
||||
uctx.User = &user.User
|
||||
}
|
||||
return uctx
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
||||
|
|
@ -37,7 +38,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
|
|||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
RenderTemplate: passwordTemplate,
|
||||
RenderTemplateContext: panel.UserFormContext,
|
||||
RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Change Password", Path: "/user/password/"}),
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
{{ template "_form.html" . }}
|
||||
{{ define "form/title" }}Change Password{{ end }}
|
||||
{{ define "form/button" }}Update{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/user/">{{ .User.User }}</a> >
|
||||
<a class="pure-button pure-button-primary" href="/user/password/">Change Password</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,6 @@
|
|||
{{ define "form/title" }}Disable TOTP{{ end }}
|
||||
{{ define "form/button" }}Disable{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/user/">{{ .User.User }}</a> >
|
||||
<a class="pure-button pure-button-primary" href="/user/totp/disable/">Disable TOTP</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "form/inside" }}
|
||||
<div>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
{{ template "_form.html" . }}
|
||||
{{ define "form/title" }}Enable TOTP{{ end }}
|
||||
{{ define "form/button" }}Enable{{ end }}
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/user/">{{ .User.User }}</a> >
|
||||
<a class="pure-button pure-button-primary" href="/user/totp/enable/">Enable TOTP</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ define "form/inside" }}
|
||||
<div>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
{{ template "_form.html" . }}
|
||||
{{ define "form/title" }}Enable TOTP{{ end }}
|
||||
{{ define "form/button" }}Enable{{ end }}
|
||||
{{ define "header" }}
|
||||
<p>
|
||||
<a class="pure-button" href="/user/">{{ .User.User }}</a> >
|
||||
<a class="pure-button pure-button-primary" href="/user/totp/enroll/">Enroll TOTP</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ define "form/inside" }}
|
||||
<div>
|
||||
<a href="{{ .TOTPURL }}">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}User{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button pure-button-primary" href="/user/">{{ .User.User }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"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"
|
||||
|
|
@ -32,7 +33,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
|
|||
},
|
||||
|
||||
RenderTemplate: totpEnableTemplate,
|
||||
RenderTemplateContext: panel.UserFormContext,
|
||||
RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}),
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
password := values["password"]
|
||||
|
|
@ -77,7 +78,10 @@ type totpEnrollContext struct {
|
|||
|
||||
func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
|
||||
totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate)
|
||||
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "User", Path: "/user/"},
|
||||
{Title: "Enable TOTP", Path: "/user/totp/enable/"},
|
||||
}
|
||||
return &httpx.Form[struct{}]{
|
||||
Fields: []field.Field{
|
||||
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
|
||||
|
|
@ -99,7 +103,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
|
|||
FormContext: context,
|
||||
},
|
||||
}
|
||||
panel.Dependencies.Custom.Update(&ctx.userFormContext, r)
|
||||
panel.Dependencies.Custom.Update(&ctx.userFormContext, r, crumbs)
|
||||
|
||||
if err == nil && user != nil {
|
||||
ctx.userFormContext.User = &user.User
|
||||
|
|
@ -164,7 +168,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
|
|||
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
|
||||
},
|
||||
RenderTemplate: totpDisableTemplate,
|
||||
RenderTemplateContext: panel.UserFormContext,
|
||||
RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}),
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
password, otp := values["password"], values["otp"]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"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/internal/dis/component/control/static/custom"
|
||||
|
|
@ -35,9 +36,13 @@ type GrantWithURL struct {
|
|||
|
||||
func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
|
||||
userTemplate := panel.Dependencies.Custom.Template(userTemplate)
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "User", Path: "/user/"},
|
||||
}
|
||||
|
||||
return &httpx.HTMLHandler[routeUserContext]{
|
||||
Handler: func(r *http.Request) (ruc routeUserContext, err error) {
|
||||
panel.Dependencies.Custom.Update(&ruc, r)
|
||||
panel.Dependencies.Custom.Update(&ruc, r, crumbs)
|
||||
|
||||
// find the user
|
||||
ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,17 @@ func (auth *Auth) Require(perm Permission) func(http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
// Has checks if the given request has the given permission.
|
||||
// If an error occurs, returns false.
|
||||
func (auth *Auth) Has(perm Permission, r *http.Request) bool {
|
||||
user, err := auth.UserOf(r)
|
||||
if err != nil || user == nil {
|
||||
return false
|
||||
}
|
||||
ok, err := perm.Permit(user, r)
|
||||
return err == nil && ok.Granted()
|
||||
}
|
||||
|
||||
// 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.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and passcode enabled"), nil
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package auth
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
|
|
@ -67,6 +69,22 @@ func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
|
|||
}).Get(r, control.SessionCookie)
|
||||
}
|
||||
|
||||
func (auth *Auth) Menu(r *http.Request) []component.MenuItem {
|
||||
|
||||
user, err := auth.UserOf(r)
|
||||
if user == nil || err != nil {
|
||||
return nil
|
||||
}
|
||||
return []component.MenuItem{
|
||||
{
|
||||
Title: "Logout",
|
||||
Path: "/auth/logout",
|
||||
Priority: component.MenuAuth,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type contextUserKey struct{}
|
||||
|
||||
var ctxUserKey = contextUserKey{}
|
||||
|
|
@ -126,7 +144,9 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
|
|||
if context.Err != nil {
|
||||
context.Err = errLoginFailed
|
||||
}
|
||||
httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r), nil, loginTemplate, "", w, r)
|
||||
httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r, []component.MenuItem{
|
||||
{Title: "Login", Path: template.URL(r.URL.RequestURI())},
|
||||
}), nil, loginTemplate, "", w, r)
|
||||
},
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type Admin struct {
|
|||
var (
|
||||
_ component.DistilleryFetcher = (*Admin)(nil)
|
||||
_ component.Routeable = (*Admin)(nil)
|
||||
_ component.Menuable = (*Admin)(nil)
|
||||
)
|
||||
|
||||
func (admin *Admin) Routes() component.Routes {
|
||||
|
|
@ -50,6 +51,19 @@ func (admin *Admin) Routes() component.Routes {
|
|||
}
|
||||
}
|
||||
|
||||
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
|
||||
if !admin.Dependencies.Auth.Has(auth.Admin, r) {
|
||||
return nil
|
||||
}
|
||||
return []component.MenuItem{
|
||||
{
|
||||
Title: "Admin",
|
||||
Path: "/admin/",
|
||||
Priority: component.MenuAdmin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
|
||||
|
||||
router := httprouter.New()
|
||||
|
|
@ -62,17 +76,17 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
|
|||
}
|
||||
}
|
||||
|
||||
// handle everything
|
||||
router.HandlerFunc(http.MethodGet, route, func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, route+"index", http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
// add a handler for the index page
|
||||
router.Handler(http.MethodGet, route+"index", httpx.HTMLHandler[indexContext]{
|
||||
router.Handler(http.MethodGet, route, httpx.HTMLHandler[indexContext]{
|
||||
Handler: admin.index,
|
||||
Template: admin.Dependencies.Custom.Template(indexTemplate),
|
||||
})
|
||||
|
||||
// fallback to the "/" page
|
||||
router.HandlerFunc(http.MethodGet, route+"index", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, route, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
// add a handler for the user page
|
||||
router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{
|
||||
Handler: admin.users,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
|
|
@ -28,7 +30,10 @@ type componentContext struct {
|
|||
}
|
||||
|
||||
func (admin *Admin) components(r *http.Request) (cp componentContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&cp, r)
|
||||
admin.Dependencies.Custom.Update(&cp, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Components", Path: "/admin/components/"},
|
||||
})
|
||||
|
||||
cp.Analytics = *admin.Analytics
|
||||
return
|
||||
|
|
@ -49,10 +54,15 @@ type ingredientsContext struct {
|
|||
}
|
||||
|
||||
func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&cp, r)
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
|
||||
admin.Dependencies.Custom.Update(&cp, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
|
||||
{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")},
|
||||
})
|
||||
|
||||
// find the instance itself!
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return cp, httpx.ErrNotFound
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package admin
|
|||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
|
|
@ -38,7 +40,11 @@ type grantsContext struct {
|
|||
}
|
||||
|
||||
func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (err error) {
|
||||
admin.Dependencies.Custom.Update(gc, r)
|
||||
admin.Dependencies.Custom.Update(gc, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
|
||||
{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")},
|
||||
})
|
||||
|
||||
// find the instance itself
|
||||
gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin - Components Page{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/components">Components</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ template "_anal.html" .Analytics }}
|
||||
{{ end }}
|
||||
|
|
@ -1,14 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Grants{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/grants/{{ .Instance.Slug }}">Grants</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ $csrf := .CSRF }}
|
||||
{{ $slug := .Instance.Slug }}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button pure-button-primary" href="/admin/index">Admin</a>
|
||||
</p>
|
||||
{{ define "header" }}
|
||||
<p>
|
||||
<div class="pure-button-group" role="group" aria-label="Actions">
|
||||
<a class="pure-button" href="/admin/users">Users</a>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Ingredients{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/ingredients/{{ .Instance.Slug }}">Ingredients</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ template "_anal.html" .Analytics }}
|
||||
{{ end }}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }}{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/instance/{{ .Instance.Slug }}">Instance</a>
|
||||
</p>
|
||||
{{ define "header" }}
|
||||
<p>
|
||||
<div class="pure-button-group" role="group" aria-label="Actions">
|
||||
<a class="pure-button" href="/admin/grants/{{ .Info.Slug }}">Grants</a>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,3 @@
|
|||
{{ define "form/title" }}Distillery Admin - Create User{{ end }}
|
||||
{{ define "form/button" }}Create{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button" href="/admin/users">Users</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/users/create">Create</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Distillery Admin - Users{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/admin/index">Admin</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/users">Users</a>
|
||||
</p>
|
||||
{{ define "header" }}
|
||||
<p>
|
||||
<div class="pure-button-group" role="group" aria-label="Actions">
|
||||
<a class="pure-button" href="/admin/users/create">Create New</a>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ type indexContext struct {
|
|||
}
|
||||
|
||||
func (admin *Admin) index(r *http.Request) (idx indexContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&idx, r)
|
||||
admin.Dependencies.Custom.Update(&idx, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
})
|
||||
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package admin
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
|
|
@ -28,10 +30,14 @@ type instanceContext struct {
|
|||
}
|
||||
|
||||
func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&is, r)
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
|
||||
admin.Dependencies.Custom.Update(&is, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
|
||||
})
|
||||
|
||||
// find the instance itself!
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return is, httpx.ErrNotFound
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"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/internal/dis/component/control/static/custom"
|
||||
|
|
@ -31,7 +32,10 @@ type userContext struct {
|
|||
}
|
||||
|
||||
func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&uc, r)
|
||||
admin.Dependencies.Custom.Update(&uc, r, []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Users", Path: "/admin/users/"},
|
||||
})
|
||||
|
||||
uc.Error = r.URL.Query().Get("error")
|
||||
uc.Users, err = admin.Dependencies.Auth.Users(r.Context())
|
||||
|
|
@ -58,6 +62,11 @@ type createUserResult struct {
|
|||
|
||||
func (admin *Admin) createUser(ctx context.Context) http.Handler {
|
||||
userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate)
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Users", Path: "/admin/users"},
|
||||
{Title: "Create", Path: "/admin/users/create"},
|
||||
}
|
||||
|
||||
return &httpx.Form[createUserResult]{
|
||||
Fields: []field.Field{
|
||||
|
|
@ -67,8 +76,10 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
|
|||
},
|
||||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
RenderTemplate: userCreateTemplate,
|
||||
RenderTemplateContext: admin.Dependencies.Custom.RenderContext,
|
||||
RenderTemplate: userCreateTemplate,
|
||||
RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any {
|
||||
return admin.Dependencies.Custom.NewForm(ctx, r, crumbs)
|
||||
},
|
||||
|
||||
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"] == field.CheckboxChecked
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ func (*Home) Routes() component.Routes {
|
|||
Prefix: "/",
|
||||
MatchAllDomains: true,
|
||||
CSRF: false,
|
||||
|
||||
MenuTitle: "WissKI Distillery",
|
||||
MenuPriority: component.MenuHome,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
|
|
@ -14,7 +15,7 @@ import (
|
|||
|
||||
//go:embed "public.html"
|
||||
var publicHTMLStr string
|
||||
var publicTemplate = static.AssetsHome.MustParseShared("public.html", publicHTMLStr)
|
||||
var publicTemplate = static.AssetsDefault.MustParseShared("public.html", publicHTMLStr)
|
||||
|
||||
type publicContext struct {
|
||||
custom.BaseContext
|
||||
|
|
@ -24,6 +25,9 @@ type publicContext struct {
|
|||
}
|
||||
|
||||
func (home *Home) publicHandler(ctx context.Context) http.Handler {
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "WissKI Distillery", Path: "/"},
|
||||
}
|
||||
return httpx.HTMLHandler[publicContext]{
|
||||
Handler: func(r *http.Request) (pc publicContext, err error) {
|
||||
// only act on the root path!
|
||||
|
|
@ -31,7 +35,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
|
|||
return pc, httpx.ErrNotFound
|
||||
}
|
||||
|
||||
home.Dependencies.Custom.Update(&pc, r)
|
||||
home.Dependencies.Custom.Update(&pc, r, crumbs)
|
||||
|
||||
pc.Instances = home.homeInstances.Get(nil)
|
||||
pc.SelfRedirect = home.Config.SelfRedirect.String()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}WissKI Distillery{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button pure-button-primary" href="/">Home</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="pure-button" href="/news/">News</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ define "content" }}
|
||||
{{ block "@custom/about" . }}
|
||||
<div class="pure-u-1">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ var (
|
|||
|
||||
//go:embed "legal.html"
|
||||
var legalTemplateString string
|
||||
var legalTemplate = static.AssetsLegal.MustParseShared("legal.html", legalTemplateString)
|
||||
var legalTemplate = static.AssetsDefault.MustParseShared("legal.html", legalTemplateString)
|
||||
|
||||
func (legal *Legal) Routes() component.Routes {
|
||||
return component.Routes{
|
||||
|
|
@ -59,7 +59,9 @@ type legalContext struct {
|
|||
}
|
||||
|
||||
func (legal *Legal) context(r *http.Request) (lc legalContext, err error) {
|
||||
legal.Dependencies.Custom.Update(&lc, r)
|
||||
legal.Dependencies.Custom.Update(&lc, r, []component.MenuItem{
|
||||
{Title: "Legal", Path: "/legal/"},
|
||||
})
|
||||
|
||||
lc.LegalNotices = cli.LegalNotices
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Legal{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<!-- no header -->
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1">
|
||||
<h2 id="cookies">Cookie Usage</h2>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ func (*News) Routes() component.Routes {
|
|||
Prefix: "/news/",
|
||||
Exact: true,
|
||||
CSRF: false,
|
||||
|
||||
MenuTitle: "News",
|
||||
MenuPriority: component.MenuNews,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +114,7 @@ func Items() ([]Item, error) {
|
|||
|
||||
//go:embed "news.html"
|
||||
var newsHTMLStr string
|
||||
var newsTemplate = static.AssetsHome.MustParseShared("news.html", newsHTMLStr)
|
||||
var newsTemplate = static.AssetsDefault.MustParseShared("news.html", newsHTMLStr)
|
||||
|
||||
type newsContext struct {
|
||||
custom.BaseContext
|
||||
|
|
@ -120,6 +123,10 @@ type newsContext struct {
|
|||
|
||||
// HandleRoute returns the handler for the requested path
|
||||
func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "News", Path: "/news/"},
|
||||
}
|
||||
|
||||
items, itemsErr := Items()
|
||||
if itemsErr != nil {
|
||||
zerolog.Ctx(ctx).Err(itemsErr).Msg("Unable to load news items")
|
||||
|
|
@ -127,7 +134,7 @@ func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, e
|
|||
|
||||
return httpx.HTMLHandler[newsContext]{
|
||||
Handler: func(r *http.Request) (nc newsContext, err error) {
|
||||
news.Dependencies.Custom.Update(&nc, r)
|
||||
news.Dependencies.Custom.Update(&nc, r, crumbs)
|
||||
nc.Items, err = items, itemsErr
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}News{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<p>
|
||||
<a class="pure-button" href="/">Home</a> >
|
||||
<a class="pure-button pure-button-primary" href="/news/">News</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
||||
<div class="pure-u-1">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ type Assets struct {
|
|||
Styles string // <link> tags inserted by the asset
|
||||
}
|
||||
|
||||
//go:generate node build.mjs Home User Admin Legal
|
||||
//go:generate node build.mjs Default User Admin
|
||||
|
||||
// MustParse parses a new template from the given source
|
||||
// and calls [RegisterAssoc] on it.
|
||||
|
|
|
|||
|
|
@ -7,26 +7,20 @@ import _ "embed"
|
|||
//go:embed "assets_disclaimer.txt"
|
||||
var AssetsDisclaimer string
|
||||
|
||||
// AssetsHome contains assets for the 'Home' entrypoint.
|
||||
var AssetsHome = Assets{
|
||||
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Home.f9675eae.css">`,
|
||||
// AssetsDefault contains assets for the 'Default' entrypoint.
|
||||
var AssetsDefault = Assets{
|
||||
Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Default.f9675eae.css">`,
|
||||
}
|
||||
|
||||
// AssetsUser contains assets for the 'User' entrypoint.
|
||||
var AssetsUser = Assets{
|
||||
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
|
||||
Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
|
||||
}
|
||||
|
||||
// AssetsAdmin contains assets for the 'Admin' entrypoint.
|
||||
var AssetsAdmin = Assets{
|
||||
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
|
||||
}
|
||||
|
||||
// AssetsLegal contains assets for the 'Legal' entrypoint.
|
||||
var AssetsLegal = Assets{
|
||||
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Legal.38d394c2.js"></script><script src="/static/Legal.38d394c2.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Legal.d1531eba.css"><link rel="stylesheet" href="/static/Legal.20259812.css">`,
|
||||
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/tkw1536/goprogram/lib/reflectx"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// baseContextName is the name of the [BaseContext] type
|
||||
|
|
@ -25,6 +26,10 @@ type BaseContext struct {
|
|||
|
||||
GeneratedAt time.Time // time this page was generated at
|
||||
|
||||
// Menu and breadcrumbs
|
||||
Menu []component.MenuItem
|
||||
Crumbs []component.MenuItem
|
||||
|
||||
CSRF template.HTML // CSRF Field
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +48,8 @@ const (
|
|||
// The given request *must not* be nil.
|
||||
//
|
||||
// For convenience the passed context is also returned.
|
||||
func (tc *BaseContext) use(base component.Base, r *http.Request) *BaseContext {
|
||||
func (tc *BaseContext) use(custom *Custom, r *http.Request, crumbs []component.MenuItem) *BaseContext {
|
||||
// tc.custom = custom
|
||||
tc.inited = true
|
||||
tc.requestWasNil = r == nil
|
||||
|
||||
|
|
@ -55,9 +61,20 @@ func (tc *BaseContext) use(base component.Base, r *http.Request) *BaseContext {
|
|||
tc.CSRF = csrf.TemplateField(r)
|
||||
}
|
||||
|
||||
// build the menu
|
||||
tc.Menu = custom.BuildMenu(r)
|
||||
|
||||
// build the breadcrumbs
|
||||
tc.Crumbs = slices.Clone(crumbs)
|
||||
last := len(tc.Crumbs) - 1
|
||||
for i := range tc.Crumbs {
|
||||
tc.Crumbs[i].Active = i == last
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
// DoInitCheck is called by the template to check that the BaseContext was initialized properly
|
||||
func (bc BaseContext) DoInitCheck() template.HTML {
|
||||
if !bc.inited {
|
||||
return initError
|
||||
|
|
@ -69,26 +86,21 @@ func (bc BaseContext) DoInitCheck() template.HTML {
|
|||
}
|
||||
|
||||
// NewForm is like New, but returns a new BaseFormContext
|
||||
func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request) (ctx BaseFormContext) {
|
||||
func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request, crumbs []component.MenuItem) (ctx BaseFormContext) {
|
||||
ctx.FormContext = context
|
||||
ctx.use(custom.Base, r)
|
||||
ctx.use(custom, r, crumbs)
|
||||
return
|
||||
}
|
||||
|
||||
// RenderContext is exactly like NewForm, but returns any to be used as httpx.Form.RenderTemplateContext
|
||||
func (custom *Custom) RenderContext(ctx httpx.FormContext, r *http.Request) any {
|
||||
return custom.NewForm(ctx, r)
|
||||
}
|
||||
|
||||
// Update updates an embedded BaseContext field in context.
|
||||
//
|
||||
// Assumes that context is a pointer to a struct type.
|
||||
// If this is not the case, might call panic().
|
||||
func (custom *Custom) Update(context any, r *http.Request) *BaseContext {
|
||||
func (custom *Custom) Update(context any, r *http.Request, crumbs []component.MenuItem) *BaseContext {
|
||||
ctx := reflect.ValueOf(context).
|
||||
Elem().FieldByName(baseContextName).Addr().
|
||||
Interface().(*BaseContext)
|
||||
ctx.use(custom.Base, r)
|
||||
ctx.use(custom, r, crumbs)
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@ package custom
|
|||
|
||||
import (
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
)
|
||||
|
||||
// Custom implements theme and page customization.
|
||||
type Custom struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
// nothing yet
|
||||
Routeables []component.Routeable
|
||||
Menuable []component.Menuable
|
||||
}
|
||||
menu lazy.Lazy[[]component.MenuItem]
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.Backupable = (*Custom)(nil)
|
||||
_ component.Menuable = (*Custom)(nil)
|
||||
)
|
||||
|
|
|
|||
47
internal/dis/component/control/static/custom/menu.go
Normal file
47
internal/dis/component/control/static/custom/menu.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package custom
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/mux"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// getMenuItems gets a fresh copy of the cached slice of menu items
|
||||
func (custom *Custom) Menu(r *http.Request) []component.MenuItem {
|
||||
return custom.menu.Get(func() []component.MenuItem {
|
||||
items := make([]component.MenuItem, 0, len(custom.Dependencies.Routeables))
|
||||
for _, route := range custom.Dependencies.Routeables {
|
||||
routes := route.Routes()
|
||||
if routes.MenuTitle == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, component.MenuItem{
|
||||
Title: routes.MenuTitle,
|
||||
Priority: routes.MenuPriority,
|
||||
Path: template.URL(routes.Prefix),
|
||||
})
|
||||
}
|
||||
slices.SortFunc(items, component.MenuItemSort)
|
||||
return items
|
||||
})
|
||||
}
|
||||
|
||||
func (custom *Custom) BuildMenu(r *http.Request) []component.MenuItem {
|
||||
// NOTE(twiesing): Don't name this method "Menu", as it will cause
|
||||
// a stack overflow.
|
||||
path := mux.NormalizePath(r.URL.Path)
|
||||
|
||||
// get the static menu items, and then return all the regular ones
|
||||
var items []component.MenuItem
|
||||
for _, m := range custom.Dependencies.Menuable {
|
||||
items = append(items, m.Menu(r)...)
|
||||
}
|
||||
for i, item := range items {
|
||||
items[i].Active = string(item.Path) == path
|
||||
}
|
||||
slices.SortFunc(items, component.MenuItemSort)
|
||||
return items
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
html{background-color:#87a485}body{max-width:80vw!important}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -8,6 +8,30 @@ footer {
|
|||
margin: 2em;
|
||||
}
|
||||
|
||||
nav.pure-menu, nav.breadcrumbs {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
nav.breadcrumbs {
|
||||
padding-left: 1em;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
nav.breadcrumbs a:not(:last-child)::after {
|
||||
cursor: default;
|
||||
content: " > ";
|
||||
color: black;
|
||||
}
|
||||
nav.breadcrumbs a {
|
||||
text-decoration: none;
|
||||
color: blue !important;
|
||||
}
|
||||
nav.breadcrumbs a.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: small;
|
||||
border-top: 1px solid black;
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
html {
|
||||
background-color: #87A485;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 80vw !important;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
import "latex.css/style.min.css"
|
||||
|
|
@ -10,6 +10,21 @@
|
|||
|
||||
<body>
|
||||
{{ .BaseContext.DoInitCheck }}
|
||||
<nav class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
{{ range .BaseContext.Menu }}
|
||||
<li class="pure-menu-item{{ if .Active }} pure-menu-selected{{ end }}">
|
||||
<a href="{{ .Path }}" class="pure-menu-link">{{ .Title }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="breadcrumbs" role="group" aria-label="Breadcrumbs">
|
||||
{{ range .BaseContext.Crumbs }}
|
||||
<a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<h1 id="top">{{ template "title" . }}</h1>
|
||||
{{ block "header" . }}<!-- no header by default -->{{ end }}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }}
|
||||
|
||||
{{ define "header" }}<!-- no header -->{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1">
|
||||
{{ block "form/extra" . }}<!-- no extra -->{{ end }}
|
||||
|
|
|
|||
36
internal/dis/component/menu.go
Normal file
36
internal/dis/component/menu.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package component
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Menuable is a component that provides a menu
|
||||
type Menuable interface {
|
||||
Component
|
||||
|
||||
Menu(r *http.Request) []MenuItem
|
||||
}
|
||||
type MenuItem struct {
|
||||
Title string
|
||||
Path template.URL
|
||||
Active bool
|
||||
|
||||
Priority MenuPriority // menu priority
|
||||
}
|
||||
|
||||
func MenuItemSort(a, b MenuItem) bool {
|
||||
return a.Priority < b.Priority
|
||||
}
|
||||
|
||||
type MenuPriority int
|
||||
|
||||
// Menu* indicates priorities of the menu
|
||||
const (
|
||||
MenuHome MenuPriority = iota
|
||||
MenuNews
|
||||
MenuResolver
|
||||
MenuUser
|
||||
MenuAdmin
|
||||
MenuAuth
|
||||
)
|
||||
|
|
@ -41,12 +41,15 @@ func (resolver *Resolver) Routes() component.Routes {
|
|||
Prefix: "/wisski/get/",
|
||||
Aliases: []string{"/go/"},
|
||||
CSRF: false,
|
||||
|
||||
MenuTitle: "Resolver",
|
||||
MenuPriority: component.MenuResolver,
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed "resolver.html"
|
||||
var resolverHTMLStr string
|
||||
var resolverTemplate = static.AssetsHome.MustParseShared("resolver.html", resolverHTMLStr)
|
||||
var resolverTemplate = static.AssetsDefault.MustParseShared("resolver.html", resolverHTMLStr)
|
||||
|
||||
type resolverContext struct {
|
||||
custom.BaseContext
|
||||
|
|
@ -55,6 +58,9 @@ type resolverContext struct {
|
|||
|
||||
func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
|
||||
resolverTemplate := resolver.Dependencies.Custom.Template(resolverTemplate)
|
||||
crumbs := []component.MenuItem{
|
||||
{Title: "Resolver", Path: "/wisski/get/"},
|
||||
}
|
||||
|
||||
logger := zerolog.Ctx(ctx)
|
||||
|
||||
|
|
@ -65,7 +71,7 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
|
|||
ctx := resolverContext{
|
||||
IndexContext: context,
|
||||
}
|
||||
resolver.Dependencies.Custom.Update(&ctx, r)
|
||||
resolver.Dependencies.Custom.Update(&ctx, r, crumbs)
|
||||
|
||||
httpx.WriteHTML(ctx, nil, resolverTemplate, "", w, r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}WissKI Resolver{{ end }}
|
||||
|
||||
{{ define "header"}}
|
||||
<!-- no header -->
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1">
|
||||
<h1>wdresolve</h1>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ type Routes struct {
|
|||
// MatchAllDomains indicates that all domains, even the non-default domain, should be matched
|
||||
MatchAllDomains bool
|
||||
|
||||
// MenuTitle and MenuPriority return the priority and title of this menu item
|
||||
MenuTitle string
|
||||
MenuPriority MenuPriority
|
||||
|
||||
// Exact indicates that only the exact prefix, as opposed to any sub-paths, are matched.
|
||||
// Trailing '/'s are automatically trimmed, even with an exact match.
|
||||
Exact bool
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func (mux *Mux[T]) Add(path string, predicate Predicate, exact bool, h http.Hand
|
|||
mux.prefixes = make(map[string][]handler)
|
||||
}
|
||||
|
||||
mPath := normalizePath(path)
|
||||
mPath := NormalizePath(path)
|
||||
mHandler := handler{Predicate: predicate, Handler: h}
|
||||
if exact {
|
||||
mux.exacts[mPath] = append(mux.exacts[mPath], mHandler)
|
||||
|
|
@ -71,7 +71,7 @@ func (mux *Mux[T]) Match(r *http.Request, prepare bool) (http.Handler, bool) {
|
|||
r = mux.Prepare(r)
|
||||
}
|
||||
|
||||
candidate := normalizePath(r.URL.Path)
|
||||
candidate := NormalizePath(r.URL.Path)
|
||||
|
||||
// match the exact path first
|
||||
for _, h := range mux.exacts[candidate] {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"path"
|
||||
)
|
||||
|
||||
// normalizePath normalizes the provided path.
|
||||
// NormalizePath normalizes the provided path.
|
||||
// It ensures that there is both a leading and trailing slash.
|
||||
func normalizePath(value string) string {
|
||||
func NormalizePath(value string) string {
|
||||
value = path.Clean(value)
|
||||
if value != "/" {
|
||||
value = value + "/"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue