xtablo-source/backend/templates/layout.templ
Arthur Belleville 389e1bc8b4
feat(02-07): gorilla/csrf integration — mount middleware, wire all forms, env-driven key
- auth.Mount(env, key) wraps csrf.Protect with locked D-14/D-24 options
- auth.LoadKeyFromEnv() reads SESSION_SECRET, hex-decodes, validates 32 bytes; fails fast on error
- ui.CSRFField(token) templ component renders hidden _csrf input
- Layout, LoginPage/Fragment, SignupPage/Fragment, Index all embed @ui.CSRFField(csrfToken)
- Handlers thread csrf.Token(r) into every page/fragment render call
- NewRouter mounts auth.Mount after ResolveSession, before all route groups (D-24)
- main.go calls auth.LoadKeyFromEnv(); logs.Fatalf on missing/invalid SESSION_SECRET
- SESSION_SECRET documented in .env.example with openssl rand -hex 32 instruction
- go.mod: gorilla/csrf v1.7.3 (direct); prior tests updated with getCSRFToken helper
- All Plan 04/05/06 tests updated to acquire and submit valid _csrf tokens
2026-05-14 22:59:06 +02:00

57 lines
2.2 KiB
Text

// Package templates owns the server-rendered HTML for the Phase 1 walking
// skeleton. Each *.templ file compiles to a *_templ.go file via `templ
// generate`; generated files are gitignored.
package templates
import (
"backend/internal/auth"
"backend/internal/web/ui"
)
// Layout is the base HTML shell every page renders inside. The structural
// classes, container width (max-w-5xl), horizontal padding, header strip,
// footer, and asset references (/static/tailwind.css, /static/htmx.min.js)
// are locked by UI-SPEC §Base Layout Contract and CONTEXT D-10 — do NOT
// load HTMX from a CDN.
//
// user is non-nil when the request context carries an authenticated session.
// When non-nil, the header renders a Log out POST form (D-22). Auth pages
// pass nil since they're gated behind RedirectIfAuthed and never shown to
// authed users.
//
// csrfToken is threaded from the handler via csrf.Token(r) so the logout
// form can embed @ui.CSRFField(csrfToken) (AUTH-06, D-14).
templ Layout(title string, user *auth.User, csrfToken string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{ title }</title>
<link rel="stylesheet" href="/static/tailwind.css"/>
</head>
<body class="min-h-screen bg-white text-slate-900 antialiased">
<header class="bg-slate-50 border-b border-slate-200">
<div class="mx-auto max-w-5xl px-4 sm:px-6 py-4 flex items-center justify-between">
<span class="text-sm font-semibold text-slate-800">Xtablo</span>
if user != nil {
<div class="flex items-center gap-3">
<span class="text-sm text-slate-600">{ user.Email }</span>
<form method="POST" action="/logout" class="inline">
@ui.CSRFField(csrfToken)
<button type="submit" class="text-sm text-slate-700 hover:underline">Log out</button>
</form>
</div>
}
</div>
</header>
<main class="mx-auto max-w-5xl px-4 sm:px-6 py-8">
{ children... }
</main>
<footer class="mx-auto max-w-5xl px-4 sm:px-6 py-6 text-sm text-slate-600">
Phase 2 · Authentication
</footer>
<script src="/static/htmx.min.js" defer></script>
</body>
</html>
}