diff --git a/Makefile b/Makefile index cb06e2d..acbe82e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,9 @@ wdcli: go generate ./internal/dis/component/control/static/ go build -o ./wdcli ./cmd/wdcli -deps: +deps: internal/dis/component/control/static/node_modules + +internal/dis/component/control/static/node_modules: cd internal/dis/component/control/static/ && yarn install clean: diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go index fc2c4ee..ea27728 100644 --- a/internal/dis/component/auth/panel/panel.go +++ b/internal/dis/component/auth/panel/panel.go @@ -112,20 +112,24 @@ type userFormContext struct { User *models.User } -func (panel *UserPanel) UserFormContext(last component.MenuItem, gaps custom.BaseContextGaps) func(ctx httpx.FormContext, r *http.Request) any { - gaps.Crumbs = []component.MenuItem{ +func (panel *UserPanel) UserFormContext2(tpl *custom.Template[userFormContext], last component.MenuItem, gaps ...custom.BaseContextGaps) func(ctx httpx.FormContext, r *http.Request) any { + var g custom.BaseContextGaps + if len(gaps) > 1 { + panic("UserFormContext2: gaps must be of length 0 or 1") + } + if len(gaps) == 1 { + g = gaps[0] + } + g.Crumbs = []component.MenuItem{ {Title: "User", Path: "/user/"}, last, } - return func(ctx httpx.FormContext, r *http.Request) any { - user, err := panel.Dependencies.Auth.UserOf(r) + return custom.MappedHandler(tpl, func(ctx httpx.FormContext, r *http.Request) (userFormContext, custom.BaseContextGaps) { uctx := userFormContext{FormContext: ctx} - panel.Dependencies.Custom.Update(&uctx, r, gaps) - if err == nil { + if user, err := panel.Dependencies.Auth.UserOf(r); err == nil { uctx.User = &user.User } - return uctx - } - + return uctx, g + }) } diff --git a/internal/dis/component/auth/panel/password.go b/internal/dis/component/auth/panel/password.go index b11f8d6..4376546 100644 --- a/internal/dis/component/auth/panel/password.go +++ b/internal/dis/component/auth/panel/password.go @@ -15,8 +15,8 @@ import ( ) //go:embed "templates/password.html" -var passwordHTMLString string -var passwordTemplate = static.AssetsUser.MustParseShared("password.html", passwordHTMLString) +var passwordHTML []byte +var passwordTemplate = custom.Parse[userFormContext]("password.html", passwordHTML, static.AssetsUser) var ( errPasswordsNotIdentical = errors.New("passwords are not identical") @@ -28,7 +28,7 @@ var ( ) func (panel *UserPanel) routePassword(ctx context.Context) http.Handler { - passwordTemplate := panel.Dependencies.Custom.Template(passwordTemplate) + tpl := passwordTemplate.Prepare(panel.Dependencies.Custom) return &httpx.Form[struct{}]{ Fields: []field.Field{ @@ -39,8 +39,8 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler { }, FieldTemplate: field.PureCSSFieldTemplate, - RenderTemplate: passwordTemplate, - RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Change Password", Path: "/user/password/"}, custom.BaseContextGaps{}), + RenderTemplate: tpl.Template(), + RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Change Password", Path: "/user/password/"}), Validate: func(r *http.Request, values map[string]string) (struct{}, error) { old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"] diff --git a/internal/dis/component/auth/panel/ssh.go b/internal/dis/component/auth/panel/ssh.go index e2849dc..62e8a15 100644 --- a/internal/dis/component/auth/panel/ssh.go +++ b/internal/dis/component/auth/panel/ssh.go @@ -21,8 +21,8 @@ import ( ) //go:embed "templates/ssh.html" -var sshTemplateStr string -var sshTemplate = static.AssetsUser.MustParseShared("ssh.html", sshTemplateStr) +var sshHTML []byte +var sshTemplate = custom.Parse[SSHTemplateContext]("ssh.html", sshHTML, static.AssetsUser) type SSHTemplateContext struct { custom.BaseContext @@ -37,8 +37,7 @@ type SSHTemplateContext struct { } func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { - sshTemplate := panel.Dependencies.Custom.Template(sshTemplate) - gaps := custom.BaseContextGaps{ + tpl := sshTemplate.Prepare(panel.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "User", Path: "/user/"}, {Title: "SSH Keys", Path: "/user/ssh/"}, @@ -46,48 +45,33 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { Actions: []component.MenuItem{ {Title: "Add New Key", Path: "/user/ssh/add/"}, }, - } + }) - return httpx.HTMLHandler[SSHTemplateContext]{ - Handler: func(r *http.Request) (sc SSHTemplateContext, err error) { - panel.Dependencies.Custom.Update(&sc, r, gaps) + return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) { + user, err := panel.Dependencies.Auth.UserOf(r) + if err != nil { + return sc, err + } - user, err := panel.Dependencies.Auth.UserOf(r) - if err != nil { - return sc, err - } + sc.Domain = panel.Config.DefaultDomain + sc.Port = panel.Config.PublicSSHPort - sc.Domain = panel.Config.DefaultDomain - sc.Port = panel.Config.PublicSSHPort + // pick the first domain that the user has access to as an example + grants, err := panel.Dependencies.Policy.User(r.Context(), user.User.User) + if err != nil && len(grants) > 0 { + sc.Slug = grants[0].Slug + } else { + sc.Slug = "example" + } + sc.Hostname = panel.Config.HostFromSlug(sc.Slug) - // pick the first domain that the user has access to as an example - grants, err := panel.Dependencies.Policy.User(r.Context(), user.User.User) - if err != nil && len(grants) > 0 { - sc.Slug = grants[0].Slug - } else { - sc.Slug = "example" - } - sc.Hostname = panel.Config.HostFromSlug(sc.Slug) + sc.Keys, err = panel.Dependencies.Keys.Keys(r.Context(), user.User.User) + if err != nil { + return sc, err + } - sc.Keys, err = panel.Dependencies.Keys.Keys(r.Context(), user.User.User) - if err != nil { - return sc, err - } - - return sc, nil - }, - Template: sshTemplate, - } -} - -//go:embed "templates/ssh_add.html" -var sshAddTemplateStr string -var sshAddTemplate = static.AssetsUser.MustParseShared("ssh_add.html", sshAddTemplateStr) - -type addKeyResult struct { - User *auth.AuthUser - Comment string - Key ssh.PublicKey + return sc, nil + }) } var ( @@ -128,15 +112,24 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler { }) } +//go:embed "templates/ssh_add.html" +var sshAddHTML []byte +var sshAddTemplate = custom.ParseForm("ssh_add.html", sshAddHTML, static.AssetsUser) + +type addKeyResult struct { + User *auth.AuthUser + Comment string + Key ssh.PublicKey +} + func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler { - sshAddTemplate := panel.Dependencies.Custom.Template(sshAddTemplate) - gaps := custom.BaseContextGaps{ + tpl := sshAddTemplate.Prepare(panel.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "User", Path: "/user/"}, {Title: "SSH Keys", Path: "/user/ssh/"}, {Title: "Add New Key", Path: "/user/ssh/add/"}, }, - } + }) return &httpx.Form[addKeyResult]{ Fields: []field.Field{ @@ -145,10 +138,8 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler { }, FieldTemplate: field.PureCSSFieldTemplate, - RenderTemplate: sshAddTemplate, - RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any { - return panel.Dependencies.Custom.NewForm(ctx, r, gaps) - }, + RenderTemplate: tpl.Template(), + RenderTemplateContext: custom.FormTemplateContext(tpl), Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) { ak.User, err = panel.Dependencies.Auth.UserOf(r) diff --git a/internal/dis/component/auth/panel/totp.go b/internal/dis/component/auth/panel/totp.go index c49bacd..e59b99b 100644 --- a/internal/dis/component/auth/panel/totp.go +++ b/internal/dis/component/auth/panel/totp.go @@ -16,11 +16,11 @@ import ( ) //go:embed "templates/totp_enable.html" -var totpEnableStr string -var totpEnableTemplate = static.AssetsUser.MustParseShared("totp_enable.html", totpEnableStr) +var totpEnableHTML []byte +var totpEnable = custom.Parse[userFormContext]("totp_enable.html", totpEnableHTML, static.AssetsUser) func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { - totpEnableTemplate := panel.Dependencies.Custom.Template(totpEnableTemplate) + tpl := totpEnable.Prepare(panel.Dependencies.Custom) return &httpx.Form[struct{}]{ Fields: []field.Field{ @@ -33,8 +33,8 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, - RenderTemplate: totpEnableTemplate, - RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}, custom.BaseContextGaps{}), + RenderTemplate: tpl.Template(), + RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}), Validate: func(r *http.Request, values map[string]string) (struct{}, error) { password := values["password"] @@ -68,8 +68,8 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { } //go:embed "templates/totp_enroll.html" -var totpEnrollStr string -var totpEnrollTemplate = static.AssetsUser.MustParseShared("totp_enroll.html", totpEnrollStr) +var totpEnrollHTML []byte +var totpEnrollTemplate = custom.Parse[totpEnrollContext]("totp_enroll.html", totpEnrollHTML, static.AssetsUser) type totpEnrollContext struct { userFormContext @@ -80,13 +80,13 @@ type totpEnrollContext struct { } func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { - totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate) - gaps := custom.BaseContextGaps{ + tpl := totpEnrollTemplate.Prepare(panel.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "User", Path: "/user/"}, {Title: "Enable TOTP", Path: "/user/totp/enable/"}, }, - } + }) + return &httpx.Form[struct{}]{ Fields: []field.Field{ {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, @@ -108,7 +108,6 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { FormContext: context, }, } - panel.Dependencies.Custom.Update(&ctx.userFormContext, r, gaps) if err == nil && user != nil { ctx.userFormContext.User = &user.User @@ -121,7 +120,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { ctx.TOTPURL = template.URL(secret.URL()) } } - httpx.WriteHTML(ctx, nil, totpEnrollTemplate, "", w, r) + tpl.Execute(w, r, ctx) }, Validate: func(r *http.Request, values map[string]string) (struct{}, error) { @@ -156,11 +155,11 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { } //go:embed "templates/totp_disable.html" -var totpDisableStr string -var totpDisableTemplate = static.AssetsUser.MustParseShared("totp_disable.html", totpDisableStr) +var totpDisableHTML []byte +var totpDisableTemplate = custom.Parse[userFormContext]("totp_disable.html", totpDisableHTML, static.AssetsUser) func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { - totpDisableTemplate := panel.Dependencies.Custom.Template(totpDisableTemplate) + tpl := totpDisableTemplate.Prepare(panel.Dependencies.Custom) return &httpx.Form[struct{}]{ Fields: []field.Field{ @@ -173,8 +172,8 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { user, err := panel.Dependencies.Auth.UserOf(r) return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled() }, - RenderTemplate: totpDisableTemplate, - RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}, custom.BaseContextGaps{}), + RenderTemplate: tpl.Template(), + RenderTemplateContext: panel.UserFormContext2(tpl, component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}), Validate: func(r *http.Request, values map[string]string) (struct{}, error) { password, otp := values["password"], values["otp"] diff --git a/internal/dis/component/auth/panel/user.go b/internal/dis/component/auth/panel/user.go index f9c7ef7..9cd2c12 100644 --- a/internal/dis/component/auth/panel/user.go +++ b/internal/dis/component/auth/panel/user.go @@ -12,17 +12,13 @@ import ( "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/models" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" ) //go:embed "templates/user.html" -var userHTMLStr string -var userTemplate = static.AssetsUser.MustParseShared( - "user.html", - userHTMLStr, -) +var userHTML []byte +var userTemplate = custom.Parse[userContext]("user.html", userHTML, static.AssetsUser) -type routeUserContext struct { +type userContext struct { custom.BaseContext *auth.AuthUser @@ -35,8 +31,7 @@ type GrantWithURL struct { } func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { - userTemplate := panel.Dependencies.Custom.Template(userTemplate) - gaps := custom.BaseContextGaps{ + tpl := userTemplate.Prepare(panel.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "User", Path: "/user/"}, }, @@ -45,50 +40,45 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { {Title: "*to be replaced*", Path: ""}, {Title: "SSH Keys", Path: "/user/ssh/"}, }, - } + }) - return &httpx.HTMLHandler[routeUserContext]{ - Handler: func(r *http.Request) (ruc routeUserContext, err error) { - // find the user - ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r) - if err != nil || ruc.AuthUser == nil { - return ruc, err + return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *custom.BaseContextGaps) (uc userContext, err error) { + // find the user + uc.AuthUser, err = panel.Dependencies.Auth.UserOf(r) + if err != nil || uc.AuthUser == nil { + return uc, err + } + + // build the gaps + if uc.AuthUser.IsTOTPEnabled() { + gaps.Actions[1] = component.MenuItem{ + Title: "Disable Passcode (TOTP)", + Path: "/user/totp/disable/", } - - // build the gaps - gaps := gaps.Clone() - if ruc.AuthUser.IsTOTPEnabled() { - gaps.Actions[1] = component.MenuItem{ - Title: "Disable Passcode (TOTP)", - Path: "/user/totp/disable/", - } - } else { - gaps.Actions[1] = component.MenuItem{ - Title: "Enable Passcode (TOTP)", - Path: "/user/totp/enable/", - } + } else { + gaps.Actions[1] = component.MenuItem{ + Title: "Enable Passcode (TOTP)", + Path: "/user/totp/enable/", } - panel.Dependencies.Custom.Update(&ruc, r, gaps) + } - // find the grants - grants, err := panel.Dependencies.Policy.User(r.Context(), ruc.AuthUser.User.User) + // find the grants + grants, err := panel.Dependencies.Policy.User(r.Context(), uc.AuthUser.User.User) + if err != nil { + return uc, err + } + + uc.Grants = make([]GrantWithURL, len(grants)) + for i, grant := range grants { + uc.Grants[i].Grant = grant + + url, err := panel.Dependencies.Next.Next(r.Context(), grant.Slug, "/") if err != nil { - return ruc, err + return uc, err } + uc.Grants[i].URL = template.URL(url) + } - ruc.Grants = make([]GrantWithURL, len(grants)) - for i, grant := range grants { - ruc.Grants[i].Grant = grant - - url, err := panel.Dependencies.Next.Next(r.Context(), grant.Slug, "/") - if err != nil { - return ruc, err - } - ruc.Grants[i].URL = template.URL(url) - } - - return ruc, err - }, - Template: userTemplate, - } + return uc, err + }) } diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index 0894a01..6d5044a 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -119,8 +119,8 @@ func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error { } //go:embed "login.html" -var loginHTMLStr string -var loginTemplate = static.AssetsUser.MustParseShared("login.html", loginHTMLStr) +var loginHTML []byte +var loginTemplate = custom.ParseForm("login.html", loginHTML, static.AssetsUser) var loginResponse = httpx.Response{ ContentType: "text/plain", @@ -131,7 +131,7 @@ 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) + tpl := loginTemplate.Prepare(auth.Dependencies.Custom) return &httpx.Form[*AuthUser]{ Fields: []field.Field{ @@ -145,11 +145,11 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { if context.Err != nil { context.Err = errLoginFailed } - httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r, custom.BaseContextGaps{ + tpl.Execute(w, r, custom.BaseFormContext{FormContext: context}, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Login", Path: template.URL(r.URL.RequestURI())}, }, - }), nil, loginTemplate, "", w, r) + }) }, Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { diff --git a/internal/dis/component/control/admin/admin.go b/internal/dis/component/control/admin/admin.go index b5d8241..897132e 100644 --- a/internal/dis/component/control/admin/admin.go +++ b/internal/dis/component/control/admin/admin.go @@ -80,21 +80,16 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http } // add a handler for the index page - router.Handler(http.MethodGet, route, httpx.HTMLHandler[indexContext]{ - Handler: admin.index, - Template: admin.Dependencies.Custom.Template(indexTemplate), - }) - - // fallback to the "/" page - router.HandlerFunc(http.MethodGet, route+"index", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, route, http.StatusTemporaryRedirect) - }) + { + index := admin.index(ctx) + router.Handler(http.MethodGet, route, index) + } // add a handler for the user page - router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{ - Handler: admin.users, - Template: admin.Dependencies.Custom.Template(userTemplate), - }) + { + users := admin.users(ctx) + router.Handler(http.MethodGet, route+"users", users) + } // add a user create form { @@ -113,32 +108,28 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http router.Handler(http.MethodPost, route+"users/unsetpassword", admin.usersUnsetPasswordHandler(ctx)) // add a handler for the component page - router.Handler(http.MethodGet, route+"components", httpx.HTMLHandler[componentContext]{ - Handler: admin.components, - Template: admin.Dependencies.Custom.Template(componentsTemplate), - }) + { + components := admin.components(ctx) + router.Handler(http.MethodGet, route+"components", components) + } - // add a handler for the component page - router.Handler(http.MethodGet, route+"ingredients/:slug", httpx.HTMLHandler[ingredientsContext]{ - Handler: admin.ingredients, - Template: admin.Dependencies.Custom.Template(ingredientsTemplate), - }) + // add a handler for the ingredients page + { + ingredients := admin.ingredients(ctx) + router.Handler(http.MethodGet, route+"ingredients/:slug", ingredients) + } // add a handler for the instance page - router.Handler(http.MethodGet, route+"instance/:slug", httpx.HTMLHandler[instanceContext]{ - Handler: admin.instance, - Template: admin.Dependencies.Custom.Template(instanceTemplate), - }) + { + instance := admin.instance(ctx) + router.Handler(http.MethodGet, route+"instance/:slug", instance) + } - // add a router for the grants pages - router.Handler(http.MethodGet, route+"grants/:slug", httpx.HTMLHandler[grantsContext]{ - Handler: admin.getGrants, - Template: admin.Dependencies.Custom.Template(grantsTemplate), - }) - router.Handler(http.MethodPost, route+"grants/", httpx.HTMLHandler[grantsContext]{ - Handler: admin.postGrants, - Template: admin.Dependencies.Custom.Template(grantsTemplate), - }) + { + grants := admin.grants(ctx) + router.Handler(http.MethodGet, route+"grants/:slug", grants) + router.Handler(http.MethodPost, route+"grants/", grants) // NOTE(twiesing): This path is intentionally different! + } // add a router for the login page router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx)) diff --git a/internal/dis/component/control/admin/components.go b/internal/dis/component/control/admin/components.go index bd34a64..a901adf 100644 --- a/internal/dis/component/control/admin/components.go +++ b/internal/dis/component/control/admin/components.go @@ -1,6 +1,7 @@ package admin import ( + "context" "html/template" "net/http" @@ -17,11 +18,8 @@ import ( ) //go:embed "html/components.html" -var componentsTemplateString string -var componentsTemplate = static.AssetsAdmin.MustParseShared( - "components.html", - componentsTemplateString, -) +var componentsHTML []byte +var componentsTemplate = custom.Parse[componentContext]("components.html", componentsHTML, static.AssetsAdmin) type componentContext struct { custom.BaseContext @@ -29,24 +27,23 @@ type componentContext struct { Analytics lazy.PoolAnalytics } -func (admin *Admin) components(r *http.Request) (cp componentContext, err error) { - admin.Dependencies.Custom.Update(&cp, r, custom.BaseContextGaps{ +func (admin *Admin) components(ctx context.Context) http.Handler { + tpl := componentsTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, {Title: "Components", Path: "/admin/components/"}, }, }) - cp.Analytics = *admin.Analytics - return + return tpl.HTMLHandler(func(r *http.Request) (cp componentContext, err error) { + cp.Analytics = *admin.Analytics + return + }) } //go:embed "html/ingredients.html" -var ingredientsTemplateString string -var ingredientsTemplate = static.AssetsAdmin.MustParseShared( - "ingredients.html", - ingredientsTemplateString, -) +var ingredientsHTML []byte +var ingredientsTemplate = custom.Parse[ingredientsContext]("ingredients.html", ingredientsHTML, static.AssetsAdmin) type ingredientsContext struct { custom.BaseContext @@ -55,29 +52,34 @@ type ingredientsContext struct { Analytics *lazy.PoolAnalytics } -func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) { - slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") - - admin.Dependencies.Custom.Update(&cp, r, custom.BaseContextGaps{ +func (admin *Admin) ingredients(ctx context.Context) http.Handler { + tpl := ingredientsTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, - {Title: "Instance", Path: template.URL("/admin/instance/" + slug)}, - {Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}, + {Title: "Instance", Path: "* to be updated *"}, + {Title: "Ingredients", Path: "* to be updated *"}, }, }) - // find the instance itself! - instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) - if err == instances.ErrWissKINotFound { - return cp, httpx.ErrNotFound - } - if err != nil { - return cp, err - } - cp.Instance = instance.Instance + return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *custom.BaseContextGaps) (ic ingredientsContext, err error) { + slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") - // and get the components - cp.Analytics = instance.Info().Analytics + 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/")} - return + // find the instance itself! + instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) + if err == instances.ErrWissKINotFound { + return ic, httpx.ErrNotFound + } + if err != nil { + return ic, err + } + ic.Instance = instance.Instance + + // and get the components + ic.Analytics = instance.Info().Analytics + + return + }) } diff --git a/internal/dis/component/control/admin/grants.go b/internal/dis/component/control/admin/grants.go index 0b5b339..d45d25a 100644 --- a/internal/dis/component/control/admin/grants.go +++ b/internal/dis/component/control/admin/grants.go @@ -1,6 +1,7 @@ package admin import ( + "context" _ "embed" "fmt" "html/template" @@ -20,11 +21,8 @@ import ( ) //go:embed "html/grants.html" -var grantsStr string -var grantsTemplate = static.AssetsAdmin.MustParseShared( - "grants.html", - grantsStr, -) +var grantsHTML []byte +var grantsTemplate = custom.Parse[grantsContext]("grants.html", grantsHTML, static.AssetsAdmin) type grantsContext struct { custom.BaseContext @@ -39,15 +37,88 @@ type grantsContext struct { Drupals []string // unusued drupal usernames } -func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (err error) { - admin.Dependencies.Custom.Update(gc, r, custom.BaseContextGaps{ +func (admin *Admin) grants(ctx context.Context) http.Handler { + tpl := grantsTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, - {Title: "Instance", Path: template.URL("/admin/instance/" + slug)}, - {Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}, + {Title: "Instance", Path: "*to be updated*"}, + {Title: "Grants", Path: "*to be updated*"}, }, }) + return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *custom.BaseContextGaps) (grantsContext, error) { + if r.Method == http.MethodGet { + return admin.getGrants(r, gaps) + } else { + return admin.postGrants(r, gaps) + } + }) +} + +func (admin *Admin) getGrants(r *http.Request, gaps *custom.BaseContextGaps) (gc grantsContext, err error) { + slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") + if err := gc.use(r, gaps, slug, admin); err != nil { + return gc, err + } + + if err := gc.useGrants(r, admin); err != nil { + return gc, err + } + + return gc, nil +} + +func (admin *Admin) postGrants(r *http.Request, gaps *custom.BaseContextGaps) (gc grantsContext, err error) { + // parse the form + if err := r.ParseForm(); err != nil { + return gc, err + } + + // read out the form values + var ( + slug = r.PostFormValue("slug") + delete = r.PostFormValue("action") == "delete" + distilleryUser = r.PostFormValue("distillery-user") + drupalUser = r.PostFormValue("drupal-user") + adminRole = r.PostFormValue("admin") == field.CheckboxChecked + ) + + // set the common fields + if err := gc.use(r, gaps, slug, admin); err != nil { + return gc, err + } + + if delete { + // delete the user grant + err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug) + if err != nil { + return gc, err + } + } else { + // update the grant + err := admin.Dependencies.Policy.Set(r.Context(), models.Grant{ + User: distilleryUser, + Slug: slug, + + DrupalUsername: drupalUser, + DrupalAdminRole: adminRole, + }) + if err != nil { + gc.Error = fmt.Sprintf("Unable to update grant for user %s: %s", distilleryUser, err.Error()) + } + } + + // fetch the grants for the instance + if err := gc.useGrants(r, admin); err != nil { + return gc, err + } + return gc, nil +} + +func (gc *grantsContext) use(r *http.Request, gaps *custom.BaseContextGaps, slug string, admin *Admin) (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 gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug) if err == instances.ErrWissKINotFound { @@ -100,63 +171,3 @@ func (gc *grantsContext) useGrants(r *http.Request, admin *Admin) (err error) { return nil } - -func (admin *Admin) getGrants(r *http.Request) (gc grantsContext, err error) { - slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") - if err := gc.use(r, slug, admin); err != nil { - return gc, err - } - - if err := gc.useGrants(r, admin); err != nil { - return gc, err - } - - return gc, nil -} - -func (admin *Admin) postGrants(r *http.Request) (gc grantsContext, err error) { - // parse the form - if err := r.ParseForm(); err != nil { - return gc, err - } - - // read out the form values - var ( - slug = r.PostFormValue("slug") - delete = r.PostFormValue("action") == "delete" - distilleryUser = r.PostFormValue("distillery-user") - drupalUser = r.PostFormValue("drupal-user") - adminRole = r.PostFormValue("admin") == field.CheckboxChecked - ) - - // set the common fields - if err := gc.use(r, slug, admin); err != nil { - return gc, err - } - - if delete { - // delete the user grant - err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug) - if err != nil { - return gc, err - } - } else { - // update the grant - err := admin.Dependencies.Policy.Set(r.Context(), models.Grant{ - User: distilleryUser, - Slug: slug, - - DrupalUsername: drupalUser, - DrupalAdminRole: adminRole, - }) - if err != nil { - gc.Error = fmt.Sprintf("Unable to update grant for user %s: %s", distilleryUser, err.Error()) - } - } - - // fetch the grants for the instance - if err := gc.useGrants(r, admin); err != nil { - return gc, err - } - return gc, nil -} diff --git a/internal/dis/component/control/admin/index.go b/internal/dis/component/control/admin/index.go index f735eff..4cde93d 100644 --- a/internal/dis/component/control/admin/index.go +++ b/internal/dis/component/control/admin/index.go @@ -14,13 +14,6 @@ import ( "golang.org/x/sync/errgroup" ) -//go:embed "html/index.html" -var indexTemplateStr string -var indexTemplate = static.AssetsAdmin.MustParseShared( - "index.html", - indexTemplateStr, -) - // Status produces a new observation of the distillery, and a new information of all instances // The information on all instances is passed the given quick flag. func (admin *Admin) Status(ctx context.Context, QuickInformation bool) (target status.Distillery, information []status.WissKI, err error) { @@ -79,6 +72,16 @@ func (admin *Admin) Status(ctx context.Context, QuickInformation bool) (target s return } +func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distillery) error { + target.Time = time.Now().UTC() + target.Config = admin.Config + return nil +} + +//go:embed "html/index.html" +var indexHTML []byte +var indexTemplate = custom.Parse[indexContext]("index.html", indexHTML, static.AssetsAdmin) + type indexContext struct { custom.BaseContext @@ -86,8 +89,8 @@ type indexContext struct { Instances []status.WissKI } -func (admin *Admin) index(r *http.Request) (idx indexContext, err error) { - admin.Dependencies.Custom.Update(&idx, r, custom.BaseContextGaps{ +func (admin *Admin) index(ctx context.Context) http.Handler { + tpl := indexTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, }, @@ -96,12 +99,9 @@ func (admin *Admin) index(r *http.Request) (idx indexContext, err error) { {Title: "Components", Path: "/admin/components/", Priority: component.SmallButton}, }, }) - idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true) - return -} -func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distillery) error { - target.Time = time.Now().UTC() - target.Config = admin.Config - return nil + return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *custom.BaseContextGaps) (idx indexContext, err error) { + 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 1a71e03..f3885b4 100644 --- a/internal/dis/component/control/admin/instance.go +++ b/internal/dis/component/control/admin/instance.go @@ -1,6 +1,7 @@ package admin import ( + "context" _ "embed" "html/template" "net/http" @@ -16,11 +17,8 @@ import ( ) //go:embed "html/instance.html" -var instanceTemplateString string -var instanceTemplate = static.AssetsAdmin.MustParseShared( - "instance.html", - instanceTemplateString, -) +var instanceHTML []byte +var instanceTemplate = custom.Parse[instanceContext]("instance.html", instanceHTML, static.AssetsAdmin) type instanceContext struct { custom.BaseContext @@ -29,35 +27,42 @@ type instanceContext struct { Info status.WissKI } -func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) { - slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") - - admin.Dependencies.Custom.Update(&is, r, custom.BaseContextGaps{ +func (admin *Admin) instance(ctx context.Context) http.Handler { + tpl := instanceTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, - {Title: "Instance", Path: template.URL("/admin/instance/" + slug)}, + {Title: "Instance", Path: "*to be replaced*"}, }, Actions: []component.MenuItem{ - {Title: "Grants", Path: template.URL("/admin/grants/" + slug)}, - {Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}, + {Title: "Grants", Path: "*to be replaced*"}, + {Title: "Ingredients", Path: "*to be replaced*", Priority: component.SmallButton}, }, }) - // find the instance itself! - instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) - if err == instances.ErrWissKINotFound { - return is, httpx.ErrNotFound - } - if err != nil { - return is, err - } - is.Instance = instance.Instance + return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *custom.BaseContextGaps) (ic instanceContext, err error) { + slug := httprouter.ParamsFromContext(r.Context()).ByName("slug") - // get some more info about the wisski - is.Info, err = instance.Info().Information(r.Context(), false) - if err != nil { - return is, err - } + gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)} - return + 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! + instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) + if err == instances.ErrWissKINotFound { + return ic, httpx.ErrNotFound + } + if err != nil { + return ic, err + } + ic.Instance = instance.Instance + + // get some more info about the wisski + ic.Info, err = instance.Info().Information(r.Context(), false) + if err != nil { + return ic, err + } + + return + }) } diff --git a/internal/dis/component/control/admin/users.go b/internal/dis/component/control/admin/users.go index af565d7..0a0ca59 100644 --- a/internal/dis/component/control/admin/users.go +++ b/internal/dis/component/control/admin/users.go @@ -18,21 +18,18 @@ import ( ) //go:embed "html/users.html" -var userTemplateString string -var userTemplate = static.AssetsAdmin.MustParseShared( - "users.html", - userTemplateString, -) +var usersHTML []byte +var usersTemplate = custom.Parse[usersContext]("user.html", usersHTML, static.AssetsAdmin) -type userContext struct { +type usersContext struct { custom.BaseContext Error string Users []*auth.AuthUser } -func (admin *Admin) users(r *http.Request) (uc userContext, err error) { - admin.Dependencies.Custom.Update(&uc, r, custom.BaseContextGaps{ +func (admin *Admin) users(ctx context.Context) http.Handler { + tpl := usersTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, {Title: "Users", Path: "/admin/users/"}, @@ -42,17 +39,16 @@ func (admin *Admin) users(r *http.Request) (uc userContext, err error) { }, }) - uc.Error = r.URL.Query().Get("error") - uc.Users, err = admin.Dependencies.Auth.Users(r.Context()) - return + return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) { + uc.Error = r.URL.Query().Get("error") + uc.Users, err = admin.Dependencies.Auth.Users(r.Context()) + return + }) } //go:embed "html/user_create.html" -var userCreateTemplateString string -var userCreateTemplate = static.AssetsAdmin.MustParseShared( - "user_create.html", - userCreateTemplateString, -) +var userCreateHTML []byte +var userCreateTemplate = custom.ParseForm("user_create.html", userCreateHTML, static.AssetsAdmin) var ( errCreateInvalidUsername = errors.New("invalid username") @@ -66,14 +62,13 @@ type createUserResult struct { } func (admin *Admin) createUser(ctx context.Context) http.Handler { - userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate) - gaps := custom.BaseContextGaps{ + tpl := userCreateTemplate.Prepare(admin.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Admin", Path: "/admin/"}, {Title: "Users", Path: "/admin/users"}, {Title: "Create", Path: "/admin/users/create"}, }, - } + }) return &httpx.Form[createUserResult]{ Fields: []field.Field{ @@ -83,10 +78,8 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler { }, FieldTemplate: field.PureCSSFieldTemplate, - RenderTemplate: userCreateTemplate, - RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any { - return admin.Dependencies.Custom.NewForm(ctx, r, gaps) - }, + RenderTemplate: tpl.Template(), + RenderTemplateContext: custom.FormTemplateContext(tpl), 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 diff --git a/internal/dis/component/control/home/public.go b/internal/dis/component/control/home/public.go index a274a5b..6f42d09 100644 --- a/internal/dis/component/control/home/public.go +++ b/internal/dis/component/control/home/public.go @@ -14,8 +14,8 @@ import ( ) //go:embed "public.html" -var publicHTMLStr string -var publicTemplate = static.AssetsDefault.MustParseShared("public.html", publicHTMLStr) +var publicHTML []byte +var publicTemplate = custom.Parse[publicContext]("public.html", publicHTML, static.AssetsDefault) type publicContext struct { custom.BaseContext @@ -25,25 +25,20 @@ type publicContext struct { } func (home *Home) publicHandler(ctx context.Context) http.Handler { - gaps := custom.BaseContextGaps{ + tpl := publicTemplate.Prepare(home.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "WissKI Distillery", Path: "/"}, }, - } - return httpx.HTMLHandler[publicContext]{ - Handler: func(r *http.Request) (pc publicContext, err error) { - // only act on the root path! - if strings.TrimSuffix(r.URL.Path, "/") != "" { - return pc, httpx.ErrNotFound - } + }) + return tpl.HTMLHandler(func(r *http.Request) (pc publicContext, err error) { + // only act on the root path! + if strings.TrimSuffix(r.URL.Path, "/") != "" { + return pc, httpx.ErrNotFound + } - home.Dependencies.Custom.Update(&pc, r, gaps) + pc.Instances = home.homeInstances.Get(nil) + pc.SelfRedirect = home.Config.SelfRedirect.String() - pc.Instances = home.homeInstances.Get(nil) - pc.SelfRedirect = home.Config.SelfRedirect.String() - - return - }, - Template: home.Dependencies.Custom.Template(publicTemplate), - } + return + }) } diff --git a/internal/dis/component/control/legal/legal.go b/internal/dis/component/control/legal/legal.go index fde1f1b..c149822 100644 --- a/internal/dis/component/control/legal/legal.go +++ b/internal/dis/component/control/legal/legal.go @@ -9,7 +9,6 @@ import ( "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" ) @@ -27,26 +26,8 @@ var ( ) //go:embed "legal.html" -var legalTemplateString string -var legalTemplate = static.AssetsDefault.MustParseShared("legal.html", legalTemplateString) - -func (legal *Legal) Routes() component.Routes { - return component.Routes{ - Prefix: "/legal/", - Exact: true, - - CSRF: false, - } -} - -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, - }, nil -} +var legalHTML []byte +var legalTemplate = custom.Parse[legalContext]("legal.html", legalHTML, static.AssetsDefault) type legalContext struct { custom.BaseContext @@ -58,17 +39,29 @@ type legalContext struct { AssetsDisclaimer string } -func (legal *Legal) context(r *http.Request) (lc legalContext, err error) { - legal.Dependencies.Custom.Update(&lc, r, custom.BaseContextGaps{ +func (legal *Legal) Routes() component.Routes { + return component.Routes{ + Prefix: "/legal/", + Exact: true, + + CSRF: false, + } +} + +func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler, error) { + tpl := legalTemplate.Prepare(legal.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Legal", Path: "/legal/"}, }, }) - lc.LegalNotices = cli.LegalNotices + return tpl.HTMLHandler(func(r *http.Request) (lc legalContext, err error) { + lc.LegalNotices = cli.LegalNotices - lc.CSRFCookie = control.CSRFCookie - lc.SessionCookie = control.SessionCookie - lc.AssetsDisclaimer = static.AssetsDisclaimer - return + lc.CSRFCookie = control.CSRFCookie + lc.SessionCookie = control.SessionCookie + lc.AssetsDisclaimer = static.AssetsDisclaimer + + return + }), nil } diff --git a/internal/dis/component/control/news/news.go b/internal/dis/component/control/news/news.go index 677d703..ff7b4f3 100644 --- a/internal/dis/component/control/news/news.go +++ b/internal/dis/component/control/news/news.go @@ -12,7 +12,6 @@ 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/pkg/httpx" "github.com/rs/zerolog" "github.com/yuin/goldmark" gmmeta "github.com/yuin/goldmark-meta" @@ -113,8 +112,8 @@ func Items() ([]Item, error) { } //go:embed "news.html" -var newsHTMLStr string -var newsTemplate = static.AssetsDefault.MustParseShared("news.html", newsHTMLStr) +var newsHTML []byte +var newsTemplate = custom.Parse[newsContext]("news.html", newsHTML, static.AssetsDefault) type newsContext struct { custom.BaseContext @@ -123,24 +122,19 @@ type newsContext struct { // HandleRoute returns the handler for the requested path func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) { - gaps := custom.BaseContextGaps{ + tpl := newsTemplate.Prepare(news.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "News", Path: "/news/"}, }, - } + }) items, itemsErr := Items() if itemsErr != nil { zerolog.Ctx(ctx).Err(itemsErr).Msg("Unable to load news items") } - return httpx.HTMLHandler[newsContext]{ - Handler: func(r *http.Request) (nc newsContext, err error) { - news.Dependencies.Custom.Update(&nc, r, gaps) - nc.Items, err = items, itemsErr - - return - }, - Template: news.Dependencies.Custom.Template(newsTemplate), - }, nil + return tpl.HTMLHandler(func(r *http.Request) (nc newsContext, err error) { + nc.Items, err = items, itemsErr + return + }), nil } diff --git a/internal/dis/component/control/static/custom/context.go b/internal/dis/component/control/static/custom/context.go index 01aa3cf..481eb18 100644 --- a/internal/dis/component/control/static/custom/context.go +++ b/internal/dis/component/control/static/custom/context.go @@ -48,19 +48,18 @@ type BaseContextGaps struct { Actions []component.MenuItem } -func (bcg BaseContextGaps) Clone() BaseContextGaps { +func (bcg BaseContextGaps) clone() BaseContextGaps { return BaseContextGaps{ Crumbs: slices.Clone(bcg.Crumbs), Actions: slices.Clone(bcg.Actions), } } -// Use updates this context to use the values from the given base. -// -// The given request *must not* be nil. -// -// For convenience the passed context is also returned. -func (tc *BaseContext) use(custom *Custom, r *http.Request, gaps BaseContextGaps) *BaseContext { +// update updates an embedded BaseContext field in context. +func (custom *Custom) update(context any, r *http.Request, bcg BaseContextGaps) *BaseContext { + tc := reflect.ValueOf(context). + Elem().FieldByName(baseContextName).Addr(). + Interface().(*BaseContext) // tc.custom = custom tc.inited = true tc.requestWasNil = r == nil @@ -77,7 +76,7 @@ func (tc *BaseContext) use(custom *Custom, r *http.Request, gaps BaseContextGaps tc.Menu = custom.BuildMenu(r) // build the breadcrumbs - tc.BaseContextGaps = gaps.Clone() + tc.BaseContextGaps = bcg.clone() last := len(tc.Crumbs) - 1 for i := range tc.Crumbs { tc.Crumbs[i].Active = i == last @@ -97,25 +96,6 @@ func (bc BaseContext) DoInitCheck() template.HTML { return "" } -// NewForm is like New, but returns a new BaseFormContext -func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request, bcg BaseContextGaps) (ctx BaseFormContext) { - ctx.FormContext = context - ctx.use(custom, r, bcg) - return -} - -// Update updates an embedded BaseContext field in context. -// -// Assumes that context is a pointer to a struct type. -// If this is not the case, might call panic(). -func (custom *Custom) Update(context any, r *http.Request, bcg BaseContextGaps) *BaseContext { - ctx := reflect.ValueOf(context). - Elem().FieldByName(baseContextName).Addr(). - Interface().(*BaseContext) - ctx.use(custom, r, bcg) - return ctx -} - // BaseFormContext combines BaseContext and FormContext type BaseFormContext struct { BaseContext diff --git a/internal/dis/component/control/static/custom/new.go b/internal/dis/component/control/static/custom/new.go new file mode 100644 index 0000000..be0e54e --- /dev/null +++ b/internal/dis/component/control/static/custom/new.go @@ -0,0 +1,139 @@ +package custom + +import ( + "html/template" + "net/http" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "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 static.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(custom *Custom, gaps ...BaseContextGaps) *Template[C] { + wrap := Template[C]{ + custom: custom, + template: custom.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 { + custom *Custom + 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.custom.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) +} diff --git a/internal/dis/component/resolver/resolver.go b/internal/dis/component/resolver/resolver.go index 4b3bae9..7f3d282 100644 --- a/internal/dis/component/resolver/resolver.go +++ b/internal/dis/component/resolver/resolver.go @@ -14,7 +14,6 @@ import ( "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/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/rs/zerolog" @@ -50,8 +49,8 @@ func (resolver *Resolver) Routes() component.Routes { } //go:embed "resolver.html" -var resolverHTMLStr string -var resolverTemplate = static.AssetsDefault.MustParseShared("resolver.html", resolverHTMLStr) +var resolverHTML []byte +var resolverTemplate = custom.Parse[resolverContext]("resolver.html", resolverHTML, static.AssetsDefault) type resolverContext struct { custom.BaseContext @@ -59,13 +58,11 @@ type resolverContext struct { } func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) { - resolverTemplate := resolver.Dependencies.Custom.Template(resolverTemplate) - gaps := custom.BaseContextGaps{ + tpl := resolverTemplate.Prepare(resolver.Dependencies.Custom, custom.BaseContextGaps{ Crumbs: []component.MenuItem{ {Title: "Resolver", Path: "/wisski/get/"}, }, - } - + }) logger := zerolog.Ctx(ctx) var p wdresolve.ResolveHandler @@ -75,13 +72,11 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H ctx := resolverContext{ IndexContext: context, } - resolver.Dependencies.Custom.Update(&ctx, r, gaps) - if !resolver.Dependencies.Auth.Has(auth.User, r) { ctx.IndexContext.Prefixes = nil } - httpx.WriteHTML(ctx, nil, resolverTemplate, "", w, r) + tpl.Execute(w, r, ctx) } p.TrustXForwardedProto = true