// Package web provides session cookie management, auth middleware, and // template rendering helpers for Metacircular web UIs built with htmx // and Go html/template. package web import ( "html/template" "io/fs" "net/http" "git.wntrmute.dev/kyle/mcdsl/auth" ) // SetSessionCookie sets a session cookie with the standard Metacircular // security flags: HttpOnly, Secure, SameSite=Strict. func SetSessionCookie(w http.ResponseWriter, name, token string) { http.SetCookie(w, &http.Cookie{ Name: name, Value: token, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, }) } // ClearSessionCookie removes a session cookie by setting it to empty // with MaxAge=-1. func ClearSessionCookie(w http.ResponseWriter, name string) { http.SetCookie(w, &http.Cookie{ Name: name, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, }) } // GetSessionToken extracts the session token from the named cookie. // Returns empty string if the cookie is missing or empty. func GetSessionToken(r *http.Request, name string) string { c, err := r.Cookie(name) if err != nil { return "" } return c.Value } // RequireAuth returns middleware that validates the session token via // the Authenticator. If the token is missing or invalid, the user is // redirected to loginPath. On success, the [auth.TokenInfo] is stored // in the request context (retrievable via [auth.TokenInfoFromContext]). func RequireAuth(authenticator *auth.Authenticator, cookieName, loginPath string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := GetSessionToken(r, cookieName) if token == "" { http.Redirect(w, r, loginPath, http.StatusFound) return } info, err := authenticator.ValidateToken(token) if err != nil { ClearSessionCookie(w, cookieName) http.Redirect(w, r, loginPath, http.StatusFound) return } ctx := auth.ContextWithTokenInfo(r.Context(), info) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // RenderTemplate parses and executes a template from an embedded FS. // It parses "templates/layout.html" and "templates/", merges // any provided FuncMaps, and executes the "layout" template with data. // // This matches the layout + page block pattern used by all Metacircular // web UIs. func RenderTemplate(w http.ResponseWriter, fsys fs.FS, name string, data any, funcs ...template.FuncMap) { merged := template.FuncMap{} for _, fm := range funcs { for k, v := range fm { merged[k] = v } } tmpl, err := template.New("").Funcs(merged).ParseFS(fsys, "templates/layout.html", "templates/"+name, ) if err != nil { http.Error(w, "template error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(w, "layout", data); err != nil { http.Error(w, "template error", http.StatusInternalServerError) } }