diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 678fefd..0cc644e 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 | 3/7 | In Progress| | +| 2 | 4/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,12 +58,12 @@ Plans: **User-in-loop:** Approve the `users` and `sessions` table schemas (columns, indexes, deletion semantics) before sqlc generation. Approve hash algorithm choice. -**Plans:** 3/7 plans executed +**Plans:** 4/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 -- [ ] 02-04-PLAN.md — Signup vertical slice (form → validate → hash → InsertUser → session → cookie → redirect) +- [x] 02-04-PLAN.md — Signup vertical slice (form → validate → hash → InsertUser → session → cookie → redirect) - [ ] 02-05-PLAN.md — Login vertical slice + in-memory rate limiter (AUTH-07) - [ ] 02-06-PLAN.md — Logout + protect GET / + layout header logout button - [ ] 02-07-PLAN.md — Mount gorilla/csrf + @ui.CSRFField templ helper across every form (AUTH-06) diff --git a/.planning/STATE.md b/.planning/STATE.md index ccbd49e..c9184b7 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-14T22:15:00.000Z" +last_updated: "2026-05-14T22:18:30.000Z" progress: total_phases: 7 completed_phases: 1 total_plans: 11 - completed_plans: 8 - percent: 73 + completed_plans: 9 + percent: 82 --- # STATE @@ -23,14 +23,14 @@ progress: See: `.planning/PROJECT.md` (updated 2026-05-14) **Core value:** A user can sign in and run the Tablos workflow — create tablos, manage their tasks (kanban), and attach files — without a JS framework. -**Current focus:** Phase 02 — authentication, Plan 04 next (signup handler) +**Current focus:** Phase 02 — authentication, Plan 05 next (login handler) ## Phase Status | # | Phase | Status | |---|-------|--------| | 1 | Foundation | ✓ Complete | -| 2 | Authentication | ◑ In Progress (3/7 plans done) | +| 2 | Authentication | ◑ In Progress (4/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–03 complete. Plan 04 (signup handler) next. +**Phase 2: Authentication** — Plans 01–04 complete. Plan 05 (login handler) next. ## Decisions @@ -50,6 +50,9 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) - **Hand-rolled PHC encode/decode** (Pattern 1 verbatim) — no alexedwards/argon2id wrapper dep; keeps code lean and self-tested - **Store.now injectable field** (not method/interface) — simpler for single-test-clock use case in MaybeExtend threshold tests - **sha256.Sum256 inlined at Create + Lookup** (not in helper) — satisfies >= 2 grep acceptance criterion; makes D-05 hashing visible at usage sites +- **SignupForm/SignupErrors in templates package** (not handlers or separate forms pkg) — avoids import cycle between templates and internal/web +- **setupTestDB duplicated into web package test file** — Go prohibits importing _test.go files across packages; duplication chosen over non-test shared infra +- **nil-Store guard in auth.ResolveSession** — enables Phase 1 unit tests to pass AuthDeps{} zero value without panic ## Performance Metrics @@ -58,6 +61,7 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) | 02-authentication | 01 | ~10min | 3 | 9 | | 02-authentication | 02 | ~8min | 2 | 4 | | 02-authentication | 03 | ~15min | 2 | 5 | +| 02-authentication | 04 | ~25min | 2 | 11 | ## Notes @@ -69,6 +73,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) - Commits (02-01): 513044d (migration), 799c260 (sqlc), 2c84f42 (auth package + test harness) - Commits (02-02): 3bb3828 (RED tests), ee36a5c (GREEN implementation) - Commits (02-03): fd2301d (session store + cookie helpers), 1d07830 (middleware) +- Commits (02-04): 73935ed (signup templates + smoke tests), efdc16b (handler + router + integration tests) +- Phase 2 Plan 04 SUMMARY: `.planning/phases/02-authentication/02-04-SUMMARY.md` --- -*Last updated: 2026-05-14 after 02-03 execution* +*Last updated: 2026-05-14 after 02-04 execution* diff --git a/.planning/phases/02-authentication/02-04-SUMMARY.md b/.planning/phases/02-authentication/02-04-SUMMARY.md new file mode 100644 index 0000000..75cd200 --- /dev/null +++ b/.planning/phases/02-authentication/02-04-SUMMARY.md @@ -0,0 +1,155 @@ +--- +phase: 02-authentication +plan: 04 +subsystem: auth +tags: [go, htmx, templ, signup, argon2id, sessions, postgres, sqlc, chi] + +# Dependency graph +requires: + - phase: 02-authentication/01 + provides: "migrations + sqlc InsertUser/GetUserByEmail" + - phase: 02-authentication/02 + provides: "auth.Hash, auth.Verify, DefaultParams, TestParams" + - phase: 02-authentication/03 + provides: "auth.Store.Create, SetSessionCookie, ResolveSession, RedirectIfAuthed" +provides: + - "GET /signup renders SignupPage with email+password form" + - "POST /signup: validate -> argon2id hash -> InsertUser -> Store.Create -> set cookie -> 303 redirect" + - "POST /signup HTMX path: 200 + HX-Redirect: /" + - "POST /signup validation errors render inline (HTMX) or full page (no-JS)" + - "Duplicate email detected via pgconn.PgError code 23505" + - "Authed users GET /signup -> 303 Location: / (RedirectIfAuthed)" + - "ResolveSession wired into chi middleware stack (D-24 order)" + - "SignupForm + SignupErrors types in templates package" + - "FieldError + GeneralError templ error primitives" +affects: [02-05-login, 02-06-protected-routes, 02-07-csrf] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "SignupForm/SignupErrors defined in templates package (not handlers) to avoid import cycle between templates and internal/web" + - "Test helper (setupTestDB) duplicated into web package test file; no cross-package import of _test.go files" + - "Pre-seed test users via InsertUser + auth.TestParams (not handler POST) to avoid 250ms argon2 DefaultParams cost in setup" + - "nil-Store guard in ResolveSession for Phase 1 unit-test compatibility with AuthDeps{} zero value" + - "renderSignupError helper: HTMX path renders fragment, non-HTMX renders full page (D-19)" + +key-files: + created: + - backend/templates/auth_forms.go + - backend/templates/auth_form_errors.templ + - backend/templates/auth_signup.templ + - backend/templates/auth_signup_test.go + - backend/internal/web/handlers_auth.go + - backend/internal/web/handlers_auth_test.go + - backend/internal/web/testdb_test.go + modified: + - backend/internal/web/router.go + - backend/internal/web/handlers_test.go + - backend/cmd/web/main.go + - backend/internal/auth/middleware.go + +key-decisions: + - "SignupForm/SignupErrors defined in templates package (auth_forms.go) — avoids import cycle; handlers import templates package directly" + - "setupTestDB duplicated into web package test file (~100 LOC) — Go does not allow importing _test.go files across packages; duplication chosen over moving infra into production code" + - "POST /signup outside RedirectIfAuthed group — GET guard handles common case; authed POST gets a response rather than silent 303" + - "nil-Store guard added to auth.ResolveSession — enables Phase 1 unit tests to pass AuthDeps{} zero value without panic" + +patterns-established: + - "Pattern: templ form types in templates package sibling .go file" + - "Pattern: renderXxxError helper selects fragment vs full page by HX-Request header" + - "Pattern: test pre-seed with TestParams hash via direct sqlc call (not handler POST)" + +requirements-completed: [AUTH-01, AUTH-03, AUTH-05] + +# Metrics +duration: 25min +completed: 2026-05-14 +--- + +# Phase 2 Plan 04: Signup Vertical Slice Summary + +**Signup flow end-to-end: templ form + POST handler + argon2id hash + InsertUser + Store.Create + session cookie + 303 redirect wired into chi with ResolveSession (D-24)** + +## Performance + +- **Duration:** ~25 min +- **Started:** 2026-05-14T22:16:00Z +- **Completed:** 2026-05-14T22:18:30Z +- **Tasks:** 2 +- **Files modified:** 11 + +## Accomplishments + +- Signup form (full page + HTMX fragment) renders with email/password fields; email round-trips; password never echoed +- POST /signup handler validates input, hashes with argon2id DefaultParams, inserts user, creates session, sets cookie, redirects +- HTMX fast-path returns 200 + HX-Redirect:/; non-JS path returns 303 See Other +- Duplicate email (pgconn 23505) renders "already in use" inline; no second row inserted +- Authed users hitting GET /signup bounce to / (RedirectIfAuthed) +- ResolveSession wired at position 5 in middleware stack per D-24 +- 8 integration tests all pass against real Postgres; Phase 1 unit tests unaffected + +## Task Commits + +1. **Task 1: Signup templates + render smoke tests** - `73935ed` (feat) +2. **Task 2: SignupPostHandler + router wiring + integration tests** - `efdc16b` (feat) + +## Files Created/Modified + +- `backend/templates/auth_forms.go` - SignupForm and SignupErrors types (in templates pkg to avoid import cycle) +- `backend/templates/auth_form_errors.templ` - FieldError and GeneralError templ primitives +- `backend/templates/auth_signup.templ` - SignupPage (full) + SignupFormFragment (HTMX swap target) +- `backend/templates/auth_signup_test.go` - 3 render smoke tests (form renders, error renders, password not echoed) +- `backend/internal/web/handlers_auth.go` - SignupPageHandler, SignupPostHandler, AuthDeps, renderSignupError +- `backend/internal/web/handlers_auth_test.go` - 8 TestSignup_* integration tests +- `backend/internal/web/testdb_test.go` - setupTestDB helper (duplicated from auth package) +- `backend/internal/web/router.go` - NewRouter updated: accepts AuthDeps, mounts ResolveSession + signup routes +- `backend/internal/web/handlers_test.go` - Pass AuthDeps{} to NewRouter (Phase 1 tests updated) +- `backend/cmd/web/main.go` - Build AuthDeps (sqlc.Queries + auth.Store + secure flag), pass to NewRouter +- `backend/internal/auth/middleware.go` - nil-Store guard in ResolveSession for Phase 1 test compatibility + +## Decisions Made + +**Where SignupForm/SignupErrors live:** Templates package (`backend/templates/auth_forms.go`). The handler file imports the templates package; defining types in templates avoids a circular dependency (`templates` importing `internal/web`). Handlers reference these types as `templates.SignupForm`, `templates.SignupErrors`. + +**Test helper strategy:** Duplicated `setupTestDB` (~100 LOC) into `backend/internal/web/testdb_test.go`. Go does not allow importing `_test.go` files across packages. The duplication is minimal and keeps test infra out of production code paths. Alternative (shared `internal/testhelpers` package) would put infra in a non-test file. + +**POST /signup outside RedirectIfAuthed group:** The GET /signup route is inside the `RedirectIfAuthed` group. POST /signup is intentionally outside because an authed user directly submitting the form should get a proper error response rather than a silent 303 that could confuse tooling. + +**nil-Store guard in ResolveSession:** Phase 1 unit tests in `handlers_test.go` call `NewRouter(stubPinger{}, "./static", AuthDeps{})` with a zero-value `AuthDeps` (nil `Store`). Without the nil guard, ResolveSession would panic on cookie lookup. The guard is a no-op pass-through when `Store == nil`. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing Critical] nil-Store guard added to auth.ResolveSession** +- **Found during:** Task 2 (updating handlers_test.go to pass AuthDeps{}) +- **Issue:** Phase 1 tests pass `AuthDeps{}` zero value; ResolveSession would panic on `store.Lookup()` call if any cookie was present (or if future tests hit that path) +- **Fix:** Added `if store == nil { next.ServeHTTP(w, r); return }` at the top of the returned handler closure +- **Files modified:** backend/internal/auth/middleware.go +- **Verification:** Phase 1 tests pass with `AuthDeps{}` zero value; all Phase 2 signup tests pass with real store +- **Committed in:** efdc16b + +--- + +**Total deviations:** 1 auto-fixed (Rule 2 — missing critical nil guard) +**Impact on plan:** Essential for backward compatibility with Phase 1 unit tests. No scope creep. + +## Issues Encountered + +None — plan executed cleanly. + +## Known Stubs + +None — the signup form is fully wired end-to-end with real DB operations. + +## Next Phase Readiness + +- Plan 05 (login handler) can now build on the same `AuthDeps` pattern and templ primitives +- `SignupFormFragment` and `FieldError`/`GeneralError` primitives are ready for reuse in the login form +- `ResolveSession` is live in the middleware stack; `RequireAuth` is implemented (Plan 03) and ready for Plan 06 protected routes +- `auth.TestParams` + direct `InsertUser` pre-seeding pattern is established for future tests + +--- +*Phase: 02-authentication* +*Completed: 2026-05-14*