Files
mcdsl/web/web.go
Kyle Isom ebe2079a83 Migrate module path from kyle/ to mc/ org
All import paths updated from git.wntrmute.dev/kyle/mcdsl to
git.wntrmute.dev/mc/mcdsl to match the Gitea organization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:03:45 -07:00

105 lines
3.0 KiB
Go

// 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/mc/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/<name>", 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)
}
}