1package web
2
3import (
4 "bytes"
5 "html/template"
6 "log/slog"
7 "net/http"
8
9 transporthttp "tug/internal/transport/http"
10 "tug/internal/transport/web/text"
11)
12
13type page struct {
14 Styled bool
15 Script bool
16 Narrow bool
17 Wide bool
18 Host Ident
19 Site string
20 Assets assetNames
21 Flash template.HTML
22 CSRF string
23 Once string
24 Data any
25}
26
27type errorData struct {
28 Title text.Key
29 Body text.Key
30}
31
32var errorKeys = map[int]errorData{
33 http.StatusBadRequest: {text.ErrorBadRequestTitle, text.ErrorBadRequest},
34 http.StatusForbidden: {text.ErrorForbiddenTitle, text.ErrorForbidden},
35 http.StatusNotFound: {text.ErrorNotFoundTitle, text.ErrorNotFound},
36 http.StatusTooManyRequests: {text.ErrorTooManyTitle, text.ErrorTooMany},
37 http.StatusInternalServerError: {text.ErrorInternalTitle, text.ErrorInternal},
38 http.StatusServiceUnavailable: {text.ErrorUnavailableTitle, text.ErrorUnavailable},
39}
40
41func (k *Kit) hostAssets(r *http.Request) (assetNames, bool) {
42 h := transporthttp.CanonicalHost(r.Host)
43 switch {
44 case h == k.server.DomainAccount || h == k.server.DomainAdmin:
45 return k.assets.names, true
46 case k.apps[h]:
47 return k.ssoAssets, true
48 }
49 return assetNames{}, false
50}
51
52func (k *Kit) newPage(w http.ResponseWriter, r *http.Request, data any) page {
53 names, styled := k.hostAssets(r)
54 p := page{Styled: styled, Script: styled && k.enhancement, Assets: names, Site: k.server.SiteName, Data: data}
55 if r.Method == http.MethodGet {
56 p.Flash = k.takeFlash(w, r)
57 }
58 return p
59}
60
61func (k *Kit) formPage(w http.ResponseWriter, r *http.Request, data any) page {
62 p := k.newPage(w, r, data)
63 p.CSRF = k.csrfToken(w, r)
64 return p
65}
66
67func (k *Kit) execute(name string, p page) ([]byte, error) {
68 var buf bytes.Buffer
69 tpl, ok := k.pages[name]
70 err := errNoPage
71 if ok {
72 err = tpl.ExecuteTemplate(&buf, "layout", p)
73 }
74 return buf.Bytes(), err
75}
76
77func (k *Kit) render(w http.ResponseWriter, r *http.Request, status int, name string, p page) {
78 body, err := k.execute(name, p)
79 if err != nil {
80 k.renderFailed(w, r, name, err)
81 return
82 }
83 k.write(w, status, body)
84}
85
86func (k *Kit) renderFailed(w http.ResponseWriter, r *http.Request, name string, err error) {
87 slog.Error("page render failed", "page", name, "err", err)
88 if name == "error" {
89 k.plainInternalError(w)
90 return
91 }
92 k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal)
93}
94
95func (k *Kit) write(w http.ResponseWriter, status int, body []byte) {
96 h := w.Header()
97 h.Set("Content-Type", "text/html; charset=utf-8")
98 h.Set("Cache-Control", "no-store")
99 w.WriteHeader(status)
100 w.Write(body)
101}
102
103func (k *Kit) plainInternalError(w http.ResponseWriter) {
104 body, _ := text.Lookup(text.ErrorInternal)
105 h := w.Header()
106 h.Set("Content-Type", "text/plain; charset=utf-8")
107 h.Set("Cache-Control", "no-store")
108 w.WriteHeader(http.StatusInternalServerError)
109 w.Write([]byte(body + "\n"))
110}
111
112func (k *Kit) renderError(w http.ResponseWriter, r *http.Request, status int, title, body text.Key) {
113 k.render(w, r, status, "error", k.newPage(w, r, errorData{Title: title, Body: body}))
114}
115
116func (k *Kit) ErrorPage(w http.ResponseWriter, r *http.Request, status int) {
117 d, ok := errorKeys[status]
118 if status == http.StatusServiceUnavailable && transporthttp.Busy(r) {
119 d, ok = errorData{text.ErrorBusyTitle, text.ErrorBusy}, true
120 }
121 if !ok {
122 d = errorData{text.ErrorOtherTitle, text.ErrorOther}
123 }
124 k.renderError(w, r, status, d.Title, d.Body)
125}
126
127func (k *Kit) FailurePage(w http.ResponseWriter, r *http.Request, status int) {
128 k.renderError(w, r, status, text.ErrorInternalTitle, text.ErrorInternal)
129}