mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-28 00:18:58 -07:00
[www] password/email recovery (#146)
* pw/email recovery * fix migration
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
-- 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.
|
||||
--
|
||||
-- The clean-up runs over a throwaway plain index on the column. Without one, finding each
|
||||
-- address's keeper is a scan of the whole table — parsing every row's JSON — PER ROW with
|
||||
-- an address, which is what a correlated `MIN(account_id)` did on the first attempt and
|
||||
-- what D1 killed for exceeding its CPU limit. With it, the duplicates are one grouped
|
||||
-- index walk and the update one indexed join.
|
||||
--
|
||||
-- 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;
|
||||
|
||||
CREATE INDEX idx_account_email_lower_dedupe ON account (email_lower) WHERE email_lower IS NOT NULL;
|
||||
|
||||
UPDATE account SET data = json_remove(data, '$.email')
|
||||
WHERE account_id IN (
|
||||
SELECT a.account_id
|
||||
FROM account a
|
||||
JOIN (
|
||||
SELECT email_lower, MIN(account_id) AS keeper
|
||||
FROM account
|
||||
WHERE email_lower IS NOT NULL
|
||||
GROUP BY email_lower
|
||||
HAVING COUNT(*) > 1
|
||||
) d ON a.email_lower = d.email_lower
|
||||
WHERE a.account_id > d.keeper
|
||||
);
|
||||
|
||||
DROP INDEX idx_account_email_lower_dedupe;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email_lower ON account (email_lower) WHERE email_lower IS NOT NULL;
|
||||
@@ -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)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Fixed-window counters: "at most N of these per key per window". What caps the emails
|
||||
-- this server sends — password-recovery codes (`auth`) and email-change codes (`accounts`)
|
||||
-- — per target account and per requesting IP, so a mailbox can't be flooded by anyone who
|
||||
-- knows its address. Mirrored by RATE_LIMIT_SCHEMA_DDL in @repo/domain's rate-limit-db.ts,
|
||||
-- which says how a key is taken and when rows are purged.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limit (
|
||||
key TEXT PRIMARY KEY,
|
||||
window_started_at INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL
|
||||
);
|
||||
Reference in New Issue
Block a user