diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index fb266b1..ddaaf8e 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -281,7 +281,7 @@ Plans: **Wave 2** *(blocked on 07-01)* -- [ ] 07-02-PLAN.md — User/Throttle schema and the core session loop: login/logout/fetch/refresh/register +- [x] 07-02-PLAN.md — User/Throttle schema and the core session loop: login/logout/fetch/refresh/register **Wave 3** *(blocked on 07-02)* @@ -448,7 +448,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 | | 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 | -| 7. User plugin and authentication | 1/6 | In Progress| | +| 7. User plugin and authentication | 2/6 | In Progress| | | 8. OAuth2.1 authorization server | 0/TBD | Not started | - | | 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - | | 10. Admin Vue SPA | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index f306ab0..21fccdc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,13 +4,13 @@ milestone: v1.0 milestone_name: milestone status: executing stopped_at: Completed 07-01-PLAN.md -last_updated: "2026-09-22T11:43:03.651Z" +last_updated: "2026-09-22T13:14:46.649Z" last_activity: 2026-09-22 progress: total_phases: 15 completed_phases: 6 total_plans: 43 - completed_plans: 38 + completed_plans: 39 percent: 40 --- @@ -26,11 +26,11 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 07 (user-plugin-and-authentication) — EXECUTING -Plan: 2 of 6 +Plan: 3 of 6 Status: Ready to execute Last activity: 2026-09-22 -Progress: [█████████░] 88% +Progress: [█████████░] 91% ## Performance Metrics @@ -82,6 +82,7 @@ Progress: [█████████░] 88% | Phase 06 P10 | 3h 15m | 1 tasks | 2 files | | Phase 06 P11 | 12h 30m | 1 tasks | 1 files | | Phase 07 P01 | 12 min | 3 tasks | 20 files | +| Phase 07 P02 | 83m | 3 tasks | 22 files | ## Accumulated Context diff --git a/.planning/phases/07-user-plugin-and-authentication/07-02-SUMMARY.md b/.planning/phases/07-user-plugin-and-authentication/07-02-SUMMARY.md new file mode 100644 index 0000000..4c1f666 --- /dev/null +++ b/.planning/phases/07-user-plugin-and-authentication/07-02-SUMMARY.md @@ -0,0 +1,118 @@ +--- +phase: 07-user-plugin-and-authentication +plan: 02 +subsystem: auth +tags: [user, jwt, throttle, register, festival] + +requires: + - phase: 07-user-plugin-and-authentication + provides: bouncer Mint, Refresh, BlacklistStore, bcrypt, lagoon email/confirmed +provides: + - golem15.user login, logout, fetch, refresh, register, oauth-providers + - user_throttle and jwt_blacklist migrations + - GetApiArrayEvent collected by golem15.fonoteka +affects: [07-03, 07-04, 07-05] + +tech-stack: + added: [] + patterns: [Bearer-only session handlers, cookie fallback only on the registry jwt guard, pre-password throttle gate] + +key-files: + created: + - ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go + - ../fonoteka.go/plugins/golem15/user/routes.go + - ../fonoteka.go/plugins/golem15/user/classes/events.go + - ../fonoteka.go/plugins/golem15/user/classes/throttle.go + modified: + - ../fonoteka.go/plugins/golem15/user/plugin.go + - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go + +key-decisions: + - "Login gates with RejectIfThrottled before the password check and records the attempt once afterward" + - "Disabled and throttled registration return the production SafeExceptionResponse body unless app.debug is true" + - "Fonoteka listens for GetApiArrayEvent; the user plugin does not import fonoteka" + +patterns-established: + - "Pattern: /_user/api/v1 group middleware is only throttle:user-api; handlers authenticate Bearer-only" + - "Pattern: mail template names go through MailTemplate and ResolveMailLocale" + +requirements-completed: [] + +duration: 83min +completed: 2026-09-22 +--- + +# Phase 7 Plan 02: User session loop Summary + +**Login, logout, fetch, refresh, and register on `/_user/api/v1`, with a per-user login throttle, a Postgres JWT blacklist, and fonoteka's organisation fields merged through `GetApiArrayEvent`.** + +## Performance + +- **Duration:** 83 min +- **Started:** 2026-09-22T11:48:00Z +- **Completed:** 2026-09-22T13:11:00Z +- **Tasks:** 3 +- **Files modified:** 22 + +## Accomplishments + +- Email and password login returns a JWT whose `sub` is the user id, `prv` is the hardcoded User hash, and `iss` is the request URL. Logout forever-blacklists that jti so the next fetch is 401. +- Wrong password, an unknown email, and a suspended account share the body `{"error":true,"message":"Invalid email or password"}`. +- Register covers auto (token), user (activation mail), and admin (`{}`). Production hides disabled and throttled causes behind `{"error":"Internal server error"}`. +- `golem15.fonoteka` adds `organisation_id`, `organisation_role`, `must_change_password`, and `preferred_locale` on `GetApiArrayEvent`. + +## Task Commits + +1. **Task 1: User session schema and config** — `7046213` (test), `4cc7433` (feat) in `fonoteka.go` +2. **Task 2: Throttle, mail locale, login/logout/fetch/refresh** — `1076db9`, `1dd4aba` in `fonoteka.go` +3. **Task 3: Register and GetApiArrayEvent** — `bac71fe` in `fonoteka.go` + +## Files Created/Modified + +- `plugins/golem15/user/controllers/api_controller.go` — session and register handlers +- `plugins/golem15/user/routes.go` — `/_user/api/v1` group with `throttle:user-api` +- `plugins/golem15/user/plugin.go` — Postgres blacklist, cookie-capable jwt guard, user-api bucket, sweep +- `plugins/golem15/user/classes/throttle.go` — failed-login counter and the pre-password gate +- `plugins/golem15/user/classes/events.go` — `GetApiArrayEvent` and `RegisterEvent` +- `plugins/golem15/fonoteka/plugin.go` — payload listener + +## Decisions Made + +The login gate does not increment the attempt counter. `CheckAndRecordLogin(..., false)` both checks a ban and records a failure, so calling it before and after the password check would suspend on the third HTTP failure. `RejectIfThrottled` only reports an existing ban or suspension. The outcome is recorded once. + +Registration's disabled and throttled branches use `{"error":"..."}` at 500. That is distinct from `wire.WriteOpaque500`, which is `{"error":true,"message":"Internal server error"}`. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] The pre-password throttle call must not increment attempts** +- **Found during:** Task 2 +- **Issue:** The plan called `CheckAndRecordLogin(..., false)` before the password check and again on failure. That function increments on `ok=false`, so each failed login counted twice and the sixth HTTP login was not the one rejected before the password comparison. +- **Fix:** `RejectIfThrottled` enforces ban and suspension without writing. `CheckAndRecordLogin` runs once with the outcome. +- **Files modified:** `classes/throttle.go`, `controllers/api_controller.go` +- **Verification:** `TestLoginSixthAttempt` — five failures suspend, the sixth correct password returns the generic 401, and `attempts` stays 5. +- **Committed in:** `1dd4aba` + +--- + +**Total deviations:** 1 auto-fixed (Rule 1) +**Impact on plan:** Keeps the 5-attempt / 15-minute suspend aligned with one failed HTTP login. No new route. + +## Issues Encountered + +None + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Ready for 07-03 (account management) and 07-04 (personal tokens). AUTH-01 stays open: password reset and email verification are still 07-03. AUTH-02's payload seam is in place; the requirement checkbox stays pending until the phase requirement is signed off. + +## Self-Check: PASSED + +- Session and register handlers exist under `plugins/golem15/user/controllers/api_controller.go`. +- `fonoteka.go` commits `7046213`, `4cc7433`, `1076db9`, `1dd4aba`, and `bac71fe` are on master. +- `go test` for the session, throttle, register, and `TestGetApiArray` filters passed. `go vet` on the user and fonoteka plugins passed.