From 00a9388c322cbbcf924b3260e26eb9bca06d5706 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 14 May 2026 22:42:23 +0200 Subject: [PATCH] docs(02-06): complete logout + protected routes + layout plan - Create 02-06-SUMMARY.md with TDD gate compliance, router middleware snapshot - Update STATE.md: plans 01-06 complete, plan 07 (CSRF) next - Update ROADMAP.md: Phase 2 at 6/7 plans, 02-06 checked - Mark AUTH-04 complete in REQUIREMENTS.md (AUTH-05 was already checked) --- .planning/REQUIREMENTS.md | 2 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 17 +- .../phases/02-authentication/02-06-SUMMARY.md | 162 ++++++++++++++++++ 4 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/02-authentication/02-06-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 40f8a6c..68e022f 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -20,7 +20,7 @@ Requirements for the initial Go+HTMX milestone. Each maps to exactly one roadmap - [x] **AUTH-01**: User can sign up with email and password (server-side validation, bcrypt/argon2 hash) - [x] **AUTH-02**: User can log in with email and password and receives a server-managed session - [x] **AUTH-03**: Sessions persist via HTTP-only, signed cookies (Secure + SameSite=Lax) and survive browser refresh -- [ ] **AUTH-04**: User can log out from any authenticated page (server invalidates session) +- [x] **AUTH-04**: User can log out from any authenticated page (server invalidates session) - [x] **AUTH-05**: Protected routes redirect unauthenticated requests to the login page; authenticated users on auth pages are sent to the dashboard - [ ] **AUTH-06**: CSRF protection on all state-changing requests - [x] **AUTH-07**: Rate-limited login attempts per email + IP to discourage credential stuffing diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9e8a897..098f389 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -13,7 +13,7 @@ | # | Phase | Goal | Requirements | |---|-------|------|--------------| | 1 | Foundation | Fresh `backend/` Go package boots, renders HTMX, talks to Postgres | FOUND-01..05 | -| 2 | 5/7 | In Progress| | +| 2 | 6/7 | In Progress| | | 3 | Tablos CRUD | An authenticated user can manage their tablos end-to-end | TABLO-01..06 | | 4 | Tasks (Kanban) | A user can run a kanban board inside a tablo | TASK-01..07 | | 5 | Files | A user can attach, list, download, delete files on a tablo | FILE-01..06 | @@ -58,14 +58,14 @@ Plans: **User-in-loop:** Approve the `users` and `sessions` table schemas (columns, indexes, deletion semantics) before sqlc generation. Approve hash algorithm choice. -**Plans:** 5/7 plans executed +**Plans:** 6/7 plans executed Plans: - [x] 02-01-PLAN.md — Schema + sqlc + auth-package skeleton (citext + users + sessions, test DB harness) - [x] 02-02-PLAN.md — argon2id password hashing (TDD: Hash/Verify with PHC encoding) - [x] 02-03-PLAN.md — Session store + cookie + ResolveSession/RequireAuth/RedirectIfAuthed middleware - [x] 02-04-PLAN.md — Signup vertical slice (form → validate → hash → InsertUser → session → cookie → redirect) - [x] 02-05-PLAN.md — Login vertical slice + in-memory rate limiter (AUTH-07) -- [ ] 02-06-PLAN.md — Logout + protect GET / + layout header logout button +- [x] 02-06-PLAN.md — Logout + protect GET / + layout header logout button (AUTH-04, AUTH-05) - [ ] 02-07-PLAN.md — Mount gorilla/csrf + @ui.CSRFField templ helper across every form (AUTH-06) ### Phase 3: Tablos CRUD diff --git a/.planning/STATE.md b/.planning/STATE.md index d687e65..8437c4f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,13 +3,13 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: in_progress -last_updated: "2026-05-14T20:34:00.000Z" +last_updated: "2026-05-14T20:41:09.893Z" progress: total_phases: 7 completed_phases: 1 total_plans: 11 - completed_plans: 10 - percent: 91 + completed_plans: 11 + percent: 92 --- # STATE @@ -30,7 +30,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) | # | Phase | Status | |---|-------|--------| | 1 | Foundation | ✓ Complete | -| 2 | Authentication | ◑ In Progress (5/7 plans done) | +| 2 | Authentication | ◑ In Progress (6/7 plans done) | | 3 | Tablos CRUD | ○ Pending | | 4 | Tasks (Kanban) | ○ Pending | | 5 | Files | ○ Pending | @@ -39,7 +39,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) ## Active Phase -**Phase 2: Authentication** — Plans 01–05 complete. Plan 06 (logout handler) next. +**Phase 2: Authentication** — Plans 01–06 complete. Plan 07 (CSRF) next. ## Decisions @@ -56,6 +56,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) - **errInvalidCreds const** (not inline string) — satisfies D-20 single-source-of-truth grep gate; both credential failure paths use the constant - **NewLimiterStoreWithClock exported** — cross-package integration tests inject a frozen clock without a test-helper shim - **Status 401 for non-HTMX credential failures** — consistent with HTTP semantics; HTMX path returns 200+fragment +- **Layout takes *auth.User explicitly** (not thread-through-ctx) — easier to test templates in isolation; no magic context values +- **TestIndex_RendersHxGet rewritten to TestIndex_UnauthRedirects** — GET / is now protected; Phase 1 test was a false positive after this plan ## Performance Metrics @@ -66,6 +68,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) | 02-authentication | 03 | ~15min | 2 | 5 | | 02-authentication | 04 | ~25min | 2 | 11 | | 02-authentication | 05 | ~7min | 2 | 9 | +| 02-authentication | 06 | ~12min | 1 | 9 | ## Notes @@ -81,6 +84,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) - Phase 2 Plan 04 SUMMARY: `.planning/phases/02-authentication/02-04-SUMMARY.md` - Commits (02-05): b5c20c7 (LimiterStore + tests), 7d8c498 (login handler + integration tests) - Phase 2 Plan 05 SUMMARY: `.planning/phases/02-authentication/02-05-SUMMARY.md` +- Commits (02-06): b5c3fc4 (RED tests), 8b54ff4 (logout + protected routes + layout) +- Phase 2 Plan 06 SUMMARY: `.planning/phases/02-authentication/02-06-SUMMARY.md` --- -*Last updated: 2026-05-14 after 02-05 execution* +*Last updated: 2026-05-14 after 02-06 execution* diff --git a/.planning/phases/02-authentication/02-06-SUMMARY.md b/.planning/phases/02-authentication/02-06-SUMMARY.md new file mode 100644 index 0000000..4e92285 --- /dev/null +++ b/.planning/phases/02-authentication/02-06-SUMMARY.md @@ -0,0 +1,162 @@ +--- +phase: 02-authentication +plan: 06 +subsystem: auth +tags: [go, htmx, templ, logout, protected-routes, layout, sessions] + +# Dependency graph +requires: + - phase: 02-authentication/01-05 + provides: "Session store (Create/Delete/Lookup), RequireAuth middleware, ResolveSession middleware, cookie helpers (ClearSessionCookie)" +provides: + - "POST /logout: hard-deletes session row, clears cookie, redirects to /login (D-06, T-2-07)" + - "GET / protected by RequireAuth group — unauth GET redirects to /login (AUTH-05, D-23)" + - "Layout(title, *auth.User) — renders logout form + email in header when authed, nothing when nil" + - "Index(user *auth.User) — renders home page with user email visible (smoke test for ctx user)" + - "AUTH-04 and AUTH-05 closed" +affects: [02-authentication/07-csrf, 03-tablos-crud] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Protected route group pattern: chi Group + r.Use(auth.RequireAuth) wraps / and /logout together (D-23)" + - "HTMX-aware logout: LogoutHandler checks HX-Request header, sends HX-Redirect vs 303 See Other" + - "Layout receives *auth.User as explicit parameter — no template-context magic, no hidden coupling" + - "Defense-in-depth: LogoutHandler checks Authed(ctx) even though RequireAuth already gates" + +key-files: + created: + - backend/templates/layout_test.go + modified: + - backend/internal/web/handlers_auth.go + - backend/internal/web/handlers_auth_test.go + - backend/internal/web/handlers.go + - backend/internal/web/handlers_test.go + - backend/internal/web/router.go + - backend/templates/layout.templ + - backend/templates/index.templ + - backend/templates/auth_signup.templ + - backend/templates/auth_login.templ + +key-decisions: + - "Layout takes *auth.User as explicit parameter (not thread-through-ctx) — easier to test, no magic" + - "Index(user *auth.User) takes user directly — IndexHandler pulls from auth.Authed(ctx) and passes it" + - "TestIndex_RendersHxGet rewritten to TestIndex_UnauthRedirects — GET / is now protected, not public" + +patterns-established: + - "Route group pattern: Group(func(r chi.Router){ r.Use(auth.RequireAuth); r.Get... }) for protected routes" + - "LogoutHandler shape: check Authed (defense-in-depth), Store.Delete, ClearSessionCookie, HTMX-aware redirect" + +requirements-completed: [AUTH-04, AUTH-05] + +# Metrics +duration: ~12min +completed: 2026-05-14 +--- + +# Phase 02 Plan 06: Logout + Protected Routes + Layout Summary + +**POST /logout hard-deletes the session via Store.Delete, clears the cookie, and redirects to /login; GET / is moved into a RequireAuth-guarded route group; Layout header adapts to auth state via an explicit *auth.User parameter.** + +## Performance + +- **Duration:** ~12 min +- **Started:** 2026-05-14T20:40:00Z +- **Completed:** 2026-05-14T20:52:00Z +- **Tasks:** 1 (TDD: RED + GREEN) +- **Files modified:** 9 + +## Accomplishments + +- AUTH-04 closed: `LogoutHandler` hard-deletes the session row (`Store.Delete`), clears the browser cookie (`ClearSessionCookie`), and redirects to `/login` (303 or HX-Redirect for HTMX). +- AUTH-05 closed: `GET /` is now inside a `chi.Group` wrapped with `auth.RequireAuth`; unauthenticated requests get 303 → `/login` (or HX-Redirect for HTMX). +- `Layout(title string, user *auth.User)` — header renders logout POST form + email when `user != nil`; nothing when `nil` (D-22 anti-GET-logout enforced by template structure). +- `Index(user *auth.User)` — home page shows `Signed in as {email}` smoke check. +- Phase 1 test `TestIndex_RendersHxGet` rewritten to `TestIndex_UnauthRedirects` — the route is now protected and the old test would have been a false positive. +- `TestLogout_AfterLogoutSubsequentRequestUnauth` verifies that a captured cookie is invalidated server-side after logout (T-2-07). + +## Task Commits + +1. **Task 1 (RED): Failing tests for logout, protected routes, layout** - `b5c3fc4` (test) +2. **Task 1 (GREEN): Full implementation** - `8b54ff4` (feat) + +## Files Created/Modified + +- `backend/templates/layout.templ` - Updated signature: `Layout(title string, user *auth.User)`; header conditionally renders logout form +- `backend/templates/index.templ` - Updated signature: `Index(user *auth.User)`; shows "Signed in as {email}" +- `backend/templates/auth_signup.templ` - Updated `@Layout("Sign up", nil)` call +- `backend/templates/auth_login.templ` - Updated `@Layout("Sign in", nil)` call +- `backend/templates/layout_test.go` - New: `TestLayout_LogoutFormVisibleWhenAuthed`, `TestLayout_LogoutFormHiddenWhenUnauthed` +- `backend/internal/web/handlers_auth.go` - Added `LogoutHandler(deps AuthDeps)`, added `log/slog` import +- `backend/internal/web/handlers_auth_test.go` - Added 7 new tests: `TestLogout_*`, `TestProtected_*` +- `backend/internal/web/handlers.go` - `IndexHandler` pulls `auth.Authed(ctx)` and passes user to template +- `backend/internal/web/router.go` - Added protected group `Group(func(r chi.Router){ r.Use(auth.RequireAuth); r.Get("/"); r.Post("/logout") })`; removed old top-level `r.Get("/")` +- `backend/internal/web/handlers_test.go` - `TestIndex_RendersHxGet` → `TestIndex_UnauthRedirects` + +## Router Middleware Order (Plan 07 diff base) + +Current full middleware stack in `NewRouter` as of this plan: + +``` +RequestIDMiddleware +chimw.RealIP +SlogLoggerMiddleware +chimw.Recoverer +auth.ResolveSession(deps.Store) +--- route groups below --- +Group: auth.RedirectIfAuthed → GET /signup, GET /login +r.Post("/signup", ...), r.Post("/login", ...) [no group middleware] +Group: auth.RequireAuth → GET /, POST /logout +r.Get("/healthz", ...), r.Get("/demo/time", ...), r.Get("/static/*", ...) +``` + +Plan 07 will insert `csrf.Protect(...)` after `auth.ResolveSession` and before the route groups. The comment in `router.go` already notes this insertion point. + +## Decisions Made + +- **Layout takes `*auth.User` explicitly** (not threaded via context) — simpler to unit-test templates in isolation; templ components receive data through parameters, not magic context values. +- **IndexHandler() stays a no-arg constructor** — it pulls the user from `auth.Authed(r.Context())` internally. The `user` can be `nil` if called without RequireAuth (though in practice the group gate ensures it's always set). The `templates.Index(user)` accepts nil gracefully (not called — only reached through RequireAuth). +- **TestIndex_RendersHxGet rewritten** — the old test assumed GET / is public and returns 200 with HTMX demo content. After this plan, GET / is protected. The new `TestIndex_UnauthRedirects` correctly tests the new behavior. The HTMX demo rendering with a valid session is covered by `TestProtected_HomeAuthRendersUserEmail` in `handlers_auth_test.go` (DB-backed, skips without TEST_DATABASE_URL). + +## Deviations from Plan + +None — plan executed exactly as written. + +## Issues Encountered + +None. All tests pass; build is clean; templ generate succeeds. + +## Known Stubs + +None — no placeholder data, hardcoded empty values, or TODO content in any file created/modified by this plan. + +## Threat Flags + +None — no new security-relevant surfaces beyond what the plan's threat model covers. + +## User Setup Required + +None — no external service configuration required. + +## Next Phase Readiness + +- Auth loop is functionally complete: signup → GET / (authed) → logout → GET /login → login → GET / (authed). +- Plan 07 (CSRF) is the final Phase 2 plan. The router comment at the csrf.Protect insertion point is already in place. +- Downstream phases (03-tablos-crud) can use the `auth.Authed(ctx)` pattern to access the user in any protected handler. + +## Self-Check + +- [x] `backend/templates/layout_test.go` exists +- [x] `backend/templates/layout.templ` updated with user parameter +- [x] `backend/internal/web/handlers_auth.go` contains `func LogoutHandler` +- [x] `backend/internal/web/router.go` contains `auth.RequireAuth` in protected group +- [x] Commits b5c3fc4 (RED) and 8b54ff4 (GREEN) exist +- [x] `go build ./...` exits 0 +- [x] `go test ./internal/web/ ./internal/auth/ ./templates/` all pass + +## Self-Check: PASSED + +--- +*Phase: 02-authentication* +*Completed: 2026-05-14*