SCRUM-259 (backend) — admin-auth self-service account endpoints¶
Plan ref: UI-14 (docs/11-admin-plane-plan.md) needs these; D5 governs MFA.
What exists¶
All under /admin-auth/account/*, bearer access token of an active account (re-read
from the database), strict JSON:
| Route | Behaviour |
|---|---|
POST …/password {current_password, new_password} |
current verified before the lock check (same work as login); a wrong one counts toward the lockout; new must pass the password policy and differ; one transaction: new hash, every other session revoked (the caller's own family is kept, found from the refresh cookie), audit account.password_changed; 204 |
GET …/sessions |
the caller's live sessions (one per rotation family): created, last used (newest rotation), ip, user agent, current |
POST …/sessions/revoke-others |
revokes all but the current family ({revoked, current_kept}); without a cookie revokes all; audit account.sessions_revoked |
POST …/mfa/recovery-codes {code} |
fresh TOTP required (replay-proof); 10 new codes replace the old; audit mfa.recovery_regenerated |
POST …/mfa/disable {code} |
TOTP or recovery code; only accounts without the admin role (admin → 403 mfa_required, root → 403 root_protected, not enrolled → 409); clears factor, codes, tickets; audit mfa.disabled |
Limit worth knowing: revoking a session stops its refresh token at once, but an access token it already holds stays valid until it expires (15 minutes), as for logout.
How to verify¶
cd services/admin_auth
ADMIN_AUTH_TEST_DATABASE_URL=postgres://auth_rw:pw@127.0.0.1:5433/admin_auth_test?sslmode=disable \
go test -race -count=1 ./...
internal/server/account_test.go covers every row above incl. lockout, the current
session surviving a password change while others are refused, replayed TOTP, old
recovery codes dying, and 401s for missing/invalid tokens and disabled accounts.
GET /admin-auth/me also returns is_root and mfa_enabled, read from the row, so the
account page can show the factor state without calling enroll (which would write a new
pending secret on every visit). internal/server/session_test.go:TestMeReadsTheUserFromTheDatabase
checks both are present and that mfa_enabled flips once totp_confirmed_at is set.
Results at time of writing¶
gofmt,go vet,go test -racewith the DB (9 packages): pass.
How it was built¶
DeepSeek run scoped (Landlock) to services/admin_auth (256 s, ~38k output tokens,
reasoning effort low). Claude review: password/lockout order and the revocation
transaction checked. Added by Claude while building the UI: the two /me fields.