domain: Write lockout state only through narrow atomic repo ops #86
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier!86
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "domain/lockout-narrow-atomic-repo-ops"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #56 per ADR 0011 and the Authentication vocabulary in CONTEXT.md.
What changed
The two lock columns (
failed_login_attempts,locked_until) are now written by exactly two repository operations and nothing else:record_failed_login(user_id, &LockoutPolicy)— one atomic act (a singleUPDATEin the Postgres adapter) that counts the failure and, on reaching the threshold, enters the Lockout and restarts the count in the same statement. Reports aLockoutOutcome:Counted { attempts }orLocked { until }.clear_lockout(user_id)— zeroes the count and lifts the Lockout; called unconditionally on every successful login. Idempotent.UpdateUserRequestloses the lock-column fields entirely, so no profile round-trip can clobber Lockout state — the rule is a compile-time fact, not discipline. The oldincrement_login_attemptsport op is removed, and its now-infallibleTryFrom<User>becameFrom<User>.New domain types alongside
User:LockoutPolicy(threshold + duration,Defaultof 10 attempts / 15 minutes, held by the service, no app-config surface) andLockoutOutcome.Behavior changes (deliberate, per ADR 0011)
UserLocked(until)instead of reading as one more wrong password.Tests
lockout_repository_conformance.rspins both adapters to the same semantics in the establishedboth_adapters!pattern: below-threshold counting, the crossing recording (expiry ≈ now + duration, count restarted), idempotent clearing, andupdate_userleaving both a mid-count and a locked user untouched.confirm_emailleaving a Lockout in place.The
cimise task passes; the sqlx offline cache is regenerated.Closes #56