Raw
 1package web
 2
 3import (
 4	"context"
 5	"errors"
 6	"log/slog"
 7	"net/http"
 8	"time"
 9
10	"tug/internal/transport/web/text"
11)
12
13var ErrNoSession = errors.New("web: no live session")
14
15type Principal struct {
16	UserID    string
17	Username  Ident
18	SessionID string
19	Admin     bool
20	SudoUntil time.Time
21}
22
23type Guard interface {
24	Resolve(r *http.Request) (Principal, error)
25	SignIn(w http.ResponseWriter, r *http.Request)
26}
27
28type principalKey struct{}
29
30func withPrincipal(r *http.Request, p Principal) *http.Request {
31	return r.WithContext(context.WithValue(r.Context(), principalKey{}, p))
32}
33
34func PrincipalFrom(r *http.Request) (Principal, bool) {
35	p, ok := r.Context().Value(principalKey{}).(Principal)
36	return p, ok
37}
38
39func sessionID(r *http.Request) string {
40	p, _ := PrincipalFrom(r)
41	return p.SessionID
42}
43
44func (k *Kit) AccountHost(g Guard, routes ...Route) http.Handler {
45	h := k.guardedHost(g, false, routes)
46	h.fallback = k.statusPage(http.StatusNotFound)
47	return h
48}
49
50func (k *Kit) AdminHost(g Guard, routes ...Route) http.Handler {
51	if k.server.AdminGroup == "" {
52		panic("web: the admin host needs an admin group")
53	}
54	h := k.guardedHost(g, true, routes)
55	h.fallback = k.statusPage(http.StatusNotFound)
56	return h
57}
58
59func (k *Kit) guardedHost(g Guard, admin bool, routes []Route) *host {
60	if g == nil {
61		panic("web: a guarded host needs a guard")
62	}
63	return k.newHost(routes, func(rt Route, next http.Handler) http.Handler {
64		return k.guard(g, admin, rt.Access, next)
65	})
66}
67
68func (k *Kit) guard(g Guard, admin bool, access Access, next http.Handler) http.Handler {
69	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70		p, err := g.Resolve(r)
71		if err == nil {
72			r = withPrincipal(r, p)
73		}
74		switch {
75		case access == Public:
76			next.ServeHTTP(w, r)
77		case errors.Is(err, ErrNoSession) && access == SignedIn:
78			k.SeeOther(w, r, SignedOutPath)
79		case errors.Is(err, ErrNoSession) && r.Method == http.MethodGet:
80			g.SignIn(w, r)
81		case errors.Is(err, ErrNoSession):
82			k.SeeOther(w, r, "/")
83		case err != nil:
84			slog.Error("session check failed", "err", err)
85			k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal)
86		case admin && access == Protected && !p.Admin:
87			k.renderAdminForbidden(w, r, p)
88		default:
89			next.ServeHTTP(w, r)
90		}
91	})
92}