---
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*
