package web import ( "bytes" "html/template" "log/slog" "net/http" transporthttp "tug/internal/transport/http" "tug/internal/transport/web/text" ) type page struct { Styled bool Script bool Narrow bool Wide bool Host Ident Site string Assets assetNames Flash template.HTML CSRF string Once string Data any } type errorData struct { Title text.Key Body text.Key } var errorKeys = map[int]errorData{ http.StatusBadRequest: {text.ErrorBadRequestTitle, text.ErrorBadRequest}, http.StatusForbidden: {text.ErrorForbiddenTitle, text.ErrorForbidden}, http.StatusNotFound: {text.ErrorNotFoundTitle, text.ErrorNotFound}, http.StatusTooManyRequests: {text.ErrorTooManyTitle, text.ErrorTooMany}, http.StatusInternalServerError: {text.ErrorInternalTitle, text.ErrorInternal}, http.StatusServiceUnavailable: {text.ErrorUnavailableTitle, text.ErrorUnavailable}, } func (k *Kit) hostAssets(r *http.Request) (assetNames, bool) { h := transporthttp.CanonicalHost(r.Host) switch { case h == k.server.DomainAccount || h == k.server.DomainAdmin: return k.assets.names, true case k.apps[h]: return k.ssoAssets, true } return assetNames{}, false } func (k *Kit) newPage(w http.ResponseWriter, r *http.Request, data any) page { names, styled := k.hostAssets(r) p := page{Styled: styled, Script: styled && k.enhancement, Assets: names, Site: k.server.SiteName, Data: data} if r.Method == http.MethodGet { p.Flash = k.takeFlash(w, r) } return p } func (k *Kit) formPage(w http.ResponseWriter, r *http.Request, data any) page { p := k.newPage(w, r, data) p.CSRF = k.csrfToken(w, r) return p } func (k *Kit) execute(name string, p page) ([]byte, error) { var buf bytes.Buffer tpl, ok := k.pages[name] err := errNoPage if ok { err = tpl.ExecuteTemplate(&buf, "layout", p) } return buf.Bytes(), err } func (k *Kit) render(w http.ResponseWriter, r *http.Request, status int, name string, p page) { body, err := k.execute(name, p) if err != nil { k.renderFailed(w, r, name, err) return } k.write(w, status, body) } func (k *Kit) renderFailed(w http.ResponseWriter, r *http.Request, name string, err error) { slog.Error("page render failed", "page", name, "err", err) if name == "error" { k.plainInternalError(w) return } k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal) } func (k *Kit) write(w http.ResponseWriter, status int, body []byte) { h := w.Header() h.Set("Content-Type", "text/html; charset=utf-8") h.Set("Cache-Control", "no-store") w.WriteHeader(status) w.Write(body) } func (k *Kit) plainInternalError(w http.ResponseWriter) { body, _ := text.Lookup(text.ErrorInternal) h := w.Header() h.Set("Content-Type", "text/plain; charset=utf-8") h.Set("Cache-Control", "no-store") w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(body + "\n")) } func (k *Kit) renderError(w http.ResponseWriter, r *http.Request, status int, title, body text.Key) { k.render(w, r, status, "error", k.newPage(w, r, errorData{Title: title, Body: body})) } func (k *Kit) ErrorPage(w http.ResponseWriter, r *http.Request, status int) { d, ok := errorKeys[status] if status == http.StatusServiceUnavailable && transporthttp.Busy(r) { d, ok = errorData{text.ErrorBusyTitle, text.ErrorBusy}, true } if !ok { d = errorData{text.ErrorOtherTitle, text.ErrorOther} } k.renderError(w, r, status, d.Title, d.Body) } func (k *Kit) FailurePage(w http.ResponseWriter, r *http.Request, status int) { k.renderError(w, r, status, text.ErrorInternalTitle, text.ErrorInternal) }