docs(07-02): complete the user session plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-22 15:15:16 +02:00
parent ae9e11f65d
commit 3cf938867c
3 changed files with 125 additions and 6 deletions

View File

@@ -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.