diff --git a/internal/dis/component/auth/auth.go b/internal/dis/component/auth/auth.go index 776a076..d45800f 100644 --- a/internal/dis/component/auth/auth.go +++ b/internal/dis/component/auth/auth.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/gorilla/sessions" @@ -16,6 +17,7 @@ type Auth struct { Dependencies struct { SQL *sql.SQL UserDeleteHooks []component.UserDeleteHook + Custom *custom.Custom } store lazy.Lazy[sessions.Store] diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go index 6405e27..1a7de3f 100644 --- a/internal/dis/component/auth/panel/panel.go +++ b/internal/dis/component/auth/panel/panel.go @@ -6,6 +6,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/julienschmidt/httprouter" @@ -14,7 +15,8 @@ import ( type UserPanel struct { component.Base Dependencies struct { - Auth *auth.Auth + Auth *auth.Auth + Custom *custom.Custom } } @@ -67,7 +69,9 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han } type userFormContext struct { + custom.BaseContext httpx.FormContext + User *models.User } @@ -75,6 +79,7 @@ func (panel *UserPanel) UserFormContext(ctx httpx.FormContext, r *http.Request) user, err := panel.Dependencies.Auth.UserOf(r) uctx := userFormContext{FormContext: ctx} + panel.Dependencies.Custom.Update(&uctx) if err == nil { uctx.User = &user.User } diff --git a/internal/dis/component/auth/panel/password.go b/internal/dis/component/auth/panel/password.go index 8671239..4c614d2 100644 --- a/internal/dis/component/auth/panel/password.go +++ b/internal/dis/component/auth/panel/password.go @@ -25,6 +25,8 @@ var ( ) func (panel *UserPanel) routePassword(ctx context.Context) http.Handler { + passwordTemplate := panel.Dependencies.Custom.Template(passwordTemplate) + return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "old", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, diff --git a/internal/dis/component/auth/panel/totp.go b/internal/dis/component/auth/panel/totp.go index 5e1ad19..d88cf7b 100644 --- a/internal/dis/component/auth/panel/totp.go +++ b/internal/dis/component/auth/panel/totp.go @@ -17,6 +17,8 @@ var totpEnableStr string var totpEnableTemplate = static.AssetsUser.MustParseShared("totp_enable.html", totpEnableStr) func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { + totpEnableTemplate := panel.Dependencies.Custom.Template(totpEnableTemplate) + return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, @@ -73,6 +75,8 @@ type totpEnrollContext struct { } func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { + totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate) + return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, @@ -85,6 +89,8 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { + // TODO: Do we want to reuse the same function here? + user, err := panel.Dependencies.Auth.UserOf(r) ctx := totpEnrollContext{ @@ -92,6 +98,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { FormContext: context, }, } + panel.Dependencies.Custom.Update(&ctx.userFormContext) if err == nil && user != nil { ctx.userFormContext.User = &user.User @@ -142,6 +149,8 @@ var totpDisableStr string var totpDisableTemplate = static.AssetsUser.MustParseShared("totp_disable.html", totpDisableStr) func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { + totpDisableTemplate := panel.Dependencies.Custom.Template(totpDisableTemplate) + return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, diff --git a/internal/dis/component/auth/panel/user.go b/internal/dis/component/auth/panel/user.go index 6b31e35..b9629a3 100644 --- a/internal/dis/component/auth/panel/user.go +++ b/internal/dis/component/auth/panel/user.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" ) @@ -18,9 +19,19 @@ var userTemplate = static.AssetsUser.MustParseShared( userHTMLStr, ) +type routeUserContext struct { + custom.BaseContext + *auth.AuthUser +} + func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { - return &httpx.HTMLHandler[*auth.AuthUser]{ - Handler: panel.Dependencies.Auth.UserOf, + userTemplate := panel.Dependencies.Custom.Template(userTemplate) + return &httpx.HTMLHandler[routeUserContext]{ + Handler: func(r *http.Request) (ruc routeUserContext, err error) { + panel.Dependencies.Custom.Update(&ruc) + ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r) + return routeUserContext{}, err + }, Template: userTemplate, } } diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index 4ba10c3..750552a 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -111,6 +111,8 @@ var errLoginFailed = errors.New("Login failed") // authLogin implements a view to login a user func (auth *Auth) authLogin(ctx context.Context) http.Handler { + loginTemplate := auth.Dependencies.Custom.Template(loginTemplate) + return &httpx.Form[*AuthUser]{ Fields: []httpx.Field{ {Name: "username", Type: httpx.TextField, Label: "Username"}, @@ -123,7 +125,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { if context.Err != nil { context.Err = errLoginFailed } - httpx.WriteHTML(context, nil, loginTemplate, "", w, r) + httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context), nil, loginTemplate, "", w, r) }, Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { diff --git a/internal/dis/component/control/admin/admin.go b/internal/dis/component/control/admin/admin.go index 7e90695..7c4a2fc 100644 --- a/internal/dis/component/control/admin/admin.go +++ b/internal/dis/component/control/admin/admin.go @@ -6,6 +6,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger" "github.com/julienschmidt/httprouter" @@ -26,6 +27,8 @@ type Admin struct { SnapshotsLog *logger.Logger Auth *auth.Auth + + Custom *custom.Custom } Analytics *lazy.PoolAnalytics @@ -64,13 +67,13 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http // add a handler for the index page router.Handler(http.MethodGet, route+"index", httpx.HTMLHandler[indexContext]{ Handler: admin.index, - Template: indexTemplate, + Template: admin.Dependencies.Custom.Template(indexTemplate), }) // add a handler for the user page router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{ Handler: admin.users, - Template: userTemplate, + Template: admin.Dependencies.Custom.Template(userTemplate), }) // add a user create form @@ -90,19 +93,19 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http // add a handler for the component page router.Handler(http.MethodGet, route+"components", httpx.HTMLHandler[componentContext]{ Handler: admin.components, - Template: componentsTemplate, + Template: admin.Dependencies.Custom.Template(componentsTemplate), }) // add a handler for the component page router.Handler(http.MethodGet, route+"ingredients/:slug", httpx.HTMLHandler[ingredientsContext]{ Handler: admin.ingredients, - Template: ingredientsTemplate, + Template: admin.Dependencies.Custom.Template(ingredientsTemplate), }) // add a handler for the instance page router.Handler(http.MethodGet, route+"instance/:slug", httpx.HTMLHandler[instanceContext]{ Handler: admin.instance, - Template: instanceTemplate, + Template: admin.Dependencies.Custom.Template(instanceTemplate), }) // add a router for the login page diff --git a/internal/dis/component/control/admin/components.go b/internal/dis/component/control/admin/components.go index 87355de..dbc43a7 100644 --- a/internal/dis/component/control/admin/components.go +++ b/internal/dis/component/control/admin/components.go @@ -2,11 +2,11 @@ package admin import ( "net/http" - "time" _ "embed" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" @@ -22,15 +22,15 @@ var componentsTemplate = static.AssetsAdmin.MustParseShared( ) type componentContext struct { - Time time.Time + custom.BaseContext Analytics lazy.PoolAnalytics } func (admin *Admin) components(r *http.Request) (cp componentContext, err error) { - cp.Analytics = *admin.Analytics - cp.Time = time.Now().UTC() + admin.Dependencies.Custom.Update(&cp) + cp.Analytics = *admin.Analytics return } @@ -42,14 +42,14 @@ var ingredientsTemplate = static.AssetsAdmin.MustParseShared( ) type ingredientsContext struct { - Time time.Time + custom.BaseContext Instance models.Instance Analytics *lazy.PoolAnalytics } func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) { - cp.Time = time.Now().UTC() + admin.Dependencies.Custom.Update(&cp) // find the instance itself! instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["slug"]) diff --git a/internal/dis/component/control/admin/index.go b/internal/dis/component/control/admin/index.go index cfc19f9..372a5fc 100644 --- a/internal/dis/component/control/admin/index.go +++ b/internal/dis/component/control/admin/index.go @@ -9,6 +9,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/status" "golang.org/x/sync/errgroup" ) @@ -79,11 +80,14 @@ func (admin *Admin) Status(ctx context.Context, QuickInformation bool) (target s } type indexContext struct { + custom.BaseContext + status.Distillery Instances []status.WissKI } func (admin *Admin) index(r *http.Request) (idx indexContext, err error) { + admin.Dependencies.Custom.Update(&idx) idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true) return } diff --git a/internal/dis/component/control/admin/instance.go b/internal/dis/component/control/admin/instance.go index cb2d4b8..e75c39d 100644 --- a/internal/dis/component/control/admin/instance.go +++ b/internal/dis/component/control/admin/instance.go @@ -4,9 +4,9 @@ import ( _ "embed" "html/template" "net/http" - "time" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/status" @@ -23,7 +23,7 @@ var instanceTemplate = static.AssetsAdmin.MustParseShared( ) type instanceContext struct { - Time time.Time + custom.BaseContext CSRF template.HTML Instance models.Instance @@ -31,6 +31,8 @@ type instanceContext struct { } func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) { + admin.Dependencies.Custom.Update(&is) + is.CSRF = csrf.TemplateField(r) // find the instance itself! @@ -49,8 +51,5 @@ func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) { return is, err } - // current time - is.Time = time.Now().UTC() - return } diff --git a/internal/dis/component/control/admin/users.go b/internal/dis/component/control/admin/users.go index a493355..8eeb155 100644 --- a/internal/dis/component/control/admin/users.go +++ b/internal/dis/component/control/admin/users.go @@ -5,12 +5,12 @@ import ( "errors" "html/template" "net/http" - "time" _ "embed" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/gorilla/csrf" "github.com/rs/zerolog" @@ -24,15 +24,17 @@ var userTemplate = static.AssetsAdmin.MustParseShared( ) type userContext struct { - Time time.Time + custom.BaseContext + httpx.FormContext CSRF template.HTML Users []*auth.AuthUser } func (admin *Admin) users(r *http.Request) (uc userContext, err error) { + admin.Dependencies.Custom.Update(&uc) + uc.CSRF = csrf.TemplateField(r) - uc.Time = time.Now() uc.Users, err = admin.Dependencies.Auth.Users(r.Context()) return } @@ -56,6 +58,8 @@ type createUserResult struct { } func (admin *Admin) createUser(ctx context.Context) http.Handler { + userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate) + return &httpx.Form[createUserResult]{ Fields: []httpx.Field{ {Name: "username", Type: httpx.TextField, Label: "Username"}, @@ -64,7 +68,8 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler { }, FieldTemplate: httpx.PureCSSFieldTemplate, - RenderTemplate: userCreateTemplate, + RenderTemplate: userCreateTemplate, + RenderTemplateContext: admin.Dependencies.Custom.RenderContext, 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"] == "on" diff --git a/internal/dis/component/control/home/home.go b/internal/dis/component/control/home/home.go index 007b494..39b0350 100644 --- a/internal/dis/component/control/home/home.go +++ b/internal/dis/component/control/home/home.go @@ -3,9 +3,11 @@ package home import ( "context" "fmt" + "html/template" "net/http" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/pkg/lazy" ) @@ -14,11 +16,13 @@ type Home struct { component.Base Dependencies struct { Instances *instances.Instances + Custom *custom.Custom } redirect lazy.Lazy[*Redirect] instanceNames lazy.Lazy[map[string]struct{}] homeBytes lazy.Lazy[[]byte] + homeTemplate lazy.Lazy[*template.Template] } var ( diff --git a/internal/dis/component/control/home/public.go b/internal/dis/component/control/home/public.go index be9741b..3852259 100644 --- a/internal/dis/component/control/home/public.go +++ b/internal/dis/component/control/home/public.go @@ -3,11 +3,13 @@ package home import ( "bytes" "context" + "html/template" "time" _ "embed" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/status" "golang.org/x/sync/errgroup" ) @@ -30,7 +32,8 @@ var homeHTMLStr string var homeTemplate = static.AssetsHome.MustParseShared("home.html", homeHTMLStr) func (home *Home) homeRender(ctx context.Context) ([]byte, error) { - var context HomeContext + var context homeContext + home.Dependencies.Custom.Update(&context) // setup a couple of static things context.Time = time.Now().UTC() @@ -57,11 +60,15 @@ func (home *Home) homeRender(ctx context.Context) ([]byte, error) { // render the template var buffer bytes.Buffer - homeTemplate.Execute(&buffer, context) + home.homeTemplate.Get(func() *template.Template { + return home.Dependencies.Custom.Template(homeTemplate) + }).Execute(&buffer, context) return buffer.Bytes(), nil } -type HomeContext struct { +type homeContext struct { + custom.BaseContext + Instances []status.WissKI Time time.Time diff --git a/internal/dis/component/control/legal/legal.go b/internal/dis/component/control/legal/legal.go index e799e4b..0c05ab9 100644 --- a/internal/dis/component/control/legal/legal.go +++ b/internal/dis/component/control/legal/legal.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" _ "embed" @@ -15,6 +16,10 @@ import ( type Legal struct { component.Base + Dependencies struct { + Static *static.Static + Custom *custom.Custom + } } var ( @@ -33,6 +38,8 @@ func (legal *Legal) Routes() component.Routes { } func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler, error) { + legalTemplate := legal.Dependencies.Custom.Template(legalTemplate) + return httpx.HTMLHandler[legalContext]{ Handler: legal.context, Template: legalTemplate, @@ -40,6 +47,8 @@ func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler } type legalContext struct { + custom.BaseContext + LegalNotices string CSRFCookie string @@ -49,6 +58,7 @@ type legalContext struct { func (legal *Legal) context(r *http.Request) (legalContext, error) { return legalContext{ + BaseContext: legal.Dependencies.Custom.New(), LegalNotices: cli.LegalNotices, CSRFCookie: control.CSRFCookie, diff --git a/internal/dis/component/control/static/custom/custom.go b/internal/dis/component/control/static/custom/custom.go new file mode 100644 index 0000000..60c214d --- /dev/null +++ b/internal/dis/component/control/static/custom/custom.go @@ -0,0 +1,8 @@ +package custom + +import "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + +// Custom implements theme and page customization. +type Custom struct { + component.Base +} diff --git a/internal/dis/component/control/static/custom/template.go b/internal/dis/component/control/static/custom/template.go new file mode 100644 index 0000000..b3606f7 --- /dev/null +++ b/internal/dis/component/control/static/custom/template.go @@ -0,0 +1,82 @@ +package custom + +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/tkw1536/goprogram/lib/reflectx" +) + +const footerName = "footer" + +// defaultTemplate is the default footer template +var defaultTemplate = template.Must(template.New("footer.html").Parse(`
Powered By WissKI Distillery
`)) + +// 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 (custom *Custom) Template(tpl *template.Template) *template.Template { + tree := defaultTemplate.Tree.Copy() + + clone := template.Must(tpl.Clone()) // create a clone of the template + template.Must(clone.AddParseTree(footerName, tree)) // add the parse tree to it + return clone // and return the tree +} + +// NewContext returns a new BaseContext +func (custom *Custom) New() (ctx BaseContext) { + ctx.Use(custom.Base) + return +} + +// NewForm is like New, but returns a new BaseFormContext +func (custom *Custom) NewForm(context httpx.FormContext) (ctx BaseFormContext) { + ctx.FormContext = context + ctx.Use(custom.Base) + return +} + +// RenderContext can be used as httpx.Form.RenderTemplateContext. +// It returns a new [BaseFormContext]. +func (custom *Custom) RenderContext(ctx httpx.FormContext, r *http.Request) any { + return BaseFormContext{ + FormContext: ctx, + BaseContext: custom.New(), + } +} + +// Update updates an embedded BaseContext field in context. +// +// Assumes that context is a pointer to a struct type. +// If this is not the case, might call panic(). +func (custom *Custom) Update(context any) *BaseContext { + ctx := reflect.ValueOf(context). + Elem().FieldByName(contextName).Addr(). + Interface().(*BaseContext) + ctx.Use(custom.Base) + return ctx +} + +// contextName is the name of the [BaseContext] field. +var contextName = reflectx.TypeOf[BaseContext]().Name() + +// BaseContext is a context struct shared by all contexts +type BaseContext struct { + Time time.Time // time this page was generated at +} + +// Use updates this context to use the values from the given base. +// For convenience the passed context is also returned. +func (tc *BaseContext) Use(base component.Base) *BaseContext { + tc.Time = time.Now().UTC() + return tc +} + +// BaseFormContext combines BaseContext and FormContext +type BaseFormContext struct { + BaseContext + httpx.FormContext +} diff --git a/internal/dis/component/control/static/templates/_base.html b/internal/dis/component/control/static/templates/_base.html index 2a8d0eb..1e42f15 100644 --- a/internal/dis/component/control/static/templates/_base.html +++ b/internal/dis/component/control/static/templates/_base.html @@ -2,7 +2,7 @@ - +