From 0c5246913a2c772ded4e8604b47d96accef77594 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 30 Jul 2026 09:40:05 +0300 Subject: [PATCH] feat(tradein/db): tradein_users + tradein_sessions schema (#2551) --- .../data/sql/192_tradein_users_auth.sql | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/192_tradein_users_auth.sql diff --git a/tradein-mvp/backend/data/sql/192_tradein_users_auth.sql b/tradein-mvp/backend/data/sql/192_tradein_users_auth.sql new file mode 100644 index 00000000..df5193c8 --- /dev/null +++ b/tradein-mvp/backend/data/sql/192_tradein_users_auth.sql @@ -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;