package web import ( "context" "errors" "log/slog" "net/http" "time" "tug/internal/transport/web/text" ) var ErrNoSession = errors.New("web: no live session") type Principal struct { UserID string Username Ident SessionID string Admin bool SudoUntil time.Time } type Guard interface { Resolve(r *http.Request) (Principal, error) SignIn(w http.ResponseWriter, r *http.Request) } type principalKey struct{} func withPrincipal(r *http.Request, p Principal) *http.Request { return r.WithContext(context.WithValue(r.Context(), principalKey{}, p)) } func PrincipalFrom(r *http.Request) (Principal, bool) { p, ok := r.Context().Value(principalKey{}).(Principal) return p, ok } func sessionID(r *http.Request) string { p, _ := PrincipalFrom(r) return p.SessionID } func (k *Kit) AccountHost(g Guard, routes ...Route) http.Handler { h := k.guardedHost(g, false, routes) h.fallback = k.statusPage(http.StatusNotFound) return h } func (k *Kit) AdminHost(g Guard, routes ...Route) http.Handler { if k.server.AdminGroup == "" { panic("web: the admin host needs an admin group") } h := k.guardedHost(g, true, routes) h.fallback = k.statusPage(http.StatusNotFound) return h } func (k *Kit) guardedHost(g Guard, admin bool, routes []Route) *host { if g == nil { panic("web: a guarded host needs a guard") } return k.newHost(routes, func(rt Route, next http.Handler) http.Handler { return k.guard(g, admin, rt.Access, next) }) } func (k *Kit) guard(g Guard, admin bool, access Access, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, err := g.Resolve(r) if err == nil { r = withPrincipal(r, p) } switch { case access == Public: next.ServeHTTP(w, r) case errors.Is(err, ErrNoSession) && access == SignedIn: k.SeeOther(w, r, SignedOutPath) case errors.Is(err, ErrNoSession) && r.Method == http.MethodGet: g.SignIn(w, r) case errors.Is(err, ErrNoSession): k.SeeOther(w, r, "/") case err != nil: slog.Error("session check failed", "err", err) k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal) case admin && access == Protected && !p.Admin: k.renderAdminForbidden(w, r, p) default: next.ServeHTTP(w, r) } }) }