Release 0.10.0: user-set passcodes (OTC stays as fallback)

After a successful OTC sign-in, a contributor can set a passcode
(4-20 characters, bcrypt-hashed) and use email + passcode for
subsequent sign-ins. OTC remains the structural fallback: five
consecutive failed verifies lock the passcode path for 15 minutes
(HTTP 423), and a forgotten passcode is recovered by requesting a
fresh code. Migration 015_passcode.sql adds four nullable columns
to the users table; existing rows pass through as OTC-only and can
opt into a passcode from a new Sign-in tab in /settings/notifications.
The /login surface is extended to a five-step flow (email → either
passcode or OTC code → optional post-OTC passcode offer → optional
set-passcode). SPEC corrections per §19.3 rule 2: §6 names the
three auth paths, §14.1 documents the stepped login flow, §17 lists
the four new /auth/passcode/* endpoints, §19.2 surfaces four new
candidates and refreshes the cross-refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-28 02:24:38 -07:00
parent 8aa65014b4
commit 8f3a8ec33a
13 changed files with 1794 additions and 62 deletions
+13
View File
@@ -120,6 +120,17 @@ def make_router(
user = auth.current_user(request)
if user is None:
return {"authenticated": False, "user": None}
# v0.10.0: surface a single `has_passcode` flag so the §6.2
# settings tab can render "Set passcode" vs. "Change/Remove
# passcode" without a second round trip. The set-at timestamp
# rides along for the same reason. The hash itself is never
# exposed.
row = db.conn().execute(
"SELECT passcode_hash, passcode_set_at FROM users WHERE id = ?",
(user.user_id,),
).fetchone()
has_passcode = bool(row and row["passcode_hash"])
passcode_set_at = row["passcode_set_at"] if (row and has_passcode) else None
return {
"authenticated": True,
"user": {
@@ -129,6 +140,8 @@ def make_router(
"email": user.email,
"avatar_url": user.avatar_url,
"role": user.role,
"has_passcode": has_passcode,
"passcode_set_at": passcode_set_at,
},
}