feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
package web
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-14 20:17:50 +00:00
|
|
|
"backend/internal/auth"
|
|
|
|
|
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
chimw "github.com/go-chi/chi/v5/middleware"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Pinger is the contract /healthz uses to probe the data plane. *pgxpool.Pool
|
|
|
|
|
// satisfies this interface out of the box, which is why cmd/web passes the
|
|
|
|
|
// pool directly to NewRouter (no adapter required).
|
|
|
|
|
type Pinger interface {
|
|
|
|
|
Ping(ctx context.Context) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewRouter constructs the chi router with the middleware stack locked by
|
2026-05-14 20:17:50 +00:00
|
|
|
// CONTEXT D-24:
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
//
|
2026-05-14 20:17:50 +00:00
|
|
|
// 1. RequestIDMiddleware (UUIDv4 — NOT chi's base32 RequestID)
|
|
|
|
|
// 2. chi RealIP
|
|
|
|
|
// 3. SlogLoggerMiddleware (REPLACES chi's middleware.Logger — Pitfall 6)
|
|
|
|
|
// 4. chi Recoverer (after Logger so panics carry request_id)
|
|
|
|
|
// 5. auth.ResolveSession (reads session cookie, attaches user to context)
|
|
|
|
|
// NOTE: csrf.Protect is added in Plan 07.
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
//
|
2026-05-14 20:17:50 +00:00
|
|
|
// Routes: GET / · GET /healthz · GET /demo/time · GET /static/*
|
|
|
|
|
// GET /signup (auth pages, behind RedirectIfAuthed) · POST /signup.
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
// staticDir is the on-disk path served at /static/*; path traversal is
|
|
|
|
|
// blocked by http.Dir's default behavior (T-01-08).
|
2026-05-14 20:17:50 +00:00
|
|
|
//
|
|
|
|
|
// deps.Store may be nil during unit tests for Phase 1 routes (those routes
|
|
|
|
|
// never exercise session resolution). ResolveSession guards against nil Store.
|
|
|
|
|
func NewRouter(pinger Pinger, staticDir string, deps AuthDeps) http.Handler {
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
r := chi.NewRouter()
|
|
|
|
|
r.Use(RequestIDMiddleware)
|
|
|
|
|
r.Use(chimw.RealIP)
|
|
|
|
|
r.Use(SlogLoggerMiddleware(slog.Default()))
|
|
|
|
|
r.Use(chimw.Recoverer)
|
2026-05-14 20:17:50 +00:00
|
|
|
r.Use(auth.ResolveSession(deps.Store))
|
|
|
|
|
|
|
|
|
|
// Auth pages — redirect to / if already authenticated.
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(auth.RedirectIfAuthed)
|
|
|
|
|
r.Get("/signup", SignupPageHandler())
|
2026-05-14 20:27:54 +00:00
|
|
|
r.Get("/login", LoginPageHandler())
|
2026-05-14 20:17:50 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-14 20:27:54 +00:00
|
|
|
// Signup and login POSTs are intentionally outside the RedirectIfAuthed group:
|
2026-05-14 20:17:50 +00:00
|
|
|
// an authed user submitting the form directly should still get a useful
|
|
|
|
|
// response; the GET guard handles the common case.
|
|
|
|
|
r.Post("/signup", SignupPostHandler(deps))
|
2026-05-14 20:27:54 +00:00
|
|
|
r.Post("/login", LoginPostHandler(deps))
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
|
2026-05-14 20:40:10 +00:00
|
|
|
// Protected routes — require an authenticated session (D-23, AUTH-05).
|
|
|
|
|
// RequireAuth checks the context set by ResolveSession above and redirects
|
|
|
|
|
// unauthenticated requests to /login (HTMX: HX-Redirect, plain: 303).
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(auth.RequireAuth)
|
|
|
|
|
r.Get("/", IndexHandler())
|
|
|
|
|
r.Post("/logout", LogoutHandler(deps))
|
|
|
|
|
})
|
|
|
|
|
|
feat(01-03): templ layout/index/fragments + handlers + chi router
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
2026-05-14 17:25:43 +00:00
|
|
|
r.Get("/healthz", HealthzHandler(pinger))
|
|
|
|
|
r.Get("/demo/time", DemoTimeHandler(func() time.Time { return time.Now() }))
|
|
|
|
|
|
|
|
|
|
fs := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
|
|
|
|
|
r.Get("/static/*", fs.ServeHTTP)
|
|
|
|
|
|
|
|
|
|
return r
|
|
|
|
|
}
|