Run Docker runtime stage as non-root user app (UID 65532). Add styled full-page 404/500 error rendering for navigation requests while preserving plain-text responses for HTMX partials. Reuse recent unconsumed OIDC login state to avoid state mismatch on parallel login hits, and merge resource_access in role extraction. Re-level template headings, add autocomplete tokens, and resolve catalog resource display names. Self-label test-stack secrets and document CSRF secret rotation.
73 lines
2.6 KiB
Go
73 lines
2.6 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
)
|
|
|
|
// SafeTemplates wraps *html/template.Template to ensure all rendering goes
|
|
// through a buffer. This prevents partial HTML from being written to the
|
|
// response if template execution fails mid-way — the stdlib streams directly
|
|
// to the writer and cannot roll back a partial write.
|
|
//
|
|
// Handlers hold a *SafeTemplates instead of *template.Template. The underlying
|
|
// template set is unexported, so callers cannot bypass the buffer by calling
|
|
// ExecuteTemplate on the ResponseWriter directly — the compiler prevents it.
|
|
type SafeTemplates struct {
|
|
tmpl *template.Template
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewSafeTemplates wraps tmpl and logger for safe buffered rendering.
|
|
func NewSafeTemplates(tmpl *template.Template, logger *slog.Logger) *SafeTemplates {
|
|
return &SafeTemplates{tmpl: tmpl, logger: logger}
|
|
}
|
|
|
|
// RenderErrorPage writes a styled full-page error response for navigation
|
|
// requests. HTMX partial requests (HX-Request header) get the plain-text
|
|
// error the client-side toast contract expects, and a failure to render the
|
|
// error template itself also falls back to plain text, so this never emits a
|
|
// partial page.
|
|
func (s *SafeTemplates) RenderErrorPage(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
http.Error(w, msg, status)
|
|
return
|
|
}
|
|
data := struct {
|
|
Status int
|
|
StatusText string
|
|
Message string
|
|
}{Status: status, StatusText: http.StatusText(status), Message: msg}
|
|
var buf bytes.Buffer
|
|
if err := s.tmpl.ExecuteTemplate(&buf, "error.html", data); err != nil {
|
|
s.logger.Error("error page render failed",
|
|
slog.Int("status", status),
|
|
slog.Any("error", err))
|
|
http.Error(w, msg, status)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
w.Write(buf.Bytes())
|
|
}
|
|
|
|
// Render executes the named template into a buffer and writes it to w only on
|
|
// success. On failure it logs the error and writes a 500 error fragment,
|
|
// guaranteeing the response is never partial or truncated.
|
|
func (s *SafeTemplates) Render(w http.ResponseWriter, name string, data any) {
|
|
var buf bytes.Buffer
|
|
if err := s.tmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
|
s.logger.Error("template execution failed",
|
|
slog.String("template", name),
|
|
slog.Any("error", err))
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(`<div class="alert alert-danger" role="alert"><strong>Error:</strong> Failed to render content. Please try again.</div>`))
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write(buf.Bytes())
|
|
}
|