Причина редиректа: `FastAPI(root_path="/")` — путь маршрута для `GET /` после вычитания root_path пуст, монтирование SPA его не находит, и роутер Starlette отвечал 307 на `//`. Теперь `root_path` нормализуется (`/` → "", хвостовой `/` снимается) — `_normalize_root_path` существовал, но не применялся. `SPAStaticFiles` для путей `api/...` сверяется с маршрутами приложения: путь совпал, метод нет (`Match.PARTIAL`) — 405 с заголовком `Allow` и русским `detail`; иначе — прежний 404 (dev-маршруты при выключенных флагах — 404). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
605 lines
54 KiB
Markdown
605 lines
54 KiB
Markdown
# arm-core — ядро АРМ КА
|
||
|
||
Ядро автоматизированного рабочего места кредитного аналитика Департамента крупного бизнеса (АРМ КА, АО «Россельхозбанк»):
|
||
справочники, файлы и документы, приём результатов распознавания, вычисления консолидации (КФО ГСК), маршрут
|
||
согласования, Единое хранилище (ЕХ), аудит и права. **Единственный писатель в PostgreSQL**: сервисы распознавания
|
||
(`tabular-parser`, `scan-gateway`) видят только NATS и S3.
|
||
|
||
Монорепозиторий по шаблону команды «Шаблон FastAPI»: `api/` — бэкенд (FastAPI, SQLAlchemy 2 async, Alembic,
|
||
PostgreSQL 16, core NATS без JetStream), `web/` — фронтенд (React + TypeScript, rsbuild).
|
||
|
||
Этот репозиторий — часть workspace АРМ КА. Общий контекст, модель данных, правила предметной области и контракты —
|
||
в соседних репозиториях:
|
||
|
||
- [`../CLAUDE.md`](../CLAUDE.md) — инварианты и процесс работы всего workspace;
|
||
- [`../docs/`](../docs/) — контекст (`context/`), модель данных (`model/`), контракты (`contracts/`), окружение (`dev/`);
|
||
- [`CLAUDE.md`](CLAUDE.md) — соглашения этого репозитория.
|
||
|
||
Срез 0 («скелет ядра»): схема БД по DBML, справочники (seed), аутентификация и права, `GET /api/v1/me`,
|
||
`/healthcheck`. Спецификация — `openspec/changes/slice-0-skeleton/`.
|
||
|
||
Срез 1 («справочники»): схема по DBML v5.14, API ГК / ЮЛ / ГСК/ГСЗ с политикой видимости, импорт из ЕЦП CRM,
|
||
администрирование пользователей и ролей, экран `/admin`, аудит. Спецификация — `openspec/changes/slice-1-directories/`;
|
||
разделы ниже: «API справочников», «Импорт из ЕЦП CRM», «Администрирование и экран /admin», «Ошибки API».
|
||
|
||
## Предварительные требования
|
||
|
||
- **Python 3.11** (версия платформы) и **[`uv`](https://docs.astral.sh/uv/)** — `make venv` создаёт окружение и ставит
|
||
зависимости через `uv`; без него цель не работает. Системный `python3` другой версии не подойдёт: `uv` сам подберёт
|
||
интерпретатор 3.11.
|
||
- **Docker** и compose v2 — для PostgreSQL, NATS и MinIO (compose лежит в `../docs/dev/`).
|
||
- **Node.js** и `npm` — только для `web/`. Для запуска бэкенда не нужны.
|
||
- `make`, `curl`.
|
||
|
||
Репозитории workspace должны лежать рядом: `arm-core/` и `docs/` — соседние каталоги (так же устроено зеркало
|
||
`.wt/<срез>/`). Пути `../docs/...` ниже и в `Makefile` рассчитаны на это.
|
||
|
||
## Быстрый старт
|
||
|
||
Путь «с нуля до зелёного `/api/v1/me`». Команды `make` выполняются из корня репозитория.
|
||
|
||
### 1. Виртуальное окружение
|
||
|
||
```bash
|
||
make venv
|
||
```
|
||
|
||
Создаёт `api/.venv` (Python 3.11) и ставит `api/requirements.txt` и `api/requirements-dev.txt`. Пакеты `raisa_*`
|
||
из Nexus банка вне банка недоступны, поэтому цель пропускает строки `raisa_*` (подробнее — «Работа без Nexus»).
|
||
Повторный запуск безопасен.
|
||
|
||
### 2. Инфраструктура (PostgreSQL, NATS, MinIO)
|
||
|
||
Файл `.env` compose — отдельный от `api/.env`; он создаётся один раз и в git не попадает:
|
||
|
||
```bash
|
||
cp ../docs/dev/.env.example ../docs/dev/.env
|
||
make up # docker compose -f ../docs/dev/docker-compose.yml up -d
|
||
```
|
||
|
||
`make up` поднимает `postgres` (5432), `nats` (4222, мониторинг 8222), `minio` (9000, консоль 9001) и одноразовый
|
||
`minio-init`, который создаёт бакет `arm-ka` и завершается с кодом 0. Команда возвращается сразу, а PostgreSQL становится
|
||
готовым примерно через 6 секунд: **дождитесь `healthy`** у трёх долгоживущих сервисов, прежде чем выполнять `make migrate`:
|
||
|
||
```bash
|
||
docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio # возвращается, когда все healthy
|
||
docker compose -f ../docs/dev/docker-compose.yml ps # проверка статуса
|
||
```
|
||
|
||
(голый `--wait` без списка сервисов возвращает код 1 из-за завершающегося `minio-init`; поэтому `make up` для бакета
|
||
и `--wait` со списком — вместе). Подробности, смена портов, сброс — [`../docs/dev/README.md`](../docs/dev/README.md).
|
||
|
||
### 3. Настройки приложения
|
||
|
||
```bash
|
||
cp api/.env.example api/.env
|
||
```
|
||
|
||
Значения из `api/.env.example` уже совпадают с `../docs/dev/.env.example` (стандартные порты и учётные данные compose),
|
||
менять ничего не нужно. Менять `api/.env` придётся, только если вы изменили порты или пароли в `../docs/dev/.env`
|
||
(правьте обе части). `api/.env` в git не попадает.
|
||
|
||
### 4. Миграции и справочники
|
||
|
||
```bash
|
||
make migrate # alembic upgrade head: схема dkb, 27 enum-типов, 43 таблицы
|
||
make seed-dev # справочники продукта + dev-пользователи (по одному на каждую роль)
|
||
```
|
||
|
||
`make seed` загружает только справочники (роли, права, филиалы, план счетов, правила элиминации, чек-лист), без
|
||
dev-пользователей. Seed идемпотентен: повторный запуск обновляет данные и ничего не дублирует. `make seed-dev`
|
||
(`--dev`) работает только при `DEBUG=true` в `api/.env`; при `DEBUG=false` загрузчик отказывает с кодом 2
|
||
(«`--dev` доступен только при DEBUG=true (dev-пользователи не для прода)»), не трогая БД.
|
||
|
||
### 5. Запуск
|
||
|
||
```bash
|
||
cd api && .venv/bin/python run_server.py
|
||
```
|
||
|
||
Сервер слушает `127.0.0.1:8000` (`SERVER__HOST`, `SERVER__PORT` из `api/.env`). Для dev-запуска оставляйте loopback:
|
||
при `DEBUG=true` и `AUTH_DEV_ISSUER=true` эндпоинт dev-токенов выдаёт токен любому, кто знает email пользователя, —
|
||
не открывайте его в сеть. Дальнейшие команды — из другого терминала.
|
||
|
||
### 6. Проверка
|
||
|
||
```bash
|
||
curl -s localhost:8000/healthcheck
|
||
```
|
||
|
||
```json
|
||
{"uptime":"2026-10-03T18:37:15.121394+00:00","version":"1.0.0","nats":"ok"}
|
||
```
|
||
|
||
`nats`: `ok` — подключён, `down` — включён (`NATS_ENABLED=true`), но связи нет, `off` — выключен.
|
||
|
||
Dev-токен по email любого dev-пользователя и профиль текущего пользователя:
|
||
|
||
```bash
|
||
curl -s -X POST localhost:8000/api/v1/dev/token \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"email":"ka_rf@armka.local"}'
|
||
# {"access_token":"<JWT>","token_type":"bearer"}
|
||
|
||
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer <JWT>"
|
||
```
|
||
|
||
```json
|
||
{
|
||
"id": "b8d7595a-787f-4bcb-8e93-7612cc8afba3",
|
||
"email": "ka_rf@armka.local",
|
||
"full_name": "Dev: Аналитик РФ",
|
||
"regional_office": {
|
||
"id": "3b38e823-e09a-47ce-a296-061948a200b0",
|
||
"name": "Ростовский региональный филиал",
|
||
"is_head_office": false
|
||
},
|
||
"business_segment": "DKB",
|
||
"roles": ["KA_RF"],
|
||
"permissions": [
|
||
"CONSOLIDATION_EDIT", "CONSOLIDATION_VIEW", "DIRECTORY_EDIT", "DIRECTORY_VIEW", "DOCUMENT_EDIT",
|
||
"DOCUMENT_VIEW", "EXPORT", "EX_VIEW", "KFO_EXPORT", "KFO_VIEW"
|
||
]
|
||
}
|
||
```
|
||
|
||
(идентификаторы в вашей БД будут другими). Токен действует 8 часов. Интерактивная документация API — `/docs`
|
||
(Swagger UI), схема — `/openapi.json`; обе — только при `DEBUG=true` (в проде их нет, 404).
|
||
|
||
Dev-пользователи (`api/seed/dev/users.yaml`, создаются `make seed-dev`):
|
||
|
||
| Email | Роль | Филиал |
|
||
|---|---|---|
|
||
| `ka_rf@armka.local` | `KA_RF` | Ростовский региональный филиал |
|
||
| `rka@armka.local` | `RKA` | Ростовский региональный филиал |
|
||
| `client_manager@armka.local` | `CLIENT_MANAGER` | Краснодарский региональный филиал |
|
||
| `ka_go@armka.local` | `KA_GO` | Головной офис |
|
||
| `curator_go@armka.local` | `CURATOR_GO` | Головной офис |
|
||
| `expert@armka.local` | `EXPERT` | Головной офис |
|
||
| `admin@armka.local` | `ADMIN` | Головной офис |
|
||
|
||
Права не зашиты в код роли: `effective_permissions(user)` — объединение прав ролей пользователя (таблицы `permission`,
|
||
`role_permission`).
|
||
|
||
### Что вы увидите в логе при запуске
|
||
|
||
Старт занимает около секунды; трейсбэков в логе нет. SMB (горячие папки ИС Сканирование из шаблона) необязателен и по
|
||
умолчанию выключен: в `.env.example` `OPENBAO__SMB__SMB_HOST` пуст, сессия SMB не регистрируется (сообщение об этом —
|
||
только на уровне DEBUG). Если `SMB_HOST` задан, регистрация выполняется в отдельном потоке (цикл событий не
|
||
блокируется; старт ждёт её завершения не дольше тайм-аута `smbclient`, 60 с); ошибка подключения логируется трейсбэком
|
||
`Ошибка при регистрации сессии SMB`, приложение продолжает работу.
|
||
|
||
Строки `Бакет проверен`, `Подключение к NATS установлено` и `Подписка NATS активна. subject=recognition.result,
|
||
queue=arm-core` означают, что S3 и NATS из compose подключены (MinIO из compose слушает `http`, поэтому в `.env`
|
||
задано `OPENBAO__MINIO__MINIO_SECURE=false`).
|
||
|
||
Недоступность S3 или NATS при старте приложение не роняет и не задерживает (недоступный SMB, если он настроен, — не
|
||
роняет): ошибка логируется, состояние NATS видно в `/healthcheck`. К NATS ядро подключается в фоне и повторяет попытки
|
||
само (`nats=down`, пока связи нет; warning в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва
|
||
nats-py переподключается и восстанавливает подписку.
|
||
|
||
При остановке ресурсы закрываются по шагам: фоновые задачи обмена → NATS → S3 → SMB → пул соединений БД. Ошибка
|
||
одного шага логируется (`Ошибка при остановке приложения. step=...`) и не отменяет следующие.
|
||
|
||
Предупреждение «Включён dev-эмитент токенов … только для разработки» появляется не при старте, а при первом
|
||
запросе к защищённому эндпоинту (`/api/v1/me`): проверяющий токены создаётся лениво (штатно, см. «Безопасность
|
||
dev-режима»).
|
||
|
||
### Остановка и сброс
|
||
|
||
```bash
|
||
docker compose -f ../docs/dev/docker-compose.yml down # остановить, данные (тома) сохраняются
|
||
docker compose -f ../docs/dev/docker-compose.yml down -v # полный сброс: БД и бакет с нуля (затем снова migrate и seed-dev)
|
||
```
|
||
|
||
Имя проекта compose фиксировано (`arm-ka`), тома общие для всех worktree.
|
||
|
||
## API справочников
|
||
|
||
Срез 1: справочники и их политика видимости. Все маршруты — под `/api/v1`, нужен токен (`Authorization: Bearer …`),
|
||
схема — `/openapi.json` (интерактивно `/docs`); состав маршрутов проверяет `api/tests/test_openapi_routes.py`.
|
||
Права — данные (`permission` × `role_permission`), код роли в логике не проверяется. Видимость записей решает
|
||
`src/domain/policy.py`: пользователь головного офиса видит всё, пользователь филиала — ГК своего филиала, их ЮЛ и
|
||
ГСК/ГСЗ и монозаёмщиков своего филиала. Объект вне видимости — **404 с тем же телом, что у несуществующего** (не 403).
|
||
|
||
| Метод и путь | Право | Что делает |
|
||
|---|---|---|
|
||
| `GET /directories/regional-offices` | `DIRECTORY_VIEW` | филиалы (включая Головной офис) |
|
||
| `GET /directories/company-groups` | `DIRECTORY_VIEW` | видимые ГК: `q` (имя/`crm_id`/код), `is_archived`, `limit`/`offset` |
|
||
| `POST /directories/company-groups` | `DIRECTORY_EDIT` | создать ГК (пользователь филиала закрепляет только за своим филиалом, иначе `OFFICE_NOT_ALLOWED`) |
|
||
| `GET /directories/company-groups/{group_id}` | `DIRECTORY_VIEW` | карточка ГК |
|
||
| `PATCH /directories/company-groups/{group_id}` | `DIRECTORY_EDIT` | изменить ГК (в т.ч. перезакрепление за филиалом) |
|
||
| `POST /directories/company-groups/{group_id}/archive`, `/unarchive` | `DIRECTORY_EDIT` | архивировать / вернуть из архива |
|
||
| `GET /directories/company-groups/{group_id}/history` | `DIRECTORY_VIEW` | история ГК, новые первыми |
|
||
| `GET`, `POST /directories/company-groups/{company_group_id}/related-groups` | `DIRECTORY_VIEW` / `DIRECTORY_EDIT` | ГСК/ГСЗ группы компаний (простой массив) / создать |
|
||
| `GET /directories/related-groups/{group_id}` | `DIRECTORY_VIEW` | карточка ГСК/ГСЗ с текущими членами |
|
||
| `PATCH /directories/related-groups/{group_id}` | `DIRECTORY_EDIT` | изменить ГСК/ГСЗ |
|
||
| `DELETE /directories/related-groups/{group_id}` | `DIRECTORY_DELETE` | удалить **пустую** ГСК/ГСЗ (`GROUP_NOT_EMPTY`) |
|
||
| `POST /directories/related-groups/{group_id}/members` | `DIRECTORY_EDIT` | добавить ЮЛ (только из той же ГК) |
|
||
| `DELETE /directories/related-groups/{group_id}/members/{legal_entity_id}` | `DIRECTORY_EDIT` | исключить ЮЛ (членство закрывается, не удаляется) |
|
||
| `GET /directories/legal-entities` | `DIRECTORY_VIEW` | видимые ЮЛ/ИП: `company_group_id`, `outside_group`, `inn`, `q`, `closed`, `limit`/`offset` |
|
||
| `POST /directories/legal-entities` | `DIRECTORY_EDIT` | создать ЮЛ/ИП (ИНН: ЮЛ — 10 цифр, ИП — 12) |
|
||
| `GET`, `PATCH /directories/legal-entities/{entity_id}` | `DIRECTORY_VIEW` / `DIRECTORY_EDIT` | карточка (с членствами и алиасами) / изменить |
|
||
| `POST /directories/legal-entities/{entity_id}/move` | `DIRECTORY_EDIT` | перевести в другую ГК с датой `effective_date` ≥ сегодня; членства ГСК/ГСЗ закрываются |
|
||
| `POST /directories/legal-entities/{entity_id}/withdraw` | `DIRECTORY_EDIT` | вывести из ГК: `CLOSE` (с `close_date`) или `REMOVE_FROM_GROUP` |
|
||
| `GET /directories/legal-entities/{entity_id}/history` | `DIRECTORY_VIEW` | история ЮЛ/ИП, новые первыми |
|
||
| `POST /directories/legal-entities/{entity_id}/aliases` | `DIRECTORY_EDIT` | добавить алиас (`normalized_alias` по §3) |
|
||
| `DELETE /directories/legal-entities/{entity_id}/aliases/{alias_id}` | `DIRECTORY_DELETE` | удалить алиас |
|
||
| `POST /directories/import` | `DIRECTORY_ADMIN` | импорт выгрузки ЕЦП CRM (см. ниже) |
|
||
| `GET /admin/users`, `POST /admin/users`, `PATCH /admin/users/{user_id}`, `PUT /admin/users/{user_id}/roles` | `USERS_ADMIN` | пользователи и их роли (см. «Администрирование») |
|
||
| `GET /admin/roles`, `GET /admin/permissions`, `PUT /admin/roles/{code}/permissions` | `USERS_ADMIN` | матрица «роль × право» |
|
||
|
||
Списки верхнего уровня (ГК, ЮЛ, пользователи) отдают страницу `{items, total, limit, offset}` (`limit` 1–200, по
|
||
умолчанию 50); список ГСК/ГСЗ одной ГК — простой массив. Каждая мутация пишет `audit_event` в той же транзакции, что и
|
||
сама правка; повтор того же состояния ничего не пишет. Примеры:
|
||
|
||
```bash
|
||
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
|
||
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
|
||
curl -s 'localhost:8000/api/v1/directories/company-groups?q=бисквит' -H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
## Импорт из ЕЦП CRM
|
||
|
||
`POST /api/v1/directories/import` (multipart) загружает справочник из выгрузки ЕЦП CRM: ГК, ЮЛ/ИП, филиалы и членство
|
||
в ГСК/ГСЗ. Нужно право `DIRECTORY_ADMIN` (seed: `ADMIN`). Алгоритм — `../docs/context/05-domain-rules.md` §13
|
||
(§13.1 формат файла, §13.2 «строка → решение», §13.3 транзакция и отчёт), чистое ядро — `src/domain/directory_import.py`.
|
||
|
||
**Порядок работы.**
|
||
|
||
1. Сначала **пробный прогон**: `dry_run=true` (это значение по умолчанию). Прогон идёт тем же кодом, но транзакция
|
||
откатывается; в БД, аудит и S3 ничего не попадает, отчёт тот же, что будет при записи.
|
||
2. Прочитайте отчёт: счётчики и построчный список `lines` (`row`, `inn`, `code`, `message`). Пропущенные строки
|
||
(`errors`) исправляются в файле и прогоняются заново.
|
||
3. Когда отчёт устраивает — тот же файл с `dry_run=false`. Один импорт — одна транзакция; результат — отчёт в ответе,
|
||
`audit_event(DIRECTORY_IMPORT)` с отчётом в `after_value` и файл в S3 (`uploads/imports/{audit_event_id}/{имя}`).
|
||
4. Повторный импорт того же файла при `on_existing=skip` идемпотентен: все ранее принятые строки получают коды `*_EXISTS`;
|
||
ранее пропущенные (ошибочные) строки остаются ошибками.
|
||
|
||
```bash
|
||
# шаг 1: пробный прогон (dry_run по умолчанию true, on_existing по умолчанию skip)
|
||
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||
-F file=@ecp-export.xlsx -F dry_run=true -F on_existing=skip
|
||
# шаг 3: запись
|
||
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||
-F file=@ecp-export.xlsx -F dry_run=false -F on_existing=update
|
||
```
|
||
|
||
**Формат файла (§13.1).** xlsx (первый лист) или csv (UTF-8, разделитель `;` или `,`); формат выбирается по расширению
|
||
имени. Заголовки — первая непустая строка, сопоставляются без регистра и пробелов, `ё` = `е`; порядок колонок не важен,
|
||
лишние игнорируются.
|
||
|
||
| Колонка | Обязательна | Смысл |
|
||
|---|---|---|
|
||
| `ИНН` | да | пробелы убираются; ЮЛ — 10 цифр, ИП — 12 |
|
||
| `iD ГК` | да | `company_group.crm_id` |
|
||
| `Группа компаний` | да | имя ГК |
|
||
| `Организация` | да | имя ЮЛ/ИП |
|
||
| `Тип` | да | `ЮЛ` или `ИП` (без регистра) |
|
||
| `Базовый РФ ГК в CRM` | нет | филиал по точному имени |
|
||
| `ГСК`, `ГСЗ` | нет | флаги «входит в ГСК» / «входит в ГСЗ» |
|
||
|
||
Нет любой обязательной колонки — файл отвергается целиком (422 `MISSING_COLUMNS` с перечнем в поле `missing`).
|
||
|
||
**Параметр `on_existing`.** `skip` (по умолчанию) — существующие ГК/ЮЛ не меняются; `update` — обновляются имя и
|
||
филиал ГК и имя/тип ЮЛ (со снимками в историю и аудитом).
|
||
|
||
**Коды отчёта (§13.2).** У каждой строки ровно один итоговый код, плюс сопутствующие события той же строки.
|
||
Пропуск строки (учитывается в `errors`): `UNSUPPORTED_TYPE`, `INVALID_INN`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`,
|
||
`GK_NAME_CONFLICT` (разные имена одной ГК в файле или пустой `iD ГК`). Итоговые: `LE_CREATED`, `LE_UPDATED`,
|
||
`LE_EXISTS`. Сопутствующие: `RF_CREATED`, `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS`, `GSK_CREATED`, `GSZ_CREATED`,
|
||
`MEMBER_ADDED`, `UNKNOWN_FLAG`.
|
||
|
||
**Допущения (открытые вопросы, в коде помечены `ASSUMPTION(OQ-…)`, список — `../docs/context/07-open-questions.md`).**
|
||
|
||
- OQ-87: типы вне {ЮЛ, ИП} (`нерез.`, `ФЛ`, `нет инн`, пусто) пропускаются — `UNSUPPORTED_TYPE`.
|
||
- OQ-88: ИНН проверяется только по длине и составу (ASCII-цифры), контрольные разряды не считаются; нечисловые
|
||
значения (`LV…`, `РНН…`) — `INVALID_INN`.
|
||
- OQ-89: филиала из файла нет в справочнике — он создаётся как обычный (`is_head_office=false`), в том числе «ЦКБ» и
|
||
«Головная организация» (`RF_CREATED`).
|
||
- OQ-90: ЮЛ, которое уже состоит в другой ГК (или вне ГК), импорт не переносит: `LE_IN_OTHER_GROUP`, перевод — только
|
||
`POST …/move`. Сообщение называет имя этого ЮЛ из другой группы — импорт доступен только `DIRECTORY_ADMIN`, который
|
||
видит весь справочник.
|
||
- OQ-91: флаги ГСК/ГСЗ создают не более одной ГСК и одной ГСЗ на ГК с именем ГК; отсутствие флага членство не закрывает.
|
||
|
||
**Особенности, о которых стоит знать.**
|
||
|
||
- **ИНН — только текстом.** Число из ячейки xlsx теряет ведущий ноль (`0123456789` → `123456789`) и получает
|
||
`INVALID_INN`; в Excel задавайте текстовый формат колонки (или используйте csv).
|
||
- Пустые «Организация» или «Группа компаний» при непустом `iD ГК` не отвергаются: создаётся запись с пустым именем
|
||
(допущение, правила на пустое имя в §13 нет).
|
||
- У **существующей** ГК при `skip` новый филиал из файла всё равно создаётся (`RF_CREATED`), но ГК не меняется.
|
||
- Архивные ГК входят в снимок справочника; `update` не возвращает ГК из архива.
|
||
- Пустые строки пропускаются, но входят в счётчик строк `MAX_IMPORT_ROWS`.
|
||
|
||
**Пределы.**
|
||
|
||
| Предел | Значение | Результат при превышении |
|
||
|---|---|---|
|
||
| `IMPORT_MAX_BYTES` (настройка) | 20 МиБ | 413, файл не читается в память целиком |
|
||
| `MAX_IMPORT_ROWS` | 100 000 строк листа/записей csv, **включая пустые** | 422 `UNSUPPORTED_FILE` |
|
||
| `MAX_UNCOMPRESSED_BYTES` | 200 МиБ суммарно по частям xlsx (zip), проверяется до разбора | 422 `UNSUPPORTED_FILE` |
|
||
|
||
**Требование к ingress / reverse proxy (M-1).** FastAPI/Starlette разбирают multipart-тело **до** аутентификации: файл
|
||
сбрасывается во временный файл на диске ещё до ответов 401/403 и до проверки `IMPORT_MAX_BYTES`. Поэтому лимит тела
|
||
запроса должен ставить ingress: `client_max_body_size` (или аналог) не меньше 20 МиБ — только для пути
|
||
`/api/v1/directories/import`, а для остальных путей — небольшой (например, 1 МиБ).
|
||
|
||
Остальные причины `UNSUPPORTED_FILE`: расширение не `xlsx`/`csv`, повреждённый xlsx, csv не в UTF-8.
|
||
|
||
## Администрирование и экран /admin
|
||
|
||
Бэкенд — `/api/v1/admin/*`, все маршруты требуют `USERS_ADMIN` (без права — 403 до любых проверок тела и существования
|
||
объектов). Фронтенд — страница `/admin` («Пользователи и роли» в боковом меню; видна только с `USERS_ADMIN`): таблица
|
||
пользователей (создание, смена ролей и активности) и вкладка «Матрица прав» (роль × право). Без права экран показывает
|
||
«Нет доступа» и запросов к `/admin/*` не делает (настоящую защиту обеспечивает сервер).
|
||
|
||
Форма создания пользователя берёт список филиалов из `GET /directories/regional-offices` (право `DIRECTORY_VIEW`).
|
||
Матрица прав на этом же экране позволяет снять `DIRECTORY_VIEW` с роли администратора — тогда форма получит пустой
|
||
список / «недостаточно прав»; держите `DIRECTORY_VIEW` у роли администратора.
|
||
|
||
**Как запустить.**
|
||
|
||
```bash
|
||
make migrate && make seed-dev # схема, справочники и dev-пользователи
|
||
make up # если инфраструктура ещё не поднята
|
||
cd api && .venv/bin/python run_server.py # бэкенд, :8000 (в api/.env — DEBUG=true и AUTH_DEV_ISSUER=true)
|
||
cd web && npm install && npm run start # фронтенд (rsbuild dev), откроется браузер
|
||
```
|
||
|
||
Вход — dev-форма «Вход для разработки» (`web/src/auth/DevLogin.tsx`): введите `admin@armka.local`; форма вызывает
|
||
`POST /api/v1/dev/token`, токен хранится в `sessionStorage` вкладки. В production-сборке формы нет — вход через SSO
|
||
РАИСА (параметры JWT — открытый вопрос).
|
||
|
||
**Что нужно знать.**
|
||
|
||
- **Роли и права — данные.** Права роли правятся матрицей (`PUT /admin/roles/{code}/permissions`), набор ролей
|
||
пользователя — `PUT /admin/users/{user_id}/roles`; код роли нигде в логике не проверяется. Новых прав и ролей через
|
||
API создать нельзя: неизвестный код — 422 `UNKNOWN_PERMISSION` / `UNKNOWN_ROLE` (неактивная роль не назначается).
|
||
- **`LAST_ADMIN` (409).** После операции хотя бы у одного активного пользователя должно остаться право `USERS_ADMIN`
|
||
через активную роль. Защищены: деактивация пользователя (в том числе себя), замена его ролей и снятие права с роли;
|
||
параллельные запросы двух администраторов идут по очереди (блокировка строки права).
|
||
- **Email.** В теле запроса — `EmailStr`, поэтому адрес с зарезервированным доменом `*.local` (`x@corp.local`) через
|
||
API и UI **отклоняется** (422); dev-пользователи `*@armka.local` создаёт `make seed-dev` напрямую в БД. Для ручной
|
||
проверки заводите пользователей с `example.com`. Email уникален без учёта регистра (409 `EMAIL_EXISTS`), хранится как введён.
|
||
- **Повторный seed возвращает снятые права.** `make seed` / `make seed-dev` идемпотентны и только **добавляют**
|
||
недостающие связи «роль × право» из `api/seed/core/role_permissions.yaml`: право, которое администратор снял с роли
|
||
в матрице, следующий запуск seed выдаст снова (`granted_by` — пусто); права, добавленные администратором сверх seed,
|
||
seed не отзывает. На стенде с правками матрицы seed повторно не запускайте (follow-up к M-8 Среза 0).
|
||
- **Профиль `/me` не перечитывается** после того, как администратор правит собственные роли или права: перезагрузите
|
||
страницу или войдите заново.
|
||
- **Имя и филиал ГК одним PATCH** пишут одно событие аудита `ASSIGNMENT_CHANGE` с обоими полями в `before`/`after`
|
||
(решение №35): выборка журнала по `GROUP_CHANGE` такое переименование не найдёт.
|
||
- **Один выбор роли — один запрос.** Мульти-выбор ролей в строке таблицы отправляет по одному `PUT …/roles` на каждое
|
||
нажатие, поэтому каждое нажатие — отдельное событие `audit_event(USER_ROLE_CHANGE)`.
|
||
- **Показываются первые 200 пользователей** (`limit=200`, максимум страницы); постраничного просмотра в UI пока нет —
|
||
остальных ищет `GET /admin/users?q=…` (по email/ФИО).
|
||
- **Аудит.** `USER_CHANGE` (создание, правка полей, деактивация) и `USER_ROLE_CHANGE` — раздел `USERS`;
|
||
`ROLE_PERMISSION_CHANGE` — раздел `ACCESS`; `before_value`/`after_value` — прежнее и новое состояние.
|
||
|
||
## Ошибки API
|
||
|
||
Доменные ошибки (`src/common/exceptions.py`, обработчик — `src/core/exception_handlers.py`) отдаются объектом:
|
||
|
||
```json
|
||
{"detail": {"code": "ALIAS_TAKEN", "message": "Такой алиас уже закреплён за другим ЮЛ", "legal_entity_id": "…"}}
|
||
```
|
||
|
||
`code` — машинный код (по нему работает клиент), `message` — текст по-русски, дополнительные поля зависят от ошибки
|
||
(например, `legal_entity_id`, `related_group_id`, `permissions`, `roles`, `missing`). Остальные ответы — `{"detail": …}`
|
||
другой формы: 401/403/413 — строка, 422 валидации тела — массив ошибок pydantic, 409 нарушения целостности БД — строка.
|
||
Объект вне видимости — 404 `NOT_FOUND` с тем же телом, что у несуществующего.
|
||
|
||
**Гонки уникальных ограничений.** Если параллельный запрос успел вставить запись между проверкой сервиса и INSERT,
|
||
БД отклоняет вставку (`unique_violation`), и ответ тот же 409 с тем же кодом, что у проверки (реестр —
|
||
`src/common/constraint_conflicts.py`): `CRM_ID_EXISTS` (ГК и ЮЛ), `INN_EXISTS`, `ALIAS_EXISTS`, `EMAIL_EXISTS`,
|
||
`MEMBER_EXISTS`/`MEMBER_OF_OTHER_GROUP`. В теле гонки — только код и постоянный текст, без дополнительных полей
|
||
(`legal_entity_id`, `related_group_id`) и без значений ключей. Особенности: повторное добавление в ту же группу
|
||
отдаёт `MEMBER_OF_OTHER_GROUP` с нейтральным текстом «ЮЛ уже входит в группу этого вида» (его раньше срабатывает индекс
|
||
«ЮЛ + вид»); гонка за один алиас у разных ЮЛ (`ALIAS_TAKEN`) в БД не защищена (уникален только `(ЮЛ, алиас)`);
|
||
email без учёта регистра проверяет только сервис, поэтому гонка с другим регистром остаётся открытым ограничением.
|
||
Любое иное нарушение целостности (FK, check, неизвестное unique) — прежняя строка `нарушение целостности данных в БД`.
|
||
|
||
| Код | HTTP | Где |
|
||
|---|---|---|
|
||
| `NOT_FOUND` | 404 | объекта нет или он вне видимости |
|
||
| `OFFICE_NOT_FOUND` | 422 | филиал из тела запроса не существует (ГК, ЮЛ, пользователь) |
|
||
| `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом |
|
||
| `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр |
|
||
| `EFFECTIVE_DATE_IN_PAST` | 422 | дата перевода/вывода раньше сегодняшней |
|
||
| `EMPTY_ALIAS` | 422 | алиас пуст после нормализации (одни ОПФ и знаки препинания) |
|
||
| `DIFFERENT_COMPANY_GROUP` | 422 | ЮЛ не входит в ГК этой ГСК/ГСЗ |
|
||
| `UNKNOWN_ROLE` / `UNKNOWN_PERMISSION` | 422 | неизвестная или неактивная роль / неизвестный код права (поля `roles` / `permissions`) |
|
||
| `MISSING_COLUMNS` / `UNSUPPORTED_FILE` | 422 | файл импорта целиком (`missing` — перечень колонок) |
|
||
| `CRM_ID_EXISTS` | 409 | ГК или ЮЛ с таким `crm_id` уже есть |
|
||
| `INN_EXISTS` | 409 | ЮЛ с таким ИНН уже есть (без дополнительных полей) |
|
||
| `LEGAL_ENTITY_CLOSED` | 409 | перевод закрытого ЮЛ |
|
||
| `SAME_COMPANY_GROUP` | 409 | перевод в ту же ГК |
|
||
| `ALREADY_CLOSED` | 409 | ЮЛ уже закрыто |
|
||
| `NOT_IN_COMPANY_GROUP` | 409 | ЮЛ не входит в ГК (вывод невозможен) |
|
||
| `GROUP_NOT_EMPTY` | 409 | удаление ГСК/ГСЗ с текущими членами |
|
||
| `MEMBER_EXISTS` | 409 | ЮЛ уже входит в эту группу |
|
||
| `MEMBER_OF_OTHER_GROUP` | 409 | ЮЛ уже входит в другую группу этого вида (`related_group_id`) |
|
||
| `ALIAS_EXISTS` | 409 | у ЮЛ уже есть такой алиас |
|
||
| `ALIAS_TAKEN` | 409 | алиас закреплён за другим ЮЛ (`legal_entity_id`) |
|
||
| `EMAIL_EXISTS` | 409 | пользователь с таким email уже есть |
|
||
| `LAST_ADMIN` | 409 | операция оставила бы систему без активного `USERS_ADMIN` |
|
||
|
||
Файл импорта больше `IMPORT_MAX_BYTES` — 413 со строкой в `detail`. Недостаточно прав — 403 строкой
|
||
(`недостаточно прав: требуется <ПРАВО>`), нет токена или он неверен — 401. Под `/api/v1` SPA не отвечает: неверный
|
||
метод на существующий маршрут — 405 `{"detail": "метод не поддерживается этим маршрутом"}` с заголовком `Allow`
|
||
(например, `POST /api/v1/me`), несуществующий путь (и dev-маршрут при выключенных флагах) — 404 `{"detail": "Not Found"}`.
|
||
Все остальные пути — SPA: `GET /` и клиентские маршруты (`/admin`) отдают `index.html` без редиректов.
|
||
|
||
## Конфигурация
|
||
|
||
Настройки — pydantic-settings; читаются из переменных окружения и файла `api/.env`. Вложенные настройки
|
||
разделяются `__`. Файл окружения выбирается переменной `ENV_FILE` (по умолчанию `.env`, путь относительно текущего
|
||
каталога — `api/`; значение читается при импорте). Тесты используют `api/.env.test` (`pytest.ini`).
|
||
Полный пример — `api/.env.example`.
|
||
|
||
| Переменная | По умолчанию | Назначение |
|
||
|---|---|---|
|
||
| `ENV_FILE` | `.env` | файл окружения (относительно `api/`) |
|
||
| `DEBUG` | `false` | режим разработки. `true` необходим dev-эмитенту токенов и `make seed-dev`, включает документацию API (`/docs`, `/redoc`, `/openapi.json`) и CORS `*`; при `false` документации нет (404), CORS — из `CORS_ORIGINS`, пакеты РАИСА подгружаются условно |
|
||
| `CORS_ORIGINS` | пусто | при `DEBUG=false` — источники, которым разрешены кросс-доменные запросы: строка через запятую (`https://a.example.ru,https://b.example.ru`) или JSON-список. Пусто — кросс-доменные запросы запрещены (middleware CORS не подключается); `allow_credentials` — только для явного списка без `*`. При `DEBUG=true` не используется (всегда `*`) |
|
||
| `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес и порт сервера (`run_server.py`); в `.env.example` — `127.0.0.1` (dev-запуск только на loopback) |
|
||
| `ROOT_PATH` | `/` | корневой путь за прокси (должен совпадать с `REACT_APP_ROOT_PATH` фронта) |
|
||
| `VERSION` | `1.0.0` | версия в `/healthcheck` и OpenAPI |
|
||
| `OPENBAO__DATABASE__DATABASE_URL` | — (обязательно) | PostgreSQL, драйвер asyncpg: `postgresql+asyncpg://armka:armka@localhost:5432/armka` |
|
||
| `OPENBAO__MINIO__MINIO_URL` | — (обязательно) | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) |
|
||
| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | — (обязательно) | логин MinIO |
|
||
| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | — (обязательно) | пароль MinIO |
|
||
| `OPENBAO__MINIO__MINIO_BUCKET` | — (обязательно) | бакет MinIO (`arm-ka`) |
|
||
| `OPENBAO__MINIO__MINIO_SECURE` | `true` | `true` — MinIO по `https` (платформа), `false` — по `http` (MinIO из compose; так в `.env.example`) |
|
||
| `OPENBAO__NATS__NATS_URL` | — (обязательно) | адрес NATS (`nats://localhost:4222`) |
|
||
| `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без учётных данных (compose, тесты). Задаётся вместе с `NATS_JWT` или не задаётся вовсе |
|
||
| `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; см. `NATS_SEED` |
|
||
| `OPENBAO__SMB__SMB_HOST` | пусто | SMB из шаблона, необязателен (ядру в Срезе 0 не нужен). Пусто — сессия SMB не регистрируется; задан — регистрация при старте (в отдельном потоке) и сброс кэша соединений при остановке |
|
||
| `OPENBAO__SMB__SMB_PATH` | пусто | путь SMB (в Срезе 0 не используется) |
|
||
| `OPENBAO__SMB__SMB_LOGIN` | пусто | логин SMB |
|
||
| `OPENBAO__SMB__SMB_PASSWORD` | пусто | пароль SMB |
|
||
| `NATS_ENABLED` | `false` | подключение к NATS и обмен через `recognition_job`: фоновые outbox-публикатор и вахтёр сроков ответа (период 30 с, `processing`/`published` с прошедшим `reply_deadline` → `timeout`); без него фоновых задач нет, `/healthcheck` показывает `nats=off` |
|
||
| `OUTBOX_POLL_SECONDS` | `5` | период опроса outbox (`queued` → публикация → `published`) |
|
||
| `ACK_TIMEOUT_SECONDS` | `30` | ожидание ack сервиса распознавания до повторной публикации |
|
||
| `OUTBOX_MAX_REPUBLISH` | `3` | число повторных публикаций до перевода в `dlq` |
|
||
| `IMPORT_MAX_BYTES` | `20971520` (20 МиБ) | предельный размер файла импорта справочников; больше — 413 |
|
||
| `AUTH_DEV_ISSUER` | `false` | dev-эмитент токенов: работает **только** при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` |
|
||
| `AUTH_DEV_SECRET` | пусто | секрет HS256 dev-эмитента (не короче 32 байт); только для разработки |
|
||
| `AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` | пусто | проверка токенов РАИСА: JWKS по URL либо публичный ключ (PEM) |
|
||
| `AUTH_ISSUER`, `AUTH_AUDIENCE` | пусто | проверка `iss` и `aud` токена РАИСА |
|
||
| `AUTH_EMAIL_CLAIM` | `email` | claim токена с email пользователя (сопоставляется с `app_user.email`) |
|
||
|
||
Параметры JWT РАИСА (алгоритм, claim, эмитент) пока неизвестны — это открытый вопрос
|
||
([`../docs/context/07-open-questions.md`](../docs/context/07-open-questions.md)); `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY`
|
||
и связанные поля — каркас под него. Если не настроено ни dev-эмитента, ни ключей РАИСА, токены не принимаются вовсе
|
||
(`/me` отвечает 401).
|
||
|
||
Порты по умолчанию: приложение 8000; compose — PostgreSQL 5432, NATS 4222 (мониторинг 8222), MinIO 9000 (консоль 9001).
|
||
Порты compose меняются в `../docs/dev/.env`, тогда же — соответствующие `OPENBAO__*` в `api/.env`.
|
||
|
||
## Команды `make`
|
||
|
||
Все цели выполняются из корня репозитория (сами переходят в `api/`).
|
||
|
||
| Цель | Что делает |
|
||
|---|---|
|
||
| `make venv` | `api/.venv` на Python 3.11 через `uv`, зависимости без `raisa_*` |
|
||
| `make up` | поднимает compose из `../docs/dev` (PostgreSQL, NATS, MinIO, бакет) |
|
||
| `make migrate` | `alembic upgrade head` на БД из `api/.env` |
|
||
| `make seed` | загрузка справочников продукта (идемпотентна) |
|
||
| `make seed-dev` | то же и dev-пользователи/филиалы (`--dev`) — только для разработки, требует `DEBUG=true` |
|
||
| `make lint` | `ruff check`, `ruff format --check`, `mypy src scripts` |
|
||
| `make test` | юнит-тесты: `pytest -m "not integration"`, Docker не нужен |
|
||
| `make test-integration` | интеграционные тесты: `pytest -m integration`, нужны PostgreSQL (и NATS) |
|
||
| `make dbml-check` | сверка ORM с `../docs/model/data-model-v5.14.dbml` |
|
||
| `make migration-cycle` | миграции на временной БД: `upgrade head` → `downgrade base` → `upgrade head`, пустой autogenerate |
|
||
| `make check` | быстрая проверка без Docker: `lint` + `test` |
|
||
| `make docker-build` | сборка образа: `docker build -t arm-core .` |
|
||
| `make web-check` | фронт: `npm install`, `biome check src`, `tsc --noEmit`, `npm run build` |
|
||
| `make check-infra` | быстрая проверка, что PostgreSQL и NATS из compose доступны (иначе: `make up`) |
|
||
| `make check-full` | полная проверка перед PR: `check` + `dbml-check` + `check-infra` + `test-integration` (в нём цикл миграций) + `docker-build` + `web-check`; нужна поднятая инфраструктура (`make up`) и Docker |
|
||
|
||
|
||
### Тесты
|
||
|
||
- **Юнит-тесты** (`make test`) не обращаются ни к БД, ни к NATS, Docker не нужен.
|
||
- **Интеграционные** (`make test-integration`) требуют PostgreSQL, тесты обмена через NATS — ещё и NATS. Рабочую БД
|
||
`armka` они не трогают: на каждую сессию pytest создаётся свежая БД `armcore_pytest_<hex>`, мигрируется `alembic`
|
||
и удаляется в конце. Параметры подключения берутся из `api/.env.test` (`pytest.ini` задаёт `ENV_FILE=.env.test`).
|
||
Файл указывает на стандартные порты и учётные данные compose из `../docs/dev` (PostgreSQL `5432`, NATS `4222`,
|
||
MinIO `9000`), поэтому после `make up` тесты работают «из коробки». То же относится к `make migration-cycle` и
|
||
`make check-full`: они тоже читают `.env.test` через `pytest.ini`.
|
||
- **Реальные данные** — репозиторий `../fixtures/` (путь можно задать переменной `ARM_KA_FIXTURES`). Тесты с маркером
|
||
`fixtures` без каталога пропускаются с понятной причиной.
|
||
|
||
### Проверка ORM и DBML
|
||
|
||
Модель данных — `../docs/model/data-model-v5.14.dbml`. ORM обязана совпадать с ней один в один:
|
||
|
||
```bash
|
||
make dbml-check # пустой вывод и код 0 — совпадение; расхождения печатаются построчно
|
||
```
|
||
|
||
Другой путь к DBML: `make dbml-check DBML=<путь относительно api/>`.
|
||
|
||
### Миграции
|
||
|
||
```bash
|
||
make migrate # применить: alembic upgrade head
|
||
cd api && .venv/bin/alembic downgrade base # откатить до пустой БД
|
||
make migration-cycle # проверить цикл upgrade -> downgrade -> upgrade на временной БД
|
||
```
|
||
|
||
Ревизии лежат в `api/alembic/versions/`; автогенерация — только черновик, финал сверяется с DBML (`make dbml-check`).
|
||
|
||
## Раскладка репозитория
|
||
|
||
```
|
||
api/
|
||
run_server.py точка входа (uvicorn; хост и порт — из настроек)
|
||
alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14)
|
||
seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист)
|
||
seed/dev/ dev-данные: филиалы и пользователи по ролям
|
||
scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников)
|
||
src/
|
||
app.py FastAPI-приложение: lifespan (S3, NATS, SMB), /healthcheck, раздача SPA
|
||
api/endpoints/ эндпоинты (me.py, dev_auth.py, directories/, admin/); router.py собирает /api/v1
|
||
api/dependencies/ зависимости FastAPI (current_user, проверка прав)
|
||
core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка),
|
||
logger, exception_handlers
|
||
db/ движок и сессия (схема dkb)
|
||
models/ ORM-модели, 1:1 с DBML
|
||
repositories/ доступ к данным
|
||
schemas/ pydantic-схемы запросов и ответов
|
||
services/ бизнес-логика
|
||
common/ доменные ошибки (`{detail: {code, message}}`), часы
|
||
domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn
|
||
tests/ pytest (юнит и integration/fixtures по маркерам)
|
||
web/ фронтенд (React 18 + TypeScript, rsbuild, antd): api/ (клиент), auth/ (вход, профиль), layouts/Admin (/admin); сборка web/dist -> api/web/
|
||
setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh)
|
||
openspec/ спецификации и изменения (OpenSpec)
|
||
docs/superpowers/ планы реализации
|
||
raisa_config.yml ресурсы для платформы
|
||
Dockerfile только для локального compose
|
||
```
|
||
|
||
Слои: `src/api/endpoints` → `services` → `repositories` → `models`.
|
||
|
||
## Работа без Nexus
|
||
|
||
Пакеты `raisa_*` (Python) и `raisa-*` (npm) лежат в Nexus банка и вне банка недоступны. Что с этим сделано:
|
||
|
||
- `make venv` ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v '^raisa_'`) и поэтому требует `uv`, а не
|
||
`setup/setup.sh` (тот рассчитан на `pip.conf` с Nexus).
|
||
- Код импортирует `raisa_*` условно: пакет middleware РАИСА импортируется только при `DEBUG=false`, и `ImportError`
|
||
допустим — приложение работает без него. Аутентификация Среза 0 — собственная проверка JWT
|
||
(`api/src/core/auth.py`).
|
||
- В `web/package.json` пакеты `raisa-*` вынесены в `optionalDependencies`: `npm install` не падает, если Nexus
|
||
недоступен. Файл `.npmrc` с адресом реестра (см. `setup/setup.sh`) вне банка не нужен.
|
||
|
||
## Безопасность dev-режима
|
||
|
||
- `POST /api/v1/dev/token` выдаёт токен **любому**, кто знает email существующего активного пользователя: пароля нет.
|
||
Только для локальной разработки и тестовых стендов.
|
||
- Маршрут существует лишь при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`; при любом другом сочетании его нет вовсе
|
||
(404), а dev-проверяющий токенов не создаётся даже при заданном `AUTH_DEV_SECRET`. В проде `DEBUG=false`, поэтому
|
||
dev-эмитент там невозможен.
|
||
- `AUTH_DEV_SECRET` из `api/.env.example` — публичный пример, в проде не использовать. `make seed-dev`
|
||
(`python -m scripts.seed --dev`) при `DEBUG=false` отказывает (код 2), dev-пользователи в прод не попадут.
|
||
- Порты compose по умолчанию публикуются только на `127.0.0.1` (`BIND_ADDR`); пароли `armka`/`minioadmin` — только dev.
|
||
|
||
## Соседние документы
|
||
|
||
- [`CLAUDE.md`](CLAUDE.md) — соглашения этого репозитория;
|
||
- [`../CLAUDE.md`](../CLAUDE.md) — workspace: инварианты, источники истины, процесс;
|
||
- [`../docs/dev/README.md`](../docs/dev/README.md) — окружение разработки (compose), порты, типичные проблемы;
|
||
- [`../docs/context/03-architecture.md`](../docs/context/03-architecture.md) — архитектура, соглашения шаблона, NATS и S3;
|
||
- [`../docs/context/04-data-model-guide.md`](../docs/context/04-data-model-guide.md) и
|
||
[`../docs/model/data-model-v5.14.dbml`](../docs/model/data-model-v5.14.dbml) — модель данных;
|
||
- [`../docs/context/05-domain-rules.md`](../docs/context/05-domain-rules.md) — алгоритмы предметной области;
|
||
- [`../docs/context/07-open-questions.md`](../docs/context/07-open-questions.md) — открытые вопросы;
|
||
- [`../docs/context/09-delivery-plan.md`](../docs/context/09-delivery-plan.md) — план срезов;
|
||
- [`../docs/contracts/`](../docs/contracts/) — JSON-схемы обмена с сервисами распознавания;
|
||
- `openspec/changes/slice-0-skeleton/` — спецификация и решения Среза 0.
|