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:
@@ -65,6 +65,49 @@ export async function submitBetaRequest({ first_name, last_name, beta_request_re
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
// ── v0.10.0: user-set passcodes after OTC (§6.2, roadmap item #8) ─────────
|
||||
//
|
||||
// After a successful OTC sign-in, a contributor may set a passcode and
|
||||
// use email + passcode for subsequent sign-ins. OTC remains the
|
||||
// forgot-passcode fallback — 5 consecutive verify failures locks the
|
||||
// passcode path for 15 minutes (HTTP 423); the OTC path is unaffected.
|
||||
|
||||
export async function checkPasscode(email) {
|
||||
// Anonymous endpoint. Returns `{has_passcode: boolean}` so the
|
||||
// Login.jsx flow can decide whether to render a passcode input or
|
||||
// fall back to OTC. We URL-encode the email so addresses with '+'
|
||||
// round-trip cleanly.
|
||||
const params = new URLSearchParams({ email })
|
||||
const res = await fetch(`/auth/passcode/check?${params}`)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function verifyPasscode(email, passcode) {
|
||||
const res = await fetch('/auth/passcode/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, passcode }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function setPasscode(passcode) {
|
||||
// Requires an active session — the server returns 401 if not signed
|
||||
// in. The signed-in user is the implicit subject; the body carries
|
||||
// only the new passcode.
|
||||
const res = await fetch('/auth/passcode/set', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ passcode }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function clearPasscode() {
|
||||
const res = await fetch('/auth/passcode', { method: 'DELETE' })
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function listRFCs() {
|
||||
return jsonOrThrow(await fetch('/api/rfcs'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user