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
Pattern: /_user/api/v1 group middleware is only throttle:user-api; handlers authenticate Bearer-only
Pattern: mail template names go through MailTemplate and ResolveMailLocale
83min
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
Task 1: User session schema and config — 7046213 (test), 4cc7433 (feat) in fonoteka.go
Task 2: Throttle, mail locale, login/logout/fetch/refresh — 1076db9, 1dd4aba in fonoteka.go
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
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.
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.