55beba5c0a
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>
53 lines
2.7 KiB
SQL
53 lines
2.7 KiB
SQL
-- §6.2 / v0.10.0: user-set passcodes after OTC (roadmap item #8).
|
|
--
|
|
-- After a successful OTC sign-in, a contributor may set a passcode
|
|
-- (numeric PIN or short alphanumeric). Subsequent sign-ins on the same
|
|
-- account can use email + passcode instead of email + OTC. OTC remains
|
|
-- the structural fallback — a forgotten passcode is recovered by
|
|
-- requesting a fresh OTC and signing in via that path. Per-account
|
|
-- lockout after 5 consecutive verify failures redirects the user to
|
|
-- the OTC path for 15 minutes; the OTC path itself is unaffected by
|
|
-- the passcode lockout (a locked-out user can still receive a fresh
|
|
-- code and sign in).
|
|
--
|
|
-- The columns are additive to the `users` table from `012_otc.sql`.
|
|
-- v0.8.0's `permission_state` column (roadmap item #6) lands in the
|
|
-- driver's integration order ahead of this migration; we do not touch
|
|
-- that column here. v0.7.0's nullable-`gitea_id`/`gitea_login` shape
|
|
-- is preserved verbatim.
|
|
--
|
|
-- Storage shape:
|
|
--
|
|
-- * `passcode_hash` (nullable) — bcrypt hash of the passcode.
|
|
-- NULL means "no passcode set"; the user is OTC-only.
|
|
-- * `passcode_set_at` (nullable) — timestamp of the most recent
|
|
-- `passcode/set` call. Updated when a passcode is set or
|
|
-- replaced; cleared when the passcode is removed.
|
|
-- * `passcode_failed_attempts` — count of consecutive failed
|
|
-- verify attempts since the last successful verify (or since
|
|
-- the lockout cleared). Resets to 0 on success and on lockout
|
|
-- expiry. Defaults to 0 so existing rows post-migration are
|
|
-- not implicitly half-locked.
|
|
-- * `passcode_locked_until` (nullable) — if populated and the
|
|
-- timestamp is in the future, passcode verify is refused with
|
|
-- HTTP 423. Cleared on successful verify after the window
|
|
-- expires, or by the operator via direct DB intervention if
|
|
-- ever needed (no admin endpoint surfaces this in v1).
|
|
--
|
|
-- v0.10.0 introduces no new env vars. The lockout window (5 attempts,
|
|
-- 15 minutes) is hard-coded in `backend/app/passcode.py`; raising or
|
|
-- lowering it is a future-§19.2 candidate. Passcode hashing reuses
|
|
-- the bcrypt dependency added in v0.7.0 for OTC; no new secret is
|
|
-- required (the existing `SECRET_KEY` continues to sign sessions).
|
|
--
|
|
-- Note on SQLite: ALTER TABLE ... ADD COLUMN is supported, so this
|
|
-- migration does not need the rebuild dance that `012_otc.sql`
|
|
-- required. The runner wraps each file in a single BEGIN/COMMIT
|
|
-- block — see `backend/app/db.py` — so either every ADD COLUMN
|
|
-- here lands or none do.
|
|
|
|
ALTER TABLE users ADD COLUMN passcode_hash TEXT;
|
|
ALTER TABLE users ADD COLUMN passcode_set_at TEXT;
|
|
ALTER TABLE users ADD COLUMN passcode_failed_attempts INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE users ADD COLUMN passcode_locked_until TEXT;
|