pw/email recovery

This commit is contained in:
Devin Zuczek
2026-10-26 22:09:50 -04:00
parent 619b4c772c
commit ad2b84b358
29 changed files with 3847 additions and 525 deletions
@@ -0,0 +1,24 @@
-- An email address belongs to ONE account. Password recovery (`auth`
-- `POST /account/recoverpassword`) is handed nothing but the address the player typed and
-- has to find their account from it, which only works if no two accounts share one.
--
-- Exposed as a generated column, case-folded, with an empty or absent email read as NULL so
-- accounts without one never collide — and a UNIQUE partial index over it, which is what
-- actually refuses a second holder. `accounts` `POST /account/me/email` checks first and
-- answers a refusal the player can read; the index is the backstop against a race.
--
-- Before the index can be built, existing duplicates have to go: the LOWEST account id
-- keeps the address (the first account to claim it) and the others lose theirs, since a
-- recovery sent to a shared address could only ever reach one of them anyway. Those
-- players set it again from the account page or in game.
--
-- Kept in sync with SCHEMA_DDL in @repo/domain's accounts-db.ts (and its mirror in
-- apps/econ/src/avatar-db.ts).
ALTER TABLE account ADD COLUMN email_lower TEXT GENERATED ALWAYS AS (NULLIF(lower(json_extract(data, '$.email')), '')) VIRTUAL;
UPDATE account SET data = json_remove(data, '$.email')
WHERE email_lower IS NOT NULL
AND account_id > (SELECT MIN(a2.account_id) FROM account a2 WHERE a2.email_lower = account.email_lower);
CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email_lower ON account (email_lower) WHERE email_lower IS NOT NULL;
+20
View File
@@ -0,0 +1,20 @@
-- One-shot codes sent by email: the PBKDF2 hash of the six-digit code the player was
-- mailed, what it is for (`purpose`: a `password_reset`; or an `email_change` — the
-- current address approving a change — followed by an `email_verify` — the new address
-- confirming itself — both with the new address as `payload`), and when it was issued and
-- lapses. One pending code per
-- account and purpose. Mirrored by EMAIL_CODE_SCHEMA_DDL in @repo/domain's
-- email-code-db.ts, which says how the row is used — in short, a wrong guess BURNS it
-- (`code_hash` goes NULL, the row stays for the resend cooldown), a right one deletes it,
-- and a new request replaces it. Written by `auth` and `accounts`; owned here with the
-- rest of the account tables.
CREATE TABLE IF NOT EXISTS email_code (
account_id INTEGER NOT NULL,
purpose TEXT NOT NULL,
code_hash TEXT,
payload TEXT,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
PRIMARY KEY (account_id, purpose)
);