feat(accounts): outgoing mail over SMTP (Resend), email verification, fail-closed password reset
- mail/ module (lettre, ru/en templates), links built only from PUBLIC_BASE_URL - migration 0006: accounts.email_verified_at, shared email_tokens table (verify + reset), existing accounts marked verified - reset mailer no longer logs tokens; RESET_MAIL_MODE=log is dev-only and refused with COOKIE_SECURE=true; Disabled by default answers 503 - forgot-password and resend-verification do their work in a background task (no timing oracle) - device linking requires a verified email; email_verified exposed via /me and gRPC - gateway rate limits, SMTP_* in compose and .env.example - frontend: verify, forgot-password, reset-password pages, verify banner, ru/en strings
This commit is contained in:
parent
12ce6216b6
commit
c57f851a8b
48 changed files with 1810 additions and 120 deletions
33
backend/accounts-service/migrations/0006_email_tokens.sql
Normal file
33
backend/accounts-service/migrations/0006_email_tokens.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Email verification + shared single-use tokens.
|
||||
--
|
||||
-- One table for both token purposes (email_verify / password_reset): the
|
||||
-- semantics are identical (hashed opaque token, short TTL, single use,
|
||||
-- issuing a new one supersedes the pending one), so 0005's dedicated
|
||||
-- password_reset_tokens table is folded into it. Pending reset links (30
|
||||
-- minute TTL) are carried over instead of silently invalidated.
|
||||
--
|
||||
-- Existing accounts are marked verified at migration time: they signed up
|
||||
-- before verification existed, and locking them out of device linking would
|
||||
-- break every linked mod on deploy.
|
||||
|
||||
ALTER TABLE accounts ADD COLUMN email_verified_at TIMESTAMPTZ;
|
||||
|
||||
UPDATE accounts SET email_verified_at = now();
|
||||
|
||||
CREATE TABLE email_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
purpose TEXT NOT NULL CHECK (purpose IN ('email_verify', 'password_reset')),
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_email_tokens_account_id ON email_tokens(account_id);
|
||||
|
||||
INSERT INTO email_tokens (account_id, purpose, token_hash, expires_at, used_at, created_at)
|
||||
SELECT account_id, 'password_reset', token_hash, expires_at, used_at, created_at
|
||||
FROM password_reset_tokens;
|
||||
|
||||
DROP TABLE password_reset_tokens;
|
||||
Loading…
Add table
Add a link
Reference in a new issue