Raw
 1package web
 2
 3import (
 4	"errors"
 5	"log/slog"
 6	"net/http"
 7
 8	"tug/internal/transport/web/text"
 9)
10
11const AppLogoutPath = "/.sso/logout"
12
13type appPageData struct {
14	Username Ident
15	Account  string
16}
17
18func (k *Kit) AppHost(g Guard, routes ...Route) http.Handler {
19	if g == nil {
20		panic("web: an app host needs a guard")
21	}
22	for _, rt := range routes {
23		if rt.Access != Public {
24			panic("web: an app host serves only public routes: " + rt.Method + " " + rt.Path)
25		}
26	}
27	return k.newHost(routes, func(_ Route, next http.Handler) http.Handler {
28		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29			p, err := g.Resolve(r)
30			switch {
31			case err == nil:
32				next.ServeHTTP(w, withPrincipal(r, p))
33			case errors.Is(err, ErrNoSession):
34				next.ServeHTTP(w, r)
35			default:
36				slog.Error("session check failed", "err", err)
37				k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal)
38			}
39		})
40	})
41}
42
43func (k *Kit) AppLogoutRoutes(e SessionEnder) []Route {
44	if e == nil {
45		panic("web: the app logout pages need a session ender")
46	}
47	show := func(w http.ResponseWriter, r *http.Request) {
48		d := appPageData{Account: k.account + LogoutPath}
49		p, ok := PrincipalFrom(r)
50		if !ok {
51			k.render(w, r, http.StatusOK, "app-signed-out", k.newPage(w, r, d))
52			return
53		}
54		d.Username = p.Username
55		k.render(w, r, http.StatusOK, "app-logout", k.formPage(w, r, d))
56	}
57	submit := func(w http.ResponseWriter, r *http.Request) {
58		if p, ok := PrincipalFrom(r); ok {
59			if err := e.EndSession(w, r, p); err != nil && !errors.Is(err, ErrNoSession) {
60				slog.Error("app logout failed", "err", err)
61				k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal)
62				return
63			}
64			k.RotateCSRF(w)
65		}
66		k.SeeOther(w, r, AppLogoutPath)
67	}
68	ended := func(w http.ResponseWriter, r *http.Request) {
69		if _, ok := PrincipalFrom(r); ok {
70			k.renderError(w, r, http.StatusForbidden, text.ErrorForbiddenTitle, text.ErrorFormExpired)
71			return
72		}
73		k.SeeOther(w, r, AppLogoutPath)
74	}
75	return []Route{
76		{Method: http.MethodGet, Path: AppLogoutPath, Access: Public, Handler: http.HandlerFunc(show)},
77		{Method: http.MethodPost, Path: AppLogoutPath, Access: Public, Handler: http.HandlerFunc(submit), csrfFallback: http.HandlerFunc(ended)},
78	}
79}
80
81func (k *Kit) RenderAppForbidden(w http.ResponseWriter, r *http.Request, username Ident) {
82	k.render(w, r, http.StatusForbidden, "app-forbidden", k.newPage(w, r, appPageData{Username: username, Account: k.account + "/"}))
83}