docs(07-07): complete the user-api parity gap plan
Record the PHP-does-blacklist finding, the accepted Go 401 after logout, and the 22-ported corpus so later phases do not revive the harness artifact. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,7 +62,7 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b
|
|||||||
|
|
||||||
### Authentication and users (AUTH)
|
### Authentication and users (AUTH)
|
||||||
|
|
||||||
- [ ] **AUTH-01**: User plugin port: registration, login, logout, password reset, email verification, and JWT issue/refresh (golang-jwt) with the same claims and cookie behavior the Nuxt app expects
|
- [x] **AUTH-01**: User plugin port: registration, login, logout, password reset, email verification, and JWT issue/refresh (golang-jwt) with the same claims and cookie behavior the Nuxt app expects
|
||||||
- [ ] **AUTH-02**: Organizations with roles; organization fields appear on the user payload through a fire-and-collect event so the fonoteka plugin extends the user plugin without editing it
|
- [ ] **AUTH-02**: Organizations with roles; organization fields appear on the user payload through a fire-and-collect event so the fonoteka plugin extends the user plugin without editing it
|
||||||
- [ ] **AUTH-03**: Personal API tokens with a read|write|ai scope ceiling, token CRUD endpoints, and a scope-checking middleware
|
- [ ] **AUTH-03**: Personal API tokens with a read|write|ai scope ceiling, token CRUD endpoints, and a scope-checking middleware
|
||||||
- [ ] **AUTH-04**: The must-change-password flag locks the authenticated surface with 423 except the locale and password-change routes
|
- [ ] **AUTH-04**: The must-change-password flag locks the authenticated surface with 423 except the locale and password-change routes
|
||||||
@@ -192,7 +192,7 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
| HTTP-07 | Phase 6 | Complete |
|
| HTTP-07 | Phase 6 | Complete |
|
||||||
| HTTP-08 | Phase 6 | Complete |
|
| HTTP-08 | Phase 6 | Complete |
|
||||||
| HTTP-09 | Phase 6 | Complete |
|
| HTTP-09 | Phase 6 | Complete |
|
||||||
| AUTH-01 | Phase 7 | Pending |
|
| AUTH-01 | Phase 7 | Complete |
|
||||||
| AUTH-02 | Phase 7 | Pending |
|
| AUTH-02 | Phase 7 | Pending |
|
||||||
| AUTH-03 | Phase 7 | Pending |
|
| AUTH-03 | Phase 7 | Pending |
|
||||||
| AUTH-04 | Phase 7 | Pending |
|
| AUTH-04 | Phase 7 | Pending |
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ Plans:
|
|||||||
|
|
||||||
**Wave 6** *(gap closure; blocked on 07-06)*
|
**Wave 6** *(gap closure; blocked on 07-06)*
|
||||||
|
|
||||||
- [ ] 07-07-PLAN.md — Re-record the logout blacklist under a persistent PHP cache, fix the already-activated HTML-500 quirk, and flip all 15 /_user/api/v1 routes plus both nuxt flows to ported
|
- [x] 07-07-PLAN.md — Re-record the logout blacklist under a persistent PHP cache, fix the already-activated HTML-500 quirk, and flip all 15 /_user/api/v1 routes plus both nuxt flows to ported
|
||||||
|
|
||||||
### Phase 8: OAuth2.1 authorization server
|
### Phase 8: OAuth2.1 authorization server
|
||||||
|
|
||||||
@@ -452,7 +452,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
|||||||
| 4. CLI scaffolding, i18n and mail | 4/4 | Complete | 2026-09-18 |
|
| 4. CLI scaffolding, i18n and mail | 4/4 | Complete | 2026-09-18 |
|
||||||
| 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 |
|
| 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 |
|
||||||
| 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 |
|
| 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 |
|
||||||
| 7. User plugin and authentication | 6/6 | Complete | 2026-09-22 |
|
| 7. User plugin and authentication | 7/7 | Complete | 2026-09-22 |
|
||||||
| 8. OAuth2.1 authorization server | 0/TBD | Not started | - |
|
| 8. OAuth2.1 authorization server | 0/TBD | Not started | - |
|
||||||
| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - |
|
| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - |
|
||||||
| 10. Admin Vue SPA | 0/TBD | Not started | - |
|
| 10. Admin Vue SPA | 0/TBD | Not started | - |
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 07-06-PLAN.md
|
stopped_at: Completed 07-07-PLAN.md
|
||||||
last_updated: "2026-09-22T18:41:15.559Z"
|
last_updated: "2026-09-22T21:58:58.925Z"
|
||||||
last_activity: 2026-09-22 -- Phase 07 planning complete
|
last_activity: 2026-09-22
|
||||||
progress:
|
progress:
|
||||||
total_phases: 15
|
total_phases: 15
|
||||||
completed_phases: 6
|
completed_phases: 7
|
||||||
total_plans: 44
|
total_plans: 44
|
||||||
completed_plans: 43
|
completed_plans: 44
|
||||||
percent: 40
|
percent: 47
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 07 (user-plugin-and-authentication) — EXECUTING
|
Phase: 07 (user-plugin-and-authentication) — PLANS COMPLETE
|
||||||
Plan: 6 of 6
|
Plan: 7 of 7
|
||||||
Status: Ready to execute
|
Status: All 7 plans have SUMMARYs. Ready for `$gsd-verify-work 7` — do not auto-advance.
|
||||||
Last activity: 2026-09-22 -- Phase 07 planning complete
|
Last activity: 2026-09-22 -- Completed 07-07-PLAN.md
|
||||||
|
|
||||||
Progress: [██████████] 100%
|
Progress: [██████████] 100%
|
||||||
|
|
||||||
@@ -87,6 +87,8 @@ Progress: [██████████] 100%
|
|||||||
| Phase 07 P04 | 75m | 3 tasks | 16 files |
|
| Phase 07 P04 | 75m | 3 tasks | 16 files |
|
||||||
| Phase 07 P05 | 45 | 3 tasks | 45 files |
|
| Phase 07 P05 | 45 | 3 tasks | 45 files |
|
||||||
| Phase 07 P06 | 40 min | 3 tasks | 8 files |
|
| Phase 07 P06 | 40 min | 3 tasks | 8 files |
|
||||||
|
| Phase 07 P07 | 3h 15m | 3 tasks | 28 files |
|
||||||
|
| Phase 07-user-plugin-and-authentication P07 | 3h 15m | 3 tasks | 28 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -198,7 +200,9 @@ Recent decisions affecting current work:
|
|||||||
- [Phase 06]: Retain all four earlier accepted risks unchanged; T-06-23 through T-06-27 are mitigated, not accepted or deferred. — Both repositories' authoritative race and vet gates passed, and each new threat has concrete source and named regression evidence.
|
- [Phase 06]: Retain all four earlier accepted risks unchanged; T-06-23 through T-06-27 are mitigated, not accepted or deferred. — Both repositories' authoritative race and vet gates passed, and each new threat has concrete source and named regression evidence.
|
||||||
- [Phase 06]: Anonymous inline limiter identity is documented only as inline:domainless|<ClientIP>, excluding policy text and request or forwarded Host inputs. — The production resolver and three executed regressions prove Host rotation and inline-parameter changes cannot create fresh anonymous budgets while authenticated principals keep isolated u:<id> keys.
|
- [Phase 06]: Anonymous inline limiter identity is documented only as inline:domainless|<ClientIP>, excluding policy text and request or forwarded Host inputs. — The production resolver and three executed regressions prove Host rotation and inline-parameter changes cannot create fresh anonymous budgets while authenticated principals keep isolated u:<id> keys.
|
||||||
- [Phase 07]: Blacklist storage expiry follows PHP jwt-auth (later of exp and iat+refreshTTL, plus one minute). — Using the raw access exp would drop a logged-out token that is still inside the refresh window.
|
- [Phase 07]: Blacklist storage expiry follows PHP jwt-auth (later of exp and iat+refreshTTL, plus one minute). — Using the raw access exp would drop a logged-out token that is still inside the refresh window.
|
||||||
- [Phase 07]: user_throttle and jwt_blacklist are allowed schema diffs — The frozen PHP snapshot predates the user plugin. jwt_blacklist is Go-only because PHP logout does not blacklist.
|
- [Phase 07]: user_throttle and jwt_blacklist are allowed schema diffs — The frozen PHP snapshot predates the user plugin. jwt_blacklist is also the production PHP path: logout calls JWTAuth::invalidate(true) and blacklist_enabled defaults true. The earlier "PHP does not blacklist" note was a harness artifact of CACHE_DRIVER=array.
|
||||||
|
- [Phase 07]: Fetch after logout stays 401 in Go — Re-recorded PHP with file cache still returned 200 on a reused token (show_black_list_exception default 0). That case is kept on disk but is not a ported corpus case.
|
||||||
|
- [Phase 07]: Already-activated activate/activate-by-code is Winter 500 HTML — User::attemptActivation throws when the user is already active; Go keys that path on IsAlreadyActivated, not wrong-code.
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -220,6 +224,6 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-09-22T17:21:05.484Z
|
Last session: 2026-09-22T21:58:58.910Z
|
||||||
Stopped at: Completed 07-06-PLAN.md
|
Stopped at: Completed 07-07-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
phase: 07-user-plugin-and-authentication
|
||||||
|
plan: 07
|
||||||
|
subsystem: auth
|
||||||
|
tags: [user-api, jwt, blacklist, parity, activation, phrasebook]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 07-user-plugin-and-authentication
|
||||||
|
provides: recorded user-api corpus, session handlers, and 07-06 unit coverage
|
||||||
|
provides:
|
||||||
|
- 15 /_user/api/v1 routes and both nuxt-auth flows replayed green and flipped to ported
|
||||||
|
- PHP-does-blacklist finding (CACHE_DRIVER=array was a harness artifact)
|
||||||
|
- already-activated Winter 500 HTML for Activate and ActivateByCode
|
||||||
|
affects: [phase-7-verification, phase-8-oauth]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "user-api seed_hook writes Alice via GORM, never unported onboarding HTTP"
|
||||||
|
- "already-activated activate is keyed on IsAlreadyActivated, not wrong-code"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- ../fonoteka.go/parity/user_api_seed_test.go
|
||||||
|
- ../fonoteka.go/parity/nuxt_flow_test.go
|
||||||
|
- ../fonoteka.go/plugins/golem15/user/controllers/winter_error_page.html
|
||||||
|
- phrasebook/lang.go
|
||||||
|
- phrasebook/lang/en/validate.yaml
|
||||||
|
- phrasebook/lang/pl/validate.yaml
|
||||||
|
- ../fonoteka.go/plugins/golem15/user/lang/en/lang.yaml
|
||||||
|
- ../fonoteka.go/plugins/golem15/user/lang/pl/lang.yaml
|
||||||
|
modified:
|
||||||
|
- ../fonoteka.go/parity/manifest.yaml
|
||||||
|
- ../fonoteka.go/parity/php_parity.sh
|
||||||
|
- ../fonoteka.go/parity/parity_test.go
|
||||||
|
- ../fonoteka.go/parity/parity_contract_test.go
|
||||||
|
- ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go
|
||||||
|
- ../fonoteka.go/plugins/golem15/user/classes/codes.go
|
||||||
|
- lagoon/validate.go
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Production PHP DOES blacklist on logout; the earlier 200-after-logout recording was CACHE_DRIVER=array in the isolated harness, not the contract"
|
||||||
|
- "Fetch after logout stays 401 in Go; the re-recorded reused PHP case still returns 200 because show_black_list_exception defaults to 0, and that one case was dropped from the ported corpus"
|
||||||
|
- "Already-activated activate/activate-by-code is Winter 500 HTML (User::attemptActivation throw), not a wrong-code 422"
|
||||||
|
- "user-api and nuxt flows seed via direct Postgres, never /_fonoteka/api/v1/onboarding/*"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern: seed_hook: user-api upserts Alice and resets leftover profile columns so the shared TestMain pool cannot leak preferred_locale"
|
||||||
|
- "Pattern: takeUser uses gorm.Session{NewDB: true} so leftover clauses on a shared *gorm.DB cannot hide an activated row"
|
||||||
|
|
||||||
|
requirements-completed: [AUTH-01]
|
||||||
|
|
||||||
|
duration: 3h 15m
|
||||||
|
completed: 2026-09-22
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7 Plan 07: User-API parity gap Summary
|
||||||
|
|
||||||
|
**The 15 `/_user/api/v1` routes and both nuxt auth flows replay green against Go and are `ported`; production PHP does blacklist on logout.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3h 15m
|
||||||
|
- **Started:** 2026-09-22T18:42:00Z
|
||||||
|
- **Completed:** 2026-09-22T21:57:22Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 28
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Isolated PHP recording now uses `CACHE_DRIVER=file` so jwt-auth blacklist state persists across requests, matching production.
|
||||||
|
- Go login/fetch/register/activate payloads match PHP (gravatar, `permissions: null`, `groups: []`, Polish validation, `Cache-Control: no-cache, private`).
|
||||||
|
- Already-activated `Activate` and `ActivateByCode` serve the embedded Winter production error page (500, `text/html`).
|
||||||
|
- `TestParityCorpus` is recorded 169 / ported 22 / pending 147 / failing 0. Both `nuxt-auth` and `nuxt-auth-lock` replay green.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
1. **Task 1: Persistent PHP cache + re-record logout fixtures** — `aa5266f` in `fonoteka.go`
|
||||||
|
2. **Task 2: Activation quirk, seed hooks, green replay, manifest flips** — `31634f7` in `summercms.go`, `de83d41` in `fonoteka.go`
|
||||||
|
3. **Task 3: Unit tests and corpus-count assertions** — `016460a` in `fonoteka.go`
|
||||||
|
|
||||||
|
**Plan metadata:** this docs commit
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `parity/php_parity.sh` — `CACHE_DRIVER=file` and cache clear on reset
|
||||||
|
- `parity/user_api_seed_test.go` — direct-DB Alice seed; resets `preferred_locale` / surname leftovers
|
||||||
|
- `parity/nuxt_flow_test.go` — in-process replay of both nuxt fixtures; fetch-after-logout asserts 401
|
||||||
|
- `parity/manifest.yaml` — 15 user-api routes `status: ported`; fetch `reused` case removed from the ported list
|
||||||
|
- `plugins/golem15/user/controllers/winter_error_page.html` — byte copy of the recorded Winter page
|
||||||
|
- `plugins/golem15/user/controllers/api_controller.go` — `apiArray`, localized errors, already-activated Winter page
|
||||||
|
- `plugins/golem15/user/classes/codes.go` — `IsAlreadyActivated`; `takeUser` uses a fresh GORM session
|
||||||
|
- `lagoon/validate.go` + `phrasebook/lang/{en,pl}/validate.yaml` — Laravel-shaped Polish messages
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
Production PHP **does** blacklist on logout (`AuthManager::logout` → `JWTAuth::invalidate(true)`, `blacklist_enabled` defaults true, production `CACHE_DRIVER=file`). The previous STATE note that "PHP does not blacklist" was a harness artifact of isolated PHP running `CACHE_DRIVER=array`, which does not persist jwt-auth cache across requests. That note is superseded.
|
||||||
|
|
||||||
|
Go fetch-after-logout stays **401**. Re-recorded PHP with file cache still returned 200 on the reused token because `show_black_list_exception` defaults to 0 (jwt-auth treats a blacklisted token as invalid without throwing). That is non-breaking for frontends; the reused fetch case remains on disk but is not a ported corpus case.
|
||||||
|
|
||||||
|
Already-activated activate is Winter's uncaught `User is already active!` → generic 500 HTML under `APP_DEBUG=false`, not a "wrong code" 422.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. Fetch reused case left recorded but not ported**
|
||||||
|
- **Found during:** Task 1/2 (logout re-record)
|
||||||
|
- **Issue:** Even with `CACHE_DRIVER=file`, PHP fetch-after-logout stayed 200 (`show_black_list_exception` default 0). Plan must-have asked for byte-identical 401 in both backends.
|
||||||
|
- **Fix:** User locked Go 401. Dropped the reused fetch case from the ported corpus; fixture file kept. Nuxt flow asserts 401 after logout and skips the PHP reuse step.
|
||||||
|
- **Verification:** `TestParityCorpus` and `TestUserAPINuxtFlows` green
|
||||||
|
- **Committed in:** `aa5266f` / `de83d41`
|
||||||
|
|
||||||
|
**2. Shared TestMain pool leaked Alice profile and user id 1**
|
||||||
|
- **Found during:** Task 3 (`go test ./... -race`)
|
||||||
|
- **Issue:** Other parity tests leave Alice `preferred_locale=en` and a not-activated row at id 1, so login/fetch failed JSON compare and `1!nope` returned 422.
|
||||||
|
- **Fix:** `seedUserAPI` nulls leftover profile columns; `ensureActivatedUserID(1)` runs before activate-by-code replay. Unit test uses the just-activated user's id, not hardcoded `1!nope`.
|
||||||
|
- **Verification:** `go test ./... -race` green including `./parity` without `-short`
|
||||||
|
- **Committed in:** `de83d41` / `016460a`
|
||||||
|
|
||||||
|
**3. `takeUser` isolated from leftover GORM clauses**
|
||||||
|
- **Found during:** Task 3 (`TestActivateByCode` after `TestActivate` on a shared `*gorm.DB`)
|
||||||
|
- **Issue:** Root-session `Where` leftovers made `IsAlreadyActivated` miss the activated row.
|
||||||
|
- **Fix:** `takeUser` uses `Session({NewDB: true, Context: ctx})`
|
||||||
|
- **Verification:** `TestActivate` + `TestActivateByCode` together pass
|
||||||
|
- **Committed in:** `de83d41`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 3 auto-fixed (1 contract clarification, 2 test-harness isolation)
|
||||||
|
**Impact on plan:** Required for a honest PHP contract and a green full-package race gate. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
PHP file-cache re-record did not produce a 401 on fetch-after-logout. Locked as Go 401; documented above.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
AUTH-01's user-api replay gap is closed. Phase 7's seven plans all have SUMMARYs. Ready for `$gsd-verify-work 7` / phase verification — do not auto-advance to Phase 8.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-user-plugin-and-authentication*
|
||||||
|
*Completed: 2026-09-22*
|
||||||
Reference in New Issue
Block a user