Refactor html templates

This commit entirely refactors the use of html templates. Instead of
inheriting from a shared template, we insert the results into a base
template.
This commit is contained in:
Tom Wiesing 2023-01-20 14:42:37 +01:00
parent 6ede99d7c6
commit d235ee4e5c
No known key found for this signature in database
59 changed files with 869 additions and 777 deletions

View file

@ -5,7 +5,7 @@ import (
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -17,7 +17,7 @@ type Auth struct {
Dependencies struct { Dependencies struct {
SQL *sql.SQL SQL *sql.SQL
UserDeleteHooks []component.UserDeleteHook UserDeleteHooks []component.UserDeleteHook
Templating *templates.Templating Templating *templating.Templating
} }
store lazy.Lazy[sessions.Store] store lazy.Lazy[sessions.Store]

View file

@ -1,5 +1,4 @@
{{ template "_form.html" . }} {{ template "form.html" . }}
{{ define "form/title" }}Login Required{{ end }}
{{ define "form/button" }}Login{{ end }} {{ define "form/button" }}Login{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div class="pure-form-group"> <div class="pure-form-group">

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -20,7 +20,7 @@ type UserPanel struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
Auth *auth.Auth Auth *auth.Auth
Templating *templates.Templating Templating *templating.Templating
Policy *policy.Policy Policy *policy.Policy
Instances *instances.Instances Instances *instances.Instances
Next *next.Next Next *next.Next
@ -106,30 +106,25 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
} }
type userFormContext struct { type userFormContext struct {
templates.BaseContext templating.RuntimeFlags
httpx.FormContext httpx.FormContext
User *models.User User *models.User
} }
func (panel *UserPanel) UserFormContext2(tpl *templates.Template[userFormContext], last component.MenuItem, gaps ...templates.BaseContextGaps) func(ctx httpx.FormContext, r *http.Request) any { func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext], last component.MenuItem, funcs ...templating.FlagFunc) func(ctx httpx.FormContext, r *http.Request) any {
var g templates.BaseContextGaps funcs = append(funcs, func(flags templating.Flags, r *http.Request) templating.Flags {
if len(gaps) > 1 { flags.Crumbs = append(flags.Crumbs, component.MenuItem{})
panic("UserFormContext2: gaps must be of length 0 or 1") copy(flags.Crumbs[1:], flags.Crumbs)
} flags.Crumbs[0] = component.MenuItem{Title: "User", Path: "/user/"}
if len(gaps) == 1 { return flags
g = gaps[0] })
}
g.Crumbs = []component.MenuItem{
{Title: "User", Path: "/user/"},
last,
}
return templates.MappedHandler(tpl, func(ctx httpx.FormContext, r *http.Request) (userFormContext, templates.BaseContextGaps) { return func(ctx httpx.FormContext, r *http.Request) any {
uctx := userFormContext{FormContext: ctx} uctx := userFormContext{FormContext: ctx}
if user, err := panel.Dependencies.Auth.UserOf(r); err == nil { if user, err := panel.Dependencies.Auth.UserOf(r); err == nil {
uctx.User = &user.User uctx.User = &user.User
} }
return uctx, g return tpl.Context(r, uctx, funcs...)
}) }
} }

View file

@ -9,14 +9,19 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
templating "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
) )
//go:embed "templates/password.html" //go:embed "templates/password.html"
var passwordHTML []byte var passwordHTML []byte
var passwordTemplate = templating.Parse[userFormContext]("password.html", passwordHTML, assets.AssetsUser) var passwordTemplate = templating.Parse[userFormContext](
"password.html", passwordHTML, httpx.FormTemplate,
templating.Title("Change Password"),
templating.Assets(assets.AssetsUser),
)
var ( var (
errPasswordsNotIdentical = errors.New("passwords are not identical") errPasswordsNotIdentical = errors.New("passwords are not identical")
@ -40,7 +45,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(), RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Change Password", Path: "/user/password/"}), RenderTemplateContext: panel.UserFormContext(tpl, component.MenuItem{Title: "Change Password", Path: "/user/password/"}),
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["otp"], values["new"], values["new2"] old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"]

View file

@ -8,7 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "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/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
@ -22,10 +22,15 @@ import (
//go:embed "templates/ssh.html" //go:embed "templates/ssh.html"
var sshHTML []byte var sshHTML []byte
var sshTemplate = templates.Parse[SSHTemplateContext]("ssh.html", sshHTML, assets.AssetsUser) var sshTemplate = templating.Parse[SSHTemplateContext](
"ssh.html", sshHTML, nil,
templating.Title("SSH Keys"),
templating.Assets(assets.AssetsUser),
)
type SSHTemplateContext struct { type SSHTemplateContext struct {
templates.BaseContext templating.RuntimeFlags
Keys []models.Keys Keys []models.Keys
@ -37,15 +42,16 @@ type SSHTemplateContext struct {
} }
func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
tpl := sshTemplate.Prepare(panel.Dependencies.Templating, templates.BaseContextGaps{ tpl := sshTemplate.Prepare(
Crumbs: []component.MenuItem{ panel.Dependencies.Templating,
{Title: "User", Path: "/user/"}, templating.Crumbs(
{Title: "SSH Keys", Path: "/user/ssh/"}, component.MenuItem{Title: "User", Path: "/user/"},
}, component.MenuItem{Title: "SSH Keys", Path: "/user/ssh/"},
Actions: []component.MenuItem{ ),
{Title: "Add New Key", Path: "/user/ssh/add/"}, templating.Actions(
}, component.MenuItem{Title: "Add New Key", Path: "/user/ssh/add/"},
}) ),
)
return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) {
user, err := panel.Dependencies.Auth.UserOf(r) user, err := panel.Dependencies.Auth.UserOf(r)
@ -114,7 +120,11 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
//go:embed "templates/ssh_add.html" //go:embed "templates/ssh_add.html"
var sshAddHTML []byte var sshAddHTML []byte
var sshAddTemplate = templates.ParseForm("ssh_add.html", sshAddHTML, assets.AssetsUser) var sshAddTemplate = templating.ParseForm(
"ssh_add.html", sshAddHTML, httpx.FormTemplate,
templating.Title("Add SSH Key"),
templating.Assets(assets.AssetsUser),
)
type addKeyResult struct { type addKeyResult struct {
User *auth.AuthUser User *auth.AuthUser
@ -123,13 +133,14 @@ type addKeyResult struct {
} }
func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler { func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
tpl := sshAddTemplate.Prepare(panel.Dependencies.Templating, templates.BaseContextGaps{ tpl := sshAddTemplate.Prepare(
Crumbs: []component.MenuItem{ panel.Dependencies.Templating,
{Title: "User", Path: "/user/"}, templating.Crumbs(
{Title: "SSH Keys", Path: "/user/ssh/"}, component.MenuItem{Title: "User", Path: "/user/"},
{Title: "Add New Key", Path: "/user/ssh/add/"}, component.MenuItem{Title: "SSH Keys", Path: "/user/ssh/"},
}, component.MenuItem{Title: "Add New Key", Path: "/user/ssh/add/"},
}) ),
)
return &httpx.Form[addKeyResult]{ return &httpx.Form[addKeyResult]{
Fields: []field.Field{ Fields: []field.Field{
@ -139,7 +150,7 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(), RenderTemplate: tpl.Template(),
RenderTemplateContext: templates.FormTemplateContext(tpl), RenderTemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) { Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) {
ak.User, err = panel.Dependencies.Auth.UserOf(r) ak.User, err = panel.Dependencies.Auth.UserOf(r)

View file

@ -1,3 +1,2 @@
{{ template "_form.html" . }} {{ template "form.html" . }}
{{ define "form/title" }}Change Password{{ end }}
{{ define "form/button" }}Update{{ end }} {{ define "form/button" }}Update{{ end }}

View file

@ -1,8 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}SSH Keys{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
This page allows you to add, view and remove ssh keys to and from your distillery account. This page allows you to add, view and remove ssh keys to and from your distillery account.
@ -101,4 +96,3 @@ Host {{ .Domain }}.proxy
</code> </code>
</div> </div>
</div> </div>
{{ end }}

View file

@ -1,7 +1,4 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Add SSH Key{{ end }}
{{ define "form/button" }}Add{{ end }} {{ define "form/button" }}Add{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>
<p> <p>

View file

@ -1,5 +1,5 @@
{{ template "_form.html" . }} {{ template "form.html" . }}
{{ define "form/title" }}Disable TOTP{{ end }}
{{ define "form/button" }}Disable{{ end }} {{ define "form/button" }}Disable{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}

View file

@ -1,5 +1,4 @@
{{ template "_form.html" . }} {{ template "form.html" . }}
{{ define "form/title" }}Enable TOTP{{ end }}
{{ define "form/button" }}Enable{{ end }} {{ define "form/button" }}Enable{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>

View file

@ -1,5 +1,3 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Enable TOTP{{ end }}
{{ define "form/button" }}Enable{{ end }} {{ define "form/button" }}Enable{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .User.User }}{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
<ul> <ul>
@ -80,5 +76,3 @@
</div> </div>
</div> </div>
</div> </div>
{{ end }}

View file

@ -8,7 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "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/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
@ -17,7 +17,12 @@ import (
//go:embed "templates/totp_enable.html" //go:embed "templates/totp_enable.html"
var totpEnableHTML []byte var totpEnableHTML []byte
var totpEnable = templates.Parse[userFormContext]("totp_enable.html", totpEnableHTML, assets.AssetsUser) var totpEnable = templating.Parse[userFormContext](
"totp_enable.html", totpEnableHTML, httpx.FormTemplate,
templating.Title("Enable TOTP"),
templating.Assets(assets.AssetsUser),
)
func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
tpl := totpEnable.Prepare(panel.Dependencies.Templating) tpl := totpEnable.Prepare(panel.Dependencies.Templating)
@ -34,7 +39,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
}, },
RenderTemplate: tpl.Template(), RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}), RenderTemplateContext: panel.UserFormContext(tpl, component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}),
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"]
@ -69,7 +74,12 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
//go:embed "templates/totp_enroll.html" //go:embed "templates/totp_enroll.html"
var totpEnrollHTML []byte var totpEnrollHTML []byte
var totpEnrollTemplate = templates.Parse[totpEnrollContext]("totp_enroll.html", totpEnrollHTML, assets.AssetsUser) var totpEnrollTemplate = templating.Parse[totpEnrollContext](
"totp_enroll.html", totpEnrollHTML, httpx.FormTemplate,
templating.Title("Enable TOTP"),
templating.Assets(assets.AssetsUser),
)
type totpEnrollContext struct { type totpEnrollContext struct {
userFormContext userFormContext
@ -80,12 +90,13 @@ type totpEnrollContext struct {
} }
func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
tpl := totpEnrollTemplate.Prepare(panel.Dependencies.Templating, templates.BaseContextGaps{ tpl := totpEnrollTemplate.Prepare(
Crumbs: []component.MenuItem{ panel.Dependencies.Templating,
{Title: "User", Path: "/user/"}, templating.Crumbs(
{Title: "Enable TOTP", Path: "/user/totp/enable/"}, component.MenuItem{Title: "User", Path: "/user/"},
}, component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"},
}) ),
)
return &httpx.Form[struct{}]{ return &httpx.Form[struct{}]{
Fields: []field.Field{ Fields: []field.Field{
@ -98,9 +109,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
user, err := panel.Dependencies.Auth.UserOf(r) user, err := panel.Dependencies.Auth.UserOf(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
}, },
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { RenderTemplateContext: func(context httpx.FormContext, r *http.Request) any {
// TODO: Do we want to reuse the same function here?
user, err := panel.Dependencies.Auth.UserOf(r) user, err := panel.Dependencies.Auth.UserOf(r)
ctx := totpEnrollContext{ ctx := totpEnrollContext{
@ -120,8 +129,10 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
ctx.TOTPURL = template.URL(secret.URL()) ctx.TOTPURL = template.URL(secret.URL())
} }
} }
tpl.Execute(w, r, ctx)
return tpl.Context(r, ctx)
}, },
RenderTemplate: tpl.Template(),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"] password, otp := values["password"], values["otp"]
@ -156,7 +167,12 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
//go:embed "templates/totp_disable.html" //go:embed "templates/totp_disable.html"
var totpDisableHTML []byte var totpDisableHTML []byte
var totpDisableTemplate = templates.Parse[userFormContext]("totp_disable.html", totpDisableHTML, assets.AssetsUser) var totpDisableTemplate = templating.Parse[userFormContext](
"totp_disable.html", totpDisableHTML, httpx.FormTemplate,
templating.Title("Disable TOTP"),
templating.Assets(assets.AssetsUser),
)
func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
tpl := totpDisableTemplate.Prepare(panel.Dependencies.Templating) tpl := totpDisableTemplate.Prepare(panel.Dependencies.Templating)
@ -173,7 +189,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled() return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
}, },
RenderTemplate: tpl.Template(), RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}), RenderTemplateContext: panel.UserFormContext(tpl, component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"] password, otp := values["password"], values["otp"]

View file

@ -10,16 +10,20 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "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/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
) )
//go:embed "templates/user.html" //go:embed "templates/user.html"
var userHTML []byte var userHTML []byte
var userTemplate = templates.Parse[userContext]("user.html", userHTML, assets.AssetsUser) var userTemplate = templating.Parse[userContext](
"user.html", userHTML, nil,
templating.Assets(assets.AssetsUser),
)
type userContext struct { type userContext struct {
templates.BaseContext templating.RuntimeFlags
*auth.AuthUser *auth.AuthUser
Grants []GrantWithURL Grants []GrantWithURL
@ -31,41 +35,47 @@ type GrantWithURL struct {
} }
func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
tpl := userTemplate.Prepare(panel.Dependencies.Templating, templates.BaseContextGaps{ tpl := userTemplate.Prepare(
Crumbs: []component.MenuItem{ panel.Dependencies.Templating,
{Title: "User", Path: "/user/"}, templating.Crumbs(
}, component.MenuItem{Title: "User", Path: "/user/"},
Actions: []component.MenuItem{ ),
{Title: "Change Password", Path: "/user/password/"}, templating.Actions(
{Title: "*to be replaced*", Path: ""}, component.MenuItem{Title: "Change Password", Path: "/user/password/"},
{Title: "SSH Keys", Path: "/user/ssh/"}, component.DummyMenuItem,
}, component.MenuItem{Title: "SSH Keys", Path: "/user/ssh/"},
}) ),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (uc userContext, err error) { return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {
// find the user // find the user
uc.AuthUser, err = panel.Dependencies.Auth.UserOf(r) uc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
if err != nil || uc.AuthUser == nil { if err != nil || uc.AuthUser == nil {
return uc, err return uc, nil, err
} }
// build the gaps // replace the totp action in the menu
var totpAction component.MenuItem
if uc.AuthUser.IsTOTPEnabled() { if uc.AuthUser.IsTOTPEnabled() {
gaps.Actions[1] = component.MenuItem{ totpAction = component.MenuItem{
Title: "Disable Passcode (TOTP)", Title: "Disable Passcode (TOTP)",
Path: "/user/totp/disable/", Path: "/user/totp/disable/",
} }
} else { } else {
gaps.Actions[1] = component.MenuItem{ totpAction = component.MenuItem{
Title: "Enable Passcode (TOTP)", Title: "Enable Passcode (TOTP)",
Path: "/user/totp/enable/", Path: "/user/totp/enable/",
} }
} }
funcs = []templating.FlagFunc{
templating.ReplaceAction(1, totpAction),
templating.Title(uc.AuthUser.User.User),
}
// find the grants // find the grants
grants, err := panel.Dependencies.Policy.User(r.Context(), uc.AuthUser.User.User) grants, err := panel.Dependencies.Policy.User(r.Context(), uc.AuthUser.User.User)
if err != nil { if err != nil {
return uc, err return uc, nil, err
} }
uc.Grants = make([]GrantWithURL, len(grants)) uc.Grants = make([]GrantWithURL, len(grants))
@ -74,11 +84,11 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
url, err := panel.Dependencies.Next.Next(r.Context(), grant.Slug, "/") url, err := panel.Dependencies.Next.Next(r.Context(), grant.Slug, "/")
if err != nil { if err != nil {
return uc, err return uc, nil, err
} }
uc.Grants[i].URL = template.URL(url) uc.Grants[i].URL = template.URL(url)
} }
return uc, err return uc, funcs, err
}) })
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -120,7 +120,12 @@ func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error {
//go:embed "login.html" //go:embed "login.html"
var loginHTML []byte var loginHTML []byte
var loginTemplate = templates.ParseForm("login.html", loginHTML, assets.AssetsUser) var loginTemplate = templating.ParseForm(
"login.html", loginHTML, httpx.FormTemplate,
templating.Title("Login Required"),
templating.Assets(assets.AssetsUser),
)
var loginResponse = httpx.Response{ var loginResponse = httpx.Response{
ContentType: "text/plain", ContentType: "text/plain",
@ -131,7 +136,15 @@ var errLoginFailed = errors.New("Login failed")
// 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 {
tpl := loginTemplate.Prepare(auth.Dependencies.Templating) tpl := loginTemplate.Prepare(
auth.Dependencies.Templating,
func(flags templating.Flags, r *http.Request) templating.Flags {
flags.Crumbs = []component.MenuItem{
{Title: "Login", Path: template.URL(r.URL.RequestURI())},
}
return flags
},
)
return &httpx.Form[*AuthUser]{ return &httpx.Form[*AuthUser]{
Fields: []field.Field{ Fields: []field.Field{
@ -141,16 +154,13 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
}, },
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any {
if context.Err != nil { if ctx.Err != nil {
context.Err = errLoginFailed ctx.Err = errLoginFailed
} }
tpl.Execute(w, r, templates.BaseFormContext{FormContext: context}, templates.BaseContextGaps{ return tpl.Context(r, templating.NewFormContext(ctx))
Crumbs: []component.MenuItem{
{Title: "Login", Path: template.URL(r.URL.RequestURI())},
},
})
}, },
RenderTemplate: tpl.Template(),
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {
username, password, passcode := values["username"], values["password"], values["otp"] username, password, passcode := values["username"], values["password"], values["otp"]

View file

@ -19,6 +19,12 @@ type MenuItem struct {
Priority MenuPriority // menu priority Priority MenuPriority // menu priority
} }
// DummyMenuItem is a dummy menu item
// It should be replaced before being displayed to the user
var DummyMenuItem = MenuItem{
Title: "* to be replaced *",
}
func MenuItemSort(a, b MenuItem) bool { func MenuItemSort(a, b MenuItem) bool {
return a.Priority < b.Priority return a.Priority < b.Priority
} }

View file

@ -13,7 +13,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -24,7 +25,7 @@ type Resolver struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
Instances *instances.Instances Instances *instances.Instances
Templating *templates.Templating Templating *templating.Templating
Auth *auth.Auth Auth *auth.Auth
} }
@ -50,36 +51,30 @@ func (resolver *Resolver) Routes() component.Routes {
//go:embed "resolver.html" //go:embed "resolver.html"
var resolverHTML []byte var resolverHTML []byte
var resolverTemplate = templates.Parse[resolverContext]("resolver.html", resolverHTML, assets.AssetsDefault) var resolverTemplate = templating.Parse[resolverContext](
"resolver.html", resolverHTML, nil,
templating.Title("Resolver"),
templating.Assets(assets.AssetsDefault),
)
type resolverContext struct { type resolverContext struct {
templates.BaseContext templating.RuntimeFlags
wdresolve.IndexContext wdresolve.IndexContext
} }
func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) { func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
tpl := resolverTemplate.Prepare(resolver.Dependencies.Templating, templates.BaseContextGaps{ // get the resolver template
Crumbs: []component.MenuItem{ tpl := resolverTemplate.Prepare(
{Title: "Resolver", Path: "/wisski/get/"}, resolver.Dependencies.Templating,
}, templating.Crumbs(
}) component.MenuItem{Title: "Resolver", Path: "/wisski/get/"},
),
)
t := tpl.Template()
// extract a logger and the fallback
logger := zerolog.Ctx(ctx) logger := zerolog.Ctx(ctx)
var p wdresolve.ResolveHandler
var err error
p.HandleIndex = func(context wdresolve.IndexContext, w http.ResponseWriter, r *http.Request) {
ctx := resolverContext{
IndexContext: context,
}
if !resolver.Dependencies.Auth.Has(auth.User, r) {
ctx.IndexContext.Prefixes = nil
}
tpl.Execute(w, r, ctx)
}
p.TrustXForwardedProto = true
fallback := &resolvers.Regexp{ fallback := &resolvers.Regexp{
Data: map[string]string{}, Data: map[string]string{},
} }
@ -97,12 +92,26 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
logger.Info().Str("name", domainName).Msg("registering legacy domain") logger.Info().Str("name", domainName).Msg("registering legacy domain")
} }
// resolve the prefixes p := wdresolve.ResolveHandler{
p.Resolver = resolvers.InOrder{ HandleIndex: func(context wdresolve.IndexContext, w http.ResponseWriter, r *http.Request) {
resolver, ctx := resolverContext{
fallback, IndexContext: context,
}
if !resolver.Dependencies.Auth.Has(auth.User, r) {
ctx.IndexContext.Prefixes = nil
}
httpx.WriteHTML(tpl.Context(r, ctx), nil, t, "", w, r)
},
Resolver: resolvers.InOrder{
resolver,
fallback,
},
TrustXForwardedProto: true,
} }
return p, err
return p, nil
} }
func (resolver *Resolver) Target(uri string) string { func (resolver *Resolver) Target(uri string) string {

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Resolver{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
This page contains the global distillery resolver. This page contains the global distillery resolver.
@ -62,4 +58,3 @@
</div> </div>
</div> </div>
{{ end }} {{ end }}
{{ end }}

View file

@ -10,7 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -32,7 +32,7 @@ type Admin struct {
Policy *policy.Policy Policy *policy.Policy
Templating *templates.Templating Templating *templating.Templating
Purger *purger.Purger Purger *purger.Purger
} }

View file

@ -10,75 +10,71 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
//go:embed "html/components.html" //go:embed "html/anal.html"
var componentsHTML []byte var analHTML []byte
var componentsTemplate = templates.Parse[componentContext]("components.html", componentsHTML, assets.AssetsAdmin) var analTemplate = templating.Parse[analContext](
"anal.html", analHTML, nil,
type componentContext struct { templating.Assets(assets.AssetsAdmin),
templates.BaseContext )
type analContext struct {
templating.RuntimeFlags
Analytics lazy.PoolAnalytics Analytics lazy.PoolAnalytics
} }
func (admin *Admin) components(ctx context.Context) http.Handler { func (admin *Admin) components(ctx context.Context) http.Handler {
tpl := componentsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := analTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Components", Path: "/admin/components/"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
}, component.MenuItem{Title: "Components", Path: "/admin/components/"},
}) ),
templating.Title("Components"),
)
return tpl.HTMLHandler(func(r *http.Request) (cp componentContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (ac analContext, err error) {
cp.Analytics = *admin.Analytics ac.Analytics = *admin.Analytics
return return
}) })
} }
//go:embed "html/ingredients.html"
var ingredientsHTML []byte
var ingredientsTemplate = templates.Parse[ingredientsContext]("ingredients.html", ingredientsHTML, assets.AssetsAdmin)
type ingredientsContext struct {
templates.BaseContext
Instance models.Instance
Analytics *lazy.PoolAnalytics
}
func (admin *Admin) ingredients(ctx context.Context) http.Handler { func (admin *Admin) ingredients(ctx context.Context) http.Handler {
tpl := ingredientsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := analTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Instance", Path: "* to be updated *"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
{Title: "Ingredients", Path: "* to be updated *"}, component.DummyMenuItem,
}, component.DummyMenuItem,
}) ),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic ingredientsContext, err error) { return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ac analContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Crumbs[2] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}
// find the instance itself! // find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
return ic, httpx.ErrNotFound return ac, nil, httpx.ErrNotFound
} }
if err != nil { if err != nil {
return ic, err return ac, nil, err
}
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(2, component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}),
templating.Title(instance.Name() + " - Ingredients"),
} }
ic.Instance = instance.Instance
// and get the components // and get the components
ic.Analytics = instance.Info().Analytics ac.Analytics = *instance.Info().Analytics
return return
}) })

View file

@ -10,7 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -22,10 +22,14 @@ import (
//go:embed "html/grants.html" //go:embed "html/grants.html"
var grantsHTML []byte var grantsHTML []byte
var grantsTemplate = templates.Parse[grantsContext]("grants.html", grantsHTML, assets.AssetsAdmin) var grantsTemplate = templating.Parse[grantsContext](
"grants.html", grantsHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type grantsContext struct { type grantsContext struct {
templates.BaseContext templating.RuntimeFlags
Error string Error string
@ -38,40 +42,43 @@ type grantsContext struct {
} }
func (admin *Admin) grants(ctx context.Context) http.Handler { func (admin *Admin) grants(ctx context.Context) http.Handler {
tpl := grantsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := grantsTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Instance", Path: "*to be updated*"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
{Title: "Grants", Path: "*to be updated*"}, component.DummyMenuItem,
}, component.DummyMenuItem,
}) ),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (grantsContext, error) { return tpl.HTMLHandlerWithFlags(func(r *http.Request) (grantsContext, []templating.FlagFunc, error) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
return admin.getGrants(r, gaps) return admin.getGrants(r)
} else { } else {
return admin.postGrants(r, gaps) return admin.postGrants(r)
} }
}) })
} }
func (admin *Admin) getGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) { func (admin *Admin) getGrants(r *http.Request) (gc grantsContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
if err := gc.use(r, gaps, slug, admin); err != nil {
return gc, err funcs, err = gc.use(r, slug, admin)
if err != nil {
return gc, nil, err
} }
if err := gc.useGrants(r, admin); err != nil { if err := gc.useGrants(r, admin); err != nil {
return gc, err return gc, nil, err
} }
return gc, nil return gc, funcs, nil
} }
func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) { func (admin *Admin) postGrants(r *http.Request) (gc grantsContext, funcs []templating.FlagFunc, err error) {
// parse the form // parse the form
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return gc, err return gc, nil, err
} }
// read out the form values // read out the form values
@ -84,15 +91,16 @@ func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps)
) )
// set the common fields // set the common fields
if err := gc.use(r, gaps, slug, admin); err != nil { funcs, err = gc.use(r, slug, admin)
return gc, err if err != nil {
return gc, nil, err
} }
if delete { if delete {
// delete the user grant // delete the user grant
err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug) err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug)
if err != nil { if err != nil {
return gc, err return gc, nil, err
} }
} else { } else {
// update the grant // update the grant
@ -110,26 +118,29 @@ func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps)
// fetch the grants for the instance // fetch the grants for the instance
if err := gc.useGrants(r, admin); err != nil { if err := gc.useGrants(r, admin); err != nil {
return gc, err return gc, nil, err
} }
return gc, nil return gc, funcs, nil
} }
func (gc *grantsContext) use(r *http.Request, gaps *templates.BaseContextGaps, slug string, admin *Admin) (err error) { func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (funcs []templating.FlagFunc, err error) {
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Crumbs[2] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}
// find the instance itself // find the instance itself
gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug) gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
return httpx.ErrNotFound return nil, httpx.ErrNotFound
} }
if err != nil { if err != nil {
return err return nil, err
} }
gc.Instance = gc.instance.Instance gc.Instance = gc.instance.Instance
return nil // replace the functions
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(2, component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}),
templating.Title(gc.Instance.Slug + " - Grants"),
}
return funcs, nil
} }
func (gc *grantsContext) useGrants(r *http.Request, admin *Admin) (err error) { func (gc *grantsContext) useGrants(r *http.Request, admin *Admin) (err error) {

View file

@ -1,6 +0,0 @@
{{ template "_base.html" . }}
{{ define "title" }}Components{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }} - Grants{{ end }}
{{ define "content" }}
{{ $csrf := .CSRF }} {{ $csrf := .CSRF }}
{{ $slug := .Instance.Slug }} {{ $slug := .Instance.Slug }}
<div class="pure-u-1-1"> <div class="pure-u-1-1">
@ -124,4 +120,3 @@
<option value="{{ $drupal }}"> <option value="{{ $drupal }}">
{{ end }} {{ end }}
</datalist> </datalist>
{{ end }}

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Admin{{ end }}
{{ define "content" }}
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<h2 id="overview">Distillery Configuration</h2> <h2 id="overview">Distillery Configuration</h2>
</div> </div>
@ -257,4 +253,3 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{ end }}

View file

@ -1,6 +0,0 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }} - Ingredients{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }}{{ end }}
{{ define "content" }}
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<h2 id="overview">Info &amp; Status</h2> <h2 id="overview">Info &amp; Status</h2>
</div> </div>
@ -485,4 +481,3 @@
</fieldset> </fieldset>
</form> </form>
</div> </div>
{{ end }}

View file

@ -1,4 +1,2 @@
{{ template "_form.html" . }} {{ template "form.html" . }}
{{ define "form/title" }}Create User{{ end }}
{{ define "form/button" }}Create{{ end }} {{ define "form/button" }}Create{{ end }}

View file

@ -1,8 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Users{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<div class="padding"> <div class="padding">
<div class="overflow"> <div class="overflow">
@ -105,5 +100,3 @@
</div> </div>
</div> </div>
</div> </div>
{{ end }}

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -80,27 +80,33 @@ func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distiller
//go:embed "html/index.html" //go:embed "html/index.html"
var indexHTML []byte var indexHTML []byte
var indexTemplate = templates.Parse[indexContext]("index.html", indexHTML, assets.AssetsAdmin) var indexTemplate = templating.Parse[indexContext](
"index.html", indexHTML, nil,
templating.Title("Admin"),
templating.Assets(assets.AssetsAdmin),
)
type indexContext struct { type indexContext struct {
templates.BaseContext templating.RuntimeFlags
status.Distillery status.Distillery
Instances []status.WissKI Instances []status.WissKI
} }
func (admin *Admin) index(ctx context.Context) http.Handler { func (admin *Admin) index(ctx context.Context) http.Handler {
tpl := indexTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := indexTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
}, component.MenuItem{Title: "Admin", Path: "/admin/"},
Actions: []component.MenuItem{ ),
{Title: "Users", Path: "/admin/users/"}, templating.Actions(
{Title: "Components", Path: "/admin/components/", Priority: component.SmallButton}, component.MenuItem{Title: "Users", Path: "/admin/users/"},
}, component.MenuItem{Title: "Components", Path: "/admin/components/", Priority: component.SmallButton},
}) ),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (idx indexContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (idx indexContext, err error) {
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true) idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return return
}) })

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -18,49 +18,57 @@ import (
//go:embed "html/instance.html" //go:embed "html/instance.html"
var instanceHTML []byte var instanceHTML []byte
var instanceTemplate = templates.Parse[instanceContext]("instance.html", instanceHTML, assets.AssetsAdmin) var instanceTemplate = templating.Parse[instanceContext](
"instance.html", instanceHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceContext struct { type instanceContext struct {
templates.BaseContext templating.RuntimeFlags
Instance models.Instance Instance models.Instance
Info status.WissKI Info status.WissKI
} }
func (admin *Admin) instance(ctx context.Context) http.Handler { func (admin *Admin) instance(ctx context.Context) http.Handler {
tpl := instanceTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := instanceTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Instance", Path: "*to be replaced*"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
}, component.DummyMenuItem,
Actions: []component.MenuItem{ ),
{Title: "Grants", Path: "*to be replaced*"}, templating.Actions(
{Title: "Ingredients", Path: "*to be replaced*", Priority: component.SmallButton}, component.DummyMenuItem,
}, component.DummyMenuItem,
}) ),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic instanceContext, err error) { return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ic instanceContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Actions[0] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}
gaps.Actions[1] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}
// find the instance itself! // find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
return ic, httpx.ErrNotFound return ic, nil, httpx.ErrNotFound
} }
if err != nil { if err != nil {
return ic, err return ic, nil, err
} }
ic.Instance = instance.Instance ic.Instance = instance.Instance
// get some more info about the wisski // get some more info about the wisski
ic.Info, err = instance.Info().Information(r.Context(), false) ic.Info, err = instance.Info().Information(r.Context(), false)
if err != nil { if err != nil {
return ic, err return ic, nil, err
}
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceAction(0, component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}),
templating.ReplaceAction(1, component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}),
templating.Title(instance.Name()),
} }
return return

View file

@ -11,7 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "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/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -19,25 +19,30 @@ import (
//go:embed "html/users.html" //go:embed "html/users.html"
var usersHTML []byte var usersHTML []byte
var usersTemplate = templates.Parse[usersContext]("user.html", usersHTML, assets.AssetsAdmin) var usersTemplate = templating.Parse[usersContext](
"users.html", usersHTML, nil,
templating.Title("Users"),
templating.Assets(assets.AssetsAdmin),
)
type usersContext struct { type usersContext struct {
templates.BaseContext templating.RuntimeFlags
Error string Error string
Users []*auth.AuthUser Users []*auth.AuthUser
} }
func (admin *Admin) users(ctx context.Context) http.Handler { func (admin *Admin) users(ctx context.Context) http.Handler {
tpl := usersTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := usersTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Users", Path: "/admin/users/"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
}, component.MenuItem{Title: "Users", Path: "/admin/users/"},
Actions: []component.MenuItem{ ),
{Title: "Create New", Path: "/admin/users/create/"}, templating.Actions(
}, component.MenuItem{Title: "Create New", Path: "/admin/users/create/"},
}) ),
)
return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) {
uc.Error = r.URL.Query().Get("error") uc.Error = r.URL.Query().Get("error")
@ -48,7 +53,12 @@ func (admin *Admin) users(ctx context.Context) http.Handler {
//go:embed "html/user_create.html" //go:embed "html/user_create.html"
var userCreateHTML []byte var userCreateHTML []byte
var userCreateTemplate = templates.ParseForm("user_create.html", userCreateHTML, assets.AssetsAdmin) var userCreateTemplate = templating.ParseForm(
"user_create.html", userCreateHTML, httpx.FormTemplate,
templating.Title("Create User"),
templating.Assets(assets.AssetsAdmin),
)
var ( var (
errCreateInvalidUsername = errors.New("invalid username") errCreateInvalidUsername = errors.New("invalid username")
@ -62,13 +72,14 @@ type createUserResult struct {
} }
func (admin *Admin) createUser(ctx context.Context) http.Handler { func (admin *Admin) createUser(ctx context.Context) http.Handler {
tpl := userCreateTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{ tpl := userCreateTemplate.Prepare(
Crumbs: []component.MenuItem{ admin.Dependencies.Templating,
{Title: "Admin", Path: "/admin/"}, templating.Crumbs(
{Title: "Users", Path: "/admin/users"}, component.MenuItem{Title: "Admin", Path: "/admin/"},
{Title: "Create", Path: "/admin/users/create"}, component.MenuItem{Title: "Users", Path: "/admin/users"},
}, component.MenuItem{Title: "Create", Path: "/admin/users/create"},
}) ),
)
return &httpx.Form[createUserResult]{ return &httpx.Form[createUserResult]{
Fields: []field.Field{ Fields: []field.Field{
@ -79,7 +90,7 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(), RenderTemplate: tpl.Template(),
RenderTemplateContext: templates.FormTemplateContext(tpl), RenderTemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) { 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 cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked

View file

@ -17,32 +17,8 @@ import (
// //
// Each asset group should be registered as a parameter to the 'go:generate' line. // Each asset group should be registered as a parameter to the 'go:generate' line.
type Assets struct { type Assets struct {
Scripts string // <script> tags inserted by the asset Scripts template.HTML // <script> tags inserted by the asset
Styles string // <link> tags inserted by the asset Styles template.HTML // <link> tags inserted by the asset
} }
//go:generate node build.mjs Default User Admin //go:generate node build.mjs Default User Admin
// MustParse parses a new template from the given source
// and calls [RegisterAssoc] on it.
func (assets *Assets) MustParse(t *template.Template, value string) *template.Template {
t = template.Must(t.Parse(value))
assets.RegisterAssoc(t)
return t
}
// MustParseShared is like [MustParse], but creates a new SharedTemplate instead
func (assets *Assets) MustParseShared(name string, value string) *template.Template {
return assets.MustParse(NewSharedTemplate(name), value)
}
// RegisterAssoc registers two new associated templates with t.
//
// The template "scripts" will render all script tags required.
// The template "styles" will render all style tags required.
//
// If either template already exists, it will be overwritten.
func (assets *Assets) RegisterAssoc(t *template.Template) {
t.New("scripts").Parse(assets.Scripts)
t.New("styles").Parse(assets.Styles)
}

View file

@ -1,23 +0,0 @@
package assets
import (
"embed"
"html/template"
)
//go:embed "templates/*.html"
var templates embed.FS
var (
shared *template.Template = template.Must(template.ParseFS(templates, "templates/*.html"))
)
// NewSharedTemplate creates a new template with the given name.
// It will be able to make use of shared templates as well as functions.
func NewSharedTemplate(name string) *template.Template {
new := template.New(name)
for _, template := range shared.Templates() {
new.AddParseTree(template.Tree.Name, template.Tree.Copy())
}
return new
}

View file

@ -0,0 +1,5 @@
<div class="pure-u-1">
<p>
For more information, see <a href="{{ .SelfRedirect }}">{{ .SelfRedirect }}</a>.
</p>
</div>

View file

@ -7,7 +7,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/FAU-CDI/wisski-distillery/pkg/lazy"
) )
@ -15,7 +15,7 @@ import (
type Home struct { type Home struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
Templating *templates.Templating Templating *templating.Templating
Instances *instances.Instances Instances *instances.Instances
} }

View file

@ -3,41 +3,73 @@ package home
import ( import (
"context" "context"
_ "embed" _ "embed"
"html/template"
"net/http" "net/http"
"strings" "strings"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
) )
//go:embed "public.html" //go:embed "public.html"
var publicHTML []byte var publicHTML []byte
var publicTemplate = templates.Parse[publicContext]("public.html", publicHTML, assets.AssetsDefault) var publicTemplate = templating.Parse[publicContext](
"public.html", publicHTML, nil,
type publicContext struct { templating.Title("WissKI Distillery"),
templates.BaseContext templating.Assets(assets.AssetsDefault),
)
//go:embed "about.html"
var aboutHTML string
var aboutTemplate = template.Must(template.New("about.html").Parse(aboutHTML))
// aboutContext is passed to about.html
type aboutContext struct {
Instances []status.WissKI Instances []status.WissKI
SelfRedirect string SelfRedirect string
} }
// publicCOntext is passed to public.html
type publicContext struct {
templating.RuntimeFlags
aboutContext
About template.HTML
}
func (home *Home) publicHandler(ctx context.Context) http.Handler { func (home *Home) publicHandler(ctx context.Context) http.Handler {
tpl := publicTemplate.Prepare(home.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{ tpl := publicTemplate.Prepare(
{Title: "WissKI Distillery", Path: "/"}, home.Dependencies.Templating,
}, templating.Crumbs(
}) component.MenuItem{Title: "WissKI Distillery", Path: "/"},
),
)
about := home.Dependencies.Templating.GetCustomizable(aboutTemplate)
return tpl.HTMLHandler(func(r *http.Request) (pc publicContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (pc publicContext, err error) {
// only act on the root path! // only act on the root path!
if strings.TrimSuffix(r.URL.Path, "/") != "" { if strings.TrimSuffix(r.URL.Path, "/") != "" {
return pc, httpx.ErrNotFound return pc, httpx.ErrNotFound
} }
pc.Instances = home.homeInstances.Get(nil) // prepare about
pc.SelfRedirect = home.Config.SelfRedirect.String() pc.aboutContext.Instances = home.homeInstances.Get(nil)
pc.aboutContext.SelfRedirect = home.Config.SelfRedirect.String()
// render the about template
var builder strings.Builder
if err := about.Execute(&builder, pc.aboutContext); err != nil {
return pc, nil
}
// and return about!
pc.About = template.HTML(builder.String())
return return
}) })

View file

@ -1,14 +1,4 @@
{{ template "_base.html" . }} {{ .About }}
{{ define "title" }}WissKI Distillery{{ end }}
{{ define "content" }}
{{ block "@custom/about" . }}
<div class="pure-u-1">
<p>
For more information, see <a href="{{ .SelfRedirect }}">{{ .SelfRedirect }}</a>.
</p>
</div>
{{ end }}
<div class="pure-u-1"> <div class="pure-u-1">
<h2>WissKIs on this Distillery</h2> <h2>WissKIs on this Distillery</h2>
@ -33,4 +23,3 @@
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }}

View file

@ -8,7 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
_ "embed" _ "embed"
) )
@ -17,7 +17,7 @@ type Legal struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
Static *assets.Static Static *assets.Static
Templating *templates.Templating Templating *templating.Templating
} }
} }
@ -27,10 +27,15 @@ var (
//go:embed "legal.html" //go:embed "legal.html"
var legalHTML []byte var legalHTML []byte
var legalTemplate = templates.Parse[legalContext]("legal.html", legalHTML, assets.AssetsDefault) var legalTemplate = templating.Parse[legalContext](
"legal.html", legalHTML, nil,
templating.Title("Legal"),
templating.Assets(assets.AssetsDefault),
)
type legalContext struct { type legalContext struct {
templates.BaseContext templating.RuntimeFlags
LegalNotices string LegalNotices string
@ -49,11 +54,12 @@ func (legal *Legal) Routes() component.Routes {
} }
func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler, error) { func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
tpl := legalTemplate.Prepare(legal.Dependencies.Templating, templates.BaseContextGaps{ tpl := legalTemplate.Prepare(
Crumbs: []component.MenuItem{ legal.Dependencies.Templating,
{Title: "Legal", Path: "/legal/"}, templating.Crumbs(
}, component.MenuItem{Title: "Legal", Path: "/legal/"},
}) ),
)
return tpl.HTMLHandler(func(r *http.Request) (lc legalContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (lc legalContext, err error) {
lc.LegalNotices = cli.LegalNotices lc.LegalNotices = cli.LegalNotices

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Legal{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<h2 id="cookies">Cookie Usage</h2> <h2 id="cookies">Cookie Usage</h2>
@ -69,4 +65,3 @@
<pre>{{ .AssetsDisclaimer }}</pre> <pre>{{ .AssetsDisclaimer }}</pre>
</div> </div>
{{ end }}

View file

@ -11,7 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
gmmeta "github.com/yuin/goldmark-meta" gmmeta "github.com/yuin/goldmark-meta"
@ -22,7 +22,7 @@ import (
type News struct { type News struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
Templating *templates.Templating Templating *templating.Templating
} }
} }
@ -113,20 +113,26 @@ func Items() ([]Item, error) {
//go:embed "news.html" //go:embed "news.html"
var newsHTML []byte var newsHTML []byte
var newsTemplate = templates.Parse[newsContext]("news.html", newsHTML, assets.AssetsDefault) var newsTemplate = templating.Parse[newsContext](
"news.html", newsHTML, nil,
templating.Title("News"),
templating.Assets(assets.AssetsDefault),
)
type newsContext struct { type newsContext struct {
templates.BaseContext templating.RuntimeFlags
Items []Item Items []Item
} }
// HandleRoute returns the handler for the requested path // HandleRoute returns the handler for the requested path
func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) { func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
tpl := newsTemplate.Prepare(news.Dependencies.Templating, templates.BaseContextGaps{ tpl := newsTemplate.Prepare(
Crumbs: []component.MenuItem{ news.Dependencies.Templating,
{Title: "News", Path: "/news/"}, templating.Crumbs(
}, component.MenuItem{Title: "News", Path: "/news/"},
}) ),
)
items, itemsErr := Items() items, itemsErr := Items()
if itemsErr != nil { if itemsErr != nil {

View file

@ -1,8 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}News{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
This page contains news items from the distillery. This page contains news items from the distillery.
</div> </div>
@ -14,5 +9,3 @@
{{ .Content }} {{ .Content }}
</div> </div>
{{ end }} {{ end }}
{{ end }}

View file

@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel" "github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/mux" "github.com/FAU-CDI/wisski-distillery/pkg/mux"
@ -22,7 +22,7 @@ type Server struct {
Routeables []component.Routeable Routeables []component.Routeable
Cronables []component.Cronable Cronables []component.Cronable
Templating *templates.Templating Templating *templating.Templating
} }
} }

View file

@ -1,102 +0,0 @@
package templates
import (
"html/template"
"net/http"
"reflect"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"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
var baseContextName = reflectx.TypeOf[BaseContext]().Name()
// BaseContext represents a context used by templates
//
// Other invocations might cause an error at runtime.
type BaseContext struct {
inited bool // has this context been inited?
requestWasNil bool // was the passed request nil
GeneratedAt time.Time // time this page was generated at
// Menu and breadcrumbs
Menu []component.MenuItem
BaseContextGaps
CSRF template.HTML // CSRF Field
}
// constants that are used in various parts of the template to render stuff
const (
errorPrefix template.HTML = `<div style="z-index:10000;position:fixed;top:0;left:0;width:100vh;height:100vw;background:red;text-align:center;padding:10vh 10vw;font-size:xx-large;font-weight:bold">`
errorSuffix template.HTML = "</div>"
csrfError template.HTML = errorPrefix + "CSRF used but not provided" + errorSuffix
initError template.HTML = errorPrefix + "<code>BaseContext.use()</code> not called" + errorSuffix
requestNilError template.HTML = errorPrefix + "<code>BaseContext.use()</code> called with nil request" + errorSuffix
)
type BaseContextGaps struct {
Crumbs []component.MenuItem
Actions []component.MenuItem
}
func (bcg BaseContextGaps) clone() BaseContextGaps {
return BaseContextGaps{
Crumbs: slices.Clone(bcg.Crumbs),
Actions: slices.Clone(bcg.Actions),
}
}
// update updates an embedded BaseContext field in context.
func (tpl *Templating) update(context any, r *http.Request, bcg BaseContextGaps) *BaseContext {
tc := reflect.ValueOf(context).
Elem().FieldByName(baseContextName).Addr().
Interface().(*BaseContext)
tc.inited = true
tc.requestWasNil = r == nil
tc.GeneratedAt = time.Now().UTC()
// setup the CSRF field
tc.CSRF = csrfError
if r != nil {
tc.CSRF = csrf.TemplateField(r)
}
// build the menu
tc.Menu = tpl.buildMenu(r)
// build the breadcrumbs
tc.BaseContextGaps = bcg.clone()
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
}
if bc.requestWasNil {
return requestNilError
}
return ""
}
// BaseFormContext combines BaseContext and FormContext
type BaseFormContext struct {
BaseContext
httpx.FormContext
}

View file

@ -1,139 +0,0 @@
package templates
import (
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
// Parsed represents a parsed template that receives an underlying context of type C
type Parsed[C any] struct {
template *template.Template
}
// Parse creates a new Parsed from a template source.
// Parse calls panic() when parsing fails.
func Parse[C any](name string, source []byte, Assets assets.Assets) Parsed[C] {
return Parsed[C]{
template: Assets.MustParseShared(name, string(source)),
}
}
// Prepare prepares this template for use inside a concrete handler.
// gaps must either be of length 0 or length 1 and may pre-fill gaps to be used when executing the template later.
func (p *Parsed[C]) Prepare(tpl *Templating, gaps ...BaseContextGaps) *Template[C] {
wrap := Template[C]{
tpl: tpl,
template: tpl.Template(p.template),
}
if len(gaps) > 1 {
panic("WrapTemplate: must provide either 1 or no gaps")
}
if len(gaps) == 1 {
wrap.gaps = gaps[0]
}
return &wrap
}
// Tempalte represents an executable template.
type Template[C any] struct {
tpl *Templating
template *template.Template
gaps BaseContextGaps
}
// Template returns a template that, if executed together with the context by the Context method, produces the desired result.
func (tw *Template[C]) Template() *template.Template {
return tw.template
}
// Context generates a context for a given request that can be used to execute the provided template.
func (tw *Template[C]) Context(r *http.Request, c C, gaps ...BaseContextGaps) any {
// make the gaps something
if len(gaps) > 1 {
panic("Context: must provide either 1 or no gaps")
}
// update the context with gaps
{
g := tw.gaps
if len(gaps) == 1 {
g = gaps[0]
}
tw.tpl.update(&c, r, g)
}
return c
}
// ParseForm is like Parse[BaseFormContext]
var ParseForm = Parse[BaseFormContext]
// FormTemplateContext returns a new handler for a form with the given base context
func FormTemplateContext(tw *Template[BaseFormContext]) func(ctx httpx.FormContext, r *http.Request) any {
return func(ctx httpx.FormContext, r *http.Request) any {
return tw.Context(r, BaseFormContext{FormContext: ctx})
}
}
// MappedHandler returns a new handler that maps the incoming context via f
func MappedHandler[In, Out any](tw *Template[Out], f func(ctx In, r *http.Request) (Out, BaseContextGaps)) func(ctx In, r *http.Request) any {
// TODO: Should this one be removed?
return func(ctx In, r *http.Request) any {
c, g := f(ctx, r)
return tw.Context(r, c, g)
}
}
// Hander returns a function that returns a context for the given template
func (tw *Template[C]) Handler(f func(r *http.Request) (C, error)) func(r *http.Request) (any, error) {
// TODO: Should this one be removed?
return tw.HandlerWithGaps(func(r *http.Request, gaps *BaseContextGaps) (C, error) {
return f(r)
})
}
// HTMLHandler returns a new HTMLHandler for this request
func (tw *Template[C]) HTMLHandler(f func(r *http.Request) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.Handler(f),
Template: tw.Template(),
}
}
// HandlerWithGaps works like handler, but additionally receives a gaps object to update.
func (tw *Template[C]) HandlerWithGaps(f func(r *http.Request, gaps *BaseContextGaps) (C, error)) func(r *http.Request) (any, error) {
// TODO: Drop this variant?
var zero C
return func(r *http.Request) (any, error) {
g := tw.gaps.clone()
c, err := f(r, &g)
if err != nil {
return zero, err
}
// update the context
return tw.Context(r, c, g), nil
}
}
func (tw *Template[C]) HTMLHandlerWithGaps(f func(r *http.Request, gaps *BaseContextGaps) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.HandlerWithGaps(f),
Template: tw.Template(),
}
}
// Execute executes this template with the given context
func (tw *Template[C]) Execute(w http.ResponseWriter, r *http.Request, c C, gaps ...BaseContextGaps) error {
return tw.ExecuteWithError(w, r, c, nil, gaps...)
}
// ExecuteWithError executes this template, or the default error handler if err != nil
func (tw *Template[C]) ExecuteWithError(w http.ResponseWriter, r *http.Request, c C, err error, gaps ...BaseContextGaps) error {
// TODO: Drop this variant?
// TODO: This should be removed!
return httpx.WriteHTML(tw.Context(r, c, gaps...), err, tw.template, "", w, r)
}

View file

@ -1,65 +0,0 @@
package templates
import (
_ "embed"
"html/template"
"text/template/parse"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
)
const (
footerName = "@custom/footer"
aboutName = "@custom/about"
)
//go:embed "footer.html"
var footerTemplateStr string
var defaultFooterTemplate = template.Must(template.New("footer.html").Parse(footerTemplateStr))
// Template creates a copy of template with shared template parts updated accordingly.
// Any template using this should use one of the template contexts in this package.
func (tpl *Templating) Template(t *template.Template) *template.Template {
// TODO: This should not be used!
// create a clone of the template
clone := template.Must(t.Clone())
// add all the fixed parse trees
footerTree := tpl.getTemplateAsset(defaultFooterTemplate)
template.Must(clone.AddParseTree(footerName, footerTree))
// optionally add the about asset
if aboutTree := tpl.readTemplateAsset("about.html"); clone.Lookup(aboutName) != nil && aboutTree != nil {
template.Must(clone.AddParseTree(aboutName, aboutTree))
}
return clone // and return the tree
}
// getTemplateAsset returns an overridable template asset.
//
// If the asset named can successfully be parsed, it is returned.
// If it can not be parsed, the default template is returned.
func (tpl *Templating) getTemplateAsset(dflt *template.Template) *parse.Tree {
tree := tpl.readTemplateAsset(dflt.Name())
if tree == nil {
return dflt.Tree.Copy()
}
return tree
}
// readTemplateAsset is like getTemplateAssets, but takes an explicit name to read.
// when the asset does not exist, or cannot be opened, returns nil.
func (tpl *Templating) readTemplateAsset(name string) *parse.Tree {
template, err := (func() (*template.Template, error) {
data, err := environment.ReadFile(tpl.Environment, tpl.CustomAssetPath(name))
if err != nil {
return nil, err
}
return template.New(name).Parse(string(data))
})()
if err != nil {
return nil
}
return template.Tree
}

View file

@ -1,4 +1,4 @@
package templates package templating
import ( import (
"path/filepath" "path/filepath"

View file

@ -0,0 +1,193 @@
package templating
import (
"context"
_ "embed"
"fmt"
"html/template"
"net/http"
"reflect"
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/rs/zerolog"
)
//go:embed "src/base.html"
var baseHTML string
var baseTemplate = template.Must(template.New("base.html").Parse(baseHTML))
// Tempalte represents an executable template.
type Template[C any] struct {
templating *Templating
p *Parsed[C]
}
// Template returns a template that, if executed together with the context by the Context method, produces the desired result.
func (tpl *Template[C]) Template() *template.Template {
return baseTemplate
}
// Context generates the context to pass to an instance of the template returned by Template.
func (tpl *Template[C]) Context(r *http.Request, c C, funcs ...FlagFunc) (ctx *tContext[C]) {
// create a new context
ctx = new(tContext[C])
// setup the basic properties
ctx.ctx = r.Context()
ctx.Runtime.RequestURI = r.URL.RequestURI()
ctx.Runtime.GeneratedAt = time.Now().UTC()
ctx.Runtime.CSRF = csrf.TemplateField(r)
ctx.Runtime.Menu = tpl.templating.buildMenu(r)
// generate the rest of the options
ctx.Runtime.Flags = ctx.Runtime.Flags.Apply(r, tpl.p.funcs...)
ctx.Runtime.Flags = ctx.Runtime.Flags.Apply(r, funcs...)
// if the context has a runtime flags embed, then set the field properly
if tpl.p.hasRuntimeFlagsEmbed {
reflect.ValueOf(&c).Elem().
FieldByName(runtimeFlagsName).
Set(reflect.ValueOf(ctx.Runtime))
}
// the main template
ctx.cMain = c
ctx.tMain = tpl.p.tpl
// the footer template
ctx.tFooter = tpl.templating.GetCustomizable(footerTemplate)
ctx.cFooter = ctx.Runtime
return
}
// ParseForm is like Parse[BaseFormContext]
var ParseForm = Parse[FormContext]
type FormContext struct {
httpx.FormContext
RuntimeFlags
}
// NewFormContext returns a new FormContext from an underlying context
func NewFormContext(context httpx.FormContext) FormContext {
return FormContext{FormContext: context}
}
// FormTemplateContext returns a new handler for a form with the given base context
func FormTemplateContext(tw *Template[FormContext]) func(ctx httpx.FormContext, r *http.Request) any {
// TODO: Is this needed?
return func(ctx httpx.FormContext, r *http.Request) any {
return tw.Context(r, FormContext{FormContext: ctx})
}
}
// Hander returns a function that returns a context for the given template
func (tw *Template[C]) Handler(f func(r *http.Request) (C, error)) func(r *http.Request) (any, error) {
// TODO: Should this one be removed?
return tw.HandlerWithFlags(func(r *http.Request) (C, []FlagFunc, error) {
c, err := f(r)
return c, nil, err
})
}
// HTMLHandler returns a new HTMLHandler for this request
func (tw *Template[C]) HTMLHandler(f func(r *http.Request) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.Handler(f),
Template: tw.Template(),
}
}
// HandlerWithFlags works like handler, but additionally receive funcs to generate flags
func (tw *Template[C]) HandlerWithFlags(f func(r *http.Request) (C, []FlagFunc, error)) func(r *http.Request) (any, error) {
return func(r *http.Request) (any, error) {
c, funcs, err := f(r)
if err != nil {
return nil, err
}
return tw.Context(r, c, funcs...), nil
}
}
func (tw *Template[C]) HTMLHandlerWithFlags(f func(r *http.Request) (C, []FlagFunc, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.HandlerWithFlags(f),
Template: tw.Template(),
}
}
// tContext is passed to the underlying template.
//
// Callers may not retain references beyond the invocation of the template.
// Callers must not rely on the internal structure of this tContext.
type tContext[C any] struct {
Runtime RuntimeFlags // underlying flags
ctx context.Context // underlying context for render
// the main template and context
tMain *template.Template
cMain C
// the footer template and context
tFooter *template.Template
cFooter RuntimeFlags
}
// Main renders the main template.
func (ctx *tContext[C]) Main() (template.HTML, error) {
return ctx.renderSafe("main", ctx.tMain, ctx.cMain)
}
// Footer renders the footer template
func (ctx *tContext[C]) Footer() (template.HTML, error) {
return ctx.renderSafe("footer", ctx.tFooter, ctx.cFooter)
}
const renderSafeError = "Error displaying page. See server log for details. "
func (ctx *tContext[C]) renderSafe(name string, t *template.Template, c any) (template.HTML, error) {
// already done
if err := ctx.ctx.Err(); err != nil {
return "", err
}
value, err, panicked := func() (value template.HTML, err error, panicked bool) {
var builder strings.Builder
defer func() {
if panicked {
r := recover()
zerolog.Ctx(ctx.ctx).Error().
Str("uri", ctx.Runtime.RequestURI).
Str("name", name).
Str("panic", fmt.Sprint(r)).
Msg("templating.Main(): template panic()ed")
}
}()
panicked = true
err = t.Execute(&builder, c)
panicked = false
if err != nil {
zerolog.Ctx(ctx.ctx).Err(err).
Str("uri", ctx.Runtime.RequestURI).
Str("name", name).
Msg("template errored")
}
return template.HTML(builder.String()), err, false
}()
if err != nil || panicked {
return renderSafeError, httpx.ErrInternalServerError
}
return value, nil
}

View file

@ -0,0 +1,101 @@
package templating
import (
"html/template"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
)
// Flags represent handle-updatable options for the base template
type Flags struct {
Title string // Title of the menu
assets.Assets // assets are the assets included in the template
Crumbs []component.MenuItem // crumbs are the breadcrumbs leading to a specific action
Actions []component.MenuItem // actions are the actions available to a specific thingy
}
// Apply applies a set of functions to this flags
func (flags Flags) Apply(r *http.Request, funcs ...FlagFunc) Flags {
for _, f := range funcs {
flags = f(flags, r)
}
return flags
}
// RuntimeFlags are passed to the template at runtime.
// Any context may e
type RuntimeFlags struct {
Flags
RequestURI string // request uri of the current page
Menu []component.MenuItem // menu at the top of the page
GeneratedAt time.Time // time the underlying data returned
CSRF template.HTML // csrf data (if any)
}
var runtimeFlagsName = reflectx.TypeOf[RuntimeFlags]().Name()
// Clone clones this flags
func (flags Flags) Clone() Flags {
flags.Crumbs = slices.Clone(flags.Crumbs)
flags.Actions = slices.Clone(flags.Actions)
return flags
}
// FlagFunc updates a flags based on a request.
// FlagFunc may not be nil.
type FlagFunc func(flags Flags, r *http.Request) Flags
// Assets sets the given assets for the given flags
func Assets(Assets assets.Assets) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Assets = Assets
return flags
}
}
// Crumbs sets the crumbs
func Crumbs(crumbs ...component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Crumbs = crumbs
return flags
}
}
// Actions sets the actions
func Actions(actions ...component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Actions = actions
return flags
}
}
// ReplaceAction replaces a specific action
func ReplaceAction(index int, action component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Actions[index] = action
return flags
}
}
// ReplaceCrumb replaces a specific crum
func ReplaceCrumb(index int, action component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Crumbs[index] = action
return flags
}
}
// Title sets the title of this template
func Title(title string) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Title = title
return flags
}
}

View file

@ -1,4 +1,4 @@
package templates package templating
import ( import (
"html/template" "html/template"

View file

@ -0,0 +1,66 @@
package templating
import (
"html/template"
"reflect"
"github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
)
// Parsed represents a parsed template that takes as argument a context of type C.
type Parsed[C any] struct {
// does the context type an embed of the runtime flags type?
hasRuntimeFlagsEmbed bool
tpl *template.Template // parsed template
funcs []FlagFunc // optionally concfigured functions.
}
// Parse parses a template with the given name and source.
// If base is not nil, every template associated with the base template is copied into the given template.
// Functions will be applied on creation time to represent the context for the given template.
func Parse[C any](name string, source []byte, base *template.Template, funcs ...FlagFunc) Parsed[C] {
tp := reflectx.TypeOf[C]()
// determine if we have an embedded field in the struct
var hasEmbed bool
if tp.Kind() == reflect.Struct {
field, ok := tp.FieldByName(runtimeFlagsName)
if ok {
hasEmbed = field.Anonymous
}
}
// create a new template, and optionally inherit from the base template
new := template.New(name)
if base != nil {
for _, tree := range base.Templates() {
root := tree.Tree.Copy()
new.AddParseTree(tree.Name(), root)
}
}
return Parsed[C]{
hasRuntimeFlagsEmbed: hasEmbed,
tpl: template.Must(new.Parse(string(source))),
funcs: funcs,
}
}
// Prepare prepares this template to be used with the given templating.
func (p *Parsed[C]) Prepare(templating *Templating, funcs ...FlagFunc) *Template[C] {
pcopy := *p // make a copy of p!
wrap := Template[C]{
templating: templating,
p: &pcopy,
}
// copy the functions!
pcopy.funcs = slices.Clone(pcopy.funcs)
pcopy.funcs = append(wrap.p.funcs, funcs...)
return &wrap
}

View file

@ -4,15 +4,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ block "title" . }}WissKI Distillery{{ end }}</title> <title>{{ .Runtime.Flags.Title }}</title>
{{ block "styles" . }}styles{{ end }} {{ .Runtime.Flags.Assets.Styles }}
</head> </head>
<body> <body>
{{ .BaseContext.DoInitCheck }}
<nav class="pure-menu pure-menu-horizontal"> <nav class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list" role="menubar"> <ul class="pure-menu-list" role="menubar">
{{ range .BaseContext.Menu }} {{ range .Runtime.Menu }}
<li class="pure-menu-item{{ if .Active }} pure-menu-selected{{ end }}"> <li class="pure-menu-item{{ if .Active }} pure-menu-selected{{ end }}">
<a href="{{ .Path }}" class="pure-menu-link">{{ .Title }}</a> <a href="{{ .Path }}" class="pure-menu-link">{{ .Title }}</a>
</li> </li>
@ -20,16 +19,16 @@
</ul> </ul>
</nav> </nav>
<nav class="breadcrumbs" role="navigation" aria-label="Breadcrumbs"> <nav class="breadcrumbs" role="navigation" aria-label="Breadcrumbs">
{{ range .BaseContext.Crumbs }} {{ range .Runtime.Flags.Crumbs }}
<a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a> <a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a>
{{ end }} {{ end }}
</nav> </nav>
<header> <header>
<h1 id="top">{{ template "title" . }}</h1> <h1 id="top">{{ .Runtime.Flags.Title }}</h1>
{{ if .BaseContext.Actions }} {{ if .Runtime.Flags.Actions }}
<div class="pure-button-group" role="group" aria-label="Actions"> <div class="pure-button-group" role="group" aria-label="Actions">
{{ range .BaseContext.Actions }} {{ range .Runtime.Flags.Actions }}
<a href="{{ .Path }}" class="pure-button{{ if eq .Priority -1 }} pure-button-small{{end}}">{{ .Title }}</a> <a href="{{ .Path }}" class="pure-button{{ if eq .Priority -1 }} pure-button-small{{end}}">{{ .Title }}</a>
{{ end }} {{ end }}
</div> </div>
@ -37,20 +36,16 @@
</header> </header>
<main> <main>
<div class="pure-g"> <div class="pure-g">
{{ block "content" . }}content{{ end }} {{ .Main }}
</div> </div>
</main> </main>
<footer> <footer>
{{ block "@custom/footer" .BaseContext }} {{ .Footer }}
<div style="z-index:10000;position:fixed;top:0;left:0;width:100vh;height:100vw;background:red;text-align:center;padding:10vh 10vw;font-size:xx-large;font-weight:bold">
<code>.Templating.Template()</code> not called
</div>
{{ end }}
</footer> </footer>
{{ block "scripts" . }}scripts{{ end }} {{ .Runtime.Flags.Assets.Scripts }}
</body> </body>
</html> </html>

View file

@ -0,0 +1,29 @@
package templating
import (
_ "embed"
"html/template"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
)
//go:embed "src/footer.html"
var footerHTML string
var footerTemplate = template.Must(template.New("footer.html").Parse(footerHTML))
// GetCustomizable returns either a clone of dflt, or the overriden template with the same name.
func (tpl *Templating) GetCustomizable(dflt *template.Template) *template.Template {
name := dflt.Name()
custom, err := (func() (*template.Template, error) {
data, err := environment.ReadFile(tpl.Environment, tpl.CustomAssetPath(name))
if err != nil {
return nil, err
}
return template.New(name).Parse(string(data))
})()
if err != nil {
return template.Must(dflt.Clone())
}
return custom
}

View file

@ -1,4 +1,4 @@
package templates package templating
import ( import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"

View file

@ -25,7 +25,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/home" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/home"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/legal" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/legal"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/news" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/news"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/solr" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/solr"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
@ -116,8 +116,8 @@ func (dis *Distillery) Info() *admin.Admin {
func (dis *Distillery) Policy() *policy.Policy { func (dis *Distillery) Policy() *policy.Policy {
return export[*policy.Policy](dis) return export[*policy.Policy](dis)
} }
func (dis *Distillery) Templating() *templates.Templating { func (dis *Distillery) Templating() *templating.Templating {
return export[*templates.Templating](dis) return export[*templating.Templating](dis)
} }
func (dis *Distillery) Purger() *purger.Purger { func (dis *Distillery) Purger() *purger.Purger {
@ -190,7 +190,7 @@ func (dis *Distillery) allComponents() []initFunc {
auto[*news.News], auto[*news.News],
auto[*assets.Static], auto[*assets.Static],
auto[*templates.Templating], auto[*templating.Templating],
// Cron // Cron
auto[*cron.Cron], auto[*cron.Cron],

View file

@ -49,10 +49,11 @@ func StatusInterceptor(contentType string, body func(code int, text string) ([]b
return ErrInterceptor{ return ErrInterceptor{
Errors: map[error]Response{ Errors: map[error]Response{
ErrBadRequest: makeResponse(http.StatusBadRequest), ErrInternalServerError: makeResponse(http.StatusInternalServerError),
ErrNotFound: makeResponse(http.StatusNotFound), ErrBadRequest: makeResponse(http.StatusBadRequest),
ErrForbidden: makeResponse(http.StatusForbidden), ErrNotFound: makeResponse(http.StatusNotFound),
ErrMethodNotAllowed: makeResponse(http.StatusMethodNotAllowed), ErrForbidden: makeResponse(http.StatusForbidden),
ErrMethodNotAllowed: makeResponse(http.StatusMethodNotAllowed),
}, },
Fallback: makeResponse(http.StatusInternalServerError), Fallback: makeResponse(http.StatusInternalServerError),
} }
@ -60,10 +61,11 @@ func StatusInterceptor(contentType string, body func(code int, text string) ([]b
// Common errors accepted by all httpx handlers // Common errors accepted by all httpx handlers
var ( var (
ErrBadRequest = errors.New("httpx: Bad Request") ErrInternalServerError = errors.New("httpx: Internal Server Error")
ErrNotFound = errors.New("httpx: Not Found") ErrBadRequest = errors.New("httpx: Bad Request")
ErrForbidden = errors.New("httpx: Forbidden") ErrNotFound = errors.New("httpx: Not Found")
ErrMethodNotAllowed = errors.New("httpx: Method Not Allowed") ErrForbidden = errors.New("httpx: Forbidden")
ErrMethodNotAllowed = errors.New("httpx: Method Not Allowed")
) )
var ( var (

View file

@ -5,6 +5,8 @@ import (
"net/http" "net/http"
"strings" "strings"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@ -173,3 +175,9 @@ 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)
} }
//go:embed "form.html"
var formBytes []byte
// FormTeplate is a template to embed a form
var FormTemplate = template.Must(template.New("form.html").Parse(string(formBytes)))

View file

@ -1,14 +1,8 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }}
{{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
{{ block "form/extra" . }}<!-- no extra -->{{ end }} {{ block "form/extra" . }}<!-- no extra -->{{ end }}
<form class="pure-form pure-form-aligned" method="POST"> <form class="pure-form pure-form-aligned" method="POST">
<fieldset> <fieldset>
<legend>{{ template "form/title" . }}</legend>
{{ block "form/message" . }} {{ block "form/message" . }}
{{ $E := .Error }} {{ $E := .Error }}
{{ if not (eq $E "") }} {{ if not (eq $E "") }}
@ -26,5 +20,3 @@
</fieldset> </fieldset>
</form> </form>
</div> </div>
{{ end }}