Netpulse_SasS/db/migrations/0012_auth.sql
byrsapty a4faf31fcd Етап 5: користувачі, вхід і права
Досі доступ давав машинний токен зі змінної збірки — одні права на всіх
і жодного способу відрізнити, хто що зробив. Тепер продукт уміє впустити
людину.

Сервер:
- argon2id для паролів, власний HS256 JWT (15 хв) + refresh-сесія в
  httpOnly-кукі на 30 днів з ротацією при кожному обміні
- Principal зводить людину й машинний токен до одного набору прав;
  права читаються з БД на кожному запиті, а не з claims, щоб відкликана
  роль не жила до кінця TTL
- 9 ендпоїнтів: auth/login|refresh|logout|password, me, team CRUD, roles
- netpulse-user — CLI для першого власника: публічна реєстрація в B2B
  це дірка, а «перший через веб, поки нікого немає» — нечесна гонка
- міграція 0012: три RLS-політики винятку для шляху входу (без них
  вхід неможливий за побудовою — щоб знайти користувача за email,
  треба знати тенант, який відомий лише після пошуку) і login_attempts
  для тротлінгу

Фронтенд:
- сторінка входу з вибором організації, access-токен у замиканні
  модуля замість localStorage, тихе відновлення сесії по кукі
- один refresh на всі паралельні запити: інакше ротація зробила б усі,
  крім першого, недійсними й викинула б людину на вхід
- дії без права не показуються; полотно нередаговане для глядача
- мобільний адаптив: висувна бічна панель, інспектор нижнім аркушем

11 нових тестів (37 у httpapi), go vet і tsc чисто, живий прогін з
8 кроків проти netpulse_it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 10:24:58 +03:00

94 lines
5 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =====================================================================
-- NetPulse :: 0012_auth.sql
-- Автентифікація людей: політики RLS для шляху входу + індекси.
--
-- Схема користувачів, ролей і сесій уже є з 0001. Бракує одного:
-- вхід відбувається ДО того, як відомий тенант. Користувач надсилає
-- лише email і пароль; поки ми не знайшли його membership, виставити
-- app.tenant_id нема з чого. Обидві таблиці, потрібні на цьому шляху,
-- закриті політикою tenant_isolation — тобто логін під роллю з RLS
-- завжди повертав би порожньо.
-- =====================================================================
-- ---------------------------------------------------------------------
-- Виняток для шляху автентифікації
--
-- Політика спрацьовує РІВНО тоді, коли тенант ще не встановлений.
-- Це навмисно вузько: у будь-якому іншому запиті порожній app.tenant_id
-- і далі означає порожній результат (див. 0011), тому забутий SET LOCAL
-- не перетворюється на витік чужих даних — він просто нічого не поверне.
--
-- Компроміс усвідомлений: у вікні «тенант ще невідомий» таблиця
-- core.users читається повністю. Прийнятно, бо запити на цьому шляху
-- пише лише сервер і всі вони одразу звужені за email або token_hash.
-- ---------------------------------------------------------------------
CREATE POLICY users_auth_lookup ON core.users
FOR SELECT
USING (core.current_tenant() IS NULL);
-- Той самий виняток для сесій: обмін refresh-токена теж відбувається
-- до вибору тенанта, бо саме сесія його й називає.
CREATE POLICY sessions_auth_lookup ON core.sessions
FOR SELECT
USING (core.current_tenant() IS NULL);
-- Створення й відкликання сесії — теж поза тенантом: у момент логіну
-- контексту ще немає, а при виході він уже не потрібен.
CREATE POLICY sessions_auth_write ON core.sessions
FOR INSERT
WITH CHECK (core.current_tenant() IS NULL OR tenant_id = core.current_tenant());
CREATE POLICY sessions_auth_revoke ON core.sessions
FOR UPDATE
USING (core.current_tenant() IS NULL OR tenant_id = core.current_tenant());
-- Членства читаються одразу після знаходження користувача, щоб
-- зрозуміти, до якого тенанта його пускати.
CREATE POLICY memberships_auth_lookup ON core.memberships
FOR SELECT
USING (core.current_tenant() IS NULL);
-- ---------------------------------------------------------------------
-- Індекси під шлях автентифікації
-- ---------------------------------------------------------------------
-- Живі сесії користувача — для сторінки «активні входи» й масового
-- відкликання при зміні пароля.
CREATE INDEX IF NOT EXISTS sessions_active_idx
ON core.sessions (user_id, expires_at DESC)
WHERE revoked_at IS NULL;
-- Прибирання протермінованих сесій фоновим завданням.
CREATE INDEX IF NOT EXISTS sessions_expiry_idx
ON core.sessions (expires_at)
WHERE revoked_at IS NULL;
-- ---------------------------------------------------------------------
-- Аудит входів
--
-- Окремо від core.audit_log: невдалі спроби входу треба рахувати ще до
-- того, як відомо, хто саме їх робить, а audit_log вимагає tenant_id.
-- Це ж джерело для блокування перебору.
-- ---------------------------------------------------------------------
CREATE TABLE core.login_attempts (
ts timestamptz NOT NULL DEFAULT now(),
id uuid NOT NULL DEFAULT core.new_id(),
email citext NOT NULL,
ip inet,
user_agent text,
success boolean NOT NULL,
-- bad_password | no_user | locked | mfa_failed
reason text,
user_id uuid,
PRIMARY KEY (ts, id)
);
SELECT create_hypertable('core.login_attempts', 'ts',
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
CREATE INDEX login_attempts_email_idx ON core.login_attempts (email, ts DESC);
CREATE INDEX login_attempts_failed_idx ON core.login_attempts (ip, ts DESC)
WHERE NOT success;
SELECT add_retention_policy('core.login_attempts', INTERVAL '180 days', if_not_exists => TRUE);