From a306e27a18bc2db05f705d907e81a111adadba0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=BA=D0=BE=D0=B2=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D1=85=20=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9?= Date: Thu, 30 Jul 2026 18:45:43 +0500 Subject: [PATCH] first commit --- Kit.Db.slnx | 7 + docs/architecture.md | 26 ++ docs/projects/kit-auth-token-db.md | 100 +++++ docs/structure-diagram.md | 103 +++++ docs/tz.md | 198 +++++++++ .../deploy/docker-compose.yml | 25 ++ projects/pg/kit_auth_token_pg/deploy/down.bat | 36 ++ projects/pg/kit_auth_token_pg/deploy/init.sh | 42 ++ .../pg/kit_auth_token_pg/deploy/restart.bat | 7 + projects/pg/kit_auth_token_pg/deploy/up.bat | 7 + .../functions/owner/owner_insert.psql | 22 + .../functions/owner/owner_select.psql | 23 ++ .../functions/token/token_consume.psql | 37 ++ .../functions/token/token_delete.psql | 9 + .../functions/token/token_insert.psql | 28 ++ .../functions/token/token_revoke.psql | 23 ++ .../token/token_revoke_by_owner.psql | 23 ++ .../functions/token/token_select.psql | 28 ++ .../functions/token/token_update.psql | 31 ++ .../functions/token/token_validate.psql | 28 ++ .../token_type/token_type_select.psql | 15 + projects/pg/kit_auth_token_pg/readme.md | 32 ++ .../scripts/create_full_20260723_124551.sql | 382 ++++++++++++++++++ .../settings/connection.json | 7 + .../pg/kit_auth_token_pg/tables/owner.psql | 10 + .../pg/kit_auth_token_pg/tables/token.psql | 28 ++ .../kit_auth_token_pg/tables/token_type.psql | 9 + .../kit_auth_token_pg/token.post-deploy.psql | 16 + projects/pg/kit_auth_token_pg/token.psql | 4 + rules/pg/db-project-rule.md | 138 +++++++ rules/sqlite/db-project-rule.md | 127 ++++++ Базы данных OT5.zip | Bin 0 -> 15154 bytes 32 files changed, 1571 insertions(+) create mode 100644 Kit.Db.slnx create mode 100644 docs/architecture.md create mode 100644 docs/projects/kit-auth-token-db.md create mode 100644 docs/structure-diagram.md create mode 100644 docs/tz.md create mode 100644 projects/pg/kit_auth_token_pg/deploy/docker-compose.yml create mode 100644 projects/pg/kit_auth_token_pg/deploy/down.bat create mode 100644 projects/pg/kit_auth_token_pg/deploy/init.sh create mode 100644 projects/pg/kit_auth_token_pg/deploy/restart.bat create mode 100644 projects/pg/kit_auth_token_pg/deploy/up.bat create mode 100644 projects/pg/kit_auth_token_pg/functions/owner/owner_insert.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/owner/owner_select.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_consume.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_delete.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_insert.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_revoke.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_revoke_by_owner.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_select.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_update.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token/token_validate.psql create mode 100644 projects/pg/kit_auth_token_pg/functions/token_type/token_type_select.psql create mode 100644 projects/pg/kit_auth_token_pg/readme.md create mode 100644 projects/pg/kit_auth_token_pg/scripts/create_full_20260723_124551.sql create mode 100644 projects/pg/kit_auth_token_pg/settings/connection.json create mode 100644 projects/pg/kit_auth_token_pg/tables/owner.psql create mode 100644 projects/pg/kit_auth_token_pg/tables/token.psql create mode 100644 projects/pg/kit_auth_token_pg/tables/token_type.psql create mode 100644 projects/pg/kit_auth_token_pg/token.post-deploy.psql create mode 100644 projects/pg/kit_auth_token_pg/token.psql create mode 100644 rules/pg/db-project-rule.md create mode 100644 rules/sqlite/db-project-rule.md create mode 100644 Базы данных OT5.zip diff --git a/Kit.Db.slnx b/Kit.Db.slnx new file mode 100644 index 0000000..f3b27c1 --- /dev/null +++ b/Kit.Db.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d5d2a49 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,26 @@ +# Kit.Db - Structure + +Файловая струтура + +- rules/ + - common/ + - pg/ + - sqlite/ +- skills/ +- projects/ + - pg/ + - kit_auth_pg/ + - README.md + - settings/ + - connection.json + - auth/ + - tables/ + - functions/ + - scripts/ + - init/ + - deploy/ + - auth.psql + - _docs/ + - diagrams/ + - openapi.yaml + - architecture.md \ No newline at end of file diff --git a/docs/projects/kit-auth-token-db.md b/docs/projects/kit-auth-token-db.md new file mode 100644 index 0000000..995f4d0 --- /dev/null +++ b/docs/projects/kit-auth-token-db.md @@ -0,0 +1,100 @@ +# Kit.Auth.Token.Db — Описание БД + +## Общее описание + +База данных для управления токенами авторизации. Хранит владельцев токенов, типы токенов и сами токены с возможностью валидации, отзыва и удаления. + +- **Движок:** PostgreSQL +- **Схема:** `token` + +--- + +## Структура проекта (по шаблону) + +``` +projects/pg/kit_auth_token_pg/ + readme.md + settings/ + connection.json + token/ + tables/ + owner.psql + token_type.psql + token.psql + functions/ + owner/ + owner_insert.psql + owner_select.psql + token/ + token_consume.psql + token_delete.psql + token_insert.psql + token_revoke_by_owner.psql + token_revoke.psql + token_select.psql + token_update.psql + token_validate.psql + token_type/ + token_type_select.psql + scripts/ + init/ + deploy/ + token.psql # Точка входа модуля + token.post-deploy.psql # Post-deploy скрипты + _docs/ + diagrams/ + openapi.yaml + architecture.md +``` + +--- + +## Таблицы + +### owner +Владельцы токенов (пользователи/системы). + +### token_type +Справочник типов токенов (access, refresh и т.д.). + +### token +Токены авторизации с привязкой к владельцу и типу. + +--- + +## Функции + +### owner/ +| Функция | Назначение | +|---------|-----------| +| `owner_insert` | Создание владельца | +| owner_select` | Получение владельца | + +### token/ +| Функция | Назначение | +|---------|-----------| +| `token_insert` | Создание токена | +| `token_select` | Получение токена | +| `token_update` | Обновление токена | +| `token_delete` | Удаление токена | +| `token_consume` | Использование токена | +| `token_validate` | Валидация токена | +| `token_revoke` | Отзыв токена | +| `token_revoke_by_owner` | Отзыв всех токенов владельца | + +### token_type/ +| Функция | Назначение | +|---------|-----------| +| `token_type_select` | Получение типа токена | + +--- + +## Миграции + +- `2026-07-23` — Полная схема (create_full) + +--- + +## Связи с другими БД + +- Связана с `Kit.Auth.Db` (модуль `auth`) — владельцы токенов могут быть пользователями из auth.users \ No newline at end of file diff --git a/docs/structure-diagram.md b/docs/structure-diagram.md new file mode 100644 index 0000000..9df6580 --- /dev/null +++ b/docs/structure-diagram.md @@ -0,0 +1,103 @@ +# Kit.Db — Структура проекта (ASCII) + +``` +Kit.Db/ +│ +├── rules/ # Правила для LLM по движкам +│ ├── common/ # Общие правила для всех СУБД +│ │ └── ... +│ ├── pg/ # Правила PostgreSQL +│ │ └── db-project-rule.md +│ └── sqlite/ # Правила SQLite +│ └── db-project-rule.md +│ +├── skills/ # LLM промпты (описания таблиц, функций) +│ └── ... +│ +└── projects/ # Проекты БД + │ + ├── pg/ # ── PostgreSQL проекты ── + │ │ + │ └── kit_auth_pg/ # Проект: БД авторизации + │ │ + │ ├── readme.md # Описание БД + │ │ + │ ├── settings/ # Настройки подключения + │ │ └── connection.json + │ │ + │ ├── auth/ # Модуль: авторизация + │ │ ├── tables/ # DDL таблиц + │ │ │ ├── user.psql + │ │ │ ├── cabinet.psql + │ │ │ └── user_cabinet.psql + │ │ │ + │ │ ├── functions/ # SQL функции + │ │ │ ├── user_select_by_ids/ + │ │ │ │ └── user_select_by_ids.psql + │ │ │ └── user_create/ + │ │ │ └── user_create.psql + │ │ │ + │ │ ├── scripts/ # Произвольные скрипты + │ │ │ └── seed_roles.sql + │ │ │ + │ │ ├── init/ # Инициализация + │ │ │ └── create_schema.psql + │ │ │ + │ │ ├── deploy/ # Миграции + │ │ │ ├── 2026-07-08_user-select-by-ids.sql + │ │ │ └── 2026-07-09-user-seed-merge.sql + │ │ │ + │ │ ├── auth.psql # Точка входа модуля + │ │ └── auth.post-deploy.psql + │ │ + │ └── _docs/ # Документация проекта + │ ├── diagrams/ + │ ├── openapi.yaml + │ └── architecture.md + │ + └── sl3/ # ── SQLite проекты ── + │ + └── kit_example_sl3/ # Проект: пример + │ + ├── readme.md + ├── settings/ + │ └── connection.json + │ + ├── main/ # Модуль: основной + │ ├── tables/ + │ ├── scripts/ + │ ├── init/ + │ ├── deploy/ + │ └── main.sql + │ + └── _docs/ + ├── diagrams/ + ├── openapi.yaml + └── architecture.md +``` + +--- + +## Связи между уровнями + +``` +rules/ ──────────► LLM использует при формировании проектов + common/ │ + pg/ │ + sqlite/ ▼ + projects/ + pg/ ─── PostgreSQL проекты + sl3/ ── SQLite проекты + +skills/ ─────────► LLM использует при генерации описаний + +projects/{engine}/{project}/ + readme.md ──────────► Описание БД + settings/ ──────────► Подключение + {module}/ ──────────► SQL исходники + tables/ ──────────► DDL таблиц + functions/ ───────► SQL функции (только PG) + scripts/ ─────────► Произвольные SQL + init/ ────────────► Инициализация + deploy/ ──────────► Миграции + _docs/ ─────────────► Документация \ No newline at end of file diff --git a/docs/tz.md b/docs/tz.md new file mode 100644 index 0000000..0965fba --- /dev/null +++ b/docs/tz.md @@ -0,0 +1,198 @@ +# ТЗ — Формирование проекта для БД + +## Общее описание + +На основании файловой структуры (см. architecture.md) формируется проект для конкретной базы данных. Каждый проект — автономная единица с собственными настройками, исходниками и документацией. + +--- + +## Шаблон структуры + +``` +Kit.Db/ +│ +├── rules/ # Правила для LLM по движкам +│ ├── common/ # Общие правила для всех СУБД +│ │ └── ... +│ ├── pg/ # Правила PostgreSQL +│ │ └── db-project-rule.md +│ └── sqlite/ # Правила SQLite +│ └── db-project-rule.md +│ +├── skills/ # LLM промпты (описания таблиц, функций) +│ └── ... +│ +└── projects/ # Проекты БД + │ + ├── pg/ # ── PostgreSQL проекты ── + │ │ + │ └── kit_auth_pg/ # Проект: БД авторизации + │ │ + │ ├── readme.md # Описание БД + │ │ + │ ├── settings/ # Настройки подключения + │ │ └── connection.json + │ │ + │ ├── auth/ # Модуль: авторизация + │ │ ├── tables/ # DDL таблиц + │ │ │ ├── user.psql + │ │ │ ├── cabinet.psql + │ │ │ └── user_cabinet.psql + │ │ │ + │ │ ├── functions/ # SQL функции + │ │ │ ├── user_select_by_ids/ + │ │ │ │ └── user_select_by_ids.psql + │ │ │ └── user_create/ + │ │ │ └── user_create.psql + │ │ │ + │ │ ├── scripts/ # Произвольные скрипты + │ │ │ └── seed_roles.sql + │ │ │ + │ │ ├── init/ # Инициализация + │ │ │ └── create_schema.psql + │ │ │ + │ │ ├── deploy/ # Миграции + │ │ │ ├── 2026-07-08_user-select-by-ids.sql + │ │ │ └── 2026-07-09-user-seed-merge.sql + │ │ │ + │ │ ├── auth.psql # Точка входа модуля + │ │ └── auth.post-deploy.psql + │ │ + │ └── _docs/ # Документация проекта + │ ├── diagrams/ + │ ├── openapi.yaml + │ └── architecture.md + │ + └── sl3/ # ── SQLite проекты ── + │ + └── kit_example_sl3/ # Проект: пример + │ + ├── readme.md + ├── settings/ + │ └── connection.json + │ + ├── main/ # Модуль: основной + │ ├── tables/ + │ ├── scripts/ + │ ├── init/ + │ ├── deploy/ + │ └── main.sql + │ + └── _docs/ + ├── diagrams/ + ├── openapi.yaml + └── architecture.md +``` + +--- + +## Структура проекта + +Каждый проект БД создаётся по пути: + +``` +projects/{engine}/{project_name}/ +``` + +Где: + +- `{engine}` — тип СУБД (pg, sqlite, mysql, mssql и т.д.) +- `{project_name}` — имя проекта в формате `имя_БД_движок` (например `kit_auth_pg`) + +--- + +## Обязательные компоненты проекта + +### 1. readme.md + +Описание проекта: + +- Назначение БД +- Движок и версия +- Краткое описание модулей +- Связи с другими БД (если есть) + +### 2. settings/ + +Папка с настройками подключения: + +- `connection.json` — параметры подключения к БД (хост, порт, БД, пользователь) + +Пример `connection.json`: + +```json +{ + "host": "localhost", + "port": 5432, + "database": "kit_auth", + "username": "postgres", + "password": "" +} +``` + +### 3. Модули (auth/, payments/, и т.д.) + +Каждый модуль содержит: + +- `tables/` — DDL-скрипты таблиц +- `functions/` — SQL-функции и хранимые процедуры +- `scripts/` — SQL-скрипты (миграции, seed-данные, утилиты) +- `init/` — скрипты инициализации (создание схемы, начальные данные) +- `deploy/` — инфраструктура развёртывания (docker-compose, bat, sh) +- `{module}.psql` — главный файл модуля (точка входа) + +### 4. _docs/ + +Документация проекта: + +- `diagrams/` — диаграммы (ER, зависимости, потоки) +- `openapi.yaml` — OpenAPI спецификация (если БД предоставляет API) +- `architecture.md` — описание архитектуры конкретной БД + +--- + +## Правила именования + +- Папки — в нижнем регистре, через подчёркивание: `kit_auth_pg` +- SQL-файлы — в нижнем регистре: `create_users_table.sql` +- Функции — по шаблону: `{module}_{action}.sql` (например `auth_get_user_by_id.sql`) +- Таблицы — по шаблону: `{module}_{entity}.sql` (например `auth_users.sql`) + +--- + +## Правила (rules/) + +При формировании проекта LLM использует правила из: + +- `rules/common/` — общие правила для всех СУБД +- `rules/{engine}/` — правила для конкретного движка + +Правила определяют: + +- Стиль написания SQL +- Шаблоны описания таблиц и функций +- Формат комментариев +- Стандарты именования + +--- + +## Skills + +При формировании проекта LLM использует промпты из `skills/` для: + +- Генерации описаний таблиц +- Генерации описаний функций +- Формирования документации +- Создания диаграмм + +--- + +## Порядок создания проекта + +1. Создать папку проекта `projects/{engine}/{project_name}/` +2. Создать `readme.md` с описанием +3. Создать `settings/connection.json` +4. Для каждого модуля БД создать папку модуля с подпапками (tables, functions, scripts, init, deploy) +5. Создать `_docs/` с диаграммами и спецификацией +6. Заполнить SQL-файлы на основании требований +7. Проверить структуру на соответствие шаблону diff --git a/projects/pg/kit_auth_token_pg/deploy/docker-compose.yml b/projects/pg/kit_auth_token_pg/deploy/docker-compose.yml new file mode 100644 index 0000000..edcb0d4 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/deploy/docker-compose.yml @@ -0,0 +1,25 @@ +name: kit-auth-token-db +networks: + default: + external: true + name: sdi_server +services: + token-db: + image: postgres + restart: always + ports: + - 6749:5432 + container_name: kit-auth-token-db + environment: + POSTGRES_DB: kit-auth-token + POSTGRES_USER: user_auth_token + POSTGRES_PASSWORD: 9b1d4c7a-2e63-4f08-a5d1-7c0e3b9f2a64 + 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 diff --git a/projects/pg/kit_auth_token_pg/deploy/down.bat b/projects/pg/kit_auth_token_pg/deploy/down.bat new file mode 100644 index 0000000..84e575c --- /dev/null +++ b/projects/pg/kit_auth_token_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 diff --git a/projects/pg/kit_auth_token_pg/deploy/init.sh b/projects/pg/kit_auth_token_pg/deploy/init.sh new file mode 100644 index 0000000..5fe0e08 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/deploy/init.sh @@ -0,0 +1,42 @@ +file_publish="/docker-entrypoint-initdb.d/scripts/publish.psql" +files=( +########################################################################## +# schemas +"/docker-entrypoint-initdb.d/schemas/token/token.psql" + +########################################################################## +# tables +# token_type и owner — раньше token (на них ссылаются type_id / owner_id) +"/docker-entrypoint-initdb.d/schemas/token/tables/token_type.psql" +"/docker-entrypoint-initdb.d/schemas/token/tables/owner.psql" +"/docker-entrypoint-initdb.d/schemas/token/tables/token.psql" + +########################################################################## +# functions +# token_type +"/docker-entrypoint-initdb.d/schemas/token/functions/token_type/token_type_select.psql" +# owner +"/docker-entrypoint-initdb.d/schemas/token/functions/owner/owner_select.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/owner/owner_insert.psql" +# token +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_select.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_insert.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_update.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_consume.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_validate.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_revoke.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_revoke_by_owner.psql" +"/docker-entrypoint-initdb.d/schemas/token/functions/token/token_delete.psql" + +########################################################################## +# post-deploy (seed справочников) +"/docker-entrypoint-initdb.d/schemas/token/token.post-deploy.psql" + +) + +> ${file_publish} +for item in "${files[@]}"; do + cat "${item}" >> "${file_publish}" && printf "\n\n\n" >> "${file_publish}" +done +# run publish.psql +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "${file_publish}" diff --git a/projects/pg/kit_auth_token_pg/deploy/restart.bat b/projects/pg/kit_auth_token_pg/deploy/restart.bat new file mode 100644 index 0000000..87fa9e9 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/deploy/restart.bat @@ -0,0 +1,7 @@ +@echo off +setlocal + +CALL "down.bat" %* +CALL "up.bat" + +endlocal diff --git a/projects/pg/kit_auth_token_pg/deploy/up.bat b/projects/pg/kit_auth_token_pg/deploy/up.bat new file mode 100644 index 0000000..e3fec99 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/deploy/up.bat @@ -0,0 +1,7 @@ +@echo off +setlocal + +docker network create -d bridge sdi_server +docker compose up -d + +endlocal diff --git a/projects/pg/kit_auth_token_pg/functions/owner/owner_insert.psql b/projects/pg/kit_auth_token_pg/functions/owner/owner_insert.psql new file mode 100644 index 0000000..b3fea8c --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/owner/owner_insert.psql @@ -0,0 +1,22 @@ +-- Явная регистрация владельца по естественному ключу key (НЕ авто-создание при выпуске). +-- Идемпотентна: существует — возвращаем его id (title не перетираем); нет — создаём. +-- ON CONFLICT DO UPDATE (no-op по key) нужен, чтобы RETURNING вернул id и при конфликте; +-- это же делает функцию устойчивой к гонке двух параллельных выпусков (правило 06). +CREATE OR REPLACE FUNCTION token.owner_insert( + _key varchar, + _title varchar, + _date_created timestamptz +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _id int; +BEGIN + INSERT INTO token.owner(key, title, date_created) + VALUES (_key, _title, _date_created) + ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key + RETURNING id INTO _id; + RETURN _id; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/owner/owner_select.psql b/projects/pg/kit_auth_token_pg/functions/owner/owner_select.psql new file mode 100644 index 0000000..c4c2992 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/owner/owner_select.psql @@ -0,0 +1,23 @@ +-- Выборка владельцев: фильтры по id/key/title, без пейджинга (правило 03). +-- key сравнивается на равенство (естественный ключ), title — текстовый ILIKE. +CREATE OR REPLACE FUNCTION token.owner_select( + _id int DEFAULT NULL, + _key varchar DEFAULT NULL, + _title varchar DEFAULT NULL +) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT id, key, title, date_created + FROM token.owner + WHERE (_id IS NULL OR id = _id) + AND (_key IS NULL OR key = _key) + AND (_title IS NULL OR title ILIKE '%' || _title || '%') + ORDER BY id; + RETURN NEXT _data; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_consume.psql b/projects/pg/kit_auth_token_pg/functions/token/token_consume.psql new file mode 100644 index 0000000..0b8372b --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_consume.psql @@ -0,0 +1,37 @@ +-- Атомарное потребление токена за одно обращение (правило 06: логика записи — в БД). +-- В одной транзакции: проверка валидности, инкремент read_used, фиксация date_last_read, +-- продление срока при скользящей экспирации, авто-погашение при достижении лимита. +-- Возвращает курсор с одной строкой: data и итоговый признак is_valid обращения. +-- Невалиден (нет токена / неактивен / истёк / исчерпан лимит) → is_valid=false, data=NULL. +CREATE OR REPLACE FUNCTION token.token_consume(_hash varchar) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _result refcursor := 'data'; + _now timestamptz := now(); + _data text; + _valid boolean := false; +BEGIN + UPDATE token.token t + SET read_used = t.read_used + 1, + date_last_read = _now, + date_expired = CASE WHEN t.is_slide_expiration + THEN _now + make_interval(secs => t.lifetime_seconds) + ELSE t.date_expired END, + -- гасим, если этим обращением исчерпали лимит (0 = без лимита) + is_active = CASE WHEN t.read_limit > 0 AND t.read_used + 1 >= t.read_limit + THEN false ELSE t.is_active END + WHERE t.hash = _hash + AND t.is_active + AND t.date_expired > _now + AND (t.read_limit = 0 OR t.read_used < t.read_limit) + RETURNING t.data INTO _data; + + _valid := FOUND; + + OPEN _result FOR + SELECT _valid AS is_valid, _data AS data; + RETURN NEXT _result; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_delete.psql b/projects/pg/kit_auth_token_pg/functions/token/token_delete.psql new file mode 100644 index 0000000..abf023c --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_delete.psql @@ -0,0 +1,9 @@ +-- Физическое удаление токена по hash (мягкое удаление таблицей не предусмотрено). +CREATE OR REPLACE FUNCTION token.token_delete(_hash varchar) +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + DELETE FROM token.token WHERE hash = _hash; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_insert.psql b/projects/pg/kit_auth_token_pg/functions/token/token_insert.psql new file mode 100644 index 0000000..1a1ec3e --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_insert.psql @@ -0,0 +1,28 @@ +-- Вставка токена. PK — текстовый hash (не serial), возвращается он же. +-- read_used/date_last_read/date_revoked при выпуске не задаются (значения по умолчанию). +-- owner_id должен существовать заранее (резолв/создание владельца — см. owner_insert). +CREATE OR REPLACE FUNCTION token.token_insert( + _hash varchar, + _date_created timestamptz, + _date_expired timestamptz, + _data text, + _lifetime_seconds int, + _is_slide_expiration boolean, + _read_limit int, + _is_active boolean, + _type_id int, + _owner_id int +) +RETURNS varchar +LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO token.token( + hash, date_created, date_expired, data, lifetime_seconds, + is_slide_expiration, read_limit, is_active, type_id, owner_id) + VALUES ( + _hash, _date_created, _date_expired, _data, _lifetime_seconds, + _is_slide_expiration, _read_limit, _is_active, _type_id, _owner_id); + RETURN _hash; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_revoke.psql b/projects/pg/kit_auth_token_pg/functions/token/token_revoke.psql new file mode 100644 index 0000000..1768f4a --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_revoke.psql @@ -0,0 +1,23 @@ +-- Мягкий отзыв одного токена по hash: is_active=false + фиксация момента и причины. +-- Отзываются только ещё активные токены (date_revoked/revoke_reason не перетираются +-- повторным вызовом). Возвращает число фактически отозванных (0 или 1). +CREATE OR REPLACE FUNCTION token.token_revoke( + _hash varchar, + _reason varchar +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _count int; +BEGIN + UPDATE token.token + SET is_active = false, + date_revoked = now(), + revoke_reason = _reason + WHERE hash = _hash + AND is_active; + GET DIAGNOSTICS _count = ROW_COUNT; + RETURN _count; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_revoke_by_owner.psql b/projects/pg/kit_auth_token_pg/functions/token/token_revoke_by_owner.psql new file mode 100644 index 0000000..52f17a6 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_revoke_by_owner.psql @@ -0,0 +1,23 @@ +-- Массовый отзыв всех активных токенов владельца (logout всех сессий приложения, +-- реакция на компрометацию источника). Опирается на индекс ix_token_owner. +-- Возвращает число фактически отозванных токенов. +CREATE OR REPLACE FUNCTION token.token_revoke_by_owner( + _owner_id int, + _reason varchar +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _count int; +BEGIN + UPDATE token.token + SET is_active = false, + date_revoked = now(), + revoke_reason = _reason + WHERE owner_id = _owner_id + AND is_active; + GET DIAGNOSTICS _count = ROW_COUNT; + RETURN _count; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_select.psql b/projects/pg/kit_auth_token_pg/functions/token/token_select.psql new file mode 100644 index 0000000..5831533 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_select.psql @@ -0,0 +1,28 @@ +-- Выборка токенов: фильтры по полям, без пейджинга (правило 03). +-- hash/owner_id/type_id — равенство; is_active — равенство; владельца фильтруем по id +-- (его естественный ключ резолвится через owner_select на стороне вызывающего кода). +CREATE OR REPLACE FUNCTION token.token_select( + _hash varchar DEFAULT NULL, + _type_id int DEFAULT NULL, + _owner_id int DEFAULT NULL, + _is_active boolean DEFAULT NULL +) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT hash, type_id, owner_id, date_created, date_expired, date_last_read, + data, lifetime_seconds, is_slide_expiration, read_limit, read_used, + is_active, date_revoked, revoke_reason + FROM token.token + WHERE (_hash IS NULL OR hash = _hash) + AND (_type_id IS NULL OR type_id = _type_id) + AND (_owner_id IS NULL OR owner_id = _owner_id) + AND (_is_active IS NULL OR is_active = _is_active) + ORDER BY date_created DESC; + RETURN NEXT _data; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_update.psql b/projects/pg/kit_auth_token_pg/functions/token/token_update.psql new file mode 100644 index 0000000..7e1179a --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_update.psql @@ -0,0 +1,31 @@ +-- Обновление токена по hash. date_created неизменяем и не обновляется. +-- read_used/date_last_read/date_revoked обновляются специализированными операциями +-- (потребление токена / отзыв), не общим update. +CREATE OR REPLACE FUNCTION token.token_update( + _hash varchar, + _date_expired timestamptz, + _data text, + _lifetime_seconds int, + _is_slide_expiration boolean, + _read_limit int, + _is_active boolean, + _type_id int, + _owner_id int +) +RETURNS varchar +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE token.token + SET date_expired = _date_expired, + data = _data, + lifetime_seconds = _lifetime_seconds, + is_slide_expiration = _is_slide_expiration, + read_limit = _read_limit, + is_active = _is_active, + type_id = _type_id, + owner_id = _owner_id + WHERE hash = _hash; + RETURN _hash; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token/token_validate.psql b/projects/pg/kit_auth_token_pg/functions/token/token_validate.psql new file mode 100644 index 0000000..5f47868 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token/token_validate.psql @@ -0,0 +1,28 @@ +-- Проверка токена БЕЗ расхода обращения (peek): read_used/date_last_read не меняются. +-- Критерий валидности тот же, что в token_consume: активен, не истёк, лимит не исчерпан. +-- Используется там, где проверка не должна «съедать» обращение (middleware, предпроверки). +-- Возвращает курсор с одной строкой: is_valid и data (data=NULL, если невалиден). +CREATE OR REPLACE FUNCTION token.token_validate(_hash varchar) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _result refcursor := 'data'; + _now timestamptz := now(); + _data text; + _valid boolean := false; +BEGIN + SELECT t.data INTO _data + FROM token.token t + WHERE t.hash = _hash + AND t.is_active + AND t.date_expired > _now + AND (t.read_limit = 0 OR t.read_used < t.read_limit); + + _valid := FOUND; + + OPEN _result FOR + SELECT _valid AS is_valid, _data AS data; + RETURN NEXT _result; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/functions/token_type/token_type_select.psql b/projects/pg/kit_auth_token_pg/functions/token_type/token_type_select.psql new file mode 100644 index 0000000..a88c871 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/functions/token_type/token_type_select.psql @@ -0,0 +1,15 @@ +-- Справочник: простой select без фильтров и пейджинга (правило 03). +CREATE OR REPLACE FUNCTION token.token_type_select() +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT id, title, key + FROM token.token_type + ORDER BY id; + RETURN NEXT _data; +END; +$$; diff --git a/projects/pg/kit_auth_token_pg/readme.md b/projects/pg/kit_auth_token_pg/readme.md new file mode 100644 index 0000000..bdd6d4a --- /dev/null +++ b/projects/pg/kit_auth_token_pg/readme.md @@ -0,0 +1,32 @@ +# Kit.Auth.Token.Db + +## Назначение + +База данных для управления токенами авторизации. Хранит владельцев токенов, типы токенов и сами токены с возможностью валидации, отзыва и удаления. + +## Движок + +PostgreSQL + +## Схема + +`token` + +## Модули + +### token +Основной модуль. Содержит таблицы и функции для работы с токенами. + +**Таблицы:** +- `owner` — владельцы токенов +- `token_type` — справочник типов токенов +- `token` — токены авторизации + +**Функции:** +- Управление владельцами: insert, select +- Управление токенами: insert, select, update, delete, consume, validate, revoke, revoke_by_owner +- Справочники: token_type_select + +## Связи + +- Связана с `Kit.Auth.Db` (модуль `auth`) \ No newline at end of file diff --git a/projects/pg/kit_auth_token_pg/scripts/create_full_20260723_124551.sql b/projects/pg/kit_auth_token_pg/scripts/create_full_20260723_124551.sql new file mode 100644 index 0000000..6671f18 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/scripts/create_full_20260723_124551.sql @@ -0,0 +1,382 @@ +-- Схема домена Token (собственная токен-БД Auth-сервиса: kit-auth-token). +-- Схема идентична Kit.Token.Db — её ожидает встроенный серверный модуль Kit.Token.Token. +-- Таблицы и функции (CRUD через хранимки) подключаются в init.sh (правила 03–04). +CREATE SCHEMA IF NOT EXISTS token; + + + +-- Справочник типов токена. id НЕ автоинкремент (на него ссылается token.type_id). +-- title обязателен (правило 03); key — естественный ключ для поиска типа из кода +-- (справочники ищутся по key/title, не по хардкод-id — правила 05–06). +CREATE TABLE token.token_type +( + id integer not null primary key, + title varchar(250) not null, + key varchar(250) not null unique +); + + + +-- Владелец/источник токена (ранее денормализованное поле token.app_name). +-- Обычная таблица: владельцы создаются динамически по ходу выпуска токенов. +-- key — естественный уникальный ключ для идемпотентного get-or-create (см. owner_insert). +CREATE TABLE token.owner +( + id serial not null primary key, + key varchar(250) not null unique, + title varchar(250) not null default '', + date_created timestamptz not null +); + + + +-- Токен: PK — текстовый hash (хеш токена, не сам токен в открытом виде). +-- owner_id — владелец/источник (ранее денормализованное поле app_name). +-- read_limit — разрешённое число обращений (0 = без лимита); read_used — уже израсходовано. +CREATE TABLE token.token +( + hash varchar(256) not null primary key, + type_id int not null references token.token_type (id), + owner_id int not null references token.owner (id), + date_created timestamptz not null, + date_expired timestamptz not null, + date_last_read timestamptz null, + data text not null, + lifetime_seconds int not null, + is_slide_expiration boolean not null, + read_limit int not null default 0, + read_used int not null default 0, + is_active boolean not null, + date_revoked timestamptz null, + revoke_reason varchar(250) null, + CONSTRAINT chk_token_lifetime CHECK (lifetime_seconds > 0), + CONSTRAINT chk_token_reads CHECK (read_used >= 0 AND (read_limit = 0 OR read_used <= read_limit)), + CONSTRAINT chk_token_expired CHECK (date_expired >= date_created) +); + +-- Индексы под фильтры token_select и фоновую чистку просроченных. +CREATE INDEX ix_token_owner ON token.token (owner_id); +CREATE INDEX ix_token_expired ON token.token (date_expired); +CREATE INDEX ix_token_active_type ON token.token (is_active, type_id); + + + +-- Справочник: простой select без фильтров и пейджинга (правило 03). +CREATE OR REPLACE FUNCTION token.token_type_select() +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT id, title, key + FROM token.token_type + ORDER BY id; + RETURN NEXT _data; +END; +$$; + + + +-- Выборка владельцев: фильтры по id/key/title, без пейджинга (правило 03). +-- key сравнивается на равенство (естественный ключ), title — текстовый ILIKE. +CREATE OR REPLACE FUNCTION token.owner_select( + _id int DEFAULT NULL, + _key varchar DEFAULT NULL, + _title varchar DEFAULT NULL +) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT id, key, title, date_created + FROM token.owner + WHERE (_id IS NULL OR id = _id) + AND (_key IS NULL OR key = _key) + AND (_title IS NULL OR title ILIKE '%' || _title || '%') + ORDER BY id; + RETURN NEXT _data; +END; +$$; + + + +-- Явная регистрация владельца по естественному ключу key (НЕ авто-создание при выпуске). +-- Идемпотентна: существует — возвращаем его id (title не перетираем); нет — создаём. +-- ON CONFLICT DO UPDATE (no-op по key) нужен, чтобы RETURNING вернул id и при конфликте; +-- это же делает функцию устойчивой к гонке двух параллельных выпусков (правило 06). +CREATE OR REPLACE FUNCTION token.owner_insert( + _key varchar, + _title varchar, + _date_created timestamptz +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _id int; +BEGIN + INSERT INTO token.owner(key, title, date_created) + VALUES (_key, _title, _date_created) + ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key + RETURNING id INTO _id; + RETURN _id; +END; +$$; + + + +-- Выборка токенов: фильтры по полям, без пейджинга (правило 03). +-- hash/owner_id/type_id — равенство; is_active — равенство; владельца фильтруем по id +-- (его естественный ключ резолвится через owner_select на стороне вызывающего кода). +CREATE OR REPLACE FUNCTION token.token_select( + _hash varchar DEFAULT NULL, + _type_id int DEFAULT NULL, + _owner_id int DEFAULT NULL, + _is_active boolean DEFAULT NULL +) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _data refcursor := 'data'; +BEGIN + OPEN _data FOR + SELECT hash, type_id, owner_id, date_created, date_expired, date_last_read, + data, lifetime_seconds, is_slide_expiration, read_limit, read_used, + is_active, date_revoked, revoke_reason + FROM token.token + WHERE (_hash IS NULL OR hash = _hash) + AND (_type_id IS NULL OR type_id = _type_id) + AND (_owner_id IS NULL OR owner_id = _owner_id) + AND (_is_active IS NULL OR is_active = _is_active) + ORDER BY date_created DESC; + RETURN NEXT _data; +END; +$$; + + + +-- Вставка токена. PK — текстовый hash (не serial), возвращается он же. +-- read_used/date_last_read/date_revoked при выпуске не задаются (значения по умолчанию). +-- owner_id должен существовать заранее (резолв/создание владельца — см. owner_insert). +CREATE OR REPLACE FUNCTION token.token_insert( + _hash varchar, + _date_created timestamptz, + _date_expired timestamptz, + _data text, + _lifetime_seconds int, + _is_slide_expiration boolean, + _read_limit int, + _is_active boolean, + _type_id int, + _owner_id int +) +RETURNS varchar +LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO token.token( + hash, date_created, date_expired, data, lifetime_seconds, + is_slide_expiration, read_limit, is_active, type_id, owner_id) + VALUES ( + _hash, _date_created, _date_expired, _data, _lifetime_seconds, + _is_slide_expiration, _read_limit, _is_active, _type_id, _owner_id); + RETURN _hash; +END; +$$; + + + +-- Обновление токена по hash. date_created неизменяем и не обновляется. +-- read_used/date_last_read/date_revoked обновляются специализированными операциями +-- (потребление токена / отзыв), не общим update. +CREATE OR REPLACE FUNCTION token.token_update( + _hash varchar, + _date_expired timestamptz, + _data text, + _lifetime_seconds int, + _is_slide_expiration boolean, + _read_limit int, + _is_active boolean, + _type_id int, + _owner_id int +) +RETURNS varchar +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE token.token + SET date_expired = _date_expired, + data = _data, + lifetime_seconds = _lifetime_seconds, + is_slide_expiration = _is_slide_expiration, + read_limit = _read_limit, + is_active = _is_active, + type_id = _type_id, + owner_id = _owner_id + WHERE hash = _hash; + RETURN _hash; +END; +$$; + + + +-- Атомарное потребление токена за одно обращение (правило 06: логика записи — в БД). +-- Ð’ одной транзакции: проверка валидности, инкремент read_used, фиксация date_last_read, +-- продление срока при скользящей экспирации, авто-погашение при достижении лимита. +-- Возвращает курсор с одной строкой: data и итоговый признак is_valid обращения. +-- Невалиден (нет токена / неактивен / истёк / исчерпан лимит) → is_valid=false, data=NULL. +CREATE OR REPLACE FUNCTION token.token_consume(_hash varchar) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _result refcursor := 'data'; + _now timestamptz := now(); + _data text; + _valid boolean := false; +BEGIN + UPDATE token.token t + SET read_used = t.read_used + 1, + date_last_read = _now, + date_expired = CASE WHEN t.is_slide_expiration + THEN _now + make_interval(secs => t.lifetime_seconds) + ELSE t.date_expired END, + -- гасим, если этим обращением исчерпали лимит (0 = без лимита) + is_active = CASE WHEN t.read_limit > 0 AND t.read_used + 1 >= t.read_limit + THEN false ELSE t.is_active END + WHERE t.hash = _hash + AND t.is_active + AND t.date_expired > _now + AND (t.read_limit = 0 OR t.read_used < t.read_limit) + RETURNING t.data INTO _data; + + _valid := FOUND; + + OPEN _result FOR + SELECT _valid AS is_valid, _data AS data; + RETURN NEXT _result; +END; +$$; + + + +-- Проверка токена БЕЗ расхода обращения (peek): read_used/date_last_read не меняются. +-- Критерий валидности тот же, что в token_consume: активен, не истёк, лимит не исчерпан. +-- Используется там, где проверка не должна «съедать» обращение (middleware, предпроверки). +-- Возвращает курсор с одной строкой: is_valid и data (data=NULL, если невалиден). +CREATE OR REPLACE FUNCTION token.token_validate(_hash varchar) +RETURNS SETOF refcursor +LANGUAGE plpgsql +AS $$ +DECLARE + _result refcursor := 'data'; + _now timestamptz := now(); + _data text; + _valid boolean := false; +BEGIN + SELECT t.data INTO _data + FROM token.token t + WHERE t.hash = _hash + AND t.is_active + AND t.date_expired > _now + AND (t.read_limit = 0 OR t.read_used < t.read_limit); + + _valid := FOUND; + + OPEN _result FOR + SELECT _valid AS is_valid, _data AS data; + RETURN NEXT _result; +END; +$$; + + + +-- Мягкий отзыв одного токена по hash: is_active=false + фиксация момента и причины. +-- Отзываются только ещё активные токены (date_revoked/revoke_reason не перетираются +-- повторным вызовом). Возвращает число фактически отозванных (0 или 1). +CREATE OR REPLACE FUNCTION token.token_revoke( + _hash varchar, + _reason varchar +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _count int; +BEGIN + UPDATE token.token + SET is_active = false, + date_revoked = now(), + revoke_reason = _reason + WHERE hash = _hash + AND is_active; + GET DIAGNOSTICS _count = ROW_COUNT; + RETURN _count; +END; +$$; + + + +-- Массовый отзыв всех активных токенов владельца (logout всех сессий приложения, +-- реакция на компрометацию источника). Опирается на индекс ix_token_owner. +-- Возвращает число фактически отозванных токенов. +CREATE OR REPLACE FUNCTION token.token_revoke_by_owner( + _owner_id int, + _reason varchar +) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + _count int; +BEGIN + UPDATE token.token + SET is_active = false, + date_revoked = now(), + revoke_reason = _reason + WHERE owner_id = _owner_id + AND is_active; + GET DIAGNOSTICS _count = ROW_COUNT; + RETURN _count; +END; +$$; + + + +-- Физическое удаление токена по hash (мягкое удаление таблицей не предусмотрено). +CREATE OR REPLACE FUNCTION token.token_delete(_hash varchar) +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + DELETE FROM token.token WHERE hash = _hash; +END; +$$; + + + +-- Стартовые данные схемы token (правило 03: seed справочников в post-deploy). +-- Типы токенов: id фиксированы (на них ссылается token.type_id), key — для поиска из кода. +INSERT INTO token.token_type(id, title, key) VALUES + (1, 'Сессия', 'session'), + (2, 'Подтверждение e-mail', 'email_confirm'), + (3, 'Сброс пароля', 'password_reset'), + (4, 'Magic-link (вход)', 'magic_link'), + (5, 'Одноразовый код', 'one_time_code') +ON CONFLICT (id) DO NOTHING; + +-- Владелец токенов этого сервиса (whitelist token.owner). Совпадает с Auth:TokenOwnerKey="auth" +-- (Program.cs дополнительно регистрирует его идемпотентно при старте). token_insert требует +-- существующий owner_id — неизвестный источник токен не получит. +INSERT INTO token.owner(key, title, date_created) VALUES + ('auth', 'Сервис авторизации', now()) +ON CONFLICT (key) DO NOTHING; + + + diff --git a/projects/pg/kit_auth_token_pg/settings/connection.json b/projects/pg/kit_auth_token_pg/settings/connection.json new file mode 100644 index 0000000..d0522ab --- /dev/null +++ b/projects/pg/kit_auth_token_pg/settings/connection.json @@ -0,0 +1,7 @@ +{ + "host": "localhost", + "port": 5432, + "database": "kit_auth_token", + "username": "postgres", + "password": "" +} \ No newline at end of file diff --git a/projects/pg/kit_auth_token_pg/tables/owner.psql b/projects/pg/kit_auth_token_pg/tables/owner.psql new file mode 100644 index 0000000..f974b51 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/tables/owner.psql @@ -0,0 +1,10 @@ +-- Владелец/источник токена (ранее денормализованное поле token.app_name). +-- Обычная таблица: владельцы создаются динамически по ходу выпуска токенов. +-- key — естественный уникальный ключ для идемпотентного get-or-create (см. owner_insert). +CREATE TABLE token.owner +( + id serial not null primary key, + key varchar(250) not null unique, + title varchar(250) not null default '', + date_created timestamptz not null +); diff --git a/projects/pg/kit_auth_token_pg/tables/token.psql b/projects/pg/kit_auth_token_pg/tables/token.psql new file mode 100644 index 0000000..a56e3f7 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/tables/token.psql @@ -0,0 +1,28 @@ +-- Токен: PK — текстовый hash (хеш токена, не сам токен в открытом виде). +-- owner_id — владелец/источник (ранее денормализованное поле app_name). +-- read_limit — разрешённое число обращений (0 = без лимита); read_used — уже израсходовано. +CREATE TABLE token.token +( + hash varchar(256) not null primary key, + type_id int not null references token.token_type (id), + owner_id int not null references token.owner (id), + date_created timestamptz not null, + date_expired timestamptz not null, + date_last_read timestamptz null, + data text not null, + lifetime_seconds int not null, + is_slide_expiration boolean not null, + read_limit int not null default 0, + read_used int not null default 0, + is_active boolean not null, + date_revoked timestamptz null, + revoke_reason varchar(250) null, + CONSTRAINT chk_token_lifetime CHECK (lifetime_seconds > 0), + CONSTRAINT chk_token_reads CHECK (read_used >= 0 AND (read_limit = 0 OR read_used <= read_limit)), + CONSTRAINT chk_token_expired CHECK (date_expired >= date_created) +); + +-- Индексы под фильтры token_select и фоновую чистку просроченных. +CREATE INDEX ix_token_owner ON token.token (owner_id); +CREATE INDEX ix_token_expired ON token.token (date_expired); +CREATE INDEX ix_token_active_type ON token.token (is_active, type_id); diff --git a/projects/pg/kit_auth_token_pg/tables/token_type.psql b/projects/pg/kit_auth_token_pg/tables/token_type.psql new file mode 100644 index 0000000..50b3904 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/tables/token_type.psql @@ -0,0 +1,9 @@ +-- Справочник типов токена. id НЕ автоинкремент (на него ссылается token.type_id). +-- title обязателен (правило 03); key — естественный ключ для поиска типа из кода +-- (справочники ищутся по key/title, не по хардкод-id — правила 05–06). +CREATE TABLE token.token_type +( + id integer not null primary key, + title varchar(250) not null, + key varchar(250) not null unique +); diff --git a/projects/pg/kit_auth_token_pg/token.post-deploy.psql b/projects/pg/kit_auth_token_pg/token.post-deploy.psql new file mode 100644 index 0000000..d705420 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/token.post-deploy.psql @@ -0,0 +1,16 @@ +-- Стартовые данные схемы token (правило 03: seed справочников в post-deploy). +-- Типы токенов: id фиксированы (на них ссылается token.type_id), key — для поиска из кода. +INSERT INTO token.token_type(id, title, key) VALUES + (1, 'Сессия', 'session'), + (2, 'Подтверждение e-mail', 'email_confirm'), + (3, 'Сброс пароля', 'password_reset'), + (4, 'Magic-link (вход)', 'magic_link'), + (5, 'Одноразовый код', 'one_time_code') +ON CONFLICT (id) DO NOTHING; + +-- Владелец токенов этого сервиса (whitelist token.owner). Совпадает с Auth:TokenOwnerKey="auth" +-- (Program.cs дополнительно регистрирует его идемпотентно при старте). token_insert требует +-- существующий owner_id — неизвестный источник токен не получит. +INSERT INTO token.owner(key, title, date_created) VALUES + ('auth', 'Сервис авторизации', now()) +ON CONFLICT (key) DO NOTHING; diff --git a/projects/pg/kit_auth_token_pg/token.psql b/projects/pg/kit_auth_token_pg/token.psql new file mode 100644 index 0000000..2fa2031 --- /dev/null +++ b/projects/pg/kit_auth_token_pg/token.psql @@ -0,0 +1,4 @@ +-- Схема домена Token (собственная токен-БД Auth-сервиса: kit-auth-token). +-- Схема идентична Kit.Token.Db — её ожидает встроенный серверный модуль Kit.Token.Token. +-- Таблицы и функции (CRUD через хранимки) подключаются в init.sh (правила 03–04). +CREATE SCHEMA IF NOT EXISTS token; diff --git a/rules/pg/db-project-rule.md b/rules/pg/db-project-rule.md new file mode 100644 index 0000000..21e477e --- /dev/null +++ b/rules/pg/db-project-rule.md @@ -0,0 +1,138 @@ +# Правило — Формирование проекта PostgreSQL БД + +--- + +## Структура проекта + +``` +projects/pg/{project_name}/ + readme.md + settings/ + connection.json + {schema}/ + tables/ + functions/ + scripts/ + init/ + {schema}.psql + deploy/ + _docs/ + diagrams/ + openapi.yaml + architecture.md +``` + +--- + +## Правила + +### 1. Схема = модуль + +Каждая схема PostgreSQL — это отдельная папка-модуль в корне проекта. + +### 2. Таблицы + +- Путь: `{schema}/tables/` +- Формат: `.psql` +- Одна таблица = один файл +- Именование: `{table_name}.psql` (например `user.psql`, `cabinet.psql`) +- Содержимое: `CREATE TABLE` с комментариями + +### 3. Функции + +- Путь: `{schema}/functions/{function_name}/` +- Каждая функция — отдельная папка +- Внутри: файл `.psql` с телом функции +- Именование папки: по имени функции (например `user_select_by_ids/`) + +### 4. Скрипты + +- Путь: `{schema}/scripts/` +- SQL-скрипты (миграции, seed-данные, утилиты) +- Именование миграций: `YYYY-MM-DD_description.sql` + +### 5. Init + +- Путь: `{schema}/init/` +- Скрипты инициализации: создание схемы, расширений, начальных данных +- Выполняются один раз при первом развёртывании + +### 6. Deploy + +- Путь: `deploy/` (в корне проекта) +- Инфраструктура развёртывания: docker-compose, bat-файлы, shell-скрипты +- НЕ SQL-миграции + +### 7. Главный файл модуля + +- Путь: `{schema}/{schema}.psql` +- Точка входа — подключает все таблицы и функции модуля +- Порядок: сначала таблицы, потом функции + +### 8. Post-deploy + +- Путь: `{schema}/{schema}.post-deploy.psql` +- Скрипты, выполняемые после деплоя (seed-данные, обновление зависимостей) + +--- + +## Пример: проект kit_auth_pg + +``` +projects/pg/kit_auth_pg/ + readme.md + settings/ + connection.json + auth/ + tables/ + user.psql + cabinet.psql + user_cabinet.psql + functions/ + user_select_by_ids/ + user_select_by_ids.psql + user_create/ + user_create.psql + scripts/ + seed_roles.sql + 2026-07-08_user-select-by-ids.sql + init/ + create_schema.psql + auth.psql + auth.post-deploy.psql + deploy/ + docker-compose.yml + up.bat + down.bat + restart.bat + init.sh + _docs/ + diagrams/ + openapi.yaml + architecture.md +``` + +--- + +## Настройки подключения + +Файл: `settings/connection.json` + +```json +{ + "host": "localhost", + "port": 5432, + "database": "{database_name}", + "username": "postgres", + "password": "" +} +``` + +--- + +## Инструменты + +- `apply-psql.sh` — применить SQL к БД +- `up.bat` / `down.bat` — запуск/остановка контейнера +- `docker-compose.yml` — конфигурация контейнера +- `export.bat` / `import.bat` — экспорт/импорт дампа \ No newline at end of file diff --git a/rules/sqlite/db-project-rule.md b/rules/sqlite/db-project-rule.md new file mode 100644 index 0000000..dbf0b2d --- /dev/null +++ b/rules/sqlite/db-project-rule.md @@ -0,0 +1,127 @@ +# Правило — Формирование проекта SQLite БД + +--- + +## Структура проекта + +``` +projects/sl3/{project_name}/ + readme.md + settings/ + connection.json + {module}/ + tables/ + scripts/ + init/ + {module}.sql + deploy/ + _docs/ + diagrams/ + openapi.yaml + architecture.md +``` + +--- + +## Правила + +### 1. Модуль + +Каждый функциональный модуль — отдельная папка в корне проекта. + +SQLite не поддерживает схемы, поэтому модуль = логическая группа объектов. + +### 2. Таблицы + +- Путь: `{module}/tables/` +- Формат: `.sql` +- Одна таблица = один файл +- Именование: `{table_name}.sql` (например `users.sql`, `sessions.sql`) +- Содержимое: `CREATE TABLE` с комментариями + +### 3. Скрипты + +- Путь: `{module}/scripts/` +- Произвольные SQL-скрипты (запросы, утилиты, seed-данные) +- SQLite не поддерживает хранимые процедуры — вся логика в скриптах + +### 4. Init + +- Путь: `{module}/init/` +- Скрипты инициализации: создание таблиц, индексов, начальных данных +- Выполняются один раз при первом развёртывании + +### 5. Deploy + +- Путь: `deploy/` (в корне проекта) +- Инфраструктура развёртывания: docker-compose, bat-файлы, shell-скрипты +- НЕ SQL-миграции + +### 6. Главный файл модуля + +- Путь: `{module}/{module}.sql` +- Точка входа — подключает все таблицы и скрипты модуля +- Порядок: сначала таблицы, потом скрипты + +--- + +## Особенности SQLite + +- Нет хранимых функций и процедур — папка `functions/` не используется +- Нет схем — все объекты в одном пространстве имён +- Один файл БД = один проект +- Типы данных ограничены: INTEGER, TEXT, REAL, BLOB, NUMERIC +- `PRAGMA` настройки размещаются в `init/` + +--- + +## Пример: проект kit_example_sl3 + +``` +projects/sl3/kit_example_sl3/ + readme.md + settings/ + connection.json + main/ + tables/ + users.sql + sessions.sql + roles.sql + scripts/ + seed_roles.sql + 2026-07-08_add-email-column.sql + init/ + create_indexes.sql + set_pragmas.sql + main.sql + deploy/ + docker-compose.yml + up.bat + down.bat + _docs/ + diagrams/ + openapi.yaml + architecture.md +``` + +--- + +## Настройки подключения + +Файл: `settings/connection.json` + +```json +{ + "database": "{database_name}.db", + "journal_mode": "WAL", + "foreign_keys": true +} +``` + +--- + +## Инструменты + +- `sqlite3 {database}.db < {script}.sql` — выполнить скрипт +- `.dump` — экспорт всей БД +- `.read` — выполнить файл SQL \ No newline at end of file diff --git a/Базы данных OT5.zip b/Базы данных OT5.zip new file mode 100644 index 0000000000000000000000000000000000000000..bcd148d5579551caa619e19084c344dca5b6c2fa GIT binary patch literal 15154 zcmd^mbyyzD^5%yH3l700XmAZqa1ZY8?(PtRy9IZ5cL?t8?iSo#zfG>?oaCOnzq`Bt zZSyeC%zWL|H9b|;UGFDU?2*qJjrSd#%jo+Qa0#-qk7#_K0S2*zu^pR|m(OoUAM{i*8l%Bd8QAO_@_#$e^q zo1@I=5uHGOXzy3y?HX5MRwc`yM{IVedIEjT>+IoK+T)gJT=!rt=uNNi&su2Ri|JoF z?>g@g%(pnS*2LHDziF-x*DRe(Uw)ntuW)CnSxHSzP29a4hBqg0IxH`30wHmtc=1Mw zJRCF-REUj-mO&*M(BwZtvtVD^kAR=g$65k5ujDWQ_p7@KA;oO1B}651Jv|PRH`W5U zF&_-}Mz(8giXtj?%@IEVzzF~#P)}_TrsbsX?eqmo+t68aJ(`pjDdDKmO3Wt+dyXiJ z-z+RrTA=9lj@K_=Fc0j6%mynE)WbHaEp(DLnpi|13E5D`8=L5pdq?piD<*N) z0aLajuJm)PR@JPiyh|^&QRwhMLuR5Y?^oq*^vYID*M-VYR9G@x&H$Ql@x^mBTGHy@*l$!0R!2|1dGjEVDpc*vgpHS1(&n zNBc=^Y=yJEdi#})$|ZJNGC|0(cY=;SUe;ZkhnYb7RGJ=Dzbf}hv?}_jjD~9M{ zD(kghz&a97Fngx*CIhVR+I+(KOGtg8mVuLn{kz%dSrmCnH3PDYn{cTGVHJcut`F|@ zrp=H6Xx!r}toHE_+y1pFE6UW-5JYy5lb_f)iY=La8k)!O29GFO+yq*1Rbvq#b0V80 zN1aDOQuUM!P0MdtbXKjp&b);}mPs08XW0j0Y=qt*)hcJQqgT8WwAsOpcyQz>mO0Nv z`HGe)S}gjN+9XXV8|{|Lgl0GZdL$)H(QHiICJ0OR+LN3w>cNrQ1kZSpI{#XD7{I(- z)37wjE=88CH&&R$MxQyWFtL-VwUD_(l&;9(<{@j8YCDUdqt@L1?LKPP`l)$My}E9T z15%+;@$z-Lm~CTJ<0^}G*~fz5!`S_nS$pg7&%vy9OI%9FJ)@qVpA+c&y{0j~S;gOD zGehwJK>umX|2TD3XkYiML2>q-iwN8g4e9K$kSS*MM5xnBEC*e^GWl3wr$?m}P)-&iBhhwhN-Jj{$?GZ^aHG zrmc>^u>a=Fe|Vbjr@O-1R8i8JFm1-}IG)&^xG&3hR8LX9`C&1=kYPy+@zR=5+(x;z zLaAse6)4wd#b?d=GrkPio&z*&Em$w8FYRMnx4>PpLJa8Ngs+SKP)u|nOXHWpMf}u$ zkK_y`Ee;6<&Sc7k3h_POwgUjte;I_a0Mx$)VLdAc+kb{(asY@QiuCKh4#PhyAE#KE zB8nwr1#hMm#_;*F4?dyr@_2Ve)pM45)C;ds zOCW{AorhP+75F3VibuV%!G<)R%%aX!jxra#iqC>jZJ(zc&%*!gDdwG}h1JdjsMmTh zbiB1Rw2IH6DzaH{Vjf6td;G>?wfZ%ð|I%0D+|$Fx+oqJO_XGmu!&Gd@y;_~QK? z+iUJsBaBvo7VxLohEpA_wl{3|-@LAQO(Je6Rd*|H))vc=Pp|MX@09NqpPH7ZZbh$R zRwG<3#ZiLN0~{_A2d%#54eoHQzHE5>gq1)gm>jPQ0hO|0aLKU=klaoT556 z^bp;1AxWd4WahwyuubWbouIfM9}}k&dtIZ{QDZ+fsP^!lQX)`Jr%-izisJZ+_;7(r z{_B8_x^xuMmWS>Pngya<;w8yPpku`^I+O0A|Z#b;FE zNGDU7tUj$0yPB}CbII7Slb)on^n(YFRANf>51iYWRL@aReQvTbHQaX?O2@Nj8!<{t z9=%`$aaF3HmQmPHYbVCYFd#`_t2RZlt5qkZe4i~_P>PbLIVfH{K3tI0Zah=dWSXyF zkSm=J?nI$6VJ4m-4pOQBG5p2y%f|;+28zKr!uK93<@?{FHFpCTOi|_J^G6Nxt1D>8 z80}&b?Nr{^o*#Tl!NHD6LRl$*A!)TLmKO8%q4?5Czhn4K)KbSaNj)jGDIu|iuQjR# z6p<%r&{ZdqO0oECj#%@3{$7y`)q$aOW>HFd;)tY1|Ac2#A9pfWdg{nG(rP<5OfRBY zS~98B=p>`2UM*T|L0%{2UY>Vgice>Yo0x=ki%_-p7A>I2$FfPMl*>wj#$U6l-rEt) zKxCOG&|W0*Og(2}7?bBp2Po9%$WiZ_yMJP*RLoSIEaj4CD%KbgmC>RpiKD(C5jOjp z)66$j6d{u#jZkm6&fEC_iwfLm@-=8E>mLY$E_A60Y1KP)4wVeYVh6LlFA!VrVp*QC zpLm74w5sWzmMUJ2w_pCO{&^&S)7DmBpy@o=>nP<&Y~Gl6szJwvImaTZFsPqucN{;mSH46!&B^(I$tF%f{DiQ#_tFEq`enn&>wN`!b*t{+flKiaJ07g%YBg=Y6OZ zDv#tcsj*L8^6tkmKFfo9zmn`wjT@QaSHh80am68$G>5|0ubxR~kj88XI~etQGO(GF z?GW}+*|{$8vSNxW20L*axfQi(-$Zog;Z?}_kYw{gJqP3XeQ3<8NCm2}#id}!6Z)%6 zm(OTX_$0^EmZ+|W@~Kwzer$&>6K77eA)+9YBR3)HVB*K|OE^HO;iDq@NLD~bbY%Gz zxuB5IqjLKr#Ye@;t|#Aw@Qy}u$uY?==kgRz4^>moT=7_q*xi6VLqUiMFbdy!{Lz_( zm}ex9VcgJQtJ@6{)1?4y^a>07>FL}w42Op@I>eLZIfpZ~!lk`}&+$iSfIArBg$3B6 zJmyvkdX5Q4ySiAw?~#sW5j~e!TeI4wA4|b)}Tvivz}!i zm5#k{<`$i5;Vcv=O_XY27fmcMg3o-wBLT=fpdtWh0ubU)|F~G!Msy}b3JdK!#;++` zayo*%iF=lNlstTh0h{O-SoRFqShH2 zaldrq144quRy5D;n{s>n2xb%apjo`ow7Mw0W4s**I_5U@GLVT;6S}nK_mubi_No3_ zUW@4>6xgU%I(O_=TK_6*96ApXzHxL^N~C3r55$AZy9bB>_o6BA$Z~vIqqcZk%ko}MZQN6sqfFEX_~A~tzrJUSM4FpRwhWKq8cPL2i`}B-lT(+BJ}cRxLw_!)AD2hU*q;*M|#2fHkq_Z9U-L5 z!EFi?>;cuXD)7hWz>d;;O`f>(o)rWhF)RTuf{$*;ABg|GsR5v$u%NH{T*F&l16k#+ zjmm zimoi_G}c{=AX?OD0J-zK89RH^9^n-R)-6o*)1-ge_Z?7zV8ZRADgLvb>Sht3( zw+QjnF0U_&mgz5U9DY}KYmDP=A8lYZo}{*bXAv)3kbiWJpORSH!Hu8r$AFN|pKXl) z>_Pvi^a;=ll!5!VHi+X6>ioieXBSE&0Q&FR?s`rCJMDQuA=AIFW)WU+wxFVZ_gujD z)f>LUPZj^*+3g)><3vlm07wpzCfyAC|AsYYJ3l=M7R}w4KDT-&?NrsZDv>S?qI7?L*@ixO;Xd4PD zg%DTO+|)FEu4KR9c+DdAYxM6c3>kU~*Hu-miA&mpLrZTZhgwBPy91?Sv->7^2m+YH z)`EE0!(%GaR5DxomAE{{R~V{(!Ap1{DhlytGcf0>6W)_)3ws%0Q`2VKenmPL@1Z;p z3zP6ZO9F+WQVep*!gRO#GdP(TkgZz98qtRzk~HSkt%7-TZe%NkG#gmlh^dOGCh{7E<0UWHR1f0=Gky^JbZb(1ZP#&h%p>tZ?#RM%0 zQvn5|G)YnExTdHC_2A38WGb2z*ee%!|cn%}rP zGAAbwj~*L5g(SwV0zqtmt5ZWxiHE?=lN93Y<43nx3q^h7qX|H)$=m z)1BRx^#_?}GA zqp?D@u0I~9^gQ|mMxywGkT)OKa*{VB^dYVEQ@|l|+(r{285wELw+Whu@X{@Mb~e+! z*9|+_6y6AH)=E=?*!JdbZs#tJG$XD#Z8vt%%;~{H15rr(y_7GNc2$%**$@(2{yPhi zb%J@Mgw=H1jIloOG=_GUbRNrMmj_+5uf$6WIK)2=J6zRUTrXrD?J?O=vO=?$_-bTV zL%XuSeg=QXwC%6Taw|iQY`&Qgz<>NC>0!SOb=+IZ=O!yRGDL6qMw9lAEP#z+`E_Lt z4zolF>M-aGU8@X$%_cbmZqYK0kqfr5a2LHQoWXHHm>wI4w@SH;hP|-_zXS6sSzeg4 zF9>`r63xeQ+yG5B3CfT)J5-u(j=Is@wE$YK|aHr#o z(T=jMrFt-ip-h{iSK(_*&DIK2Z$=oC4lG6xvId$3{ob&Gl#jVNVc=7*ZBTWKq{d+? zL+Mg8(n2em&&4wGMz4CJ84KxDdf+cnblG1)#<6nMFm1NjUR;?J-pA4?I4Y#!XCUyy%8A;s;U(q|!QM9fE#1~lLf7WQP^N^N{ch#NVZ|>x2u+y6Bu*$W}&W6#f zQUa_~;usjw79=g+6?Gp8=#{|IpbgO)MvND?*{>yNQct&R39nQ)TX0uzaTJha0hO`} zMb^@V<$Qn%cgf!Mj}i`=_s8>VVr`*0g*WyeMuz1stPKfY9x4+pJ0)gdCaqn$a~8q3DY z`*AdeM=IFTrn@dy^G>SRrM=mMs+))5X$v!@)L_?GTCkT}=~B^z+E-3B z@t3e51OH^U%8l?hWBXb)ZmAjr_5F9$JGvQ`n9<*8`~=fe!X-GNx*pdg-h-F|t)RP8%zQ*N`f)#HK5NSGQ`n z!C~J6r?`8~!n~p8oUS;d{Kb#UaxPH0J)@yO3aI0RNj-5H1`22Xce-aa(4WrHT> zmItA*G7rIHgDhE-1smA#27i(PLKyP$OyEHtYxtaMSB2ESej9hUf!PviGw`FWl=j%T+kT6!?Ft;}SXt4Q z=t^UwA9X&iMD8xod8|3AgInxL5NJg)bg?qeEpW>B$}_Uv$fy0=2JQFv8qvMxOxoR; z-;3n_n45Ho+^JbkpWwB&qBxS09so-*$B15@E`R{M_^@UUwUpu1Txq?Jl*cx31u@Gbh3urjA=eV#?JN=~?UL%`TqLM3mz7~&6$*R!UH_BJ@@UE*@1 zWit>;at9%W5~pEb=F5Izh}@+;)B4<$SxJ}hZ3c?}{=ToyEZgUkS9JKc=mYkA#)k*p z@|H9l98!)mLFzYE6>glMKF@POm3;*$Pwl`t2i>l_&z$atRI5gHt`-Z002M)`7gmWeFH}my?<~V z-@!E+DZ-e)2G@R8K3RR2&JeEl&G1=*h zFf=?7nUX8dy^yMI8>ut&OjAioOMhv;Dt-vLc%C-fIWOpa7w~tA<2w{8d#YGiKuy(; zZmW;mRD*IyZR!Mu1z(rPf+aM=6sYoSyF}n)Ta|-+vxm;aRV)!?;%*WB0Pl8UVHJ}f zwisi&#pYtpsTFRf<~;*nUAswy#1&GtQ}kl(D(hB5ARcZsRek0+dR%IKOCuQ7h~aE)aJUGYW4%Nmm!@FSADHIPiiO zLddBda4$6SVcgHKZbv&qJd9;?&&6&+zU>{kpv2U+k_Im^J36iDD-zsT?|1v{N{Lj} zU1eTJ&XBqw_m+JyU2@zN0b>3L_-wowLB5#s)}>Cti{Tc{SF$bRcIrYMC}9x-OL1H9 zjvQpI!jR2xxj01T{O0PYKHRk1L$q}dgDGAT@3u0*>zK?r@0{YtJ9*_uNjdXupK z4s9xES)aLR_0h1ZU}R*VoQZxs6HWGo%FutEjc4 z%LQ@@!@6N@l9eGeUSb;pGRf)jZVKKyQ}OW47lCsFuSL|EqXnSaz#McTZVJ)Vn{X;g z=5c6E>#c^+A9dyN_|_j~9zH~2Hb%cux-&%(MbMa`(Sk>f3)7UdPb-@ecl^fQ8h-*a zxc!Z8U}Gak-dVCL*QPUX>g0|$Cr2q(La(@~-XdB-!Amt^`eZFWy^ptht|zi}Pqthz zW=Ow@BMo|gvV+8PK+{m+dR0k}e%Q$a;(G}A`N1-^>F^n{BMtDef2taU`g2;rx^PE!@*GV3m@Xqp>Xx-H|RU7sUOv|gs@WPF^2@+#kF$p!XVUa z&ZF%FTS1vOq1k9EqPC_K(O8~YtsBAq2F(M&%c=n@?$61 zZv+Uy`;bUF8n!<6$}6P%7HE}wv2=(+Kc;eo&M9|T^Y6bP7JKyEMCh~Ixy0=;|o zDvQJJ_>~;tk%X`Yrce0^&;L`ce-a~+lpxE%UDHM+s$VIMZTU}YKy*WmLU*P|4!5q~ zhu~}#bPQ(e;lXo=-JTQcy~V*~V39A^2RrjkXC$sX5EU}bIfoeuy(Kz0Thu#?Dx`V> zi=p%BDHJT}POOqk!xpJtQv0@B;^Ug7%F?J*ni%Jr{vdTRLf1FB*~eTfB~=sMfWb1I zJpS2kI?>(tYD_GGX3R}{hAO!9f-?2o5)x_Sb8bwjhW)x$g4wze5y%P~mnt?#1~M{7 zfgM1HeiCm?7{N_QLeg2`;oM?RZN}}c%Z3!cZpB(@PomhhFF-2b`Kov*{O`mR;vGK3 zhj)9n5Bq#i17bQ2?_SjPj|_R0CRNY#Ujx6-E|5_3cjaLBKd7AX|3u}y1u*?a<(OOP z>6mNj>uTBS*qPDT*_b<6(*Z!bwRZgedeZr=O=;|yR3{y3-{}(?m+mI-SZ~*~SRm^# zPEcA>6_+TOis(||-P{s>Xk}SybOqS!#nb##4=t_7Xp)#+a1Uz`bA_n)N5oBnmsf^N zO=H&4dSA$#7(-EG&9WTTc(#xx5hzED!q;rCq9xK}j&Qj)ZpAtB5*9yUi|?|Dp;s^L z5kh?lE>cZHBj=gLcAz0H&Ovs_#3+AB_UI6w=a|y>%A!tP4;zWfKW(km`g{k@Yu=-t z$k{S+%+H`u+S8jn{$ig0#(sc8nplJh6kCEtk4CXZmwS2<9r+cLbTY$pZYWNZyebX0;Hn z;0w?51AF>YU(5L8+xSnpJuNz(B*dZ%8#uL~_ri6hOm;Q4Vz=MFW#bh$Ihd$XML>fa zRASF7^=C#GdCTLq09I^d4|%X$iItiy-LhbzBJGr~1}DGqK6Fs6@0F5OSPAi!uU`T& z35qE?G+d@h(iZe6{COtiQv#JCTa)bqCcvEFTi+VMqfSI4+Zb>TRi**1UV(-^u2vpS z8*1|KY3(ZP@_4jn{GM6r_yfLE-&JmPTQ{dbK{8hN`8-|wVFvs<9dqbeQ=SiNs36iy zFAMb)0jOTmCF~EYBYFrOz61dPP9gu3)e!&~f3rF(TO%Dy6IUI36D!NVf6YM)0I|vv ztNUxC`?bmOij~xo5Kg;I^Efy*#>ZsTOz05-J`&KkvN^{ay{bOaCVP_=aL3*T6*{vb zDT{^{w>hAA=8NJ9H#9ded0dNATqzGUH~I)pj+@+&7PVZ*9z>C}PB*Du>5u%9LH%1} zx1YP~B|8gFNoVtz%lDgZ3Jpw>3Gu158edI#b{i-T`_rirHXPsGWSlEXd9Y6i)y(>ySsV)iZ5xxQ;N=w2} z$rawSmSpgac5+E%2r^z&vQZwW-KblzF^aw#??n@^QCb%{k+TKsB!$OPl!8l>$MeyiZoDV@CKNKcGc(C&F;(#1F-|jI zv2L$ssK({ore#%GbCz}~QL}c_)Kv_*Bu;tqteX9itJtoho~(Di(hc?XlR077ZdU&h zTFH!@4==rE=Tw16a~k@+O$tKkRz>>g@KIp`>8;sy?`D?gy@TWK`Q?02C*44V!+vJ+Aw%u#G`UI=F6csFU)3D?q?`0hF# z=D?{?>Poy<>k4w+8pVCypXdcA)oO5Ry=7~+^@k}=Y5hE;y|NbpdUIP#bm<(QA5 zhrhS5Xj^C`0iS&~e>>WfKqXh7(>v%s5l(_9DAZW>224w{q`G>g|2U%8&1P0rQHuQw zLkHoSxy7pMlrODnc&cM!8X_&j zhF|X^f4~1<)xSgk5m*uW;+*Ee|98_l5rFwW239RchQG&F1^@^ZxK`o+EVllm)$#JF z)lx4er1eCdfOy?bx9Ct)DLPge60GDbhS202Bs#G>XD%+*e3;^!<>&*DQrz73rFQ_y=_&xtXjbI6N&!mnd)Ze?eO+)w!; zK4e~5^o!e4Gw3L53bYZ=bd^dkHb-vPs`oDED7CClo-%Sp{InPF)9J?n>FEl!I|e(( zEERRwqn=AnVD&2uw=FkBeYXQkp%QY%YQ17B^rP!`QpIW6S28L?))6a`7ISUT) ztx7FQ%(*ExKgq6oUdApjwD2M{>MTC2Or#ci9cB z;w@UMf&c7yxA5`FrFSGZG;9~1J5|26M{uVAS8-6u;uN+|JSqP}S`-CwEn_u%f|-(w zfW($q^jXh?whd>`0A@v8qQU_7_Oz(1uafsse!uSXr; zdoNTa+4zZoX%AR5)vXZ3MuAUY)D`OtlO~+A^9M76-PO?QbcCSU?#wjQyrz^GU4507# z+-A#y1iCee6L388QB?V=oj)g?DOjGmo+~c;dO%Yz-Sk^9OTex*Oj%AAn|J zuNU8kjcsbJ=def~ZpZvgl{*P9>zAvk(9c1tG^n3oYUHjw^X=Q0aLlw%bb_!}!Z~t? zj$beA8rWqgXTMz_zr8Z2fyNI`B^R8b(UfQB=tNu7pHtpvU?)xusyU2JAA;m3vDngG zZ4~h!yDc#9?>f4@GSfr38cBS4Y7(OTHBbEh9o}o948G=`Q((t>w$g8zG*3Es#mgF* zu|3#t9|U4#0A_)Zfe~fDCbIxg59Ia-uk?%wxL=b`9_>${%0PUA4^6-3#vo8vqJTr7 z$7dmy-*of_fzf{U`gFI4^;^C^P6ko}yoJVE`Dfz$^TWbd?>=1vK{m6^{=a#sv&S~6 z@x52S4H6xXchJRUXM$@fT_fH7+=1hDPq)*|kRJEPqgXGTN6i*RPt7L*YWOy-=`RQ4 zA)TjQb>5CbqlJr$C6g-lKZ0a!dO}^bl?(5b354O&QmyID8Hok=*VGx-4WFuBWHsOk8|y_DcJrCF`Tb8r9=`E-XZUJanLnT4LgVW#Jjvom?9sA@om&hWR?rdAg(4~;bjODwhC zZ$&n=`}!TVW-?v+zThHbRtdi6d1Y42)b(g^mKN2P%M5i_IZN2RBe9Q%4$p(RVyxXh z9!oGdOEsX2d0`we@2;lkBC`~#FMu0dfr$BoSc zL4N$D(}QEep#ei;Z%Y_~L;l#Tc@Bs*U1NruV~mzG2Iibjcl$X-)HD>r#}%@zrfyoS znvv6yz=eRt=oWK zx=t=TjY++VOoZqhB`(V=ffJDT%XLleQU_e@qLN(4XSux}w#FVh2q1l2Lop zM0X<#04{cIJIqhUW_RdowftqhV<~ZMyC>CI2#}=e=zai4c1$HI+u;h8G4euTa=0dC zGN4Ao(}ulfr4V_TVNMW#c8O_q_29&Fh4j6A``Dn?oqxfVNgHsDwDUx_Z4?S_7So zM)kMJVP%Zvq$c@F81RS5;nB@qy&!UMdHjTGaznxNG%T)W>Cy!nbz+3%T&_`$#(!;n z)aXc&u+e+hsepvs+r9Zqt90R^Tk}-i!0VK^{jwCt1PaOr3VvVpY;!;Q7drL^C|FoZ z?k8+OL&?>gISQ~t==4!?2NV;LSXj`|BJquVi$2SoPn4BE-W1HOCQ?6_a14v3$>yI- z6lvDHZ*-@VpJb-_mKrkcWE(N)&-u}FAYxMMQW*$bc#!~u{5>c?>4NV8fu~0z#dP<# z2OKCsEN>(OIJcl7lq+DJtpG-iu+d6+0iQ zX{_|QbwiFu`-#kY;8YRdR^2Gey{|%EN|bmA?_Woc#~eGNaB@EnzL``P{L zV)dMYMeG$v`UwlWZq>!u2dUlb6nLqJZ(RBOrc#e5f9}kj$Yt*w zc(c$O@3{yk#VEA4F?G(gwdowR8n?~4fum8j1=(Hk;`KS5(HQ2pshpMM2f{3d?5Fc9 z!VCbHB>0XOleP29{WguZlOugc(&eJxy+-+M%CQCod3xan!Atr#0)qga!1QPVorK>f zdK9SjTOjn=IO^{L#lX)VfnSJWx_o~@)j^eQGG`yfMnQmlaIOG&0Xe6?XDweE* zickc01ty*zVF)M$NOGK?>^)Cc`Y!#T5oAXv*QqyO8+ah!DS=#a8HNuo(sn{D7**Xv zMRU3BKIXRY`>~B{e3el78bWAwe&Di^svU-${7@K;bwJu@Bo?*b}?#G^#N__)U7v28Eg z56gJQoxPWG#qYQ^Z-8IVCsj;qSA~))s2t+Y$|n|huY}5CGWhREwzi@U~aF&unfr& z;PBoW+~&k@q?vD`0hvP_ksR;ytPHW$?4CrIsZ{HYP(0f@Qg_3-L&stu(<1sk+|xds zxeJ_b`M)6ic)jVfq6wMY#UWn(5b|Sp>@e7O-)8x7^}hf`*