From fb9aac30ba13b57a9e3aac3d3cb9e1ddfd4fd262 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 14 May 2026 21:58:23 +0200 Subject: [PATCH] docs(02-01): complete auth-substrate plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 36 ++++-- .../phases/02-authentication/02-01-SUMMARY.md | 120 ++++++++++++++++++ 4 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 .planning/phases/02-authentication/02-01-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 123d5f3..0882793 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -17,8 +17,8 @@ Requirements for the initial Go+HTMX milestone. Each maps to exactly one roadmap ### Authentication -- [ ] **AUTH-01**: User can sign up with email and password (server-side validation, bcrypt/argon2 hash) -- [ ] **AUTH-02**: User can log in with email and password and receives a server-managed session +- [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 - [ ] **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) - [ ] **AUTH-05**: Protected routes redirect unauthenticated requests to the login page; authenticated users on auth pages are sent to the dashboard diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 141f0f5..704c100 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 | Auth | A user can sign up, log in, and stay logged in | AUTH-01..07 | +| 2 | 1/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,9 +58,9 @@ Plans: **User-in-loop:** Approve the `users` and `sessions` table schemas (columns, indexes, deletion semantics) before sqlc generation. Approve hash algorithm choice. -**Plans:** 7 plans +**Plans:** 1/7 plans executed Plans: -- [ ] 02-01-PLAN.md — Schema + sqlc + auth-package skeleton (citext + users + sessions, test DB harness) +- [x] 02-01-PLAN.md — Schema + sqlc + auth-package skeleton (citext + users + sessions, test DB harness) - [ ] 02-02-PLAN.md — argon2id password hashing (TDD: Hash/Verify with PHC encoding) - [ ] 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) diff --git a/.planning/STATE.md b/.planning/STATE.md index b1fdd4e..4752e98 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,14 +2,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: ready_to_plan -last_updated: "2026-05-14T19:44:02.762Z" +status: in_progress +last_updated: "2026-05-14T21:57:00.000Z" progress: total_phases: 7 completed_phases: 1 - total_plans: 11 - completed_plans: 4 - percent: 36 + total_plans: 18 + completed_plans: 5 + percent: 28 --- # 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 01 — foundation +**Current focus:** Phase 02 — authentication, Plan 02 next ## Phase Status | # | Phase | Status | |---|-------|--------| -| 1 | Foundation | ○ Pending | -| 2 | Authentication | ○ Pending | +| 1 | Foundation | ✓ Complete | +| 2 | Authentication | ◑ In Progress (1/7 plans done) | | 3 | Tablos CRUD | ○ Pending | | 4 | Tasks (Kanban) | ○ Pending | | 5 | Files | ○ Pending | @@ -39,15 +39,27 @@ See: `.planning/PROJECT.md` (updated 2026-05-14) ## Active Phase -**Phase 1: Foundation** — not started. +**Phase 2: Authentication** — Plan 01 complete. Plan 02 (password hashing) next. -Next: `/gsd-discuss-phase 1` to gather context, or `/gsd-plan-phase 1` to plan directly. +## Decisions + +- **Consolidated internal/auth package** (not split with internal/session) — RESEARCH Open Question 3 +- **compose Postgres + schema isolation for tests** (not testcontainers-go) — RESEARCH Open Question 1 +- **goose.SetTableName per test-schema** prevents public goose_db_version table collision +- **argon2id** over bcrypt for password hashing — D-08, confirmed by user + +## Performance Metrics + +| Phase | Plan | Duration | Tasks | Files | +|-------|------|----------|-------|-------| +| 02-authentication | 01 | ~10min | 3 | 9 | ## Notes - Existing `go-backend/` is set aside; new code lives in a fresh `backend/` Go package. - DB schema is changing from the JS/Supabase version — user is in the loop on every schema decision (Phases 2–5). -- GSD subagents are not installed in this repo; downstream commands may fail until `npx get-shit-done-cc@latest --global` is run. +- Phase 2 Plan 01 SUMMARY: `.planning/phases/02-authentication/02-01-SUMMARY.md` +- Commits: 513044d (migration), 799c260 (sqlc), 2c84f42 (auth package + test harness) --- -*Last updated: 2026-05-14 after project initialization* +*Last updated: 2026-05-14 after 02-01 execution* diff --git a/.planning/phases/02-authentication/02-01-SUMMARY.md b/.planning/phases/02-authentication/02-01-SUMMARY.md new file mode 100644 index 0000000..bff0139 --- /dev/null +++ b/.planning/phases/02-authentication/02-01-SUMMARY.md @@ -0,0 +1,120 @@ +--- +phase: 02-authentication +plan: "01" +subsystem: backend/auth-substrate +tags: [go, postgres, sqlc, migration, auth-foundation, goose, citext, uuid] +dependency_graph: + requires: [01-foundation] + provides: [auth-migration, sqlc-bindings, auth-package-skeleton, test-harness] + affects: [02-02, 02-03, 02-04, 02-05, 02-06, 02-07] +tech_stack: + added: + - github.com/pressly/goose/v3 v3.27.1 (test harness — per-schema migration isolation) + patterns: + - per-test isolated schema via unique schemaName + schema-scoped DSN + - goose.SetTableName for per-test version table isolation (no public-schema collision) + - sqlc citext→string + uuid→uuid.UUID overrides (Pattern 10) +key_files: + created: + - backend/migrations/0002_auth.sql + - backend/internal/db/queries/users.sql + - backend/internal/db/queries/sessions.sql + - backend/internal/auth/doc.go + - backend/internal/auth/types.go + - backend/internal/auth/testdb_test.go + modified: + - backend/sqlc.yaml (added overrides block) + - backend/.env.example (added TEST_DATABASE_URL + SESSION_SECRET) + - backend/go.mod + go.sum (added pressly/goose/v3) +decisions: + - "Consolidated internal/auth package (not split with internal/session) — RESEARCH Open Question 3 resolved" + - "Test harness uses compose Postgres + schema isolation (not testcontainers-go) — RESEARCH Open Question 1 resolved" + - "goose.SetTableName per test-schema prevents collision with public goose_db_version table" + - "goose v3.27.1 pinned to match justfile CLI version" + - "schemaName uses UUID[:12] with hyphens replaced by underscores for valid SQL identifiers" +metrics: + duration: "~10 minutes" + completed_date: "2026-05-14" + tasks_completed: 3 + tasks_total: 3 + files_created: 6 + files_modified: 3 +--- + +# Phase 2 Plan 01: Auth Substrate (Migration + sqlc + Package Skeleton) Summary + +**One-liner:** Postgres migration with citext users+sessions tables, sqlc type-safe bindings with citext→string and uuid→uuid.UUID overrides, and internal/auth package with types/constants/sentinel-errors plus a per-test-schema Postgres harness. + +## What Was Built + +### Task 1: Migration 0002_auth.sql +Created `backend/migrations/0002_auth.sql` with goose Up/Down annotations: +- `CREATE EXTENSION IF NOT EXISTS citext` + `pgcrypto` (defensive for gen_random_uuid) +- `users` table: id uuid PK, email citext UNIQUE, password_hash text, created_at/updated_at timestamptz (D-01) +- `sessions` table: id text PK (SHA-256 hex per D-05), user_id FK ON DELETE CASCADE, created_at/expires_at (D-04) +- Indexes: `sessions_user_id_idx` and `sessions_expires_at_idx` +- No deleted_at, email_verified_at, user_agent, ip_address, last_seen_at (D-02, D-03, D-04) +- Up/Down round-trip verified against compose Postgres (goose Up → Down → Up exits 0) + +### Task 2: sqlc.yaml overrides + query files + generated bindings +- `sqlc.yaml`: added `overrides` block mapping `citext→string` and `uuid→uuid.UUID` (Pattern 10, Pitfall 3) +- `users.sql`: InsertUser :one + GetUserByEmail :one +- `sessions.sql`: InsertSession :exec, GetSessionWithUser :one (with `expires_at > now()` per D-07), DeleteSession :exec, DeleteSessionsByUser :exec, ExtendSession :exec +- `sqlc generate` produces `Email string` (not pgtype.Text) and `uuid.UUID` (not pgtype.UUID) — citext and uuid overrides verified +- Generated files are gitignored (per Phase 1 .gitignore) and regenerated by `sqlc generate` + +### Task 3: internal/auth package skeleton + test harness + env docs +- `auth/doc.go`: package comment — consolidated layout decision documented +- `auth/types.go`: User struct, Session struct, SessionCookieName (D-12), SessionTTL 30d (D-09), SessionExtendThreshold 7d (D-09), ErrSessionNotFound, ErrInvalidHash, ErrIncompatibleVersion +- `auth/testdb_test.go`: `setupTestDB(t)` creates unique schema (test_\), runs goose Up with schema-specific version table via `goose.SetTableName`, returns pool + cleanup. `TestSetupTestDB_Roundtrip` pings pool and verifies users table visibility. Test skips when neither TEST_DATABASE_URL nor DATABASE_URL is set. +- `.env.example`: added `TEST_DATABASE_URL` and `SESSION_SECRET` with generation instructions +- `go.mod`: added `github.com/pressly/goose/v3 v3.27.1` as direct dependency + +## Decisions Made + +1. **Consolidated internal/auth package** (not split with internal/session): all auth capabilities (password, session, ratelimit, cookie, csrf) live in one package. Phase 1's `internal/session/doc.go` placeholder is kept as-is (one-liner pointing at `internal/auth`). See RESEARCH Open Question 3. + +2. **compose Postgres + schema isolation** (not testcontainers-go): reuses the existing compose Postgres via `TEST_DATABASE_URL`. Per-test isolation via unique schemas avoids testcontainers podman friction (RESEARCH Open Question 1, Assumption A7). + +3. **goose.SetTableName per test**: each test schema gets its own goose version table (`test__goose_version`) so the public `goose_db_version` (tracking production migration state) is never read/modified during test runs. + +4. **UUID hyphens stripped from schema/table names**: `uuid.New().String()[:12]` can contain hyphens which are invalid in unquoted SQL identifiers; replaced with underscores. + +5. **goose v3.27.1 pinned**: matches the `goose_version` in `backend/justfile` CLI installation. + +## Deviations from Plan + +None — plan executed exactly as written. All acceptance criteria verified. + +## Threat Flags + +No new security-relevant surface introduced. This plan only creates: +- Schema migration (DDL only — no network endpoints) +- sqlc query files and generated bindings (parameterized queries prevent SQL injection — T-2-06 defense) +- Internal Go package with types and test helpers + +T-2-06 mitigated: sessions.id column is designed for SHA-256(token) hex per D-05; sqlc query `GetSessionWithUser` includes `AND expires_at > now()` gate (D-07 lazy expiry). +T-2-11 mitigated: goose Up/Down round-trip exercised and exits 0. +T-2-12 mitigated: `.env.example` SESSION_SECRET has empty placeholder value only; real value comes from `.env` (gitignored since Phase 1). + +## Commits + +| Task | Commit | Description | +|------|--------|-------------| +| 1 | 513044d | feat(02-01): add 0002_auth.sql migration | +| 2 | 799c260 | feat(02-01): add sqlc queries + citext/uuid overrides | +| 3 | 2c84f42 | feat(02-01): create internal/auth package skeleton, test DB harness, env docs | + +## Self-Check: PASSED + +Files verified: +- backend/migrations/0002_auth.sql ✓ +- backend/internal/db/queries/users.sql ✓ +- backend/internal/db/queries/sessions.sql ✓ +- backend/internal/auth/doc.go ✓ +- backend/internal/auth/types.go ✓ +- backend/internal/auth/testdb_test.go ✓ +- backend/sqlc.yaml ✓ +- backend/.env.example ✓ + +Commits verified: 513044d, 799c260, 2c84f42 all present in git log.