feat: base UI shell with themes, auth pages, profile, and role navigation (phase 4)

- embedded Go templates + vanilla ES-module JS + hand-written CSS, no build step
- exact §10 beige light / dark design tokens, square edges (radius 2px),
  system font stack, visible focus rings
- theme toggle persisted to localStorage and the user profile
- login/register/change-password pages wired to the auth API
- profile page: avatar upload (image-sniffed, old file cleanup), bio,
  contacts, arbitrary extra key/value fields, optimistic-concurrency 409
- role-based top navigation with placeholders for later-phase areas
- GET /files/{id} with scope-based access (session) or signed token (§5.1)
- security headers incl. CSP without unsafe-inline scripts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-12 18:39:38 +02:00
parent c1cc781279
commit 34bf5b5ac2
27 changed files with 1579 additions and 5 deletions
+17 -1
View File
@@ -57,7 +57,23 @@ func (r *statusRecorder) Write(b []byte) (int, error) {
func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
func (s *Server) withMiddleware(next http.Handler) http.Handler {
return s.recoverPanic(s.assignRequestID(s.resolveClientInfo(s.accessLog(next))))
return s.recoverPanic(s.assignRequestID(s.resolveClientInfo(s.securityHeaders(s.accessLog(next)))))
}
// securityHeaders applies the §12 hardening headers. CSP allows no inline
// scripts — all JS ships as external modules.
func (s *Server) securityHeaders(next http.Handler) http.Handler {
const csp = "default-src 'self'; script-src 'self'; style-src 'self'; " +
"img-src 'self' data:; connect-src 'self'; font-src 'self'; " +
"frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Security-Policy", csp)
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}
// recoverPanic guarantees no panic escapes a request path: it logs the stack