axe/internal/db/migrations/0007_password_reset.sql

43 lines
2.7 KiB
MySQL
Raw Permalink Normal View History

2026-10-18 19:07:25 +00:00
-- 0007_password_reset — сброс пароля по одноразовому токену.
--
-- SMTP в проекте нет по решению: токен выдаёт админ
-- (POST /users/:id/password-reset) и передаёт его пользователю сам —
-- любым каналом, кроме почты внутри приложения. Пользователь меняет
-- пароль через POST /auth/reset-password.
--
-- В таблице лежит ТОЛЬКО sha256 токена: утекшая база не даёт
-- переиспользовать выпущенные токены. Свойство «увидел — значит чужой»:
-- зная хеш, токен не восстановить (256 бит поискового пространства).
--
-- Правила, проверяемые в SQL, а не в хендлерах:
-- * токен одноразовый: WHERE used_at IS NULL в самом UPDATE;
-- * срок: WHERE expires_at > now(), срок всегда позже выпуска (CHECK);
-- * у пользователя действует ровно один свежий токен — выпуск нового
-- гасит предыдущие (WITH в CreateResetToken);
-- * вместе с паролем закрываются refresh-сессии (в коде, сразу после
-- успешного UPDATE — смена пароля без разлогина бессмысленна).
--
-- Строки живут после использования (история), чистка — по желанию:
-- просроченные токены бесполезны, таблица растёт медленно.
--
-- Применённый файл не редактировать (checksum в schema_migrations).
CREATE TABLE password_reset_tokens (
id bigserial PRIMARY KEY,
user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- sha256 токена в hex; сам токен в базе не хранится
token_hash text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
-- NULL — ещё не использован; момент смены пароля
used_at timestamptz,
CONSTRAINT password_reset_tokens_expiry
CHECK (expires_at > created_at)
);
-- Выпуск нового токена гасит старые конкретного пользователя,
-- поэтому выборка «незакрытые токены пользователя» должна быть быстрой.
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
ON password_reset_tokens (user_id, created_at);