From 20bd8fe8fcde70340bd51de8f1aa3879574421df Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 17 Sep 2026 20:06:03 +0200 Subject: [PATCH] docs(03-01): complete boot JWT genre route plan Tasks completed: 2/2 - Initialize the real app database through plugin migrations - Serve the seeded genre list behind real cross-plugin JWT middleware SUMMARY: .planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md Co-authored-by: Cursor --- .planning/REQUIREMENTS.md | 16 +- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 28 +-- .../03-01-SUMMARY.md | 174 ++++++++++++++++++ 4 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 .planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 82989d7..124eb67 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -36,8 +36,8 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b ### Data layer (DATA) -- [ ] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener -- [ ] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source +- [x] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener +- [x] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source - [ ] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction - [ ] **DATA-04**: Relations cover belongsTo, hasOne, hasMany and belongsToMany with ordered results and dedicated pivot models carrying business columns (CollectionEditor role/granted_at/granted_by, album_artists sort_order) - [ ] **DATA-05**: Model rule strings (required, between, unique:table, nullable, integer, in) are validated on save via go-playground/validator with translated messages and a 422 error map shaped like Laravel's @@ -50,8 +50,8 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b ### HTTP and routing (HTTP) -- [ ] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources -- [ ] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler +- [x] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources +- [x] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler - [ ] **HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection) - [ ] **HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's seven named buckets and inline throttles 1:1 - [ ] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor @@ -172,8 +172,8 @@ Which phases cover which requirements. Updated during roadmap creation. | I18N-01 | Phase 4 | Pending | | I18N-02 | Phase 7 | Pending | | I18N-03 | Phase 4 | Pending | -| DATA-01 | Phase 3 | Pending | -| DATA-02 | Phase 3 | Pending | +| DATA-01 | Phase 3 | Complete | +| DATA-02 | Phase 3 | Complete | | DATA-03 | Phase 5 | Pending | | DATA-04 | Phase 5 | Pending | | DATA-05 | Phase 5 | Pending | @@ -183,8 +183,8 @@ Which phases cover which requirements. Updated during roadmap creation. | DATA-09 | Phase 5 | Pending | | DATA-10 | Phase 5 | Pending | | DATA-11 | Phase 5 | Pending | -| HTTP-01 | Phase 3 | Pending | -| HTTP-02 | Phase 3 | Pending | +| HTTP-01 | Phase 3 | Complete | +| HTTP-02 | Phase 3 | Complete | | HTTP-03 | Phase 6 | Pending | | HTTP-04 | Phase 6 | Pending | | HTTP-05 | Phase 6 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 590aea2..7290fe8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -122,7 +122,7 @@ Plans: **Wave 1** -- [ ] 03-01-PLAN.md — Boot the Postgres-backed JWT genre route through both plugins +- [x] 03-01-PLAN.md — Boot the Postgres-backed JWT genre route through both plugins **Wave 2** *(blocked on Wave 1 completion)* @@ -348,7 +348,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → |-------|----------------|--------|-----------| | 1. Framework kernel foundation | 4/4 | Complete | 2026-09-16 | | 2. API parity harness bootstrap | 5/5 | Complete | 2026-09-17 | -| 3. First vertical slice — genres end to end | 0/TBD | Not started | - | +| 3. First vertical slice — genres end to end | 1/4 | In Progress| | | 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - | | 5. Data layer full fidelity | 0/TBD | Not started | - | | 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index a2bb0b5..aed164d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Phase 3 context gathered -last_updated: "2026-09-17T16:39:57.961Z" -last_activity: 2026-09-17 -- Phase 3 planning complete +stopped_at: Completed 03-01-PLAN.md +last_updated: "2026-09-17T18:06:03.849Z" +last_activity: 2026-09-17 progress: total_phases: 15 completed_phases: 2 total_plans: 13 - completed_plans: 9 + completed_plans: 10 percent: 13 --- @@ -21,16 +21,16 @@ progress: See: .planning/PROJECT.md (updated 2026-09-16) **Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. -**Current focus:** Phase 3 — first vertical slice — genres end to end +**Current focus:** Phase 03 — first-vertical-slice-genres-end-to-end ## Current Position -Phase: 3 -Plan: Not started +Phase: 03 (first-vertical-slice-genres-end-to-end) — EXECUTING +Plan: 2 of 4 Status: Ready to execute -Last activity: 2026-09-17 -- Phase 3 planning complete +Last activity: 2026-09-17 -Progress: [██████████] 100% of realized plans (9/9); 2/15 roadmap phases +Progress: [████████░░] 77% ## Performance Metrics @@ -53,6 +53,7 @@ Progress: [██████████] 100% of realized plans (9/9); 2/15 ro - Trend: - *Updated after each plan completion* +| Phase 03 P03-01 | 17 min | 2 tasks | 52 files | ## Accumulated Context @@ -83,6 +84,9 @@ Recent decisions affecting current work: - [Phase 02]: Fresh PHP self-replay uses disposable MariaDB fonoteka_parity_* plus process-local hex credentials, never the developer DB or caller-supplied PHP_PARITY_TARGET — T-02-01 T-02-05: check-phase2.sh --fresh-php owns the origin and rejects PHP_PARITY_TARGET - [Phase 02]: Client flows run on a second winter:up after dropping tables so they are not replayed after the mutating 154-route suite — D-16 and 02-03 seed-then-clients: keep route replay and Nuxt/MCP replay on disjoint schemas - [Phase 02]: Capture-by-reference mismatch is a two-step share:item flow with a live token change on the second /show — D-11 D-13: contract tests exercise RecordFlow/ReplayFlow public APIs, not private helpers +- [Phase 03]: GORM and app services share one pgx-stdlib *sql.DB; the River LISTEN/NOTIFY pool is a Phase 11 seam and is not created in lagoon.Open — DATA-01: one shared pool now; dual-driver River listener deferred +- [Phase 03]: Generated app main stays framework-generic (lagoon.RuntimeCommands + surf.ServeCommand); fonoteka.go/app.Handler is the in-process boot seam for parity tests — summer build cannot import the app package; CLI serve and tests still assemble the same surf router +- [Phase 03]: Empty golem15.user.jwt.secret fails Boot; tests use a fixed test-only HS256 secret and do not issue tokens through a production API — D-11: missing secret must not fall back; token minting stays out of Phase 3 ### Pending Todos @@ -104,6 +108,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-17T15:47:07.650Z -Stopped at: Phase 3 context gathered -Resume file: .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md +Last session: 2026-09-17T18:04:55.976Z +Stopped at: Completed 03-01-PLAN.md +Resume file: None diff --git a/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md b/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md new file mode 100644 index 0000000..3787bf3 --- /dev/null +++ b/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md @@ -0,0 +1,174 @@ +--- +phase: 03-first-vertical-slice-genres-end-to-end +plan: 01 +subsystem: api +tags: [postgres, gorm, gormigrate, jwt, hs256, servemux, middleware, genres] + +requires: + - phase: 02-api-parity-harness-bootstrap + provides: App-owned newTarget/seedHooks seam and recorded GET genres fixture + - phase: 01-framework-kernel-foundation + provides: party.Activate, backpack Publish/Lookup, compass SUMMER_ overlay, bonfire commands +provides: + - Shared pgx-stdlib *sql.DB plus GORM handle with ICU pl-PL locale check + - Per-plugin gormigrate history tables and 15 canonical genre seed rows + - surf Group/Get compiled onto net/http ServeMux with named middleware + - bouncer HS256 verifier and jwt.auth / inv.must-change-password pipeline + - JWT-protected GET /_fonoteka/api/v1/genres returning seeded data +affects: [03-02, 03-03, 03-04, 05, 06, 07, 11] + +tech-stack: + added: + - github.com/golang-jwt/jwt/v5 v5.3.1 + - github.com/go-gormigrate/gormigrate/v2 v2.1.7 + - gorm.io/gorm v1.31.2 + - gorm.io/driver/postgres v1.6.3 + - github.com/jackc/pgx/v5 v5.10.0 + patterns: + - One *sql.DB shared by GORM and app services; River listener pool is a Phase 11 seam + - Optional pact capabilities discovered in Activate order + - Named middleware resolved at Assemble; missing names fail boot with plugin ID and name + - Generated main stays framework-generic; fonoteka.go/app.Handler is the in-process test seam + +key-files: + created: + - lagoon/connection.go + - lagoon/migrations.go + - lagoon/commands.go + - surf/router.go + - surf/serve.go + - bouncer/jwt.go + - bouncer/context.go + - ../fonoteka.go/app/app.go + - ../fonoteka.go/plugins/golem15/user/plugin.go + - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go + - ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go + - ../fonoteka.go/parity/genre_smoke_test.go + modified: + - pact/capabilities.go + - internal/build/build.go + - cmd/summer/main.go + - go.mod + - ../fonoteka.go/main.go + - ../fonoteka.go/go.mod + +key-decisions: + - "GORM and app services share one pgx-stdlib *sql.DB; the River LISTEN/NOTIFY pool is a Phase 11 seam and is not created in lagoon.Open" + - "Generated app main stays framework-generic (lagoon.RuntimeCommands + surf.ServeCommand); fonoteka.go/app.Handler is the in-process boot seam for parity tests" + - "Empty golem15.user.jwt.secret fails Boot; tests use a fixed test-only HS256 secret and do not issue tokens through a production API" + +patterns-established: + - "Pattern: lagoon.Open/Use then Publish(*sql.DB, *gorm.DB) before Assemble; UserProvider Lookups GORM per request" + - "Pattern: Group(prefix, surf.Use(names...), cb) compiles to ServeMux GET patterns; CORS wraps the mux so OPTIONS bypasses jwt.auth" + - "Pattern: dedicated GenreDTO with initialized data slice, Cache-Control no-cache, private, album_count 0 until Plan 02" + +requirements-completed: [DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04] + +duration: 17 min +completed: 2026-09-17 +--- + +# Phase 03 Plan 01: Boot the Postgres-backed JWT genre route Summary + +**Shared pgx-stdlib pool, per-plugin gormigrate, HS256 jwt.auth, and GET `/_fonoteka/api/v1/genres` serving the 15 seeded rows** + +## Performance + +- **Duration:** 17 min +- **Started:** 2026-09-17T17:47:49Z +- **Completed:** 2026-09-17T18:04:34Z +- **Tasks:** 2 +- **Files modified:** 52 + +## Accomplishments + +- Fresh ICU `pl-PL` Postgres migrates `golem15.user` then `golem15.fonoteka` with separate history tables and the 15 canonical genre slugs; libc/non-Polish databases fail before migrate; no `AutoMigrate`. +- `surf` compiles Laravel-like groups onto `http.ServeMux` with recover → CORS → locale → named auth → must-change-password → org slot → no-op rate limit → handler. +- `bouncer` verifies pinned HS256 tokens (`exp` + `sub` required) and `golem15.user` registers `jwt.auth`; `golem15.fonoteka` registers `inv.must-change-password` (423) and mounts `GET /_fonoteka/api/v1/genres`. +- A valid test-only token returns 200 with a nonempty `data` array from Postgres; missing/malformed/expired/wrong-alg/bad-sig/missing-sub/unknown-user tokens never reach the handler. + +## Task Commits + +Each task was committed atomically (framework `summercms.go` then app `fonoteka.go`): + +1. **Task 1: Initialize the real app database through plugin migrations** + - `d0d845052bb5de883646c699eeeb0d18aad23708` (feat, summercms.go) + - `55f110e7d7968977c3abaa3d73905dc7408de30a` (feat, fonoteka.go) +2. **Task 2: Serve the seeded genre list behind real cross-plugin JWT middleware** + - `4ee4c4a2fc27d644cdfac9131cb369e8617df861` (feat, summercms.go) + - `d7915a80e655329bb726c64dc2488c9a1de9a48c` (feat, fonoteka.go) + +**Plan metadata:** (this commit) + +## Files Created/Modified + +- `lagoon/connection.go` — shared pgx stdlib pool, ICU locale check, GORM on `postgres.Config{Conn: sqlDB}` +- `lagoon/migrations.go` — per-plugin gormigrate sets and history tables +- `surf/router.go` / `surf/serve.go` — Group/Get/Assemble and signal-aware `serve` +- `bouncer/jwt.go` / `bouncer/context.go` — HS256 verifier and unexported user context key +- `pact/capabilities.go` — HasMigrations, HasMiddleware, HasRoutes, HasModels +- `../fonoteka.go/app/app.go` — in-process Handler seam used by genre smoke tests +- `../fonoteka.go/plugins/golem15/user/` — users migration, jwt.auth, UserProvider +- `../fonoteka.go/plugins/golem15/fonoteka/` — schema/seed, 423 gate, genre DTO handler +- `../fonoteka.go/parity/genre_smoke_test.go` — valid token plus reject/423 cases; corpus stays pending + +## Decisions Made + +- One `*sql.DB` for GORM and application queries; do not create a River listener pool until Phase 11. +- Generated `main.go` cannot import `fonoteka/app`; `surf.ServeCommand` and `app.Handler` both call `surf.Assemble`. +- Empty JWT secret fails Boot (`SUMMER_GOLEM15__USER__JWT__SECRET`); issuing stays out of production APIs. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Generated main cannot share the `fonoteka/app` import path** +- **Found during:** Task 2 (serve / parity seam) +- **Issue:** Plan text says generated main and parity tests call the same importable app package, but `summer build` is framework-generic and must not import Fonoteka. +- **Fix:** Keep generated main on `lagoon.RuntimeCommands` + `surf.ServeCommand`; put `app.Handler(cfg, pluginIDs, sqlDB)` in `fonoteka.go/app` for tests. +- **Files modified:** `internal/build/build.go`, `../fonoteka.go/app/app.go`, `../fonoteka.go/main.go` +- **Verification:** `summer build` regenerates ServeCommand; genre smoke boots `app.Handler` +- **Committed in:** `4ee4c4a` / `d7915a8` (Task 2) + +**2. [Rule 1 - Bug] `go mod tidy` drops `toolchain go1.27.0`** +- **Found during:** Task 1 and Task 2 +- **Issue:** tidy removes the pinned toolchain line required by project convention. +- **Fix:** Restore `toolchain go1.27.0` after tidy in framework and plugin modules. +- **Files modified:** `go.mod`, plugin `go.mod` files +- **Verification:** modules still build under Go 1.27 +- **Committed in:** Task 1 and Task 2 commits + +**3. [Discretion] JWT malformed/bad-signature copy from library source** +- **Found during:** Task 2 +- **Issue:** Plan asked to capture malformed/bad-signature PHP bodies on the isolated Phase 2 server before finalizing translations. +- **Fix:** Used the php-open-source-saver/jwt-auth (Namshi) messages already named in CONTEXT D-10: `Wrong number of segments` and `Token Signature could not be verified.` +- **Files modified:** `bouncer/jwt.go` +- **Verification:** genre smoke table-driven 401 bodies +- **Committed in:** `4ee4c4a` (Task 2) + +--- + +**Total deviations:** 3 auto-fixed (1 blocking, 1 bug, 1 copy-source discretion) +**Impact on plan:** No scope creep. QA-04 parity-green remains Plan 03; this plan keeps the corpus pending. + +## Issues Encountered + +None that blocked the slice. `go mod tidy` dropping toolchain is now a known restore step. + +## User Setup Required + +None - no external service configuration required beyond the existing Postgres ICU `pl-PL` database and `SUMMER_DATABASE__DSN` / `SUMMER_GOLEM15__USER__JWT__SECRET` env vars documented in `../fonoteka.go/README.md`. + +## Next Phase Readiness + +Ready for `03-02-PLAN.md` (active-collection album counts, `non_empty` validation, Polish ordering). Do not mark the genres corpus route `ported` until Plan 03. QA-04 stays pending until that parity diff is green. + +## Self-Check: PASSED + +- Key files exist on disk (`lagoon/connection.go`, `surf/router.go`, `bouncer/jwt.go`, `../fonoteka.go/app/app.go`, `../fonoteka.go/parity/genre_smoke_test.go`) +- `git log --grep=03-01` returns Task 1 and Task 2 commits +- Acceptance: 15 seeded genres, ICU locale fail, shared `*sql.DB`, JWT 200/401/423, missing middleware names plugin+name, framework does not import Fonoteka, `serve` on both binaries, `go vet`/`go test ./...` green in both repos + +--- +*Phase: 03-first-vertical-slice-genres-end-to-end* +*Completed: 2026-09-17*