--- phase: 07-user-plugin-and-authentication plan: 04 subsystem: auth tags: [fonoteka, api-token, locale, parity] requires: - phase: 07-user-plugin-and-authentication provides: JWT guard, locale.from-principal, inv.must-change-password provides: - MintPersonalToken and RevokeToken - POST/GET/DELETE /_fonoteka/api/v1/tokens - GET/PUT /_fonoteka/api/v1/me/locale - route-table proof that me/locale is the 423 exemption affects: [07-05, 07-06] tech-stack: added: [] patterns: [inv_ prefix plus sha256 hex shared with TokenGuard, me/locale group without the password lock] key-files: created: - ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_locale_controller.go modified: - ../fonoteka.go/plugins/golem15/fonoteka/routes.go - ../fonoteka.go/parity/manifest.yaml key-decisions: - "Personal token secrets are inv_ plus raw url-safe base64, hashed the same way TokenGuard verifies" - "me/locale is the only /_fonoteka/api/v1 route without inv.must-change-password" - "Empty preferred_locale is JSON null" patterns-established: - "Pattern: token list and locale responses send Cache-Control no-cache, private" - "Pattern: a missing or foreign token id is the same 404" requirements-completed: [] duration: 75min completed: 2026-09-22 --- # Phase 7 Plan 04: Personal tokens and locale Summary **Personal API tokens can be minted, listed, and revoked, and `me/locale` stays reachable while the password-change lock returns 423 everywhere else on `/_fonoteka/api/v1`.** ## Performance - **Duration:** 75 min - **Started:** 2026-09-22T14:50:00Z - **Completed:** 2026-09-22T16:05:00Z - **Tasks:** 3 - **Files modified:** 16 ## Accomplishments - `MintPersonalToken` returns an `inv_` secret whose sha256 hex matches `TokenGuard`. Revoke stamps `revoked_at`. - `POST /tokens` rejects a scope outside `read`, `write`, and `ai` before insert. `GET /tokens` omits the secret. `DELETE /tokens/{id}` is owner-scoped. - `GET/PUT /me/locale` persist `pl` or `en`. A locked user can still change locale and gets 423 on genres. - The five routes replay as `status: ported`. The corpus is 7 ported and 147 pending out of 154. ## Task Commits 1. **Task 1: Mint and revoke** — `3c53bf8` in `fonoteka.go` 2. **Task 2: Token and locale HTTP** — `58fcb45` in `fonoteka.go` 3. **Task 3: Exempt-route proof and parity flip** — `fa95534` in `fonoteka.go` ## Files Created/Modified - `plugins/golem15/fonoteka/classes/auth/api_token_manager.go` — mint and revoke - `plugins/golem15/fonoteka/controllers/api/token_api_controller.go` — store, index, destroy - `plugins/golem15/fonoteka/controllers/api/me_locale_controller.go` — show and update - `plugins/golem15/fonoteka/routes.go` — locale middleware placement - `parity/manifest.yaml` — five routes marked ported ## Decisions Made The captured `POST /tokens` body was a debug HTML 500 from an empty name. The Go handler returns 422 `{"error":"Validation failed","errors":{"name":["The name field is required."]}}`, and the fixture now records that. `GET /tokens` was a capture of a live `parity-mcp` row. Replay uses the genres seed token `Parity MCP` with scope `read`, and the fixture follows that seed. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Nil validation map panicked on a bad scope** - **Found during:** Task 2 - **Issue:** `lagoon.Validate` returns a nil map when `name` is valid, and assigning `errs["scopes"]` panicked into an opaque 500. - **Fix:** Allocate the map before recording the scope error. - **Files modified:** `controllers/api/token_api_controller.go` - **Committed in:** `58fcb45` **2. [Rule 1 - Bug] `created_at` used a `Z` suffix** - **Found during:** Task 3 - **Issue:** Tide accepts only Carbon `+00:00` timestamps. `time.RFC3339` emits `Z`. - **Fix:** Format token timestamps as `2006-01-02T15:04:05-07:00`. - **Files modified:** `controllers/api/token_api_controller.go` - **Committed in:** `fa95534` --- **Total deviations:** 2 auto-fixed **Impact on plan:** Bad scopes stay 422. Token timestamps match the parity normalizer. ## Issues Encountered None ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 07-05, which starts by recording the isolated PHP instance. AUTH-03 and AUTH-04 stay unchecked until the phase requirement is signed off. ## Self-Check: PASSED - `TestMintPersonalToken`, `TestTokenApi`, `TestMeLocale`, and `TestRequirePasswordChangeExemptSet` passed. - `go test ./parity/ -run 'TestParityCorpus|TestParityContract'` passed with 7 ported routes. - Commits `3c53bf8`, `58fcb45`, and `fa95534` are on `fonoteka.go` master.