templates: Add a proper menu and navigation

This commit is contained in:
Tom Wiesing 2023-01-11 14:24:13 +01:00
parent 0bb7f99fa3
commit a00195be16
No known key found for this signature in database
76 changed files with 336 additions and 233 deletions

View file

@ -25,6 +25,7 @@ type Auth struct {
var (
_ component.Routeable = (*Auth)(nil)
_ component.Menuable = (*Auth)(nil)
_ component.Table = (*Auth)(nil)
)

View file

@ -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
}
}

View file

@ -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"]

View file

@ -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> &gt;
<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 }}

View file

@ -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> &gt;
<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>

View file

@ -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> &gt;
<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>

View file

@ -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> &gt;
<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 }}">

View file

@ -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>

View file

@ -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"]

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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> &gt;
<a class="pure-button pure-button-primary" href="/admin/components">Components</a>
</p>
{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -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> &gt;
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/grants/{{ .Instance.Slug }}">Grants</a>
</p>
{{ end }}
{{ define "content" }}
{{ $csrf := .CSRF }}
{{ $slug := .Instance.Slug }}

View file

@ -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>

View file

@ -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> &gt;
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/ingredients/{{ .Instance.Slug }}">Ingredients</a>
</p>
{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -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> &gt;
<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>

View file

@ -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> &gt;
<a class="pure-button" href="/admin/users">Users</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/users/create">Create</a>
</p>
{{ end }}

View file

@ -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> &gt;
<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>

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -32,6 +32,9 @@ func (*Home) Routes() component.Routes {
Prefix: "/",
MatchAllDomains: true,
CSRF: false,
MenuTitle: "WissKI Distillery",
MenuPriority: component.MenuHome,
}
}

View file

@ -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()

View file

@ -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">

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -1,13 +1,6 @@
{{ template "_base.html" . }}
{{ define "title" }}News{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/">Home</a> &gt;
<a class="pure-button pure-button-primary" href="/news/">News</a>
</p>
{{ end }}
{{ define "content" }}
<div class="pure-u-1">

View file

@ -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.

View file

@ -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">`,
}

View file

@ -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
}

View file

@ -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)
)

View 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
}

View file

@ -1 +0,0 @@
html{background-color:#87a485}body{max-width:80vw!important}

File diff suppressed because one or more lines are too long

View file

@ -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;

View file

@ -1,7 +0,0 @@
html {
background-color: #87A485;
}
body {
max-width: 80vw !important;
}

View file

@ -1 +0,0 @@
import "latex.css/style.min.css"

View file

@ -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 }}

View file

@ -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 }}

View 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
)

View file

@ -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)
}

View file

@ -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>

View file

@ -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