Raw
  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}