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