Files
cgalo5758 8e7e0dd04d Harden container, OIDC auth, and error handling
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.
2026-07-31 23:27:15 -05:00

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