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}