feat(tradein/db): tradein_users + tradein_sessions schema (#2551) #2559
1 changed files with 88 additions and 0 deletions
88
tradein-mvp/backend/data/sql/192_tradein_users_auth.sql
Normal file
88
tradein-mvp/backend/data/sql/192_tradein_users_auth.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
-- Migration 192: tradein_users + tradein_sessions — DB-backed auth (issue #2551, эпик #2549)
|
||||||
|
--
|
||||||
|
-- WHY:
|
||||||
|
-- Trade-in auth сейчас держится на legacy Caddy basic-auth fallback (см. auth/roles.yaml,
|
||||||
|
-- упомянут в 191_account_quota_unlimited_flag.sql как "хардкод username в коде"). Эпик #2549
|
||||||
|
-- переводит auth на DB-backed модель: пользователи + сессии как данные, роли admin/manager/
|
||||||
|
-- employee с иерархией manager -> employee. Эта миграция — только схема (Foundation),
|
||||||
|
-- без seed-данных (seed — отдельная задача #2557) и без Python-кода (backend wiring — отдельно).
|
||||||
|
--
|
||||||
|
-- WHAT:
|
||||||
|
-- 1. tradein_users — identity + role + org-иерархия.
|
||||||
|
-- - password_hash NULL допустим: переходный период, когда логин ещё идёт через
|
||||||
|
-- legacy Caddy fallback, а не через password verify в приложении.
|
||||||
|
-- - role CHECK ('admin','manager','employee') — три уровня доступа.
|
||||||
|
-- - manager_id — self-FK, ON DELETE SET NULL (увольнение/удаление manager'а не должно
|
||||||
|
-- каскадно сносить его employees, они просто остаются без привязки).
|
||||||
|
-- - CHECK role_manager_hierarchy: admin/manager обязаны иметь manager_id IS NULL
|
||||||
|
-- (это top-level роли, у них нет "начальника" в этой модели); employee — manager_id
|
||||||
|
-- любой, включая NULL (свободный слот employee без организации допустим).
|
||||||
|
-- 2. tradein_sessions — токен-based сессии, привязаны к user_id, ON DELETE CASCADE
|
||||||
|
-- (удалили пользователя — его сессии теряют смысл, каскадная очистка корректна).
|
||||||
|
-- last_seen_at отдельно от created_at — для idle-timeout / активности сессии.
|
||||||
|
-- 3. Индексы: expires_at (уборка протухших сессий), user_id (список сессий юзера),
|
||||||
|
-- partial на manager_id (иерархия) — WHERE manager_id IS NOT NULL, т.к. большинство
|
||||||
|
-- admin/manager строк это NULL и не участвуют в lookup "employees этого manager'а".
|
||||||
|
--
|
||||||
|
-- IDEMPOTENCY:
|
||||||
|
-- CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS. Повторный прогон — no-op.
|
||||||
|
-- CHECK-констрейнты добавлены inline в CREATE TABLE (не через ALTER) — при повторном
|
||||||
|
-- запуске CREATE TABLE IF NOT EXISTS не выполнится вообще, констрейнт не задублируется.
|
||||||
|
--
|
||||||
|
-- Dependencies: нет (новые таблицы, ничего существующего не меняем).
|
||||||
|
-- Deploy order: эта миграция — Foundation эпика #2549. Seed (#2557) и backend auth-код —
|
||||||
|
-- отдельные PR'ы ПОСЛЕ этой (SQL-схема первой, см. .claude/rules/sql.md "Migration order").
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tradein_users (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
username text NOT NULL UNIQUE,
|
||||||
|
password_hash text NULL,
|
||||||
|
role text NOT NULL CHECK (role IN ('admin', 'manager', 'employee')),
|
||||||
|
manager_id bigint NULL REFERENCES tradein_users(id) ON DELETE SET NULL,
|
||||||
|
display_name text NULL,
|
||||||
|
org_name text NULL,
|
||||||
|
email text NULL,
|
||||||
|
is_active boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT tradein_users_role_manager_hierarchy_ck CHECK (
|
||||||
|
role NOT IN ('admin', 'manager') OR manager_id IS NULL
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE tradein_users IS
|
||||||
|
'Trade-in DB-backed auth — пользователи (issue #2551, эпик #2549). password_hash NULL '
|
||||||
|
'допустим в переходный период (логин через legacy Caddy fallback). Seed — отдельно (#2557).';
|
||||||
|
COMMENT ON COLUMN tradein_users.password_hash IS
|
||||||
|
'NULL = логин только через legacy Caddy basic-auth fallback, не через password verify.';
|
||||||
|
COMMENT ON COLUMN tradein_users.manager_id IS
|
||||||
|
'Self-FK на tradein_users(id). NULL для admin/manager (top-level, CHECK ниже) или для '
|
||||||
|
'employee без назначенной организации.';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tradein_sessions (
|
||||||
|
token text PRIMARY KEY,
|
||||||
|
user_id bigint NOT NULL REFERENCES tradein_users(id) ON DELETE CASCADE,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
ip_address inet NULL,
|
||||||
|
user_agent text NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE tradein_sessions IS
|
||||||
|
'Trade-in DB-backed auth — активные сессии (issue #2551, эпик #2549). '
|
||||||
|
'ON DELETE CASCADE от tradein_users: удалённый пользователь теряет все сессии.';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS tradein_sessions_expires_at_idx
|
||||||
|
ON tradein_sessions (expires_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS tradein_sessions_user_id_idx
|
||||||
|
ON tradein_sessions (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS tradein_users_manager_id_idx
|
||||||
|
ON tradein_users (manager_id)
|
||||||
|
WHERE manager_id IS NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
Add table
Reference in a new issue