feat: add kit_auth_pg project from Kit.Auth.Db + ER diagrams + diagram rules
This commit is contained in:
parent
a306e27a18
commit
0d839a4e37
|
|
@ -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`
|
||||||
|
|
@ -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`)
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Схема домена Auth. Таблицы и функции (CRUD через хранимки) добавляются в
|
||||||
|
-- tables/ и functions/{таблица}/ и подключаются в init.sh (правила 03–04).
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Создание схемы auth (выполняется один раз при первом развёртывании).
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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 "==> Готово."
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
CALL "down.bat" %*
|
||||||
|
CALL "up.bat"
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
docker network create -d bridge sdi_server
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
|
@ -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`) — токены авторизации ссылаются на пользователей
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "kit-auth2",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,20 @@ projects/pg/{project_name}/
|
||||||
- Путь: `{schema}/{schema}.post-deploy.psql`
|
- Путь: `{schema}/{schema}.post-deploy.psql`
|
||||||
- Скрипты, выполняемые после деплоя (seed-данные, обновление зависимостей)
|
- Скрипты, выполняемые после деплоя (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
|
## Пример: проект kit_auth_pg
|
||||||
|
|
|
||||||
|
|
@ -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
|
## Особенности SQLite
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue