-- 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;