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:
+9
-6
@@ -131,13 +131,12 @@ def make_router(
|
||||
user = auth.current_user(request)
|
||||
if user is None:
|
||||
return {"authenticated": False, "user": None}
|
||||
# v0.8.0: surface `permission_state` plus the capture-flow
|
||||
# readiness signal (`needs_profile`). The frontend gates
|
||||
# the /beta-pending page and the inline banner off these
|
||||
# fields, and decides whether to prompt for the first/last/why
|
||||
# capture on first OTC sign-in.
|
||||
# v0.8.0 + v0.10.0: single round-trip for everything the
|
||||
# frontend gates UI off of — beta-access state + passcode state.
|
||||
row = db.conn().execute(
|
||||
"SELECT first_name, last_name, beta_request_reason FROM users WHERE id = ?",
|
||||
"SELECT first_name, last_name, beta_request_reason, "
|
||||
"passcode_hash, passcode_set_at "
|
||||
"FROM users WHERE id = ?",
|
||||
(user.user_id,),
|
||||
).fetchone()
|
||||
first_name = (row["first_name"] if row else None) or ""
|
||||
@@ -153,6 +152,8 @@ def make_router(
|
||||
and not last_name
|
||||
and not beta_request_reason
|
||||
)
|
||||
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": {
|
||||
@@ -167,6 +168,8 @@ def make_router(
|
||||
"last_name": last_name,
|
||||
"beta_request_reason": beta_request_reason,
|
||||
"needs_profile": needs_profile,
|
||||
"has_passcode": has_passcode,
|
||||
"passcode_set_at": passcode_set_at,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user