diff --git a/projects/pg/kit_auth_pg/_docs/architecture.md b/projects/pg/kit_auth_pg/_docs/architecture.md new file mode 100644 index 0000000..2990da2 --- /dev/null +++ b/projects/pg/kit_auth_pg/_docs/architecture.md @@ -0,0 +1,75 @@ +# Архитектура Kit.Auth.Db + +## Обзор + +База данных авторизации платформы Kit. Хранит данные пользователей и связанные сущности (кабинеты — LEGACY). + +## Схема + +Единая схема `auth` — изолирует объекты авторизации от других доменов. + +## Таблицы + +### auth."user" + +Основная таблица пользователей. Зарезервированное слово PostgreSQL, поэтому имя экранируется кавычками. + +**Ключевые колонки:** +- `id` (serial PK) — идентификатор +- `login` (text, NOT NULL) — логин для входа +- `email` (text) — электронная почта (дополнительный вход) +- `password` (text) — хеш PBKDF2 (формат: `pbkdf2-sha256$iter$salt$hash`) +- `password_temp` (text) — DEPRECATED, временный пароль (оставлен для совместимости) +- `outer_user_id` (text) — идентификатор во внешней системе +- `is_blocked` (boolean) — флаг блокировки +- `is_deleted` / `date_deleted` — мягкое удаление + +**Примечание:** Состояние аутентификации (подтверждение email, lockout, счётчик неудач) и сессии вынесены в токен-БД (`Kit.Auth.Token.Db`). + +### auth.cabinet (LEGACY) + +Кабинеты/организации. Будет убрана в будущем. + +### auth.user_cabinet (LEGACY) + +Связь пользователей с кабинетами. Без PK и FK (legacy). + +## Функции + +### user + +| Функция | Описание | +|---------|----------| +| `user_insert` | Вставка пользователя | +| `user_select` | Выборка с пейджингом и фильтрами | +| `user_select_by_ids` | Batch-выборка по списку id | +| `user_get_by_login` | Поиск по логину и паролю | +| `user_get_for_auth` | Поиск для аутентификации (без проверки пароля) | +| `user_get_profile` | Профиль владельца токена | +| `user_delete` | Мягкое удаление | +| `user_update` | Обновление профиля | +| `user_update_password` | Установка постоянного пароля | +| `user_update_password_temp` | Установка временного пароля | +| `user_set_is_blocked` | Блокировка/разблокировка | +| `user_login_exists` | Проверка занятости логина | +| `user_email_exists` | Проверка занятости email | + +### cabinet (LEGACY) + +| Функция | Описание | +|---------|----------| +| `cabinet_insert` | Вставка кабинета | +| `cabinet_select` | Выборка с пейджингом | +| `cabinet_update` | Обновление | +| `cabinet_delete` | Мягкое удаление | + +### user_cabinet (LEGACY) + +| Функция | Описание | +|---------|----------| +| `user_cabinet_bulk_insert` | Полная замена привязок | +| `user_cabinet_select` | Кабинеты пользователя | + +## Связи с другими БД + +- **Kit.Auth.Token.Db** (схема `token`) — токены авторизации ссылаются на пользователей по `user_id` \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/_docs/diagrams/er-diagram.md b/projects/pg/kit_auth_pg/_docs/diagrams/er-diagram.md new file mode 100644 index 0000000..17243b0 --- /dev/null +++ b/projects/pg/kit_auth_pg/_docs/diagrams/er-diagram.md @@ -0,0 +1,57 @@ +# ER-диаграмма: auth schema (Kit.Auth.Db) + +```mermaid +erDiagram + %% Таблица пользователей + auth.user { + int id PK "serial" + text outer_user_id "идентификатор во внешней системе" + text login "логин (NOT NULL)" + text password "хеш PBKDF2" + text password_temp "DEPRECATED: временный пароль" + text first_name "имя" + text middle_name "отчество" + text last_name "фамилия" + text email "электронная почта" + text phone "телефон" + bool sex "пол" + datetime date_created "дата создания" + bool is_blocked "флаг блокировки" + bool is_deleted "мягкое удаление" + datetime date_deleted "дата удаления" + } + + %% LEGACY: Кабинет + auth.cabinet { + int id PK "serial" + text title "название кабинета" + text organization_inn "ИНН организации" + text organization_name "название организации" + datetime date_created "дата создания" + bool is_deleted "мягкое удаление" + datetime date_deleted "дата удаления" + } + + %% LEGACY: Связь пользователь ↔ кабинет + auth.user_cabinet { + int user_id FK "→ auth.user.id" + int cabinet_id FK "→ auth.cabinet.id" + } + + %% Связи + auth.user ||--o{ auth.user_cabinet : "имеет кабинеты" + auth.cabinet ||--o{ auth.user_cabinet : "принадлежит пользователям" +``` + +## Описание связей + +| Связь | Тип | Описание | +|-------|-----|----------| +| `auth.user` → `auth.user_cabinet` | 1:M | Пользователь может иметь несколько кабинетов | +| `auth.cabinet` → `auth.user_cabinet` | 1:M | Кабинет может принадлежать нескольким пользователям | + +## Примечания + +- `auth.user_cabinet` — LEGACY-таблица без PK и FK (совместимость со старым кодом) +- `auth.cabinet` — LEGACY-таблица, будет убрана в будущем +- Состояние аутентификации и сессии вынесены в `Kit.Auth.Token.Db` (схема `token`) \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/auth.post-deploy.psql b/projects/pg/kit_auth_pg/auth/auth.post-deploy.psql new file mode 100644 index 0000000..6492e16 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/auth.post-deploy.psql @@ -0,0 +1,41 @@ +-- Стартовые данные схемы auth (правило 03: seed в post-deploy). +-- Демо-пользователи, ВЫРОВНЕННЫЕ по user_id с seed Kit.Partner.Service (cabinet_user.user_id → этот id): +-- 1 портал ОТ5, 2 партнёр ОТ5, 3 партнёр ОТ6, 4–5 админы контрагентов ОТ5, 6–7 админы контрагентов ОТ6. +-- Порядок INSERT задаёт serial id (1..7) — менять порядок нельзя, иначе разъедется связь с cabinet_user. +-- login совпадает с outer_user_id. ВНИМАНИЕ: password — legacy-значения в открытом виде, НЕ формат +-- PBKDF2, войти по паролю нельзя (verify=false). Для проверки логина регистрируйте через POST +-- /auth/register (в dev токен подтверждения возвращается в ответе). Профили нужны для наглядности +-- списков пользователей/зависимостей. +INSERT INTO auth."user"( + outer_user_id, login, password, password_temp, + first_name, middle_name, last_name, email, phone, sex, + date_created, is_blocked, is_deleted, date_deleted) +VALUES + -- 1: администратор портала ОТ5 + ('user_ot5', 'ot5', '', 'ot5_pass', + 'Администратор', 'портала', 'ОТ5', 'admin@ot5.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 2: администратор партнёра ОТ5 + ('p_ot5', 'p_ot5', '', 'p_ot5_pass', + 'Администратор', 'партнёра', 'Kit ОТ5', 'kit_admin@ot5.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 3: администратор партнёра ОТ6 + ('p_ot6', 'p_ot6', '', 'p_ot6_pass', + 'Администратор', 'партнёра', 'Kit ОТ6', 'kit_admin@ot6.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 4: админ контрагента ОТ5-1 + ('c_ot5_1', 'c_ot5_1', '', 'c_ot5_1_pass', + 'Админ', 'контрагента', 'ОТ5 Контрагент 1', 'c1@ot5.app', '44-44-44', true, + '2025-01-01', false, false, null), + -- 5: админ контрагента ОТ5-2 + ('c_ot5_2', 'c_ot5_2', '', 'c_ot5_2_pass', + 'Админ', 'контрагента', 'ОТ5 Контрагент 2', 'c2@ot5.app', '55-55-55', false, + '2025-01-01', false, false, null), + -- 6: админ контрагента ОТ6-1 + ('c_ot6_1', 'c_ot6_1', '', 'c_ot6_1_pass', + 'Админ', 'контрагента', 'ОТ6 Контрагент 1', 'c1@ot6.app', '66-66-66', true, + '2025-01-01', false, false, null), + -- 7: админ контрагента ОТ6-2 + ('c_ot6_2', 'c_ot6_2', '', 'c_ot6_2_pass', + 'Админ', 'контрагента', 'ОТ6 Контрагент 2', 'c2@ot6.app', '77-77-77', false, + '2025-01-01', false, false, null); \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/auth.psql b/projects/pg/kit_auth_pg/auth/auth.psql new file mode 100644 index 0000000..61b2e67 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/auth.psql @@ -0,0 +1,3 @@ +-- Схема домена Auth. Таблицы и функции (CRUD через хранимки) добавляются в +-- tables/ и functions/{таблица}/ и подключаются в init.sh (правила 03–04). +CREATE SCHEMA IF NOT EXISTS auth; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_delete.psql b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_delete.psql new file mode 100644 index 0000000..ab5a019 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_delete.psql @@ -0,0 +1,12 @@ +-- LEGACY (таблица auth.cabinet будет убрана). Мягкое удаление (is_deleted/date_deleted). +CREATE OR REPLACE FUNCTION auth.cabinet_delete(_id integer) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.cabinet + set + is_deleted = true, + date_deleted = current_timestamp at time zone 'UTC' + where auth.cabinet.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_insert.psql b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_insert.psql new file mode 100644 index 0000000..b5c25d6 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_insert.psql @@ -0,0 +1,30 @@ +-- LEGACY (таблица auth.cabinet будет убрана). Вставка кабинета, возврат id. +CREATE OR REPLACE FUNCTION auth.cabinet_insert(_title text, _organization_inn text, _organization_name text, _date_created timestamp with time zone) RETURNS integer + LANGUAGE plpgsql + AS $$ +declare + _record_id int; +begin + insert into auth.cabinet + ( + title, + organization_inn, + organization_name, + date_created, + is_deleted, + date_deleted + ) + values + ( + _title, + _organization_inn, + _organization_name, + _date_created, + false, + NULL + ) + returning id into _record_id; + + return _record_id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_select.psql b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_select.psql new file mode 100644 index 0000000..5d58553 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_select.psql @@ -0,0 +1,68 @@ +-- LEGACY (таблица auth.cabinet будет убрана). Выборка с пейджингом, фильтрами и +-- динамической сортировкой (правило 03). Имена колонок сортировки — camelCase, как ждёт API. +CREATE OR REPLACE FUNCTION auth.cabinet_select(_id integer DEFAULT NULL::integer, _is_deleted boolean DEFAULT NULL::boolean, _title text DEFAULT NULL::text, _organization_inn text DEFAULT NULL::text, _organization_name text DEFAULT NULL::text, _start integer DEFAULT 0, _length integer DEFAULT 9999999, _sort_column text DEFAULT 'id'::text, _sort_dir text DEFAULT 'asc'::text) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +DECLARE + _data_rc refcursor; + _paging_rc refcursor; + _total_rows int := 0; +BEGIN + CREATE TEMP TABLE _ids (id int, no int, filtered_rows int) ON COMMIT DROP; + + WITH t1 AS ( + SELECT + main.id, + ROW_NUMBER() OVER (ORDER BY + CASE WHEN (_sort_column = 'id' AND _sort_dir = 'asc') THEN main.id END ASC, + CASE WHEN (_sort_column = 'id' AND _sort_dir = 'desc') THEN main.id END DESC, + CASE WHEN (_sort_column = 'title' AND _sort_dir = 'asc') THEN main.title END ASC, + CASE WHEN (_sort_column = 'title' AND _sort_dir = 'desc') THEN main.title END DESC, + CASE WHEN (_sort_column = 'organizationInn' AND _sort_dir = 'asc') THEN main.organization_inn END ASC, + CASE WHEN (_sort_column = 'organizationInn' AND _sort_dir = 'desc') THEN main.organization_inn END DESC, + CASE WHEN (_sort_column = 'organizationName' AND _sort_dir = 'asc') THEN main.organization_name END ASC, + CASE WHEN (_sort_column = 'organizationName' AND _sort_dir = 'desc') THEN main.organization_name END DESC + ) AS no + FROM auth.cabinet main + WHERE (_id IS NULL OR main.id = _id) + AND (_title IS NULL OR main.title ILIKE concat('%', _title, '%')) + AND (_organization_inn IS NULL OR main.organization_inn ILIKE concat('%', _organization_inn, '%')) + AND (_organization_name IS NULL OR main.organization_name ILIKE concat('%', _organization_name, '%')) + AND (_is_deleted IS NULL OR main.is_deleted = _is_deleted) + ), + t2 AS ( + SELECT COUNT(*) AS filtered_rows FROM t1 + ), + t3 AS ( + SELECT + t1.id, + t1.no + FROM t1 + ORDER BY t1.no + OFFSET _start LIMIT _length + ) + INSERT INTO _ids (id, no, filtered_rows) + SELECT + t3.id, + t3.no, + t2.filtered_rows + FROM t3 + CROSS JOIN t2; + + -- select cabinets joined with filtered ids + OPEN _data_rc FOR + SELECT + main.* + FROM auth.cabinet main + JOIN _ids ON main.id = _ids.id + ORDER BY _ids.no; + RETURN NEXT _data_rc; + + -- select paging + OPEN _paging_rc FOR + SELECT + (SELECT COUNT(id)::int FROM auth.cabinet WHERE (is_deleted = false OR is_deleted IS NULL)) AS total_rows, + COALESCE((SELECT filtered_rows::int FROM _ids LIMIT 1), 0) AS filtered_rows; + RETURN NEXT _paging_rc; +END; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_update.psql b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_update.psql new file mode 100644 index 0000000..7caecce --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/cabinet/cabinet_update.psql @@ -0,0 +1,14 @@ +-- LEGACY (таблица auth.cabinet будет убрана). Обновление реквизитов кабинета. +CREATE OR REPLACE FUNCTION auth.cabinet_update(_id integer, _title text, _organization_inn text, _organization_name text, _date_created timestamp with time zone) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.cabinet + set + title = _title, + organization_inn = _organization_inn, + organization_name = _organization_name, + date_created = _date_created + where auth.cabinet.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_delete.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_delete.psql new file mode 100644 index 0000000..787153d --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_delete.psql @@ -0,0 +1,15 @@ +-- Мягкое удаление пользователя (is_deleted/date_deleted). Добавлено: вызывается из +-- UserRepository.Delete ("auth.User_delete"), но отсутствовало в исходных init-скриптах. +-- Имя в нижнем регистре — Postgres сворачивает неэкранированный идентификатор к lower-case, +-- поэтому вызов "auth.User_delete" резолвится в auth.user_delete. +CREATE OR REPLACE FUNCTION auth.user_delete(_id integer) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.user + set + is_deleted = true, + date_deleted = current_timestamp at time zone 'UTC' + where auth.user.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_email_exists.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_email_exists.psql new file mode 100644 index 0000000..a3aa782 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_email_exists.psql @@ -0,0 +1,14 @@ +-- Проверка занятости e-mail. +CREATE OR REPLACE FUNCTION auth.user_email_exists(_email text) RETURNS boolean + LANGUAGE plpgsql + AS $$ +begin + + IF EXISTS(SELECT id FROM auth.user where email = _email) THEN + RETURN TRUE; + ELSE + RETURN false; + END IF; + +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_get_by_login.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_get_by_login.psql new file mode 100644 index 0000000..dc20cd1 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_get_by_login.psql @@ -0,0 +1,72 @@ +-- Поиск пользователя по логину и паролю (последовательно: password, password_hash, +-- password_temp, затем password_temp = password_hash). Возвращает курсор пользователя +-- и курсор его кабинетов (LEGACY-связка через auth.user_cabinet). +CREATE OR REPLACE FUNCTION auth.user_get_by_login( +_login text DEFAULT NULL::text, +_password text DEFAULT NULL::text, +_password_hash text DEFAULT NULL::text +) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +declare + _data_rc refcursor; + _cabinet_rc refcursor; +begin + CREATE TEMP TABLE _ids (id int) ON COMMIT DROP; + + INSERT INTO _ids (id) + SELECT main.id + from auth.user main + where main.login = _login + and main.password = _password; + + IF NOT EXISTS(SELECT id FROM _ids) THEN + INSERT INTO _ids (id) + SELECT main.id + from auth.user main + where main.login = _login + and main.password = _password_hash; + END IF; + + IF NOT EXISTS(SELECT id FROM _ids) THEN + INSERT INTO _ids (id) + SELECT main.id + from auth.user main + where main.login = _login + and main.password_temp = _password; + END IF; + + IF NOT EXISTS(SELECT id FROM _ids) THEN + INSERT INTO _ids (id) + SELECT main.id + from auth.user main + where main.login = _login + and main.password_temp = _password_hash; + END IF; + + + OPEN _data_rc FOR + SELECT + main.*, + CONCAT(main.first_name, ' ', main.middle_name, ' ', main.last_name) as user_title + from auth.user main + join _ids on main.id = _ids.id; + + RETURN NEXT _data_rc; + + open _cabinet_rc for + select + main.user_id as user_id, + c.id, + c.title, + c.organization_inn, + c.organization_name, + c.date_created + from auth.user_cabinet main + join _ids on main.user_id = _ids.id + inner join auth.cabinet c ON main.cabinet_id = c.id + where c.is_deleted = false; + + return next _cabinet_rc; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_get_for_auth.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_get_for_auth.psql new file mode 100644 index 0000000..9fa9855 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_get_for_auth.psql @@ -0,0 +1,23 @@ +-- Поиск пользователя для аутентификации по логину ИЛИ email (вход допускается обоими). +-- Пароль здесь НЕ проверяется: хеш PBKDF2 солёный, поэтому сверка пароля выполняется в C# +-- (PasswordHasher.Verify). Возвращает один курсор с полной строкой пользователя, включая +-- password (хеш), is_email_confirmed, is_blocked, access_failed_count, lockout_end. +CREATE OR REPLACE FUNCTION auth.user_get_for_auth( +_login text DEFAULT NULL::text +) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +declare + _data_rc refcursor; +begin + OPEN _data_rc FOR + SELECT + main.*, + CONCAT(main.first_name, ' ', main.middle_name, ' ', main.last_name) as user_title + from auth.user main + where main.is_deleted = false + and (main.login = _login OR main.email = _login); + + RETURN NEXT _data_rc; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_get_profile.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_get_profile.psql new file mode 100644 index 0000000..9ad8e36 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_get_profile.psql @@ -0,0 +1,33 @@ +-- Профиль владельца токена: одна строка пользователя с полями для страницы «Профиль», +-- ВКЛЮЧАЯ password_temp (открытый временный пароль — отдаётся только владельцу токена). +-- Только по id, один курсор, без кабинетов. В отличие от auth.user_select не тянет пейджинг, +-- кабинеты и хеш пароля — специализированное чтение под AuthService.GetProfile. +CREATE OR REPLACE FUNCTION auth.user_get_profile( +_id integer DEFAULT NULL::integer +) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +declare + _data_rc refcursor; +begin + OPEN _data_rc FOR + SELECT + main.id, + main.outer_user_id, + main.login, + main.first_name, + main.last_name, + main.middle_name, + main.email, + main.phone, + main.sex, + main.date_created, + main.is_blocked, + main.password_temp + from auth.user main + where main.is_deleted = false + and main.id = _id; + + RETURN NEXT _data_rc; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_insert.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_insert.psql new file mode 100644 index 0000000..bcfa3fe --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_insert.psql @@ -0,0 +1,46 @@ +-- Вставка пользователя, возврат id. is_blocked/is_deleted при вставке = false. +CREATE OR REPLACE FUNCTION auth.user_insert(_outer_user_id text, _login text, _password text, _password_temp text, _email text,_phone text, _first_name text, _middle_name text, _last_name text, _date_created timestamp with time zone, _sex boolean) RETURNS integer + LANGUAGE plpgsql + AS $$ +declare + _record_id int; +begin + insert into auth.user + ( + outer_user_id, + login, + password, + password_temp, + email, + phone, + first_name, + middle_name, + last_name, + date_created, + is_blocked, + is_deleted, + date_deleted, + sex + ) + values + ( + _outer_user_id, + _login, + _password, + _password_temp, + _email, + _phone, + _first_name, + _middle_name, + _last_name, + _date_created, + false, + false, + NULL, + _sex + ) + returning id into _record_id; + + return _record_id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_login_exists.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_login_exists.psql new file mode 100644 index 0000000..fa6f2bc --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_login_exists.psql @@ -0,0 +1,14 @@ +-- Проверка занятости логина. +CREATE OR REPLACE FUNCTION auth.user_login_exists(_login text) RETURNS boolean + LANGUAGE plpgsql + AS $$ +begin + + IF EXISTS(SELECT id FROM auth.user where login = _login) THEN + RETURN TRUE; + ELSE + RETURN false; + END IF; + +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_select.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_select.psql new file mode 100644 index 0000000..0b53330 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_select.psql @@ -0,0 +1,125 @@ +-- Выборка пользователей: первый курсор — пользователи (+ user_title), второй — их кабинеты +-- (LEGACY-связка через auth.user_cabinet), третий — пейджинг. Имена колонок сортировки — +-- camelCase, как ждёт API. Тело сохранено как в исходных init-скриптах. +CREATE OR REPLACE FUNCTION auth.user_select(_id integer DEFAULT NULL::integer, +_outer_user_id text DEFAULT NULL::text, +_cabinet_id integer DEFAULT NULL::integer, +_login text DEFAULT NULL::text, +_password text DEFAULT NULL::text, +_first_name text DEFAULT NULL::text, +_middle_name text DEFAULT NULL::text, +_last_name text DEFAULT NULL::text, +_email text DEFAULT NULL::text, +_is_blocked boolean DEFAULT NULL::boolean, +_is_deleted boolean DEFAULT NULL::boolean, +_start integer DEFAULT 0, _length integer DEFAULT 9999999, +_sort_column text DEFAULT 'id'::text, + _sort_dir text DEFAULT 'asc'::text) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +declare + _data_rc refcursor; + _cabinet_rc refcursor; + --_roles_rc refcursor; + _paging_rc refcursor; + + _totalRows int := 0; +begin +CREATE TEMP TABLE _ids (id int, no int, filtered_rows int) ON COMMIT DROP; + + WITH t1 AS ( + SELECT + main.id, + ROW_NUMBER() OVER (ORDER BY + CASE WHEN (_sort_column = 'id' AND _sort_dir = 'asc') THEN main.id END ASC, + CASE WHEN (_sort_column = 'id' AND _sort_dir = 'desc') THEN main.id END DESC, + CASE WHEN (_sort_column = 'firstName' AND _sort_dir = 'asc') THEN main.first_name END ASC, + CASE WHEN (_sort_column = 'firstName' AND _sort_dir = 'desc') THEN main.first_name END DESC, + CASE WHEN (_sort_column = 'middleName' AND _sort_dir = 'asc') THEN main.middle_name END ASC, + CASE WHEN (_sort_column = 'middleName' AND _sort_dir = 'desc') THEN main.middle_name END DESC, + CASE WHEN (_sort_column = 'lastName' AND _sort_dir = 'asc') THEN main.last_name END ASC, + CASE WHEN (_sort_column = 'lastName' AND _sort_dir = 'desc') THEN main.last_name END DESC, + CASE WHEN (_sort_column = 'email' AND _sort_dir = 'asc') THEN main.email END ASC, + CASE WHEN (_sort_column = 'email' AND _sort_dir = 'desc') THEN main.email END DESC + ) AS no + from auth.user main + where (_id is null or main.id = _id) + and (_outer_user_id is null or main.outer_user_id = _outer_user_id) + and (_login is null or main.login = _login) + and ((_password is null or main.password_temp = _password or main.password = _password) OR (_password is null or main.password_temp = _password or main.password = _password)) + AND (_first_name IS NULL OR main.first_name ILIKE concat('%', _first_name, '%')) + AND (_middle_name IS NULL OR main.middle_name ILIKE concat('%', _middle_name, '%')) + AND (_last_name IS NULL OR main.last_name ILIKE concat('%', _last_name, '%')) + AND (_email IS NULL OR main.email ILIKE concat('%', _email, '%')) + and (_is_deleted is null or main.is_deleted = _is_deleted) + and (_is_blocked is null or main.is_blocked = _is_blocked) + and (_cabinet_id is null or main.id IN ( + SELECT user_id + FROM auth.user_cabinet + WHERE cabinet_id = _cabinet_id + )) + ), + t2 AS ( + SELECT COUNT(*) AS filtered_rows FROM t1 + ), + t3 AS ( + SELECT + t1.id, + t1.no + FROM t1 + ORDER BY t1.no + OFFSET _start LIMIT _length + ) + INSERT INTO _ids (id, no, filtered_rows) + SELECT + t3.id, + t3.no, + t2.filtered_rows + FROM t3 + CROSS JOIN t2; + + OPEN _data_rc FOR + SELECT + main.*, + CONCAT(main.first_name, ' ', main.middle_name, ' ', main.last_name) as user_title + from auth.user main + join _ids on main.id = _ids.id + ORDER BY _ids.no; + RETURN NEXT _data_rc; + + open _cabinet_rc for + select + main.user_id as user_id, + c.id, + c.title, + c.organization_inn, + c.organization_name, + c.date_created + from auth.user_cabinet main + join _ids on main.user_id = _ids.id + inner join auth.cabinet c ON main.cabinet_id = c.id + where c.is_deleted = false + ; + return next _cabinet_rc; +/* + open _roles_rc for + select + main.cabinet_id as cabinet_id, + main.user_id as user_id, + r.id, + r.title + from auth.user_cabinet_role main + join _ids on main.user_id = _ids.id + inner join auth.user_cabinet uc ON main.cabinet_id = uc.cabinet_id AND main.user_id = uc.user_id + inner join auth.role r ON main.role_id = r.id; + return next _roles_rc; +*/ + -- select paging + OPEN _paging_rc FOR + SELECT + (SELECT COUNT(id)::int from auth.user WHERE (is_deleted = false OR is_deleted IS NULL)) AS total_rows, + COALESCE((SELECT filtered_rows::int FROM _ids LIMIT 1), 0) AS filtered_rows; + RETURN NEXT _paging_rc; + +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_select_by_ids.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_select_by_ids.psql new file mode 100644 index 0000000..ad0aad4 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_select_by_ids.psql @@ -0,0 +1,29 @@ +-- Пользователи по списку id (batch для межсервисного обогащения списков — JoinUserProfiles). +-- Один курсор, только публичные поля профиля (без пароля/кабинетов). Пустой/NULL массив → пусто +-- (id = ANY(NULL) → NULL → строк нет). Только неудалённые. +CREATE OR REPLACE FUNCTION auth.user_select_by_ids(_ids integer[] DEFAULT NULL::integer[]) + RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +DECLARE + _rc refcursor; +BEGIN + OPEN _rc FOR + SELECT + main.id, + main.outer_user_id, + main.login, + main.first_name, + main.last_name, + main.middle_name, + main.email, + main.phone, + main.sex, + main.date_created, + main.is_blocked + FROM auth.user main + WHERE main.is_deleted = false + AND main.id = ANY(_ids); + RETURN NEXT _rc; +END; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_set_is_blocked.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_set_is_blocked.psql new file mode 100644 index 0000000..9f22c24 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_set_is_blocked.psql @@ -0,0 +1,11 @@ +-- Блокировка/разблокировка пользователя. +CREATE OR REPLACE FUNCTION auth.user_set_is_blocked(_id integer, _is_blocked boolean) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.user + set + is_blocked = _is_blocked + where auth.user.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_update.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_update.psql new file mode 100644 index 0000000..be9655a --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_update.psql @@ -0,0 +1,17 @@ +-- Обновление профиля пользователя (без логина/пароля). +CREATE OR REPLACE FUNCTION auth.user_update(_id integer, _outer_user_id text, _email text, _phone text,_first_name text, _middle_name text, _last_name text, _sex boolean) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.user + set + outer_user_id = _outer_user_id, + email = _email, + phone = _phone, + first_name = _first_name, + middle_name = _middle_name, + last_name = _last_name, + sex = _sex + where auth.user.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_update_password.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_update_password.psql new file mode 100644 index 0000000..65716a3 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_update_password.psql @@ -0,0 +1,15 @@ +-- Установка постоянного пароля (сбрасывает password_temp). Только для активного +-- (не удалён, не заблокирован) пользователя. +CREATE OR REPLACE FUNCTION auth.user_update_password(_id integer, _password text) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.user + set + password_temp = null, + password = _password + where auth.user.id = _id + and auth.user.is_deleted = false + and auth.user.is_blocked = false; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user/user_update_password_temp.psql b/projects/pg/kit_auth_pg/auth/functions/user/user_update_password_temp.psql new file mode 100644 index 0000000..70a00fe --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user/user_update_password_temp.psql @@ -0,0 +1,11 @@ +-- Установка временного пароля (password_temp). +CREATE OR REPLACE FUNCTION auth.user_update_password_temp(_id integer, _password_temp text) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + update auth.user + set + password_temp = _password_temp + where auth.user.id = _id; +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_bulk_insert.psql b/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_bulk_insert.psql new file mode 100644 index 0000000..9eabe2c --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_bulk_insert.psql @@ -0,0 +1,14 @@ +-- LEGACY (таблица auth.user_cabinet будет убрана). Полная замена привязок пользователя +-- к кабинетам: удаляет старые и вставляет переданный массив cabinet_ids. +CREATE OR REPLACE FUNCTION auth.user_cabinet_bulk_insert(_user_id integer, _cabinet_ids integer[]) RETURNS void + LANGUAGE plpgsql + AS $$ +begin + -- Удаляем старые привязки к кабинету пользователя (опционально) + DELETE FROM auth.user_cabinet WHERE user_id = _user_id; + + -- Вставляем новые привязки к кабинету + INSERT INTO auth.user_cabinet (user_id, cabinet_id) + values (_user_id, unnest(_cabinet_ids)); +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_select.psql b/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_select.psql new file mode 100644 index 0000000..5001486 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/functions/user_cabinet/user_cabinet_select.psql @@ -0,0 +1,20 @@ +-- LEGACY (таблица auth.user_cabinet будет убрана). Кабинеты пользователя одним курсором. +CREATE OR REPLACE FUNCTION auth.user_cabinet_select(_user_id integer DEFAULT NULL::integer) RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +declare + _data_rc refcursor; +begin + + open _data_rc for + select + c.* + from auth.user_cabinet main + inner join auth.cabinet c ON main.cabinet_id = c.id + inner join auth.user u ON main.user_id = u.id + where main.user_id = _user_id + ; + return next _data_rc; + +end; +$$; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/init/create_schema.psql b/projects/pg/kit_auth_pg/auth/init/create_schema.psql new file mode 100644 index 0000000..719d2b9 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/init/create_schema.psql @@ -0,0 +1,2 @@ +-- Создание схемы auth (выполняется один раз при первом развёртывании). +CREATE SCHEMA IF NOT EXISTS auth; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/scripts/2026-07-08_user-select-by-ids.sql b/projects/pg/kit_auth_pg/auth/scripts/2026-07-08_user-select-by-ids.sql new file mode 100644 index 0000000..3209c76 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/scripts/2026-07-08_user-select-by-ids.sql @@ -0,0 +1,37 @@ +-- Delta-миграция (auth-БД): функция auth.user_select_by_ids — batch-выборка публичных профилей +-- пользователей по списку id. Нужна для межсервисного обогащения списков в потребителях +-- (Kit.Auth.Client.JoinUserProfiles → POST auth/user/by-ids). +-- +-- Идемпотентна: CREATE OR REPLACE. Безопасно запускать повторно; подходит для прода без +-- пересоздания БД. Соответствует init/schemas/auth/functions/user/user_select_by_ids.psql. + +BEGIN; + +CREATE OR REPLACE FUNCTION auth.user_select_by_ids(_ids integer[] DEFAULT NULL::integer[]) + RETURNS SETOF refcursor + LANGUAGE plpgsql + AS $$ +DECLARE + _rc refcursor; +BEGIN + OPEN _rc FOR + SELECT + main.id, + main.outer_user_id, + main.login, + main.first_name, + main.last_name, + main.middle_name, + main.email, + main.phone, + main.sex, + main.date_created, + main.is_blocked + FROM auth.user main + WHERE main.is_deleted = false + AND main.id = ANY(_ids); + RETURN NEXT _rc; +END; +$$; + +COMMIT; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/scripts/2026-07-09_user-seed-merge.sql b/projects/pg/kit_auth_pg/auth/scripts/2026-07-09_user-seed-merge.sql new file mode 100644 index 0000000..cb63674 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/scripts/2026-07-09_user-seed-merge.sql @@ -0,0 +1,78 @@ +-- ============================================================================ +-- Обновление демо-пользователей auth."user" на проде kit-auth2 (МЕРЖ по id). +-- +-- Как применять: открыть в pgAdmin -> Query Tool на подключении к БД kit-auth2 +-- (роль user_auth / user_auth2 — владелец таблицы, либо postgres) и выполнить (F5). +-- +-- Зачем: init.sh отрабатывает только на пустом томе, поэтому правки seed в +-- init/schemas/auth/auth.post-deploy.psql не доезжают до уже инициализированной прод-БД. +-- +-- Что делает: идемпотентный UPSERT по PK id для строк 1..7 (INSERT ... ON CONFLICT +-- (id) DO UPDATE). Строки 1..4 обновляются до нового вида, 5..7 добавляются; любые +-- строки с id вне 1..7 (реальные зарегистрированные пользователи) НЕ трогаются. +-- Можно запускать повторно — результат тот же. TRUNCATE не выполняется. +-- +-- id заданы ЯВНО (1..7) для выравнивания с cabinet_user.user_id в Kit.Partner.Service. +-- Значения синхронны с init/schemas/auth/auth.post-deploy.psql (там id неявный serial). +-- ============================================================================ + +BEGIN; + +INSERT INTO auth."user"( + id, outer_user_id, login, password, password_temp, + first_name, middle_name, last_name, email, phone, sex, + date_created, is_blocked, is_deleted, date_deleted) +VALUES + -- 1: администратор портала ОТ5 + (1, 'user_ot5', 'ot5', '', 'ot5_pass', + 'Администратор', 'портала', 'ОТ5', 'admin@ot5.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 2: администратор партнёра ОТ5 + (2, 'p_ot5', 'p_ot5', '', 'p_ot5_pass', + 'Администратор', 'партнёра', 'Kit ОТ5', 'kit_admin@ot5.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 3: администратор партнёра ОТ6 + (3, 'p_ot6', 'p_ot6', '', 'p_ot6_pass', + 'Администратор', 'партнёра', 'Kit ОТ6', 'kit_admin@ot6.app', '11-11-11', true, + '2025-01-01', false, false, null), + -- 4: админ контрагента ОТ5-1 + (4, 'c_ot5_1', 'c_ot5_1', '', 'c_ot5_1_pass', + 'Админ', 'контрагента', 'ОТ5 Контрагент 1', 'c1@ot5.app', '44-44-44', true, + '2025-01-01', false, false, null), + -- 5: админ контрагента ОТ5-2 + (5, 'c_ot5_2', 'c_ot5_2', '', 'c_ot5_2_pass', + 'Админ', 'контрагента', 'ОТ5 Контрагент 2', 'c2@ot5.app', '55-55-55', false, + '2025-01-01', false, false, null), + -- 6: админ контрагента ОТ6-1 + (6, 'c_ot6_1', 'c_ot6_1', '', 'c_ot6_1_pass', + 'Админ', 'контрагента', 'ОТ6 Контрагент 1', 'c1@ot6.app', '66-66-66', true, + '2025-01-01', false, false, null), + -- 7: админ контрагента ОТ6-2 + (7, 'c_ot6_2', 'c_ot6_2', '', 'c_ot6_2_pass', + 'Админ', 'контрагента', 'ОТ6 Контрагент 2', 'c2@ot6.app', '77-77-77', false, + '2025-01-01', false, false, null) +ON CONFLICT (id) DO UPDATE SET + outer_user_id = EXCLUDED.outer_user_id, + login = EXCLUDED.login, + password = EXCLUDED.password, + password_temp = EXCLUDED.password_temp, + first_name = EXCLUDED.first_name, + middle_name = EXCLUDED.middle_name, + last_name = EXCLUDED.last_name, + email = EXCLUDED.email, + phone = EXCLUDED.phone, + sex = EXCLUDED.sex, + date_created = EXCLUDED.date_created, + is_blocked = EXCLUDED.is_blocked, + is_deleted = EXCLUDED.is_deleted, + date_deleted = EXCLUDED.date_deleted; + +-- Досбросить serial, чтобы будущий INSERT без id (напр. /auth/register) не наткнулся +-- на занятые id 1..7. +SELECT setval(pg_get_serial_sequence('auth."user"', 'id'), + (SELECT COALESCE(MAX(id), 1) FROM auth."user")); + +COMMIT; + +-- Проверка результата (выполнить отдельно после COMMIT при необходимости): +-- SELECT id, outer_user_id, login, email, is_blocked FROM auth."user" ORDER BY id; \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/tables/cabinet.psql b/projects/pg/kit_auth_pg/auth/tables/cabinet.psql new file mode 100644 index 0000000..42445a3 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/tables/cabinet.psql @@ -0,0 +1,13 @@ +-- LEGACY (оставлена для совместимости, в дальнейшем будет убрана). +-- Кабинет: обычная таблица (entity) с мягким удалением (is_deleted/date_deleted) — правило 03. +-- id serial PK; organization_inn/organization_name — необязательные реквизиты организации. +CREATE TABLE auth.cabinet +( + id serial not null primary key, + title text not null, + organization_inn text null, + organization_name text null, + date_created timestamptz not null, + is_deleted boolean not null default false, + date_deleted timestamptz null +); \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/tables/user.psql b/projects/pg/kit_auth_pg/auth/tables/user.psql new file mode 100644 index 0000000..045455c --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/tables/user.psql @@ -0,0 +1,26 @@ +-- Пользователь: обычная таблица (entity) с мягким удалением и флагом блокировки. +-- "user" — зарезервированное слово PostgreSQL, поэтому имя экранируется кавычками. +-- password — закодированный хеш пароля в формате PBKDF2 (см. PasswordHasher: "pbkdf2-sha256$iter$salt$hash"). +-- password_temp — DEPRECATED: временный пароль не используется (восстановление пароля идёт через +-- токен-БД token.verification_token), колонка оставлена для совместимости со старыми данными. +-- outer_user_id — идентификатор во внешней системе. +-- ПРИМ.: состояние аутентификации (подтверждение email, lockout, счётчик неудач) и сессии +-- вынесены в отдельную токен-БД (проект Kit.Auth.Token.Db) — эта таблица намеренно не изменена. +CREATE TABLE auth."user" +( + id serial not null primary key, + outer_user_id text null, + login text not null, + password text null, + password_temp text null, + first_name text not null, + middle_name text not null, + last_name text not null, + email text null, + phone text null, + sex boolean null, + date_created timestamptz not null, + is_blocked boolean not null default false, + is_deleted boolean not null default false, + date_deleted timestamptz null +); \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/auth/tables/user_cabinet.psql b/projects/pg/kit_auth_pg/auth/tables/user_cabinet.psql new file mode 100644 index 0000000..3ec0bc4 --- /dev/null +++ b/projects/pg/kit_auth_pg/auth/tables/user_cabinet.psql @@ -0,0 +1,10 @@ +-- LEGACY (оставлена для совместимости, в дальнейшем будет убрана). +-- Соединительная таблица «пользователь ↔ кабинет». Структура сохранена как в исходных +-- init-скриптах: без первичного ключа и внешних ключей — чтобы не менять поведение +-- существующего кода (auth.user_cabinet_bulk_insert / auth.user_cabinet_select). +-- Конвенциям junction (правило 03: serial id / FK) намеренно НЕ приводится — это legacy. +CREATE TABLE auth.user_cabinet +( + user_id integer not null, + cabinet_id integer not null +); \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/apply-psql.sh b/projects/pg/kit_auth_pg/deploy/apply-psql.sh new file mode 100644 index 0000000..31feffe --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/apply-psql.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Точечное применение одного .psql (идемпотентный CREATE OR REPLACE FUNCTION и т.п.) +# к работающей БД auth — БЕЗ полной замены схемы (в отличие от sync-to-prod.sh). +# Для «хирургических» изменений: добавить/переопределить функцию, не трогая данные. +# +# psql берётся из docker (локальный клиент не нужен). +# +# Использование: +# ./apply-psql.sh auth/functions/user/user_get_profile.psql # локальная БД +# TARGET=prod PROD_APP_PASS=... ./apply-psql.sh <файл.psql> # прод +# +# Локально идём в контейнер kit-auth-db напрямую (docker exec); на прод — по host-адресу. +# Любой параметр ниже переопределяется одноимённой env-переменной. + +set -euo pipefail + +FILE="${1:?укажи путь к .psql (например auth/functions/user/user_get_profile.psql)}" +[ -f "$FILE" ] || { echo "ОШИБКА: нет файла: $FILE" >&2; exit 1; } +command -v docker >/dev/null || { echo "ОШИБКА: нужен docker" >&2; exit 1; } + +TARGET="${TARGET:-local}" +PG_IMAGE="${PG_IMAGE:-postgres:17-alpine}" + +if [ "$TARGET" = "local" ]; then + # локальная БД — прямо в контейнере + LOCAL_CONTAINER="${LOCAL_CONTAINER:-kit-auth-db}" + LOCAL_DB="${LOCAL_DB:-kit-auth2}" + LOCAL_USER="${LOCAL_USER:-user_auth}" + echo "==> Применяю $(basename "$FILE") -> контейнер $LOCAL_CONTAINER, БД $LOCAL_DB (пользователь $LOCAL_USER)" + docker exec -i "$LOCAL_CONTAINER" \ + psql -v ON_ERROR_STOP=1 -U "$LOCAL_USER" -d "$LOCAL_DB" < "$FILE" +else + # прод — по host-адресу, прикладным пользователем (он же владелец объектов) + PROD_HOST="${PROD_HOST:-176.99.5.31}" + PROD_PORT="${PROD_PORT:-5832}" + PROD_DB="${PROD_DB:-kit-auth2}" + PROD_APP_USER="${PROD_APP_USER:-user_auth}" + PROD_APP_PASS="${PROD_APP_PASS:?нужен PROD_APP_PASS (пароль прикладного пользователя прода)}" + echo "==> Применяю $(basename "$FILE") -> $PROD_APP_USER@$PROD_HOST:$PROD_PORT/$PROD_DB (prod)" + docker run --rm -i -e PGPASSWORD="$PROD_APP_PASS" "$PG_IMAGE" \ + psql -v ON_ERROR_STOP=1 -h "$PROD_HOST" -p "$PROD_PORT" -U "$PROD_APP_USER" -d "$PROD_DB" < "$FILE" +fi + +echo "==> Готово." \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/docker-compose.yml b/projects/pg/kit_auth_pg/deploy/docker-compose.yml new file mode 100644 index 0000000..0a9b6fe --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/docker-compose.yml @@ -0,0 +1,25 @@ +name: kit-auth-db +networks: + default: + external: true + name: sdi_server +services: + sdi-auth-db: + image: postgres + restart: always + ports: + - 5479:5432 + container_name: kit-auth-db + environment: + POSTGRES_DB: kit-auth2 + POSTGRES_USER: user_auth + POSTGRES_PASSWORD: f0e87ce7-b857-4bbc-907c-ff0ab90514ad + PGDATA: "/data" + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - ./pg_data:/var/lib/postgresql/data + - ./init:/docker-entrypoint-initdb.d \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/down.bat b/projects/pg/kit_auth_pg/deploy/down.bat new file mode 100644 index 0000000..da11f24 --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/down.bat @@ -0,0 +1,36 @@ +@echo off +setlocal + +set reset_data=0 + +if "%~1"=="" ( + goto end +) + +:checkArguments +if "%~1"=="" ( + goto end +) +if "%~1"=="--reset-data" ( + set reset_data=1 +) +if "%~1"=="-r" ( + set reset_data=1 +) +shift +goto checkArguments + +:end + +@echo on +docker compose down + +@echo off +if "%reset_data%"=="1" ( + rmdir /s /q .\pg_data + echo. + echo volume removed + echo. +) + +endlocal \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/export.bat b/projects/pg/kit_auth_pg/deploy/export.bat new file mode 100644 index 0000000..cedd8ad --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/export.bat @@ -0,0 +1,13 @@ +@echo off +setlocal + +REM Снимок работающего контейнера БД в образ и сохранение в tar для переноса на сервер. +docker commit sdi-auth-db sdi-auth-db +docker save -o sdi-auth-db.tar sdi-auth-db:latest + +REM На сервере (см. import.bat): +REM sudo docker rmi sdi-auth-db +REM sudo docker load -i sdi-auth-db.tar +REM sudo docker compose up -d + +endlocal \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/import.bat b/projects/pg/kit_auth_pg/deploy/import.bat new file mode 100644 index 0000000..f29f067 --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/import.bat @@ -0,0 +1,9 @@ +@echo off +setlocal + +REM Загрузка перенесённого образа БД на сервере и подъём контейнера. +sudo docker rmi sdi-auth-db +sudo docker load -i sdi-auth-db.tar +sudo docker compose up -d + +endlocal \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/init.sh b/projects/pg/kit_auth_pg/deploy/init.sh new file mode 100644 index 0000000..4081127 --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/init.sh @@ -0,0 +1,60 @@ +file_publish="/docker-entrypoint-initdb.d/scripts/publish.psql" +files=( +########################################################################## +# schemas +"/docker-entrypoint-initdb.d/schemas/auth/auth.psql" + +########################################################################## +# tables +# cabinet и user — раньше user_cabinet (на них ссылаются user_id / cabinet_id) +"/docker-entrypoint-initdb.d/schemas/auth/tables/cabinet.psql" +"/docker-entrypoint-initdb.d/schemas/auth/tables/user.psql" +"/docker-entrypoint-initdb.d/schemas/auth/tables/user_cabinet.psql" + +########################################################################## +# functions +# cabinet +"/docker-entrypoint-initdb.d/schemas/auth/functions/cabinet/cabinet_insert.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/cabinet/cabinet_update.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/cabinet/cabinet_delete.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/cabinet/cabinet_select.psql" +# user +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_insert.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_select.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_select_by_ids.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_get_by_login.psql" +# user_get_for_auth — единственная новая функция в схеме auth: поиск по login/email без сверки +# пароля (PBKDF2 сверяется в C#); сама таблица auth.user намеренно не изменена. +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_get_for_auth.psql" +# user_get_profile — специализированное чтение профиля владельца токена (+ password_temp) +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_get_profile.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_delete.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_set_is_blocked.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_update.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_update_password.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_update_password_temp.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_login_exists.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user/user_email_exists.psql" +# user_cabinet +"/docker-entrypoint-initdb.d/schemas/auth/functions/user_cabinet/user_cabinet_bulk_insert.psql" +"/docker-entrypoint-initdb.d/schemas/auth/functions/user_cabinet/user_cabinet_select.psql" + +########################################################################## +# post-deploy (seed) +"/docker-entrypoint-initdb.d/schemas/auth/auth.post-deploy.psql" + +) + +> ${file_publish} + +# ВАЖНО: явно устанавливаем UTF-8 перед вставкой seed-данных с кириллицей. +# Без этого psql может использовать системную кодировку клиента (LC_CTYPE), +# что приводит к двойному кодированию UTF-8 → Latin-1/CP1251 → UTF-8. +echo "SET client_encoding TO 'UTF8';" >> ${file_publish} +printf "\n\n" >> ${file_publish} + +for item in "${files[@]}"; do + cat "${item}" >> "${file_publish}" && printf "\n\n\n" >> "${file_publish}" +done +# run publish.psql (база auth — POSTGRES_DB) +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "${file_publish}" \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/restart.bat b/projects/pg/kit_auth_pg/deploy/restart.bat new file mode 100644 index 0000000..78c6a87 --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/restart.bat @@ -0,0 +1,7 @@ +@echo off +setlocal + +CALL "down.bat" %* +CALL "up.bat" + +endlocal \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/deploy/up.bat b/projects/pg/kit_auth_pg/deploy/up.bat new file mode 100644 index 0000000..079c3d8 --- /dev/null +++ b/projects/pg/kit_auth_pg/deploy/up.bat @@ -0,0 +1,8 @@ +@echo off +setlocal + +docker network create -d bridge sdi_server + +docker compose up -d + +endlocal \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/readme.md b/projects/pg/kit_auth_pg/readme.md new file mode 100644 index 0000000..457ce0d --- /dev/null +++ b/projects/pg/kit_auth_pg/readme.md @@ -0,0 +1,40 @@ +# Kit.Auth.Db + +## Назначение + +База данных авторизации. Хранит пользователей, кабинеты (LEGACY) и связи пользователей с кабинетами (LEGACY). CRUD-операции выполняются через хранимые функции. + +## Движок + +PostgreSQL + +## Схема + +`auth` + +## Модули + +### auth + +Основной модуль. Содержит таблицы и функции для работы с авторизацией. + +**Таблицы:** +- `user` — пользователи (мягкое удаление, блокировка) +- `cabinet` — кабинеты (LEGACY, будет убрана) +- `user_cabinet` — связь пользователей с кабинетами (LEGACY, будет убрана) + +**Функции (user):** +- CRUD: user_insert, user_select, user_select_by_ids, user_get_by_login, user_get_for_auth, user_get_profile, user_delete, user_update +- Управление паролями: user_update_password, user_update_password_temp +- Блокировка: user_set_is_blocked +- Проверка: user_login_exists, user_email_exists + +**Функции (cabinet):** +- CRUD: cabinet_insert, cabinet_select, cabinet_update, cabinet_delete + +**Функции (user_cabinet):** +- user_cabinet_bulk_insert, user_cabinet_select + +## Связи + +- Связана с `Kit.Auth.Token.Db` (схема `token`) — токены авторизации ссылаются на пользователей \ No newline at end of file diff --git a/projects/pg/kit_auth_pg/settings/connection.json b/projects/pg/kit_auth_pg/settings/connection.json new file mode 100644 index 0000000..ffee206 --- /dev/null +++ b/projects/pg/kit_auth_pg/settings/connection.json @@ -0,0 +1,7 @@ +{ + "host": "localhost", + "port": 5432, + "database": "kit-auth2", + "username": "postgres", + "password": "" +} \ No newline at end of file diff --git a/rules/pg/db-project-rule.md b/rules/pg/db-project-rule.md index 21e477e..277f45d 100644 --- a/rules/pg/db-project-rule.md +++ b/rules/pg/db-project-rule.md @@ -74,6 +74,20 @@ projects/pg/{project_name}/ - Путь: `{schema}/{schema}.post-deploy.psql` - Скрипты, выполняемые после деплоя (seed-данные, обновление зависимостей) +### 9. Диаграммы + +- Путь: `_docs/diagrams/` +- Формат: Mermaid `.md` файлы +- Обязательные диаграммы: + - `er-diagram.md` — ER-диаграмма всех таблиц схемы (связи, типы, ключи) +- Правила формирования: + - Каждая таблица — блок `erDiagram` с перечислением полей и типов + - Связи между таблицами отображаются через `||--o{`, `|--|{` и т.д. + - Внешние ключи помечаются комментариями + - LEGACY-таблицы помечаются `%% LEGACY` в комментарии + - Имена таблиц: `schema.table` (например `auth.user`) + - Типы данных: PostgreSQL → Mermaid (serial→int, text→string, timestamptz→datetime, boolean→bool, integer[]→int[]) + --- ## Пример: проект kit_auth_pg diff --git a/rules/sqlite/db-project-rule.md b/rules/sqlite/db-project-rule.md index dbf0b2d..c462267 100644 --- a/rules/sqlite/db-project-rule.md +++ b/rules/sqlite/db-project-rule.md @@ -63,6 +63,19 @@ SQLite не поддерживает схемы, поэтому модуль = - Точка входа — подключает все таблицы и скрипты модуля - Порядок: сначала таблицы, потом скрипты +### 7. Диаграммы + +- Путь: `_docs/diagrams/` +- Формат: Mermaid `.md` файлы +- Обязательные диаграммы: + - `er-diagram.md` — ER-диаграмма всех таблиц модуля (связи, типы, ключи) +- Правила формирования: + - Каждая таблица — блок `erDiagram` с перечислением полей и типов + - Связи между таблицами отображаются через `||--o{`, `|--|{` и т.д. + - Внешние ключи помечаются комментариями + - LEGACY-таблицы помечаются `%% LEGACY` в комментарии + - Типы данных: SQLite → Mermaid (INTEGER→int, TEXT→string, REAL→real, BLOB→blob) + --- ## Особенности SQLite