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