diff --git a/apps/ams-sync/.env.example b/apps/ams-sync/.env.example new file mode 100644 index 0000000..ba0a8a7 --- /dev/null +++ b/apps/ams-sync/.env.example @@ -0,0 +1,85 @@ +# ============================================================================= +# AMS-Sync — пример конфигурации (.env) +# Версия: 1.0.0 +# Скопируйте в .env и заполните значения. +# +# Сервис имеет два режима запуска: +# - `run` — демон-потребитель Kafka (CMD контейнера), использует блоки +# App / Kafka / Zitadel / Logger / Healthcheck (+ Auth); +# - CLI — команды migrate / sync / diff (запускаются вручную), +# дополнительно используют блоки Auth / User server / БД sarex. +# Значения по умолчанию проставлены там, где они заданы в коде. +# ============================================================================= + +# --- Приложение (без префикса) --- +# LOCAL | STAGE | PREPROD | PRODUCTION +ENVIRONMENT=LOCAL +# Топик Kafka с событиями пользователей +AMS_SYNC_TOPIC=ams-sync +# ID организации-хоста в Zitadel (обязателен) +HOST_ORGANIZATION_ID= +# При True входящие model_created обрабатываются через import_user (с верификацией) +VERIFY_USERS=False + +# --- Kafka (префикс KAFKA_) --- +# JSON-массив адресов брокеров, напр. ["host:9091"] +KAFKA_BOOTSTRAP_SERVERS= +KAFKA_SASL_PLAIN_USERNAME= +KAFKA_SASL_PLAIN_PASSWORD= +# Путь к CA-сертификату для SSL-подключения к Kafka +KAFKA_SSL_CAFILE= +# PLAINTEXT | SSL | SASL_PLAINTEXT | SASL_SSL +KAFKA_SECURITY_PROTOCOL=SASL_SSL +# PLAIN | SCRAM-SHA-256 | SCRAM-SHA-512 +KAFKA_SASL_MECHANISM=SCRAM-SHA-512 + +# --- Zitadel / AMS (префикс ZITADEL_) --- +# Хост Zitadel (обязателен для адаптера пользователей) +ZITADEL_HOST=https://idp.dev.stage.sarex.io +# Service-токен доступа к API Zitadel (обязателен) +ZITADEL_SERVICE_ACCESS_TOKEN= +# ВНИМАНИЕ: переменную читают сразу несколько классов с разным типом +# - адаптер пользователей ожидает int (минуты), напр. 10 +# - адаптеры организаций/грантов ожидают строку, напр. "10m" +# Задавайте int, иначе адаптер пользователей упадёт на разборе. +ZITADEL_TIMEOUT=10 +# Максимальный размер пачки при bulk-импорте +ZITADEL_MAX_BATCH=2000 +# Эндпоинты (обычно не переопределяются) +ZITADEL_USERS_MANAGEMENT_ENDPOINT=management/v1/users +ZITADEL_USERS_ENDPOINT=v2/users + +# --- Логирование (префикс LOG_) --- +LOG_LEVEL=INFO +# LOG_FORMAT=%(asctime)s [%(levelname)s]: %(message)s + +# --- TCP Healthcheck (префикс HEALTHCHECK_) --- +HEALTHCHECK_HOST=0.0.0.0 +HEALTHCHECK_PORT=8008 +HEALTHCHECK_MAX_CONNECTIONS=10 +HEALTHCHECK_REPLY=healthy + +# --- Auth: sarex-backend (префикс AUTH_) --- +# Используется для получения JWT под админом (режим run и CLI) +AUTH_HOST=https://stage.sarex.io +AUTH_ADMIN_USERNAME= +AUTH_ADMIN_PASSWORD= +AUTH_AUTH_ENDPOINT=api/token/ + +# --- Sarex user server (префикс USER_SERVER_, только CLI) --- +# Источник метадаты пользователя для миграции/синхронизации +USER_SERVER_HOST=https://stage.sarex.io +USER_SERVER_USERS_ENDPOINT=api/core/admin/users + +# --- База данных sarex-backend (префикс USER_DB_, только CLI) --- +# Источник пользователей/компаний для команд migrate/sync/diff +USER_DB_NAME=postgres +USER_DB_USER=postgres +USER_DB_PASSWORD=password +USER_DB_HOST=127.0.0.1 +USER_DB_PORT=5432 +# Имена таблиц (обычно не переопределяются) +USER_DB_USER_TABLE_NAME=base_baseuser +USER_DB_COMPANYUSER_TABLE_NAME=core_companyuser +USER_DB_COMPANY_TABLE_NAME=core_company +USER_DB_ITER_SIZE=10000 diff --git a/apps/ams-sync/CONFIGURATION.md b/apps/ams-sync/CONFIGURATION.md new file mode 100644 index 0000000..c034453 --- /dev/null +++ b/apps/ams-sync/CONFIGURATION.md @@ -0,0 +1,218 @@ +# Конфигурация проекта ams-sync +# Версия: 1.0.0 + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Что делает сервис + +`ams-sync` — утилита синхронизации пользователей sarex-backend с системой управления доступом (AMS) на базе **Zitadel**. У сервиса два режима работы: + +- **`run`** — фоновый демон-потребитель Kafka (`src/internal/app/service/sync_service.py`, точка входа `python3 cmd.py run`, это же `CMD` контейнера). Слушает топик событий пользователей и создаёт/обновляет пользователей в Zitadel. Параллельно поднимает TCP-healthcheck. +- **CLI-команды** (`migrate` / `sync` / `diff`, реализованы в `src/internal/app/console.py`) — разовые задачи миграции и синхронизации, запускаются вручную. Читают пользователей и компании напрямую из БД sarex-backend. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). В отличие от монолитного `AppSettings`, настройки разбиты на несколько независимых классов, каждый со своим префиксом: + +| Класс | Файл | Префикс | +| --- | --- | --- | +| `Settings` (приложение) | `src/internal/app/service/settings.py` | *(без префикса)* | +| `KafkaSettings` | `src/internal/app/service/settings.py` | `kafka_` | +| `Settings` (console) | `src/internal/app/console.py` | *(без префикса)* | +| `Settings` (logger) | `src/internal/adapter/logger/logger.py` | `log_` | +| `Settings` (healthcheck) | `src/internal/adapter/tcphealthcheck/healthcheck.py` | `healthcheck_` | +| `Settings` (auth) | `src/internal/adapter/auth/http/adapter.py` | `auth_` | +| `Settings` (user server) | `src/internal/adapter/users/http/adapter.py` | `user_server_` | +| `Settings` (zitadel users) | `src/internal/adapter/users/zitadel/adapter.py` | `zitadel_` | +| `Settings` (organizations) | `src/internal/adapter/organizations/adapter.py` | `zitadel_` | +| `Settings` (grants) | `src/internal/adapter/grants/adapter.py` | `zitadel_` | +| `Settings` (user repo) | `src/internal/repository/users/repository.py` | `user_db_` | +| `Settings` (company repo) | `src/internal/repository/companies/repository.py` | `user_db_` | + +Особенности разбора (`SettingsConfigDict`): + +- у каждого класса задан `env_file=".env"`, `env_file_encoding="utf-8"` и `extra="ignore"` — при наличии файла `.env` в рабочей директории он загружается автоматически, лишние переменные игнорируются; +- вложенного разделителя нет — каждая группа настроек читается отдельным классом по своему префиксу (либо без префикса для классов приложения); +- CLI-команды принимают флаг `--env-file=` (по умолчанию `.env`), который прокидывается в конструкторы настроек как `_env_file`; режим `run` всегда читает `.env`. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (CLI) | Файл `.env` (или указанный через `--env-file=`) в директории с `cmd.py` и/или переменные окружения процесса | +| Локально (контейнер) | `Dockerfile`: `CMD ["python3", "cmd.py", "run"]`; переменные пробрасываются в окружение контейнера | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `universal-chart.envs` (обычные значения) и `universal-chart.secretEnvs` (значения из k8s-секретов). Базовый чарт — `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна в `workflow.rules` (`RELEASE_NAME`, `STAND`, `CHART_*`, `IMAGE_NAME`, `HELM_SET_ARGS` и т.п.) | + +Команды CLI (`python3 cmd.py `, см. `README.md`): + +| Команда | Аргументы | Назначение | +| --- | --- | --- | +| `run` | — | Демон-потребитель Kafka + TCP-healthcheck (режим контейнера) | +| `migrate` | `--user-id`, `--with-password/--no-with-password` | Миграция пользователя(ей) из БД sarex-backend в Zitadel. Без `--user-id` мигрируются все (bulk) | +| `sync` | `--user-id`, `--company-id` | Синхронизация метадаты пользователя(ей) из БД sarex-backend в Zitadel | +| `diff` | — | Сравнение множества пользователей БД и Zitadel, отчёт по отсутствующим | + +## Переменные приложения + +Дефолт `—` означает, что значение обязательно (иначе ошибка старта). + +### App (без префикса) + +Класс `Settings` в `src/internal/app/service/settings.py` (режим `run`) и `Settings` в `console.py` (CLI). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENVIRONMENT` | enum | `LOCAL` | Окружение: `LOCAL`/`STAGE`/`PREPROD`/`PRODUCTION` | +| `AMS_SYNC_TOPIC` | string | `ams-sync` | Имя топика Kafka с событиями пользователей | +| `HOST_ORGANIZATION_ID` | string | — | ID организации-хоста в Zitadel (обязателен и в `run`, и в CLI) | +| `VERIFY_USERS` | bool | `False` | При `True` события `model_created` обрабатываются через `import_user` (с верификацией e-mail), иначе через обычный `create` | + +### Kafka (`KAFKA_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | — | Адреса брокеров, напр. `["host:9091"]` | +| `KAFKA_SASL_PLAIN_USERNAME` | string | — | Логин SASL | +| `KAFKA_SASL_PLAIN_PASSWORD` | string | — | Пароль SASL | +| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату для SSL | +| `KAFKA_SECURITY_PROTOCOL` | string | `SASL_SSL` | Протокол безопасности | +| `KAFKA_SASL_MECHANISM` | string | `SCRAM-SHA-512` | Механизм SASL | + +### Zitadel / AMS (`ZITADEL_*`) + +Префикс используют три класса (адаптеры пользователей, организаций и грантов). Общие поля: + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ZITADEL_SERVICE_ACCESS_TOKEN` | string | — | Service-токен доступа к API Zitadel (Bearer) | +| `ZITADEL_HOST` | string | — (у адаптера пользователей); `https://idp.dev.stage.sarex.io` (у адаптеров организаций/грантов) | Базовый URL Zitadel | +| `ZITADEL_TIMEOUT` | int / string | `10` (адаптер пользователей, минуты) / `10m` (организации, гранты) | Таймаут. См. замечание о конфликте типов ниже | +| `ZITADEL_MAX_BATCH` | int | `2000` | Максимум пользователей в одной пачке bulk-импорта | +| `ZITADEL_USERS_MANAGEMENT_ENDPOINT` | string | `management/v1/users` | Базовый путь management-API | +| `ZITADEL_USERS_ENDPOINT` | string | `v2/users` | Базовый путь users-API v2 | + +### Логирование (`LOG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `INFO` | Уровень логирования | +| `LOG_FORMAT` | string | `%(asctime)s [%(levelname)s]: %(message)s` | Формат строки лога | + +Логи пишутся в stdout и в файл `latest.log` в рабочей директории. + +### TCP Healthcheck (`HEALTHCHECK_*`) + +Только режим `run`. Поднимает сырой TCP-сокет, отвечающий строкой-ответом на каждое подключение. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HEALTHCHECK_HOST` | string | `0.0.0.0` | Адрес прослушивания | +| `HEALTHCHECK_PORT` | int | `8008` | Порт | +| `HEALTHCHECK_MAX_CONNECTIONS` | int | `10` | Размер очереди подключений (`listen`) | +| `HEALTHCHECK_REPLY` | string | `healthy` | Строка-ответ на подключение | + +### Auth: sarex-backend (`AUTH_*`) + +Получение JWT под сервисным админом (используется в обоих режимах — адаптер создаётся и в `run`, и в CLI). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_HOST` | string | — | Базовый URL sarex-backend | +| `AUTH_ADMIN_USERNAME` | string | — | Логин админа | +| `AUTH_ADMIN_PASSWORD` | string | — | Пароль админа | +| `AUTH_AUTH_ENDPOINT` | string | `api/token/` | Путь получения токена | + +### Sarex user server (`USER_SERVER_*`) — только CLI + +Источник метадаты пользователя при миграции/синхронизации. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USER_SERVER_HOST` | string | — | Базовый URL сервиса пользователей sarex-backend | +| `USER_SERVER_USERS_ENDPOINT` | string | `api/core/admin/users` | Базовый путь эндпоинта пользователей | + +### База данных sarex-backend (`USER_DB_*`) — только CLI + +Один префикс на два репозитория (пользователи и компании). Подключение через `psycopg2`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USER_DB_NAME` | string | — | Имя базы данных | +| `USER_DB_USER` | string | — | Пользователь БД | +| `USER_DB_PASSWORD` | string | — | Пароль | +| `USER_DB_HOST` | string | — | Хост PostgreSQL | +| `USER_DB_PORT` | int | — | Порт PostgreSQL | +| `USER_DB_USER_TABLE_NAME` | string | `base_baseuser` | Таблица пользователей | +| `USER_DB_COMPANYUSER_TABLE_NAME` | string | `core_companyuser` | Таблица связи пользователь↔компания | +| `USER_DB_COMPANY_TABLE_NAME` | string | `core_company` | Таблица компаний | +| `USER_DB_ITER_SIZE` | int | `10000` | Размер серверного курсора при итерации | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Базовый чарт — `universal-chart`. Обычные значения (`universal-chart.envs`) задаются на окружение (`_default`/`stage`/`preprod`/`production`): + +| Переменная | Значения по окружениям | +| --- | --- | +| `AMS_SYNC_TOPIC` | `ams-sync` | +| `ENVIRONMENT` | `STAGE` / `PREPROD` / `PRODUCTION` | +| `HOST_ORGANIZATION_ID` | stage `339439562105337368`, preprod `337394329179947547`, production `337555824748561429` | +| `USER_SERVER_HOST` | stage `https://stage.sarex.io`, preprod `https://preprod.sarex.io`, production `https://.lk.sarex.io` | +| `AUTH_HOST` | stage `https://stage.sarex.io`, preprod `https://preprod.sarex.io`, production `https://lk.sarex.io` | +| `KAFKA_SECURITY_PROTOCOL` | `SASL_SSL` | +| `KAFKA_SASL_MECHANISM` | `SCRAM-SHA-512` | + +Значения из секретов (`universal-chart.secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `ZITADEL_HOST` | `zitadel-token-secret` | `zitadel-host` | +| `ZITADEL_SERVICE_ACCESS_TOKEN` | `zitadel-token-secret` | `zitadel-access-token` | +| `KAFKA_BOOTSTRAP_SERVERS` | `ams-kafka-secret` | `kafka-bootstrap-servers` | +| `KAFKA_SASL_PLAIN_USERNAME` | `ams-kafka-secret` | `kafka-username` | +| `KAFKA_SASL_PLAIN_PASSWORD` | `ams-kafka-secret` | `kafka-password` | +| `KAFKA_SSL_CAFILE` | `ams-kafka-secret` | `kafka-ca-file` | +| `AUTH_ADMIN_USERNAME` | `auth-secret` | `admin-username` | +| `AUTH_ADMIN_PASSWORD` | `auth-secret` | `admin-password` | +| `USER_DB_NAME` | `user-db-secret` | `user-db-name` | +| `USER_DB_USER` | `user-db-secret` | `user-db-user` | +| `USER_DB_PASSWORD` | `user-db-secret` | `user-db-password` | +| `USER_DB_HOST` | `user-db-secret` | `user-db-host` | +| `USER_DB_PORT` | `user-db-secret` | `user-db-port` | + +Помимо env, чарт монтирует CA-сертификат Kafka из секрета `ya-ca-secret` в `/etc/ca-certificates/Yandex` (`volumes`/`volumeMounts`) и раздаёт `YandexInternalRootCA.crt` через configMap `ya-ca-cert`. Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, порт `8008`), `service.*` (ClusterIP, порт `8008`), `probes.*` (TCP-пробы на `8008`, по умолчанию выключены), `serviceAccount.*`, `imagePullSecrets.*`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | +| --- | --- | --- | +| ветка `stage` | `stage` | `platform` (`STAGE_NAMESPACE`) | +| ветка `master` | `preprod` | `ams-sync-preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `ams-sync-prod` | + +Ключевые переменные пайплайна: `RELEASE_NAME=ams-sync`, `CHART_NAME=ams-sync`, `CHART_VERSION` (`0.0.1-`), `IMAGE_NAME`, `IMAGE_PATH=universal-chart.image.name`, `HELM_SET_ARGS`, `DOCKERFILE_PATH=Dockerfile`, флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY`. + +## Замечания и потенциальные проблемы + +- **Конфликт `ZITADEL_TIMEOUT`.** Переменную читают три класса с общим префиксом `zitadel_`, но разного типа: адаптер пользователей ожидает `int` (минуты, дефолт `10`), адаптеры организаций и грантов — `str` (дефолт `10m`). Если задать `ZITADEL_TIMEOUT=10m`, инициализация адаптера пользователей упадёт на разборе `int`. Рекомендуется задавать целое число. +- **`ZITADEL_HOST` без дефолта у адаптера пользователей.** В `users/zitadel/adapter.py` поле `host` обязательно (дефолта нет), тогда как в адаптерах организаций/грантов есть дефолт `https://idp.dev.stage.sarex.io`. Для режима `run` переменную нужно задать явно (в Helm приходит из секрета `zitadel-token-secret`). +- **Режим `run` не использует БД и user-server.** Демон-потребитель (`sync_service.py`) поднимает только logger, healthcheck, auth-провайдер, адаптер Zitadel и Kafka. Переменные `USER_DB_*` и `USER_SERVER_*` требуются только CLI-командам (`migrate`/`sync`/`diff`), хотя в Helm они проброшены для запуска этих команд через `kubectl exec`. +- **Приложение загружает `.env` автоматически** (у всех классов задан `env_file`), в отличие от некоторых других сервисов. CLI дополнительно поддерживает `--env-file=` для альтернативного файла. +- **TLS-проверка отключена.** Все исходящие HTTP-запросы (Zitadel, sarex-backend) выполняются с `verify=False`. Отдельного флага в конфиге нет. +- **Zitadel org-ID захардкожены в коде.** Помимо `HOST_ORGANIZATION_ID`, в use-case зашиты `glorax_org_id` и `dogma_org_id`, в которые распределяются пользователи по доменам e-mail (см. `usecase/migration/usecase.py`, `usecase/user/usecase.py`). + +## Минимальный набор для режима `run` (демон) + +- `ENVIRONMENT`, `HOST_ORGANIZATION_ID`, `AMS_SYNC_TOPIC` +- `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_SASL_PLAIN_USERNAME`, `KAFKA_SASL_PLAIN_PASSWORD`, `KAFKA_SSL_CAFILE` (+ при необходимости `KAFKA_SECURITY_PROTOCOL`, `KAFKA_SASL_MECHANISM`) +- `ZITADEL_HOST`, `ZITADEL_SERVICE_ACCESS_TOKEN` +- `AUTH_HOST`, `AUTH_ADMIN_USERNAME`, `AUTH_ADMIN_PASSWORD` + +## Дополнительно для CLI (`migrate` / `sync` / `diff`) + +- `USER_SERVER_HOST` +- `USER_DB_NAME`, `USER_DB_USER`, `USER_DB_PASSWORD`, `USER_DB_HOST`, `USER_DB_PORT` diff --git a/apps/ams-sync/ENDPOINTS.md b/apps/ams-sync/ENDPOINTS.md new file mode 100644 index 0000000..7ddf070 --- /dev/null +++ b/apps/ams-sync/ENDPOINTS.md @@ -0,0 +1,99 @@ +# Интерфейсы сервиса ams-sync +# Версия: 1.0.0 + +Документ описывает интерфейсную поверхность сервиса: потребляемый топик Kafka, входящий TCP-healthcheck, исходящие HTTP-запросы к внешним сервисам (Zitadel, sarex-backend) и обращения к БД. + +> `ams-sync` — не backend-сервис с REST API, а утилита синхронизации: демон-потребитель Kafka (`run`) плюс набор CLI-команд (`migrate`/`sync`/`diff`). Публичного HTTP API у сервиса нет — единственный входящий сокет отвечает на healthcheck сырой строкой (не HTTP). Поэтому файла `openapi.yaml` для сервиса нет. + +## Как устроено взаимодействие + +Приложение построено по слоям (adapter → repository → usecase → controller). Исходящие вызовы к внешним сервисам выполняются через `requests` (`requests.Session` с ретраями на `5xx`), к БД — через `psycopg2`. Все HTTP-запросы идут с `verify=False` (без проверки TLS). Базовые хосты берутся из переменных окружения (см. `CONFIGURATION.md`): `ZITADEL_HOST`, `AUTH_HOST`, `USER_SERVER_HOST`. + +Аутентификация исходящих запросов: + +- **Zitadel** — заголовок `Authorization: Bearer ` (service-токен из конфига). Для части операций дополнительно проставляется `x-zitadel-orgid`. +- **sarex-backend** — JWT, получаемый под админом (`AUTH_ADMIN_USERNAME`/`AUTH_ADMIN_PASSWORD`) через `POST api/token/`; кешируется и обновляется за 30 секунд до `exp`. + +## Входящие интерфейсы + +### TCP Healthcheck + +Только режим `run`. Сырой TCP-сокет (не HTTP): на каждое подключение отправляет строку `HEALTHCHECK_REPLY` (по умолчанию `healthy`) и закрывает соединение. Слушает `HEALTHCHECK_HOST:HEALTHCHECK_PORT` (по умолчанию `0.0.0.0:8008`). В Helm k8s-пробы настроены на TCP-порт `8008` (по умолчанию отключены). + +### Kafka-потребитель + +Топик — переменная `AMS_SYNC_TOPIC` (по умолчанию `ams-sync`). Параметры консьюмера (`src/internal/controller/sync_service/controller.py`): + +| Параметр | Значение | +| --- | --- | +| `group_id` | `ams_sync_group` | +| `auto_offset_reset` | `earliest` | +| `enable_auto_commit` | `True` | +| десериализация | JSON (`utf-8`) | + +Формат сообщения — объект с полями `type` и `body`. Диспетчеризация по `type`: + +| `type` сообщения | Тело (`body`) | Обработчик | Действие | +| --- | --- | --- | --- | +| `model_created` | `SarexUserWithPermissions` | при `VERIFY_USERS=True` — `import_user` UC, иначе `user` UC `create` | Создание пользователя в Zitadel + запись метадаты | +| `model_updated` | `Metadata` | `user` UC `update_metadata` | Обновление метадаты существующего пользователя в Zitadel | + +Сообщения без `type` или `body` пропускаются с предупреждением. Ошибки обработки логируются (traceback), сообщение не переотправляется (auto-commit включён). + +## Исходящие HTTP-запросы + +### Zitadel (`ZITADEL_HOST`) + +Адаптеры `users/zitadel/adapter.py` и `organizations/adapter.py`. Итоговый URL = `` + путь. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/v2/users/new` | Создать пользователя (human) | +| POST | `/management/v1/users/human/_import` | Импортировать пользователя (заголовок `x-zitadel-orgid`) | +| POST | `/admin/v1/import` | Массовый импорт пользователей (bulk, с таймаутом) | +| POST | `/v2/users` | Поиск пользователей (по username / email / org) | +| GET | `/management/v1/users/{id}` | Получить пользователя по id | +| GET | `/management/v1/users/{id}/metadata/{key}` | Получить метадату пользователя по ключу | +| POST | `/v2/users/{id}/metadata` | Массово задать метадату пользователя (значения в base64) | +| POST | `/v2/users/{id}/deactivate` | Деактивировать пользователя | +| DELETE | `/v2/users/{id}` | Удалить пользователя | +| POST | `/v2/organizations/_search` | Найти организацию по имени | + +> Пути `/v2/...` частично захардкожены в адаптере, часть базовых путей управляется `ZITADEL_USERS_MANAGEMENT_ENDPOINT` (`management/v1/users`) и `ZITADEL_USERS_ENDPOINT` (`v2/users`). Методы грантов (`grants/adapter.py`) в текущей версии закомментированы. + +### sarex-backend — авторизация (`AUTH_HOST`) + +Адаптер `auth/http/adapter.py`. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{AUTH_AUTH_ENDPOINT}` (по умолчанию `api/token/`) | Получить JWT по логину/паролю админа. Возвращает поле `access` | + +### sarex-backend — сервис пользователей (`USER_SERVER_HOST`), только CLI + +Адаптер `users/http/adapter.py`. Сессия с ретраями на `500/502/503/504` (до 5 попыток). + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `{USER_SERVER_USERS_ENDPOINT}/{user_id}/metadata` (по умолчанию `api/core/admin/users/{user_id}/metadata`) | Получить метадату пользователя sarex для синхронизации в Zitadel | + +## Обращения к БД sarex-backend (`USER_DB_*`), только CLI + +Репозитории `repository/users` и `repository/companies` читают данные напрямую из PostgreSQL sarex-backend (только чтение, серверный курсор `itersize`). + +| Операция | Таблицы | Назначение | +| --- | --- | --- | +| Пользователь по id | `base_baseuser` | `migrate --user-id` / `sync --user-id` | +| Все пользователи (итерация) | `base_baseuser` | `migrate` / `sync` / `diff` (bulk) | +| Пользователи компании | `core_companyuser` ⋈ `base_baseuser` | `sync --company-id` (только с непустым email) | +| Компания по id / имени | `core_company` | Вспомогательные запросы | + +## Внешние зависимости (инфраструктура) + +| Зависимость | Режим | Назначение | +| --- | --- | --- | +| Kafka | `run` | Источник событий пользователей (топик `AMS_SYNC_TOPIC`) | +| Zitadel (AMS) | `run` + CLI | Целевая система управления доступом (создание/обновление/удаление пользователей и метадаты) | +| sarex-backend (auth) | `run` + CLI | Получение JWT под админом | +| sarex-backend (user server) | CLI | Источник метадаты пользователя | +| PostgreSQL sarex-backend | CLI | Источник пользователей и компаний для миграции/синхронизации | diff --git a/apps/attachments/.env.example b/apps/attachments/.env.example new file mode 100644 index 0000000..2fdc878 --- /dev/null +++ b/apps/attachments/.env.example @@ -0,0 +1,46 @@ +# ============================================================================= +# Attachments — пример конфигурации (.env) +# Скопируйте в .env и заполните значения. +# ============================================================================= +# --- Приложение (необязательные, есть значения по умолчанию) --- +# Версия: 0.11.1 + +# API=/api +# NAME=Attachments +# VERSION=0.0.1 +# DESCRIPTION=Attachments + +# --- База данных PostgreSQL (обязательные) --- +DATABASE_NAME=attachments +DATABASE_USER=postgres +DATABASE_PASSWORD=change_me +DATABASE_HOST=db +DATABASE_PORT=5432 +DATABASE_SSL_MODE=disable + +# Пароль суперпользователя PostgreSQL для контейнера db (docker-compose) +POSTGRES_PASSWORD=change_me + +# --- S3 (Yandex Object Storage) --- +# Вариант 1: путь к JSON с реквизитами сервисного аккаунта. +# Файл должен содержать: {"endpoint": "...", "access_key_id": "...", "secret_access_key": "..."} +YANDEX_S3_ACCOUNT_PATH=/etc/sarex/yc-s3-storage/yc-s3-service-account.json + +# Вариант 2: задать реквизиты напрямую (если не используете JSON-файл). +# YANDEX_S3_ENDPOINT_URL=storage.yandexcloud.net +# YANDEX_S3_ACCESS_KEY_ID=change_me +# YANDEX_S3_SECRET_ACCESS_KEY=change_me + +YANDEX_S3_VERIFY=true +# YANDEX_S3_USE_SSL=true +# YANDEX_S3_REGION=ru-central1 +BUCKET_NAME=attachments-stage-2 + +# --- Логирование (префикс LOG_) --- +# LOG_LEVEL=INFO + +# --- Трейсинг / OpenTelemetry (префикс TRACING_) --- +# TRACING_USE=false +# TRACING_HOST=localhost:4317 +# TRACING_SERVICE_NAME=attachments +# TRACING_INSECURE=false diff --git a/apps/attachments/CONFIGURATION.md b/apps/attachments/CONFIGURATION.md new file mode 100644 index 0000000..b3ae384 --- /dev/null +++ b/apps/attachments/CONFIGURATION.md @@ -0,0 +1,129 @@ +# Конфигурация проекта Attachments +# Версия: 0.11.1 + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Настройка сервиса выполняется **только через переменные окружения**. Отдельного файла с настройками (yaml/toml) в приложении нет — за конфигурацию отвечает `internal/config/settings.py` на базе `pydantic.BaseSettings`. + +Источники переменных окружения по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | Файл `.env` в корне (`env_file: .env` в `docker-compose.yml`), плюс `POSTGRES_PASSWORD` для контейнера БД | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна и build-args | + +Дополнительно секреты доступа к S3 не задаются напрямую, а читаются из JSON-файла (см. `YANDEX_S3_ACCOUNT_PATH` ниже). + +## Переменные приложения (класс `Settings`) + +Читаются напрямую по имени (регистрозависимо, `case_sensitive = True`). Переменные без значения по умолчанию **обязательны** — без них приложение не стартует. + +| Переменная | Тип | Обязательна | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `API` | str | нет | `/api` | Префикс всех HTTP-роутов | +| `NAME` | str | нет | `Attachments` | Имя сервиса (title в FastAPI/OpenAPI) | +| `VERSION` | str | нет | `0.0.1` | Версия сервиса | +| `DESCRIPTION` | str | нет | `Attachments` | Описание сервиса | +| `YANDEX_S3_ENDPOINT_URL` | str | да* | — | Endpoint S3 (без схемы; `https://`/`http://` добавляется в коде по `YANDEX_S3_USE_SSL`) | +| `YANDEX_S3_ACCESS_KEY_ID` | str | да* | — | Access Key ID для S3 | +| `YANDEX_S3_SECRET_ACCESS_KEY` | str | да* | — | Secret Access Key для S3 | +| `YANDEX_S3_USE_SSL` | bool | нет | `True` | Использовать ли HTTPS при обращении к S3 | +| `YANDEX_S3_REGION` | str | нет | `ru-central1` | Регион S3 | +| `YANDEX_S3_VERIFY` | bool | да | — | Проверять ли SSL-сертификат S3 | +| `BUCKET_NAME` | str | да | — | Имя бакета для вложений | +| `DATABASE_NAME` | str | да | — | Имя базы данных PostgreSQL | +| `DATABASE_USER` | str | да | — | Пользователь БД | +| `DATABASE_PASSWORD` | str | да | — | Пароль пользователя БД | +| `DATABASE_HOST` | str | да | — | Хост БД | +| `DATABASE_PORT` | int | да | — | Порт БД | +| `DATABASE_SSL_MODE` | str | да | — | Режим SSL при подключении к БД (напр. `disable`, `require`, `verify-full`) | + +\* Три переменные `YANDEX_S3_ENDPOINT_URL`, `YANDEX_S3_ACCESS_KEY_ID`, `YANDEX_S3_SECRET_ACCESS_KEY` формально обязательны, но при старте приложения они **проставляются автоматически** функцией `json_config_s3_account_settings()` из JSON-файла (см. `YANDEX_S3_ACCOUNT_PATH`). Задавать их вручную не нужно, если задан путь к файлу. + +## Настройки S3 через JSON-файл + +| Переменная | Обязательна | Назначение | +| --- | --- | --- | +| `YANDEX_S3_ACCOUNT_PATH` | да | Путь к JSON-файлу с реквизитами сервисного аккаунта S3 | + +При старте функция `json_config_s3_account_settings()` читает файл по этому пути и выставляет переменные окружения `YANDEX_S3_ENDPOINT_URL`, `YANDEX_S3_ACCESS_KEY_ID`, `YANDEX_S3_SECRET_ACCESS_KEY`. + +Ожидаемая структура JSON-файла: + +```json +{ + "endpoint": "storage.yandexcloud.net", + "access_key_id": "<ключ>", + "secret_access_key": "<секрет>" +} +``` + +В Kubernetes файл монтируется из секрета `attachments-s3-secret` (в prod — `yc-s3`) в `/etc/sarex/yc-s3-storage/yc-s3-service-account.json`. + +## Логирование (класс `LoggerSettings`, префикс `LOG_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | str | `INFO` | Уровень логирования (`DEBUG`, `INFO`, `WARNING`, ...). При неизвестном значении откатывается на `INFO` | +| `LOG_FORMAT` | str | JSON-шаблон с полями `timestamp`, `level`, `message` | Формат строк лога (используется `pythonjsonlogger`) | + +## Трейсинг / OpenTelemetry (класс `TraceSettings`, префикс `TRACING_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACING_USE` | bool | `False` | Включает OTEL-трейсинг, логгер и middleware | +| `TRACING_HOST` | str | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACING_SERVICE_NAME` | str | `attachments` | Имя сервиса в трейсах | +| `TRACING_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение к коллектору | + +Трейсинг активируется только при `TRACING_USE=true`. + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но нужны для запуска/сборки/деплоя. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `POSTGRES_PASSWORD` | `docker-compose.yml` (контейнер `db`) | Пароль суперпользователя PostgreSQL при локальном запуске | +| `PIP_EXTRA_INDEX_URL` | `docker/Dockerfile` (build-arg) | Доп. индекс pip для установки приватных пакетов при сборке образа | +| `GITLAB_PYPI_EXTRA_INDEX_URL` | `.gitlab-ci.yml` | Значение, пробрасываемое в `PIP_EXTRA_INDEX_URL` при сборке в CI | + +### Переменные CI/CD (`.gitlab-ci.yml`) + +Служебные переменные пайплайна: `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE`, а также подставляемые по окружениям `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`. Окружение выбирается по ветке/тегу: `stage` → ветка `stage`, `preprod` → ветка `master`, `production` → git-тег. + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Обычные переменные (`envs`): + +| Переменная | Значение | Примечание | +| --- | --- | --- | +| `API_ADDRESS` | `0.0.0.0:8000` | Задаётся в чарте, но **кодом приложения не читается** | +| `POSTGRES_POOL_SIZE` | `10` | Задаётся в чарте, но **кодом приложения не читается** | +| `DATABASE_SSL_MODE` | `verify-full` | | +| `YANDEX_S3_VERIFY` | `true` | | +| `YANDEX_S3_ACCOUNT_PATH` | `/etc/sarex/yc-s3-storage/yc-s3-service-account.json` | | +| `BUCKET_NAME` | `attachments-stage-2` / `attachments-prod-2` | Зависит от окружения | + +Переменные из секретов (`secretEnvs`, секрет `attachments-postgresql-secret` / `ya-pg-secret`): + +| Переменная | Ключ секрета | Примечание | +| --- | --- | --- | +| `DATABASE_PORT` | `port` | | +| `DATABASE_HOST` | `host` | | +| `DATABASE_USER` | `username` | | +| `DATABASE_PASSWORD` | `password` | | +| `DATABASE_NAME` | `database` | | +| `YC-PG-CERTIFICATE` | `ca.crt` | CA-сертификат PostgreSQL; также монтируется файлом в `/root/.postgresql/root.crt` | + +## Минимальный набор для локального запуска + +Для запуска через `docker-compose` в файле `.env` достаточно задать (см. `.env.example`): + +- `POSTGRES_PASSWORD` — для контейнера БД +- `DATABASE_*` — параметры подключения к БД +- `BUCKET_NAME`, `YANDEX_S3_VERIFY` +- `YANDEX_S3_ACCOUNT_PATH` **или** напрямую `YANDEX_S3_ENDPOINT_URL` + `YANDEX_S3_ACCESS_KEY_ID` + `YANDEX_S3_SECRET_ACCESS_KEY` diff --git a/apps/auth-flow/.env.example b/apps/auth-flow/.env.example new file mode 100644 index 0000000..191ba37 --- /dev/null +++ b/apps/auth-flow/.env.example @@ -0,0 +1,15 @@ +# auth-flow-frontend — переменные СБОРКИ (build-time), НЕ рантайм-.env. +# Приложение не читает .env: значения подставляются в код на этапе сборки +# через webpack DefinePlugin (см. webpack.config.ts) или Dockerfile --build-arg / CI BUILD_ARGS. + +# Окружение сборки: local | stage | prod | preprod | contour +# local -> mode=development, isDev=true (доступны dev-роуты /login, /logout, /) +# остальные -> mode=production +ENDPOINT=local + +# Токен приватного npm-реестра @sarex-team (nexus.infra.sarex.io), нужен для `npm i` (.npmrc) +NPM_NEXUS_TOKEN= + +# NOTE: IS_DEV вычисляется автоматически (ENDPOINT === 'local'), задавать вручную не нужно. +# NOTE: authority и client_id OIDC (Zitadel) НЕ задаются здесь — хостовое приложение +# кладёт их в localStorage (STORAGE.AUTHORITY / STORAGE.CLIENT_ID) во время работы. diff --git a/apps/auth-flow/CONFIGURATION.md b/apps/auth-flow/CONFIGURATION.md new file mode 100644 index 0000000..6a76d0e --- /dev/null +++ b/apps/auth-flow/CONFIGURATION.md @@ -0,0 +1,162 @@ +# Конфигурация проекта auth-flow-frontend + +Документ описывает способы конфигурирования микрофронтенда аутентификации `auth-flow-frontend` (React + webpack), а также все переменные сборки, деплоя и рантайма. + +## Способы конфигурирования + +В отличие от бэкенд-сервисов, приложение **не читает `.env` и не использует dotenv**. Это статический фронтенд, собираемый webpack, поэтому конфигурация распределена по трём уровням: + +1. **Сборка (build-time).** Переменная `ENDPOINT` определяет окружение. Её значение подставляется в код на этапе сборки через `webpack.DefinePlugin` (`webpack.config.ts`) — заменяет обращения `process.env.ENDPOINT` и `process.env.IS_DEV` на строковые литералы. Выбор режима (`development`/`production`) и флага `isDev` выполняется в `webpack/build.config.ts`. +2. **Установка зависимостей.** Пакеты `@sarex-team/*` тянутся из приватного npm-реестра (`.npmrc` → `nexus.infra.sarex.io`). Для доступа нужен токен `NPM_NEXUS_TOKEN`. +3. **Рантайм (runtime).** Параметры OIDC-провайдера — `authority` и `client_id` — читаются из `localStorage` (`src/config/zitadel.ts`) по ключам `STORAGE.AUTHORITY` и `STORAGE.CLIENT_ID` из `@sarex-team/sdk-js`. Их задаёт хостовое приложение, а не сборка. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (dev) | `npm run dev` → `ENDPOINT=local webpack serve`. Dev-сервер на `https://localhost:9000` | +| Локально (build) | `npm run build` → `webpack --config webpack.config.ts` (по умолчанию `ENDPOINT=prod`, см. `build.config.ts`) | +| Docker | `Dockerfile`: build-args `ENDPOINT` и `NPM_NEXUS_TOKEN`; сборка dist → отдача через nginx | +| Kubernetes (Helm) | `.helm/values.yaml` (чарт `universal-chart`): блок `services.frontend`, `global.env` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` (ветка/тег → `STAND`, `ENDPOINT`, `CHART_VERSION`), `BUILD_ARGS`, `HELM_SET_ARGS` | + +## Переменные сборки (build-time) + +Подставляются в код на этапе сборки; в рантайме это уже константы. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENDPOINT` | enum | `prod` (в `build.config.ts`, если не задана) | Окружение сборки: `local`/`stage`/`prod`/`preprod`/`contour`. Определяет `mode` (`development` для `local`, иначе `production`) и `isDev` | +| `IS_DEV` | bool | — (вычисляется) | `true`, если `ENDPOINT === 'local'`. Включает dev-роуты (`/login`, `/logout`, `/`). Задаётся автоматически через `DefinePlugin`, вручную указывать не нужно | +| `NPM_NEXUS_TOKEN` | string | — | Токен авторизации в приватном npm-реестре `@sarex-team` (`.npmrc`). Обязателен для `npm i` | + +> `mode` по окружениям (`webpack/build.config.ts`): `local` → `development`, `stage`/`prod`/`preprod`/`contour` → `production`. Неизвестное значение `ENDPOINT` приводит к ошибке сборки. + +## Переменные рантайма (localStorage) + +Читаются в браузере во время работы приложения; задаются хостовым приложением, не сборкой. + +| Ключ | Источник | Назначение | +| --- | --- | --- | +| `STORAGE.AUTHORITY` | `@sarex-team/sdk-js` | URL issuer'а Zitadel (`authority` OIDC). Обязателен — при отсутствии `src/config/zitadel.ts` бросает `Error("Authority or client ID is not set")` | +| `STORAGE.CLIENT_ID` | `@sarex-team/sdk-js` | `client_id` OIDC-клиента. Обязателен (та же проверка) | +| `sarex_logout` (`LOGOUT_STORAGE_KEY`) | `src/config/logout.ts` | Состояние логаута между вкладками (`{ logoutAt, owner, reason }`). Снимается только валидным токеном в `setTokensInfo` | +| access / refresh / id токены | `storage` из `@sarex-team/sdk-js` | Устанавливаются в `setTokensInfo` (`storage.setAccessToken`/`setRefreshToken`/`setIdentityToken`) после успешного колбэка | + +## Конфигурация OIDC (Zitadel) + +Задаётся в `src/config/zitadel.ts` (`configZitadel: ZitadelConfig`), клиент создаётся через `createZitadelAuth`. + +| Параметр | Значение | Назначение | +| --- | --- | --- | +| `authority` | из `localStorage` (`STORAGE.AUTHORITY`) | Issuer Zitadel | +| `client_id` | из `localStorage` (`STORAGE.CLIENT_ID`) | Идентификатор клиента | +| `response_type` | `code` | Authorization Code Flow | +| `scope` | `openid profile email offline_access urn:zitadel:iam:user:metadata` | Запрашиваемые области | +| `redirect_uri` | `${window.location.origin}/auth/callback` | URL возврата после логина | +| `post_logout_redirect_uri` | `${window.location.origin}/login` | URL после логаута | +| `redirectMethod` | `replace` | Замена записи в истории браузера | +| `revokeTokensOnSignout` | `false` | Не отзывать токены при выходе | +| `automaticSilentRenew` | `false` | Фоновое обновление токенов выключено | +| `validateSubOnSilentRenew` | `false` | — | +| `silentRequestTimeoutInSeconds` | `100` | Таймаут silent-запроса | + +## Роуты приложения + +Определены в `src/config/urls.ts`, диспетчеризация в `src/App.tsx`. + +| Роут | Константа | Доступность | Назначение | +| --- | --- | --- | --- | +| `/auth/callback` | `CALLBACK` | всегда | Обработка OIDC-колбэка (по умолчанию для неизвестных путей) | +| `/auth/error` | `ERROR` | всегда | Страница ошибки аутентификации | +| `/login` | `LOGIN` | только dev | Dev-страница входа | +| `/logout` | `LOGOUT` | только dev | Dev-страница выхода | +| `/` | `HOME` | только dev | Dev-навигация | + +> В production разрешены только `/auth/callback` и `/auth/error` (`ALLOWED_PATHS`); в dev дополнительно `/login`, `/logout`, `/` (`ALLOWED_PATHS_DEV`). Неразрешённый путь заменяется на `/auth/callback` (`history.replaceState`). + +## Сборка (webpack) + +`webpack.config.ts` + `webpack/build.config.ts`. + +| Параметр | Значение | Примечание | +| --- | --- | --- | +| `entry` | `src/index.tsx` | Точка входа | +| `output.path` | `dist/` | | +| `output.filename` | `index.js` | | +| `output.publicPath` | `/` (dev) или `/auth/callback/` (prod) | Зависит от `isDev` | +| loader | `esbuild-loader`, target `es2015` | Для `.tsx?/.jsx?` | +| `devServer` | `https`, `port: 9000`, `hot`, `historyApiFallback`, `open` | Только dev | +| plugins | `HtmlWebpackPlugin` (`public/index.html`), `DefinePlugin` (`ENDPOINT`, `IS_DEV`) | | + +Node-версия для разработки: `v20.0.0` (`.nvmrc`; в `package.json` заявлено `v20.0.0`, реальная база образа — `node:20`). + +## Docker + +`Dockerfile` — двухстадийная сборка. + +| Стадия | База | Действия | +| --- | --- | --- | +| build | `cr.yandex/crp3ccidau046kdj8g9q/node:20` | `npm i` (с `NPM_NEXUS_TOKEN`), `npm run build` (с `ENDPOINT`) | +| runtime | `cr.yandex/crp3ccidau046kdj8g9q/nginx:latest` | Копирует `/app/dist/` → `/dist/`, `nginx/nginx.conf` → `/etc/nginx/nginx.conf` | + +Build-args: `ENDPOINT`, `NPM_NEXUS_TOKEN`. + +## Nginx + +`nginx/nginx.conf` — отдача статики и healthcheck. + +| Локация | Поведение | +| --- | --- | +| `= /auth/callback/index.js` | `alias /dist/index.js` | +| `^~ /auth/callback` | `try_files $uri $uri/ /index.html`; заголовки `Cache-Control: no-store,...`, кеш отключён | +| `= /ping` | `200 {"result": "ok"}` (healthcheck) | + +`listen 80`, `root /dist`, логи в stdout/stderr, `gzip on`. + +## Helm-чарт (`.helm/values.yaml`) + +Деплой через зонтичный чарт `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`; `Chart.yaml` приложения — `auth-flow-frontend` `1.0.0`). Значения задаются с ключами по окружениям (`_default`/`stage`/`preprod`/`production`). + +| Параметр | Значение | Назначение | +| --- | --- | --- | +| `global.env` | `stage` (`_default` в файле) | Активное окружение чарта | +| `services.frontend.enabled` | `true` | Включение сервиса | +| `deployment.name._default` | `auth-flow-frontend` | Имя деплоймента | +| `deployment.replicaCount` | `_default: 1`, `production: 2` | Число реплик | +| `deployment.port._default` | `80` | Порт контейнера | +| `deployment.revisionHistoryLimit._default` | `5` | Хранимые ревизии | +| `deployment.resources.requests` | `memory: 128Mi`, `cpu: 100m` | Реквесты ресурсов | +| `deployment.probes.liveness/readiness` | `httpGet` `/` : `80` | Пробы | +| `image.name._default` | `cr.yandex/crp3ccidau046kdj8g9q/auth-flow-frontend` | Образ | +| `image.pullPolicy._default` | `IfNotPresent` | | +| `imagePullSecrets.name._default` | `dockerhub` (enabled `false`) | Секрет реестра | +| `service` | `ClusterIP`, port/targetPort `80`, portName `http`, имя `auth-flow-frontend-service` | Service | +| `envs` | `[]` | Переменные окружения контейнера (пусто) | +| `volumes._default` | `[]` | Тома | +| `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner` | заполняются из CI (`owner` по умолчанию `team-abc`) | Метаданные | + +## CI (`.gitlab-ci.yml`) + +Подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`). + +Базовые переменные: `SERVICE_NAME=auth-flow-frontend`, `DOCKERFILE_PATH=./Dockerfile`, `CI_TRIGGER_SOURCE=app`. + +Переключение окружения по `workflow.rules`: + +| Условие | STAND | ENDPOINT | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | +| `merge_request_event` | — | — | — (`ENABLE_BUILD_IMAGE=false`) | — | +| ветка `stage` | `stage` | `stage` | `0.0.1-stage` | `universal-chart-stage` | +| тег (`CI_COMMIT_TAG`) | `production` | `prod` | `0.0.1-prod` | `universal-chart-production` | + +> Правило для ветки `master` → `preprod` присутствует, но **закомментировано**. `NAMESPACE` для всех окружений — `platform`, `RELEASE_NAME`/`CHART_NAME` — `${SERVICE_NAME}`. + +`BUILD_ARGS` прокидывают `--build-arg ENDPOINT=` и `--build-arg NPM_NEXUS_TOKEN=${NPM_NEXUS_TOKEN}`. `HELM_SET_ARGS` устанавливают `universal-chart.services.frontend.image.name.`, `global.env`, а также `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`. + +## Замечания и потенциальные проблемы + +- **Обязательные runtime-параметры.** Если в `localStorage` нет `AUTHORITY` или `CLIENT_ID`, `src/config/zitadel.ts` бросает ошибку при инициализации — приложение не стартует. Эти значения должен положить хостовый сервис до загрузки микрофронтенда. +- **Нет `.env`.** Приложение не читает dotenv; `process.env.ENDPOINT`/`IS_DEV` существуют только на этапе сборки (замена через `DefinePlugin`). Значение `ENDPOINT` задаётся только через CLI/`--build-arg`/CI. +- **publicPath в prod** — `/auth/callback/`: приложение обслуживается nginx под префиксом `/auth/callback`, что согласовано с `redirect_uri` OIDC. +- **`silentRenew` отключён** намеренно (`automaticSilentRenew: false`) — обновление токенов не выполняется в фоне. diff --git a/apps/auth-flow/ENDPOINTS.md b/apps/auth-flow/ENDPOINTS.md new file mode 100644 index 0000000..f3dbbff --- /dev/null +++ b/apps/auth-flow/ENDPOINTS.md @@ -0,0 +1,80 @@ +# Эндпоинты, с которыми взаимодействует auth-flow-frontend + +Документ описывает внешние эндпоинты, внутренние роуты и каналы межвкладочной коммуникации микрофронтенда аутентификации `auth-flow-frontend`. + +## Как устроено взаимодействие + +Приложение отвечает только за аутентификацию по протоколу **OIDC (Authorization Code Flow + PKCE)**. Прямых REST-запросов к бэкенд-сервисам Sarex у него **нет** — всё сетевое взаимодействие идёт с провайдером идентификации **Zitadel** через обёртку `@sarex-team/sdk-js/zitadel` (поверх `oidc-client`). + +Клиент создаётся в `src/config/zitadel.ts` (`createZitadelAuth(configZitadel)`). Базовый адрес провайдера (`authority`, issuer) и `client_id` берутся из `localStorage` во время работы, а не задаются сборкой. OIDC-клиент сам находит конкретные эндпоинты issuer'а через discovery-документ `/.well-known/openid-configuration`. + +## Провайдер идентификации (Zitadel) по окружениям + +Фактический `authority` подставляется хостовым приложением через `localStorage` (`STORAGE.AUTHORITY`) — в самом `auth-flow-frontend` хосты не захардкожены. Ниже — известные для платформы Sarex значения (справочно): + +| Окружение | `authority` (issuer) | +| --- | --- | +| `stage` | `https://idp.dev.stage.sarex.io` | +| `prod` | `https://login.sarex.io` | + +## OIDC-эндпоинты провайдера + +Обнаруживаются через discovery и вызываются `oidc-client` относительно `authority`. Итоговые пути определяются метаданными issuer'а. + +| Эндпоинт (метаданные) | Метод | Где инициируется | Назначение | +| --- | --- | --- | --- | +| `/.well-known/openid-configuration` | GET | инициализация `UserManager` | Discovery метаданных провайдера | +| `authorization_endpoint` | GET (redirect) | `LoginPage` → `zitadel.authorize()` | Старт авторизации, редирект на форму входа | +| `token_endpoint` | POST | `AuthCallbackPage` → `zitadel.userManager.signinCallback()` | Обмен `code` → access/refresh/id токены | +| `jwks_uri` | GET | `oidc-client` | Ключи для проверки подписи токенов | +| `userinfo_endpoint` | GET | `oidc-client` (при необходимости) | Профиль пользователя | +| `end_session_endpoint` | GET (redirect) | `LogoutPage` → `zitadel.signout()` | Завершение сессии (логаут) | + +Параметры OIDC-запросов (из `configZitadel`): + +- `response_type`: `code` +- `scope`: `openid profile email offline_access urn:zitadel:iam:user:metadata` +- `redirect_uri`: `${window.location.origin}/auth/callback` +- `post_logout_redirect_uri`: `${window.location.origin}/login` +- `revokeTokensOnSignout`: `false`, `automaticSilentRenew`: `false` + +## Внутренние роуты приложения + +Диспетчеризация — в `src/App.tsx` по `window.location.pathname` (роуты из `src/config/urls.ts`). + +| Роут | Страница | Метод | Назначение | +| --- | --- | --- | --- | +| `/auth/callback` | `AuthCallbackPage` | — | Обработка OIDC-колбэка: `signinCallback()`, установка токенов, рассылка события в `login_channel`. Дефолт для неизвестных путей | +| `/auth/error` | `AuthErrorPage` | — | Отображение ошибки аутентификации (параметры `error`, `error_description`, `state`, `message` из query) | +| `/login` | `LoginPage` (dev) | — | Кнопка входа (`zitadel.authorize()`) | +| `/logout` | `LogoutPage` (dev) | — | Кнопка выхода (`zitadel.signout()`) | +| `/` | `MainPage` (dev) | — | Dev-навигация (login/logout) | +| `/ping` | nginx | GET | Healthcheck, отдаёт `{"result": "ok"}` | + +## Поток аутентификации (кратко) + +1. `LoginPage` вызывает `zitadel.authorize()` → редирект на `authorization_endpoint` Zitadel. +2. Провайдер возвращает пользователя на `redirect_uri` = `/auth/callback` с `code`. +3. `AuthCallbackPage` вызывает `signinCallback()` → обмен `code` на токены через `token_endpoint`. +4. Токены сохраняются (`setTokensInfo` → `storage.setAccessToken/setRefreshToken/setIdentityToken`), снимается флаг логаута. +5. Результат рассылается остальным вкладкам, происходит закрытие overlay-окна (`window.opener`) или переход на `/` (`HOME`). +6. При ошибке — редирект на `/auth/error` с деталями. + +## Межвкладочная и оконная коммуникация + +| Канал | Ключ / имя | Назначение | +| --- | --- | --- | +| `BroadcastChannel` | `login_channel` | Сообщения `{ type: "login_success" }` и `{ type: "login_error", reason }` между вкладками | +| `localStorage` | `sarex_logout` (`LOGOUT_STORAGE_KEY`) | Состояние логаута между вкладками (`logoutAt`, `owner`, `reason`) | +| window events | `setTokensInfoIntoWindowEvents` (`@sarex-team/sdk-js`) | Оповещение хостового приложения об обновлении токенов (`reason: "auth_callback"`) | +| `window.opener` | — | Режим overlay: после успешного входа окно закрывается (`window.close()`) | + +## Обработка ошибок + +Логика — в `src/utils/authError.ts`; отображение — `AuthErrorPage`. + +- Параметры ошибки читаются из query-строки колбэка: `error`, `error_description`, `state`, `message` (`parseAuthErrorFromSearch`). +- Причина ошибки: `error_description` → `message` → `error` → `"Unknown error"` (`getAuthErrorReason`). +- URL страницы ошибки собирается `buildAuthErrorUrl` (query из непустых параметров, иначе просто `/auth/error`). +- Если контекста ошибки нет — редирект на `/` (`HOME`). +- Пользователю доступны кнопки «Скопировать детали ошибки» (`formatAuthErrorForCopy`) и «Вернуться на форму входа» (`/login`), а также ссылка в поддержку `mailto:support@sarex.io`. diff --git a/apps/bim/.env.example b/apps/bim/.env.example new file mode 100644 index 0000000..30a0290 --- /dev/null +++ b/apps/bim/.env.example @@ -0,0 +1,64 @@ +# App +# APP_NAME/APP_VERSION/LOG_LEVEL читаются кодом, но в деплой-envs их обычно не задают +APP_NAME='BIM Backend v2' +APP_VERSION=local +LOG_LEVEL=info +# Адрес прослушивания HTTP API (host:port) +API_ADDRESS=0.0.0.0:5555 + +# Master-кластер PostgreSQL (BIM id <= LAST_MASTER_BIM) +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_DB=bimbackendv2 +POSTGRES_POOL_SIZE=10 +DB_CERT_PATH=./.docker/yandex_pg.pem + +# Slave-1 кластер PostgreSQL (шардирование по BIM id) +POSTGRES_ADDRESS_2=localhost +POSTGRES_PORT_2=5433 +POSTGRES_USER_2=user +POSTGRES_PASSWORD_2=password +POSTGRES_DB_2=bimbackendv2 +POSTGRES_POOL_SIZE_2=10 +DB_CERT_PATH_2=./.docker/yandex_pg.pem + +# Slave-2 кластер PostgreSQL (шардирование по BIM id) +POSTGRES_ADDRESS_3=localhost +POSTGRES_PORT_3=5434 +POSTGRES_USER_3=user +POSTGRES_PASSWORD_3=password +POSTGRES_DB_3=bimbackendv2 +POSTGRES_POOL_SIZE_3=10 +DB_CERT_PATH_3=./.docker/yandex_pg.pem + +# Границы шардирования BIM id между кластерами +LAST_MASTER_BIM=10 +LAST_MASTER_BIM_V3=0 +LAST_SLAVE_1_BIM=0 +LAST_SLAVE_1_BIM_V3=0 + +# TLS до PostgreSQL (0 — локально, 1 — в облаке) +ENABLE_SSL=0 +# Логировать SQL-запросы (1 — включено) +ENABLE_SQL_QUERY=1 + +# Внешний Django-бэкенд (проверка прав администратора) +DJANGO_HOST=https://stage.sarex.io + +# Режим интеграционных тестов (в этом режиме CheckUserIsAdmin всегда true) +INTEGRATION_TESTS=0 + +# --- Переменные окружения, не читаемые config.go (инфраструктура/тесты/сборка) --- +# Проброс портов и локальная БД в docker-compose +# API_PORT=5555 +# POSTGRES_EXTERNAL_PORT=5432 +# POSTGRES_EXTERNAL_PORT_2=5433 +# Значения для контейнеров тестов +# TEST_ENV=1 +# TEST_BIM=1 +# JWT_TEST= +# Объявлены в .env, но config.go их не использует +# GRPC_ADDRESS=0.0.0.0 +# GRPC_PORT=50051 diff --git a/apps/bim/CONFIGURATION.md b/apps/bim/CONFIGURATION.md new file mode 100644 index 0000000..0533372 --- /dev/null +++ b/apps/bim/CONFIGURATION.md @@ -0,0 +1,189 @@ +# Конфигурация проекта bim-backend-v2 + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` (функция `NewConfig`) через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (структура `Config`). + +Особенности разбора: + +- **Префикса нет** — `envconfig.Process("", &cfg)` вызывается с пустым префиксом, поэтому имена переменных задаются как есть (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`). Имя переменной определяется тегом `envconfig:"..."` у каждого поля. +- Типы приводятся автоматически по типу поля Go (`string`, `int`, `bool`, `uint64`). Для `bool` подходят `1`/`0`/`true`/`false`. +- Ошибка разбора приводит к `panic` при старте (в `NewConfig` `envconfig.Process` завёрнут в `sync.Once`, ошибка не возвращается, а паникует). +- Отсутствующая переменная не является ошибкой — поле получает нулевое значение соответствующего типа (пустая строка, `0`, `false`). Обязательность полей не проверяется. + +В отличие от python-сервисов, приложение **загружает `.env` автоматически**: в `cmd/httpserver/main.go` вызывается `godotenv.Load(".env")` (если файла нет — печатается предупреждение и используются переменные окружения процесса). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Файл `.env` в рабочем каталоге (`godotenv.Load`) либо переменные окружения процесса | +| Локально (контейнеры) | `.docker/docker-compose.yml`: `env_file` → `.docker/.env` + `.docker/.docker.env` | +| Kubernetes (Helm, репозиторий приложения) | `.helm/values-.yaml`: блоки `envs` (обычные значения) и `secrets` (из k8s-секретов); шаблон `.helm/templates/api.yaml` | +| Kubernetes (IaC, этот репозиторий) | `iac/apps/bim/base/backend-deployment.yaml`: блок `env` и секреты из Vault (аннотации `vault.hashicorp.com/*`, шаблон `bim-postgresql`) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: подключение общих пайплайнов `generic/common-ci` и переменные `workflow.rules` по ветке/тегу | + +Способы запуска процессов: + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `httpserver` (`make api` → `go install ./cmd/httpserver`) | `cmd/httpserver/main.go` | HTTP API. При старте применяет миграции (`appmigrations.RunOnStartup`), поднимает `net/http/pprof` на `:8081`, затем запускает API на `API_ADDRESS` | +| `migrations` (`go install ./cmd/migrations`) | `cmd/migrations/main.go` | Отдельный запуск миграций БД | + +Порядок запуска в контейнере (`.docker/entrypoint.sh`): запускается `/go/bin/httpserver` (строка запуска миграций закомментирована — миграции выполняются самим приложением на старте). Финальный образ (`.docker/api.dockerfile`) — `scratch` со статически слинкованным бинарником `httpserver` и вшитым CA-сертификатом Postgres (`/root/yandex_pg.pem`). + +## Переменные приложения + +Ниже перечислены все переменные, читаемые кодом (`config/config.go`). Префикса нет. Дефолт — это нулевое значение типа Go, если переменная не задана. + +### App + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `APP_NAME` | string | `""` | Имя приложения (в коде помечено TODO — в deploy-envs не задаётся) | +| `APP_VERSION` | string | `""` | Версия приложения (TODO — в deploy-envs не задаётся) | +| `LOG_LEVEL` | string | `""` | Уровень логирования, передаётся в `logging.NewLogger` (TODO — в deploy-envs не задаётся) | +| `API_ADDRESS` | string | `""` | Адрес прослушивания HTTP API в формате `host:port` (напр. `0.0.0.0:8080`) | +| `TEST_ENV` | string | `""` | Служебное поле для тестов | + +### PostgreSQL + +Сервис работает с **тремя** кластерами PostgreSQL (master, slave-1, slave-2). Выбор кластера для конкретного BIM выполняется по его id в `internal/app/http/httpserver.go` (сравнение с `LAST_MASTER_BIM*`/`LAST_SLAVE_1_BIM*`). Строка подключения собирается в `Config.GetPostgresConnectionURL` (`postgres://user:password@addr:port/db`, при `ENABLE_SSL=false` добавляется `?sslmode=disable`). + +Master-кластер: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `POSTGRES_ADDRESS` | string | Хост PostgreSQL (master) | +| `POSTGRES_PORT` | string | Порт PostgreSQL (master) | +| `POSTGRES_USER` | string | Пользователь БД | +| `POSTGRES_PASSWORD` | string | Пароль пользователя БД | +| `POSTGRES_DB` | string | Имя базы данных | +| `POSTGRES_POOL_SIZE` | int | Размер пула соединений | +| `DB_CERT_PATH` | string | Путь к CA-сертификату PostgreSQL (используется при `ENABLE_SSL=1`) | + +Slave-1 кластер — те же поля с суффиксом `_2`: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `POSTGRES_ADDRESS_2` | string | Хост PostgreSQL (slave-1) | +| `POSTGRES_PORT_2` | string | Порт | +| `POSTGRES_USER_2` | string | Пользователь | +| `POSTGRES_PASSWORD_2` | string | Пароль | +| `POSTGRES_DB_2` | string | База данных | +| `POSTGRES_POOL_SIZE_2` | int | Размер пула | +| `DB_CERT_PATH_2` | string | Путь к CA-сертификату | + +Slave-2 кластер — те же поля с суффиксом `_3`: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `POSTGRES_ADDRESS_3` | string | Хост PostgreSQL (slave-2) | +| `POSTGRES_PORT_3` | string | Порт | +| `POSTGRES_USER_3` | string | Пользователь | +| `POSTGRES_PASSWORD_3` | string | Пароль | +| `POSTGRES_DB_3` | string | База данных | +| `POSTGRES_POOL_SIZE_3` | int | Размер пула | +| `DB_CERT_PATH_3` | string | Путь к CA-сертификату | + +### Шардирование BIM по кластерам + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `LAST_MASTER_BIM` | uint64 | Верхняя граница id BIM для master-кластера (API v1) | +| `LAST_MASTER_BIM_V3` | uint64 | Верхняя граница id BIM для master-кластера (API v2 / BIM v3) | +| `LAST_SLAVE_1_BIM` | uint64 | Верхняя граница id BIM для slave-1 (API v1) | +| `LAST_SLAVE_1_BIM_V3` | uint64 | Верхняя граница id BIM для slave-1 (API v2 / BIM v3) | + +### Прочее + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_SSL` | bool | `false` | Подключение к PostgreSQL по TLS. При `false` в DSN добавляется `sslmode=disable` | +| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы | +| `DJANGO_HOST` | string | `""` | Базовый URL Django-бэкенда для проверки прав администратора (см. `ENDPOINTS.md`) | +| `INTEGRATION_TESTS` | bool | `false` | Режим интеграционных тестов. При `true` `CheckUserIsAdmin` всегда возвращает `true` | + +## Переменные, не читаемые приложением (инфраструктура/сборка/тесты) + +Присутствуют в `.docker/.env`, `.docker/.docker.env`, `docker-compose.yml` или Helm, но `config/config.go` их не разбирает. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_PORT` | `.docker/.env` | Порт API для локального compose | +| `POSTGRES_EXTERNAL_PORT`, `POSTGRES_EXTERNAL_PORT_2` | `docker-compose.yml` | Проброс портов контейнеров Postgres | +| `POSTGRES_ADDRESS2` | `.docker/.docker.env` | Опечатка/легаси (нет суффикса `_`), кодом не читается | +| `GRPC_ADDRESS`, `GRPC_PORT` | `.docker/.env` | Объявлены, но кодом не используются | +| `TEST_BIM`, `JWT_TEST` | `.docker/.env`, `.docker/.docker.env` | Данные для интеграционных тестов | +| `POSTGRES_*_4`, `DB_CERT_PATH_4` | `.helm/values-*.yaml`, IaC `backend-deployment.yaml` | Четвёртый кластер задаётся в деплое, но `config.go` доходит только до суффикса `_3` | +| `LAST_SLAVE_2_BIM(_V3)`, `LAST_SLAVE_3_BIM(_V3)`, `LAST_SLAVE_4_BIM(_V3)` | `.helm/values-*.yaml` | Заданы в values, но кодом не читаются (актуальны только `LAST_MASTER_*` и `LAST_SLAVE_1_*`) | +| `CI_COMMIT_SHORT_SHA` | `.gitlab-ci.yml` (build-arg) | Тег/версия сборки образа | + +## Переменные из Helm-чарта приложения (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `envs` для каждого окружения (`stage`/`preprod`/`production`) и содержат те же переменные `POSTGRES_*`, `API_ADDRESS`, `DJANGO_HOST`, `ENABLE_SSL`, `ENABLE_SQL_QUERY`, `LAST_*`, что описаны выше (различаются адресами БД, размерами пула, границами шардирования и доменом Django). + +Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `POSTGRES_USER` | `bim-v2-database-secret` | `username` | +| `POSTGRES_PASSWORD` | `bim-v2-database-secret` | `password` | +| `POSTGRES_USER_2` | `bim-v2-database-secret-2` | `username` | +| `POSTGRES_PASSWORD_2` | `bim-v2-database-secret-2` | `password` | +| `POSTGRES_USER_3` | `bim-v2-database-secret-3` | `username` | +| `POSTGRES_PASSWORD_3` | `bim-v2-database-secret-3` | `password` | +| `POSTGRES_USER_4` | `bim-v2-database-secret-3` | `username` | +| `POSTGRES_PASSWORD_4` | `bim-v2-database-secret-3` | `password` | + +Ключевые не-env значения чарта: `api.name`, `api.image`, `api.version`, `api.port` (`8080`), `api.replicas`, `api.service_name`/`api.service_port` (`80`), `api.service_account`, `api.requests.memory`/`cpu`, `api.api_host`, `api.api_host_prefix` (`/bimv2/api/`), `api.api_path` (`/api/`), `api.internal_path` (`/internal/`), `api.permitted_ns` (namespace'ы, которым разрешён доступ к `/internal/*`), `imagePullSecrets`. + +Сетевой слой (`.helm/templates/mesh-config.yaml`, Istio): + +- `VirtualService` (при `api.virtual_service.enabled`) маршрутизирует `api_host_prefix` → `api_path`, задаёт CORS (`allowOrigins` — `*.sarex.io` и `localhost`, `allowHeaders` включают `Authorization`, `Content-Type`, `Identity`). +- `AuthorizationPolicy` `routes-v2`: доступ к `/api/*` разрешён только от istio-ingressgateway при наличии claim `token_type=access`; доступ к `/internal/*` — только из namespace'ов `api.permitted_ns`. + +## Переменные из IaC-репозитория (`iac/apps/bim/`) + +Деплой в этом репозитории (kustomize) устроен иначе, чем чарт приложения: + +- `base/backend-deployment.yaml` — Deployment `backend` в namespace `bim`, образ `bim-api`, `containerPort: 8000`, `API_ADDRESS=0.0.0.0:8000`, health-проба `GET /ping`. +- Секреты PostgreSQL инъектируются **из Vault** (аннотации `vault.hashicorp.com/*`, роль `bim`, путь `secrets/data/postgresql/apps/bim`) в файл `/vault/secrets/bim-postgresql`, который экспортируется в окружение перед запуском (`set -a; . /vault/secrets/bim-postgresql; set +a; exec ./httpserver`). Шаблон Vault задаёт `POSTGRES_ADDRESS[_2../_4]`, `POSTGRES_PORT*`, `POSTGRES_DB*` (все указывают на `postgresql.bim.svc.cluster.local:5432`, БД `bim_db`) и `POSTGRES_USER*`/`POSTGRES_PASSWORD*` из Vault. +- Прочие env заданы прямо в манифесте: `LAST_MASTER_BIM`, `LAST_MASTER_BIM_V3`, `LAST_SLAVE_1_BIM`, `POSTGRES_POOL_SIZE`, `DB_CERT_PATH_2/3/4`, `DJANGO_HOST` (`http://backend.django.svc.cluster.local:8000`), `ENABLE_SQL_QUERY=0`, `ENABLE_SSL=0`. +- Оверлеи `brusnika-prod`, `brusnika-stage`, `yc-k8s-test` патчат base (реплики, образ, postgresql). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`universal-pipeline-.yaml`, `common-security-scan.yaml`, `common-build.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | +| --- | --- | --- | +| ветка `master` | `preprod` | `bim-api-preprod` | +| ветка `stage` | `stage` | `bim-api-stage` | +| тег (`CI_COMMIT_TAG`) | `prod` | `bim-api-prod` | + +Общие переменные job'ов: `RELEASE_NAME=bim-backend-v2`, `CHART_NAME=bim-backend-v2`, `CHART_VERSION=0.0.1-`, `IMAGE_PATH=api.image`, `DOCKERFILE_PATH=.docker/api.dockerfile`, `HELM_SET_ARGS=--set api.image=${IMAGE_NAME}`, `BUILD_ARGS=--build-arg CI_COMMIT_SHORT_SHA=...`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY=true`, `ENABLE_LINTER=false`. Стадии: `linter → test → unittest → prebuild-secscan → build → state-update → deploy`. + +## Замечания и потенциальные проблемы + +- Переменные разбираются **без префикса** (`envconfig.Process("", ...)`) — имена совпадают с тегами `envconfig` полей структуры `Config`. +- Обязательность полей не валидируется: отсутствующая переменная молча получает нулевое значение. Например пустой `API_ADDRESS` приведёт к попытке слушать на `":0"`. +- В коде объявлены только три кластера (`POSTGRES_*`, `_2`, `_3`), тогда как в деплое присутствует и четвёртый (`_4`). Переменные `_4` и дополнительные `LAST_SLAVE_2/3/4_*` в окружении задаются, но приложением не используются. +- Приложение загружает `.env` автоматически (`godotenv.Load(".env")`); при отсутствии файла ошибки нет — берутся переменные процесса. +- В `NewConfig` ошибка `envconfig.Process` не возвращается, а вызывает `panic` (обёрнута в `sync.Once`). + +## Минимальный набор для локального запуска + +Postgres поднимается через `docker-compose` (`.docker/docker-compose.yml`), приложение — через `make api` и запуск `httpserver`. Минимально необходимо задать (готовые примеры — в `.env.example`): + +- `API_ADDRESS` +- Master-кластер: `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE` +- Slave-кластеры `_2` и `_3` (те же поля) — сервис создаёт соединения ко всем трём при старте +- `ENABLE_SSL=0`, `DB_CERT_PATH*` (при `ENABLE_SSL=1`) +- `DJANGO_HOST` +- `LAST_MASTER_BIM`, `LAST_MASTER_BIM_V3`, `LAST_SLAVE_1_BIM`, `LAST_SLAVE_1_BIM_V3` +- `INTEGRATION_TESTS=0`, `ENABLE_SQL_QUERY` по необходимости diff --git a/apps/bim/ENDPOINTS.md b/apps/bim/ENDPOINTS.md new file mode 100644 index 0000000..eb75376 --- /dev/null +++ b/apps/bim/ENDPOINTS.md @@ -0,0 +1,55 @@ +# Эндпоинты, с которыми взаимодействует bim-backend-v2 + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается сам сервис `bim-backend-v2` (исходящие вызовы). Эндпоинты, которые сервис **предоставляет**, описаны в `openapi.yaml`. + +## Как устроено взаимодействие + +Исходящие вызовы выполняются HTTP-клиентом на базе [`go-resty/resty`](https://github.com/go-resty/resty) в пакете `pkg/django_client` (`DjangoClient`). Клиент создаётся в `NewRestClient`: + +- базовый хост — `SetHostURL(cfg.DjangoHost)` (переменная `DJANGO_HOST`); +- таймаут запроса — `SetTimeout(10 * time.Second)`; +- число повторов — `SetRetryCount(5)`. + +Аутентификация проксируется: JWT пользователя извлекается из контекста запроса (`auth.JWTFromContext`), при необходимости отбрасывается схема `Bearer `, и токен передаётся во внешний сервис заголовком `Authorization: Bearer ` (`SetAuthToken` + `SetAuthScheme("Bearer")`). Заголовок `Content-Type: application/json`. + +В режиме интеграционных тестов (`INTEGRATION_TESTS=1`) внешний вызов не выполняется — `CheckUserIsAdmin` возвращает `true`. + +## Базовые хосты по сервисам и окружениям + +Значение берётся из переменной `DJANGO_HOST` (см. `CONFIGURATION.md`). Итоговый URL = `` + `path` эндпоинта. + +| Сервис | Назначение | Значение `DJANGO_HOST` | +| --- | --- | --- | +| `django` (Sarex backend) | Проверка прав пользователя (админ/не админ) | локально/`stage`: `https://stage.sarex.io`; `preprod`: `https://lk.preprod.sarex.io`; `prod`: `https://lk.sarex.io`; контур (IaC): `http://backend.django.svc.cluster.local:8000` | + +## Эндпоинты по сервисам + +### `django` — Sarex backend + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `api/client/settings/` | Получить настройки текущего пользователя. Используется поле `is_admin` (проверка прав администратора в `CheckUserIsAdmin`) | + +Детали вызова `GET api/client/settings/`: + +| Параметр | Значение | +| --- | --- | +| Заголовки | `Authorization: Bearer `, `Content-Type: application/json` | +| Тело запроса | нет | +| Ожидаемый ответ | `{ "is_admin": bool }` (структура `userSettingsFromDjango`) | +| Успешные статусы | `200`, `201` | +| Поведение при ошибке | Любая ошибка транспорта, `resp == nil` или статус вне `200/201` логируется, метод трактует пользователя как **не администратора** (`false`) | + +## Где используется + +Проверка `CheckUserIsAdmin` вызывается в обработчиках, изменяющих модели статусов (требуют прав администратора). При отсутствии прав такие эндпоинты возвращают `403 No admin rights`: + +- `POST /api/v1/bims/{bim_id}/status_model` — создание модели статусов BIM; +- `DELETE /api/v1/bims/{bim_id}/delete_status_model` — удаление модели статусов BIM; +- `POST /api/v1/companies/{company_id}/status_model` — создание модели статусов компании; +- `DELETE /api/v1/companies/{company_id}/status_model` — удаление модели статусов компании. + +## Замечания + +- Единственная внешняя HTTP-зависимость сервиса — Django-бэкенд (`DJANGO_HOST`). Прочие интеграции (PostgreSQL, pprof) не являются HTTP-вызовами к внешним REST-сервисам. +- Подпись JWT самим сервисом не проверяется — проброшенный токен просто пересылается в Django, который и выполняет авторизацию (см. также раздел «Аутентификация» в `openapi.yaml`). diff --git a/apps/bim/openapi.yaml b/apps/bim/openapi.yaml new file mode 100644 index 0000000..38fc7c5 --- /dev/null +++ b/apps/bim/openapi.yaml @@ -0,0 +1,1158 @@ +openapi: 3.0.3 + +info: + title: BIM Backend v2 API + version: "2.0.0" + description: | + REST API сервиса **bim-backend-v2** (`platform/bim-backend-v2`) — работа с + BIM-моделями, их элементами, свойствами, моделями статусов и метаданными. + + Сервис написан на Go. HTTP-сервер собирается в + `internal/app/http/httpserver.go` (роутер `gorilla/mux` поверх + `pkg/gotools/rest`). Роутинг состоит из трёх групп: + + - публичный API v1 — префикс `/api/v1` (`internal/controller/http/v1`); + - публичный API v2 — префикс `/api/v2`, работа с BIM v3 + (`internal/controller/http/v2`); + - внутренний API v1 — префикс `/internal/v1` + (`internal/controller/http/v1/bim_internal` и часть роутов `bim`), + предназначен для вызовов внутри кластера (через ingress не публикуется, + доступ ограничивается Istio `AuthorizationPolicy` по namespace). + + Health-check доступен по `GET /ping` (без префикса и без аутентификации), + профилировщик `net/http/pprof` — на отдельном порту `:8081`. + + ### Аутентификация + Публичные эндпоинты (`/api/v1/*`, `/api/v2/*`) требуют JWT. Токен + передаётся заголовком `Authorization: Bearer ` либо query-параметром + `auth_jwt` (`pkg/gotools/auth.JWTToCtx`). Затем `pkg/auth` + (`JWTUserExtractorFromCtx`) извлекает `user_id`: + + 1. если передан заголовок `Identity` — id берётся из Zitadel-токена + (claim `urn:zitadel:iam:user:metadata`, поле `user_id`, base64url); + 2. иначе — из числового claim `user_id` основного JWT. + + Подпись токена приложением **не проверяется** (проверка выполняется на + уровне Istio по claim `token_type=access`). При отсутствии/непарсинге + токена возвращается `401` с пустым телом. + + Изменение моделей статусов дополнительно требует прав администратора + (проверка через Django, см. `ENDPOINTS.md`); при их отсутствии — `403`. + + Внутренние эндпоинты (`/internal/v1/*`) аутентификации на уровне + приложения не требуют — доступ ограничен сетевым слоем (Istio, namespace). + + ### Идентификаторы + Все path-параметры (`bim_id`, `project_id`, `company_id`, `sarex_id` и т.п.) + парсятся как `uint64`; неверный формат → `400`. + + ### Формат ошибок + Ошибки из обработчиков возвращаются как `{ "error": "<сообщение>" }` + (`pkg/gotools/httperror`). Исключения: `401` от middleware (пустое тело), + а также `404`/`405` от роутера (текст `404 route not found` / + `405 method not allowed`). + +servers: + - url: https://api.sarex.io/bimv2 + description: production (через api-gateway, префикс api_host_prefix) + - url: https://stage-api.sarex.io/bimv2 + description: stage + - url: http://localhost:5555 + description: локальный запуск + +tags: + - name: bim-v1 + description: BIM, элементы, свойства, статусы (API v1) + - name: metadata + description: Метаданные BIM (API v1) + - name: company-status-model + description: Модели статусов компании (API v1) + - name: internal-v1 + description: Внутренние эндпоинты (только внутри кластера) + - name: bim-v2 + description: BIM v3 (API v2) + - name: service + description: Служебные эндпоинты + +security: + - bearerAuth: [] + +paths: + /ping: + get: + tags: [service] + summary: Health-check + security: [] + responses: + "200": + description: Сервис работоспособен + + /api/v1/projects/{project_id}/bims: + get: + tags: [bim-v1] + summary: Список BIM проекта + parameters: + - $ref: '#/components/parameters/ProjectId' + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + bims: + type: array + items: { $ref: '#/components/schemas/Bim' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims: + get: + tags: [bim-v1] + summary: Список BIM по набору id + parameters: + - name: bim_id + in: query + required: true + description: Список id BIM через запятую (напр. `1,2,3`) + schema: { type: string } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/Bim' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}: + get: + tags: [bim-v1] + summary: BIM по id + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Bim' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/sarexid/{sarex_id}: + get: + tags: [bim-v1] + summary: Элементы BIM по sarex_id (из пути) + parameters: + - $ref: '#/components/parameters/BimId' + - name: sarex_id + in: path + required: true + description: Список sarex_id через запятую + schema: { type: string } + - $ref: '#/components/parameters/WithHierarchy' + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BIMElementsResult' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/sarexid: + post: + tags: [bim-v1] + summary: Элементы BIM по sarex_id (из тела) + parameters: + - $ref: '#/components/parameters/BimId' + - $ref: '#/components/parameters/WithHierarchy' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GetBimElementsRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BIMElementsResult' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/changes: + get: + tags: [bim-v1] + summary: История изменений статусов элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + - { name: offset, in: query, required: false, schema: { type: integer, format: int64, default: 0 } } + - { name: limit, in: query, required: false, schema: { type: integer, format: int64, default: 20 } } + - { name: status_type, in: query, required: false, description: 'Типы статусов через запятую', schema: { type: string } } + - { name: sarex_ids, in: query, required: false, description: 'sarex_id через запятую', schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/PaginatedChangesResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + post: + tags: [bim-v1] + summary: Установить статус элементам BIM + parameters: + - $ref: '#/components/parameters/BimId' + - $ref: '#/components/parameters/WithHierarchy' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SetStatusBodyRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/SetStatusResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/elements: + get: + tags: [bim-v1] + summary: Дерево элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + - { name: depth, in: query, required: false, schema: { type: integer, format: int64, default: 5 } } + - { name: root, in: query, required: false, schema: { type: integer, format: int64 } } + - name: statuses + in: query + required: false + description: 'Повторяемый параметр вида `type:value`' + schema: { type: array, items: { type: string } } + - { name: format, in: query, required: false, schema: { type: string, enum: [full, tiny], default: full } } + responses: + "200": + description: 'При `format=full` элементы полные, при `format=tiny` — усечённые' + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + oneOf: + - { $ref: '#/components/schemas/BIMElement' } + - { $ref: '#/components/schemas/BIMElementTiny' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + post: + tags: [bim-v1] + summary: Отфильтрованные элементы BIM + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GetFilteredElementsRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BIMElementsResult' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/statuses_color: + get: + tags: [bim-v1] + summary: Цвета статусов BIM + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: 'Ключ — имя модели статусов' + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: { $ref: '#/components/schemas/StatusColorRequest' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/status_models: + get: + tags: [bim-v1] + summary: Модели статусов BIM + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/StatusCategory' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/status_model: + post: + tags: [bim-v1] + summary: Создать модель статусов BIM (требует прав администратора) + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/StatusCategory' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/OkResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "403": { $ref: '#/components/responses/Forbidden' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/delete_status_model: + delete: + tags: [bim-v1] + summary: Удалить модель статусов BIM (требует прав администратора) + parameters: + - $ref: '#/components/parameters/BimId' + - name: status_type + in: query + required: true + description: 'Имя модели статусов. Значение по умолчанию (`building`) удалить нельзя' + schema: { type: string } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/OkResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "403": { $ref: '#/components/responses/Forbidden' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/filter_fields: + get: + tags: [bim-v1] + summary: Поля для фильтрации элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: 'Категория → свойство → дескриптор фильтра (multiselector/slider/checkbox)' + content: + application/json: + schema: + type: object + additionalProperties: + type: object + additionalProperties: + oneOf: + - { $ref: '#/components/schemas/StringsType' } + - { $ref: '#/components/schemas/MinMaxType' } + - { $ref: '#/components/schemas/BooleansType' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/properties: + get: + tags: [bim-v1] + summary: Свойства всех элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: 'sarex_id → категория → свойство → значение. Пустой объект, если таблица свойств отсутствует' + content: + application/json: + schema: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + additionalProperties: true + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/elements/{sarex_id}/properties: + get: + tags: [bim-v1] + summary: Свойства одного элемента BIM + parameters: + - $ref: '#/components/parameters/BimId' + - { name: sarex_id, in: path, required: true, schema: { type: integer, format: int64 } } + responses: + "200": + description: 'категория → свойство → значение' + content: + application/json: + schema: + type: object + additionalProperties: + type: object + additionalProperties: true + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/statuses: + get: + tags: [bim-v1] + summary: Статусы элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + - name: statuses + in: query + required: false + description: 'Повторяемый параметр вида `type:value`' + schema: { type: array, items: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/BIMElementStatuses' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/filter_by_statuses: + get: + tags: [bim-v1] + summary: sarex_id элементов, сгруппированные по статусам + parameters: + - $ref: '#/components/parameters/BimId' + - name: statuses + in: query + required: false + description: 'Повторяемый параметр вида `type:value`' + schema: { type: array, items: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/FilterElementsByStatusResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/csv_properties: + post: + tags: [bim-v1] + summary: CSV-отчёт по свойствам элементов BIM + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GetCSVPropertiesReportRequest' } + responses: + "200": + description: 'CSV-файл (разделитель `;`). Заголовки Content-Disposition: attachment; filename=.csv' + content: + text/csv: + schema: { type: string, format: binary } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/metadata: + get: + tags: [metadata] + summary: Метаданные BIM + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/GetMetadataResponse' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/bims/{bim_id}/metadata/transform: + patch: + tags: [metadata] + summary: Обновить матрицу трансформации BIM + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SetMetadataTransformRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/OkResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v1/companies/{company_id}/status_model: + post: + tags: [company-status-model] + summary: Создать модель статусов компании (требует прав администратора) + parameters: + - $ref: '#/components/parameters/CompanyId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/StatusCategory' } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + result: { $ref: '#/components/schemas/CompanyStatusModel' } + "400": { $ref: '#/components/responses/BadRequest' } + "403": { $ref: '#/components/responses/Forbidden' } + "500": { $ref: '#/components/responses/InternalError' } + get: + tags: [company-status-model] + summary: Модель статусов компании + parameters: + - $ref: '#/components/parameters/CompanyId' + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + result: { $ref: '#/components/schemas/CompanyStatusModel' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + delete: + tags: [company-status-model] + summary: Удалить модель статусов компании (требует прав администратора) + parameters: + - $ref: '#/components/parameters/CompanyId' + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/OkResponse' } + "400": { $ref: '#/components/responses/BadRequest' } + "403": { $ref: '#/components/responses/Forbidden' } + "500": { $ref: '#/components/responses/InternalError' } + + /internal/v1/bims/{bim_id}/sarexid: + post: + tags: [internal-v1] + summary: Элементы BIM по sarex_id (внутренний, без аутентификации приложения) + security: [] + parameters: + - $ref: '#/components/parameters/BimId' + - $ref: '#/components/parameters/WithHierarchy' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GetBimElementsRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BIMElementsResult' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /internal/v1/projects/{project_id}/bims: + post: + tags: [internal-v1] + summary: Создать BIM в проекте (внутренний) + security: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateBimRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Bim' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /internal/v1/bims/{bim_id}/filter_elements_by_status: + post: + tags: [internal-v1] + summary: Отфильтровать sarex_id по статусам (внутренний) + security: [] + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GetFilteredSarexIDsByStatusRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + sarex_ids: + type: array + items: { type: integer, format: int64 } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /internal/v1/bims/guid_sarex_id: + get: + tags: [internal-v1] + summary: Сопоставление GUID → sarex_id (внутренний) + security: [] + parameters: + - name: bim_ids + in: query + required: true + description: 'Список id BIM через запятую' + schema: { type: string } + responses: + "200": + description: 'Ключ — GUID элемента' + content: + application/json: + schema: + type: object + additionalProperties: { $ref: '#/components/schemas/BimIDSarexID' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v2/bims: + get: + tags: [bim-v2] + summary: Список BIM v3 + parameters: + - { name: company_id, in: query, required: false, schema: { type: integer, format: int64 } } + - { name: bundle_id, in: query, required: false, schema: { type: string, format: uuid } } + - { name: document_id, in: query, required: false, schema: { type: integer, format: int64 } } + - { name: bim_type, in: query, required: false, schema: { type: integer, format: int64 } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/BIMV3' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v2/bims/{bim_id}: + get: + tags: [bim-v2] + summary: BIM v3 по id + parameters: + - $ref: '#/components/parameters/BimId' + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BIMV3' } + "400": { $ref: '#/components/responses/BadRequest' } + "404": { $ref: '#/components/responses/NotFound' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v2/bims/{bim_id}/elements: + post: + tags: [bim-v2] + summary: Список элементов BIM v3 (с фильтрами по атрибутам) + parameters: + - $ref: '#/components/parameters/BimId' + requestBody: + required: false + content: + application/json: + schema: { $ref: '#/components/schemas/ListBIMElementsRequest' } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/BIMV3Element' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + + /api/v2/bims/{bim_id}/elements/{sarex_id}: + get: + tags: [bim-v2] + summary: Элемент(ы) BIM v3 по sarex_id + parameters: + - $ref: '#/components/parameters/BimId' + - { name: sarex_id, in: path, required: true, schema: { type: integer, format: int64 } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/BIMV3Element' } + "400": { $ref: '#/components/responses/BadRequest' } + "500": { $ref: '#/components/responses/InternalError' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + `Authorization: Bearer ` (или query-параметр `auth_jwt`). Дополнительно + может передаваться заголовок `Identity` для Zitadel-токена. + + parameters: + BimId: + name: bim_id + in: path + required: true + schema: { type: integer, format: int64 } + ProjectId: + name: project_id + in: path + required: true + schema: { type: integer, format: int64 } + CompanyId: + name: company_id + in: path + required: true + schema: { type: integer, format: int64 } + WithHierarchy: + name: with_hierarchy + in: query + required: false + schema: { type: boolean, default: false } + + responses: + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + NotFound: + description: Не найдено + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + Forbidden: + description: Недостаточно прав (нужны права администратора) + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + InternalError: + description: Внутренняя ошибка сервера + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + + schemas: + Error: + type: object + properties: + error: { type: string } + required: [error] + + OkResponse: + type: object + properties: + ok: { type: boolean, example: true } + + Bim: + type: object + properties: + id: { type: integer, format: int64 } + project_id: { type: integer, format: int64 } + document_id: { type: integer, format: int64 } + created_at: { type: string, format: date-time } + status: { type: string, description: 'BimStatusType, напр. Pending' } + transform: { type: array, items: { type: number, format: double } } + status_model: + type: array + items: { $ref: '#/components/schemas/StatusCategory' } + properties_names: { type: array, items: { type: string } } + category_properties: + type: object + additionalProperties: + type: object + additionalProperties: { type: integer, format: int64 } + data_types: { type: array, items: { type: string } } + categories_names: { type: array, items: { type: string } } + company_id: { type: integer, format: int64, nullable: true } + + StatusCategory: + type: object + required: [name, verbose_name, initial_status, statuses] + properties: + name: { type: string } + verbose_name: { type: string } + initial_status: { type: string } + statuses: + type: array + minItems: 1 + items: { $ref: '#/components/schemas/Statuses' } + + Statuses: + type: object + required: [verbose_name, color, permissions, name, allowed_transitions] + properties: + verbose_name: { type: string } + color: { type: integer, description: '0..16777215 (RGB)' } + permissions: { type: array, items: { type: string } } + name: { type: string } + allowed_transitions: { type: array, items: { type: string } } + + StatusColorRequest: + type: object + properties: + name: { type: string } + color: { type: integer } + + BIMElement: + type: object + description: 'Кастомная сериализация; hierarchy разворачивается в массив uint64' + properties: + sarex_id: { type: integer, format: int64 } + name: { type: string } + hierarchy: { type: array, items: { type: integer, format: int64 } } + updated_at: { type: string, format: date-time, nullable: true } + bim_id: { type: integer, format: int64 } + statuses: + type: object + additionalProperties: { type: string } + is_leaf: { type: boolean, nullable: true } + extras_from_converter: + type: object + additionalProperties: true + nullable: true + bboxMax: { type: array, items: { type: number, format: double } } + bboxMin: { type: array, items: { type: number, format: double } } + color: { type: integer, nullable: true } + + BIMElementTiny: + type: object + properties: + sarex_id: { type: integer, format: int64 } + parent_id: { type: integer, format: int64, nullable: true } + + BIMElementStatuses: + type: object + properties: + sarex_id: { type: integer, format: int64 } + statuses: + type: object + additionalProperties: { type: string } + + BIMElementsResult: + type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/BIMElement' } + + StringsType: + type: object + properties: + values: { type: array, items: { type: string, nullable: true } } + type: { type: string, enum: [multiselector, slider, checkbox] } + + MinMaxType: + type: object + properties: + min: { type: number, format: double, nullable: true } + max: { type: number, format: double, nullable: true } + has_null: { type: boolean } + type: { type: string, enum: [multiselector, slider, checkbox] } + + BooleansType: + type: object + properties: + values: { type: array, items: { type: boolean, nullable: true } } + type: { type: string, enum: [multiselector, slider, checkbox] } + + FilterFieldRequestStruct: + type: object + required: [category_name, property_name] + properties: + category_name: { type: string } + property_name: { type: string } + values: { type: array, items: {} } + min: { type: number, format: double, nullable: true } + max: { type: number, format: double, nullable: true } + + StatusFiltersRequestStruct: + type: object + properties: + group: { type: string } + values: { type: array, items: { type: string } } + + FiltersResponse: + type: object + properties: + properties_filters: + type: array + items: { $ref: '#/components/schemas/FilterFieldRequestStruct' } + status_filters: + type: array + items: { $ref: '#/components/schemas/StatusFiltersRequestStruct' } + + GetBimElementsRequest: + type: object + required: [sarex_ids] + properties: + sarex_ids: + type: array + minItems: 1 + items: { type: integer, format: int64 } + + GetFilteredElementsRequest: + type: object + required: [filters] + properties: + filters: { $ref: '#/components/schemas/FiltersResponse' } + + SetStatusBodyRequest: + type: object + required: [sarex_ids, new_status_type, new_status_value] + properties: + sarex_ids: + type: array + items: { type: integer, format: int64 } + new_status_type: { type: string } + new_status_value: { type: string } + + SetStatusResponse: + type: object + properties: + count: { type: integer, format: int64 } + + PaginatedChangesResponse: + type: object + properties: + data: + type: array + items: { $ref: '#/components/schemas/ChangedBimElementDTO' } + total: { type: integer, format: int64 } + count: { type: integer, format: int64 } + offset: { type: integer, format: int64 } + limit: { type: integer, format: int64 } + + ChangedBimElementDTO: + type: object + properties: + bim_id: { type: integer, format: int64 } + sarex_id: { type: integer, format: int64 } + created_at: { type: string, format: date-time } + number: { type: integer, format: int64 } + author: { type: integer, format: int64 } + new_status: + type: object + additionalProperties: { type: string } + old_statuses: + type: object + additionalProperties: { type: string } + + FilterElementsByStatusResponse: + type: object + properties: + results: + type: object + additionalProperties: + type: object + additionalProperties: { $ref: '#/components/schemas/ElementsResponse' } + + ElementsResponse: + type: object + properties: + sarex_ids: + type: array + items: { type: integer, format: int64 } + + GetCSVPropertiesReportRequest: + type: object + required: [filters] + properties: + filters: + type: object + properties: + properties_filters: + type: array + items: { $ref: '#/components/schemas/FilterFieldRequestStruct' } + statuses_filters: + type: object + additionalProperties: + type: array + items: { type: string } + element_ids: + type: array + items: { type: integer, format: int64 } + reported_category_property: + type: object + additionalProperties: + type: array + items: { type: string } + reported_statuses: + type: array + items: { type: string } + + GetMetadataResponse: + type: object + properties: + elements: + type: object + additionalProperties: { $ref: '#/components/schemas/MetaElement' } + bim: { type: integer, format: int64 } + created_at: { type: string, format: date-time } + global_transformation_matrix: + type: array + items: { type: number, format: double } + + MetaElement: + type: object + properties: + name: { type: string } + hierarchy: { type: array, items: { type: integer, format: int64 } } + bboxMax: { type: array, items: { type: number, format: double } } + bboxMin: { type: array, items: { type: number, format: double } } + children: { type: array, items: { type: integer, format: int64 } } + color: { type: integer, nullable: true } + + SetMetadataTransformRequest: + type: object + required: [transform] + properties: + transform: + type: array + minItems: 16 + maxItems: 16 + items: { type: number, format: double } + + CompanyStatusModel: + type: object + properties: + id: { type: integer, format: int64 } + updated_at: { type: string, format: date-time, nullable: true } + status_model: { $ref: '#/components/schemas/StatusCategory' } + + CreateBimRequest: + type: object + required: [document_id] + properties: + document_id: { type: integer, format: int64 } + + GetFilteredSarexIDsByStatusRequest: + type: object + properties: + sarex_ids: + type: array + items: { type: integer, format: int64 } + status_filters: + type: array + items: { $ref: '#/components/schemas/StatusFiltersRequestStruct' } + + BimIDSarexID: + type: object + properties: + bim_id: { type: integer, format: int64 } + document_id: { type: integer, format: int64 } + sarex_id: { type: integer, format: int64 } + path: { type: string } + + BIMV3: + type: object + properties: + id: { type: integer, format: int64 } + created_at: { type: string, format: date-time } + company_id: { type: integer, format: int64 } + bundle_id: { type: string, format: uuid } + document_id: { type: integer, format: int64 } + bim_type: { type: integer, description: '0 — BIM, 1 — Comparison' } + transform: { type: array, items: { type: number, format: double } } + reference_bundle_id: { type: string, format: uuid, nullable: true } + compared_to_bundle_id: { type: string, format: uuid, nullable: true } + + BIMV3Element: + type: object + description: 'Кастомная сериализация; hierarchy разворачивается в массив uint64' + properties: + id: { type: integer, format: int64 } + bim_id: { type: integer, format: int64 } + sarex_id: { type: integer, format: int64 } + name: { type: string } + hierarchy: { type: array, items: { type: integer, format: int64 } } + is_leaf: { type: boolean } + bboxMin: { type: array, items: { type: number, format: double } } + bboxMax: { type: array, items: { type: number, format: double } } + attributes: + type: object + additionalProperties: true + + ListBIMElementsRequest: + type: object + properties: + attributes_filters: + type: array + items: { $ref: '#/components/schemas/ListBIMElementsAttributesFilter' } + + ListBIMElementsAttributesFilter: + type: object + required: [id, values] + properties: + id: { type: integer, format: int64 } + values: + type: array + minItems: 1 + items: {} diff --git a/apps/cde/.env.example b/apps/cde/.env.example new file mode 100644 index 0000000..706e257 --- /dev/null +++ b/apps/cde/.env.example @@ -0,0 +1,80 @@ +# ============================================================================= +# cde-orchestration-demo (Оркестратор) — пример переменных окружения +# +# Разбор переменных: github.com/sethvargo/go-envconfig. +# Файл .env подгружается через godotenv (флаг -env-file, по умолчанию `.env`). +# Глобального префикса у переменных НЕТ (в отличие от других сервисов Sarex). +# Вложенные секции задаются префиксом в env-тегах (напр. DATABASE_URL). +# +# Значения ниже — примеры и значения по умолчанию из кода. Секреты (ключи, +# пароли, токены) оставлены пустыми — заполните собственными. +# Один и тот же .env используется всеми бинарниками (см. docker-compose.yml), +# но каждый бинарник читает только нужное ему подмножество (см. CONFIGURATION.md). +# ============================================================================= + +# --- Общие --- +ENVIRONMENT=production +LOG_LEVEL=info # cmd/http: default=info; воркеры: default=debug +IS_CONTOUR=false + +# --- HTTP-сервер (cmd/http) --- +ADDRESS=:8080 +PROCESS_CACHE_TTL=60 +PROCESS_CACHE_CLEAR_INTERVAL=60 +PUBLIC_KEY= # PEM RSA public key для проверки JWT (обязателен для http) +OPERATE_URL= # Базовый URL Camunda Operate/REST +SAREX_BACKEND_BASE_URL=https://stage.sarex.io + +# --- Camunda --- +CAMUNDA_KEYCLOAK_URL= +CAMUNDA_CLIENT_ID=operate # только cmd/http +CAMUNDA_CLIENT_SECRET=identity-secret-for-components # только cmd/http +CAMUNDA_PROCESS_DEFINITION_ID=actionsOnApproval # только cmd/http + +# --- Zeebe --- +ZEEBE_GATEWAY= +ZEEBE_CLIENT_ID=zeebe +ZEEBE_CLIENT_SECRET=identity-secret-for-components +ZEEBE_WORKER_JOB_TYPE=markDocuments # у каждого воркера своё значение по умолчанию (см. CONFIGURATION.md) + +# --- Database (PostgreSQL) --- +DATABASE_URL= +DATABASE_POOL_SIZE=10 + +# --- S3 --- +S3_ENDPOINT_URL=https://storage.yandexcloud.net +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_PARTITION_ID=yc +S3_SIGNING_REGION=ru-central1 + +# --- Auth (без префикса) --- +AUTH_HOST= +USERNAME= +PASSWORD= + +# --- Flows (сервис рабочих процессов) --- +FLOWS_URL= +FLOWS_INTERNAL_URL= + +# --- Workspaces --- +WORKSPACES_URL= + +# --- Workflows (используется воркером split_pdf) --- +WORKFLOWS_HOST= +WORKFLOWS_IMAGE_TAG=latest + +# --- System log --- +SYSTEM_LOG_URL= + +# --- Telegram (алертинг воркеров) --- +TELEGRAM_TOKEN= +TELEGRAM_ALERT_GROUP_ID= +TELEGRAM_DEBUG=false + +# --- AMQP / RabbitMQ (markings v2, copy v2) --- +AMQP_HOST= +AMQP_PORT= +AMQP_USER= +AMQP_PASSWORD= +AMQP_PATH_API= diff --git a/apps/cde/CONFIGURATION.md b/apps/cde/CONFIGURATION.md new file mode 100644 index 0000000..7e162fa --- /dev/null +++ b/apps/cde/CONFIGURATION.md @@ -0,0 +1,215 @@ +# Конфигурация проекта cde-orchestration-demo (Оркестратор) + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig): в каждом бинарнике вызывается `envconfig.Process(ctx, config)` со своей структурой `Config` (см. `internal/app/http/config.go` и `internal/app/worker/*/config.go`). + +Особенности разбора: + +- **Глобального префикса нет** — в отличие от других сервисов Sarex, переменные не имеют общего префикса (напр. просто `LOG_LEVEL`, `DATABASE_URL`). +- Вложенные секции задаются тегом `env:", prefix=XXX_"` на поле-структуре. Например поле `Database DatabaseConfig` с `prefix=DATABASE_` и полем `Url` с тегом `env:"URL"` даёт переменную `DATABASE_URL`. +- Структура `Auth` **не имеет** тега `prefix`, поэтому её поля читаются без префикса: `AUTH_HOST`, `USERNAME`, `PASSWORD`. +- Значения по умолчанию задаются в теге через `default=...`. Отсутствие поля без дефолта не приводит к ошибке `envconfig` (пустое значение), но может привести к падению при инициализации зависимого клиента (напр. пустой `PUBLIC_KEY` вызовет панику при старте http). + +Файл `.env` подгружается через [`github.com/lpernett/godotenv`](https://github.com/lpernett/godotenv): в `main` вызывается `godotenv.Load(*envFileFlag)`, путь задаётся флагом `-env-file` (по умолчанию `.env`). Если файла нет — загрузка пропускается, переменные берутся из окружения процесса. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Флаг `-env-file` → `.env` (godotenv) и/или переменные окружения процесса | +| Локально (docker-compose) | `docker-compose.yml`: у каждого сервиса `env_file: .env` | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секрета `cde-secret`) чарта `universal-chart` | +| Kubernetes (kustomize, этот репозиторий) | Vault Agent инжектит секрет `secrets/data/vault/apps/cde` в файл `/vault/secrets/cde-env`, который экспортируется в окружение перед запуском бинарника (`source /vault/secrets/cde-env`) | + +## Бинарники (точки входа) + +| Бинарник | Точка входа | Config | Назначение | +| --- | --- | --- | --- | +| `http` | `cmd/http/main.go` | `internal/app/http` | HTTP API оркестрации (процессы, подпись, загрузка BPMN) | +| `copy` | `cmd/worker/copy/main.go` | `.../worker/copy` | Воркер копирования документов (`copyDocuments`) | +| `copyv2` | `cmd/worker/copyv2/main.go` | `.../worker/copyv2` | Копирование документов v2 (`copyDocumentsv2`) | +| `create_versions` | `cmd/worker/create_versions/main.go` | `.../worker/create_versions` | Создание версий (`createVersions`) | +| `create_versionsv2` | `cmd/worker/create_versionsv2/main.go` | `.../worker/create_versionsv2` | Создание версий v2 (`createVersionsv2`) | +| `flows_callback` | `cmd/worker/flows_callback/main.go` | `.../worker/flows_callback` | Обратный вызов в сервис flows (`flowsCallback`) | +| `markings` | `cmd/worker/markings/main.go` | `.../worker/markings` | Маркировка документов (`markDocuments`) | +| `markingsv2` | `cmd/worker/markingsv2/main.go` | `.../worker/markingsv2` | Маркировка v2 (`markDocumentsv2`) | +| `sign` | `cmd/worker/sign/main.go` | `.../worker/sign` | Подпись документов (`signDocuments`) | +| `signv2` | `cmd/worker/signv2/main.go` | `.../worker/signv2` | Подпись v2 (`signDocumentsv2`) | +| `split_pdf` | `cmd/worker/split_pdf/main.go` | `.../worker/split_pdf` | Разбиение/обработка PDF (`splitPDF`) | +| `update_bundles` | `cmd/worker/update_bundles/main.go` | `.../worker/update_bundles` | Обновление бандлов (`updateBundles`) | + +Воркеры — это Zeebe job-workers: они подключаются к Zeebe-gateway и обрабатывают Service Task соответствующего типа (`ZEEBE_WORKER_JOB_TYPE`). HTTP-сервер, помимо приёма запросов, обращается к Camunda Operate/Zeebe (см. `ENDPOINTS.md`). + +## Переменные приложения + +Дефолт `—` означает, что значения по умолчанию нет. + +### Общие (для всех бинарников) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENVIRONMENT` | string | `production` (у воркеров) | Окружение развёртывания. У `http` не читается | +| `LOG_LEVEL` | string | `info` (http) / `debug` (воркеры) | Уровень логирования | +| `IS_CONTOUR` | bool | `false` | Режим изолированного контура (влияет на инициализацию S3) | + +### HTTP-сервер (`cmd/http`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ADDRESS` | string | `:8080` | Адрес прослушивания Fiber | +| `PROCESS_CACHE_TTL` | int (сек) | `60` | TTL кеша процессов | +| `PROCESS_CACHE_CLEAR_INTERVAL` | int (сек) | `60` | Интервал очистки кеша процессов | +| `PUBLIC_KEY` | string (PEM) | — | RSA public key для проверки JWT. Обязателен: при пустом/некорректном значении сервис падает при старте | +| `OPERATE_URL` | string | — | Базовый URL Camunda Operate/REST | +| `SAREX_BACKEND_BASE_URL` | string | — | Базовый URL sarex-backend (проверка MRPA при подписи) | + +### Camunda (`CAMUNDA_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | Где используется | +| --- | --- | --- | --- | --- | +| `CAMUNDA_KEYCLOAK_URL` | string | — | URL Keycloak для OAuth (Zeebe/Operate) | http + воркеры | +| `CAMUNDA_CLIENT_ID` | string | `operate` | Client ID для Operate | только http | +| `CAMUNDA_CLIENT_SECRET` | string | `identity-secret-for-components` | Client secret для Operate | только http | +| `CAMUNDA_PROCESS_DEFINITION_ID` | string | `actionsOnApproval` | ID определения процесса | только http | + +### Zeebe (`ZEEBE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ZEEBE_GATEWAY` | string | — | Адрес Zeebe gateway | +| `ZEEBE_CLIENT_ID` | string | `zeebe` | Client ID | +| `ZEEBE_CLIENT_SECRET` | string | `identity-secret-for-components` | Client secret | +| `ZEEBE_WORKER_JOB_TYPE` | string | зависит от воркера | Тип Service Task, который слушает воркер | + +Значения `ZEEBE_WORKER_JOB_TYPE` по умолчанию: `markDocuments` (http/markings), `markDocumentsv2` (markingsv2), `copyDocuments` (copy), `copyDocumentsv2` (copyv2), `createVersions` (create_versions), `createVersionsv2` (create_versionsv2), `flowsCallback` (flows_callback), `signDocuments` (sign), `signDocumentsv2` (signv2), `splitPDF` (split_pdf), `updateBundles` (update_bundles). + +### Database (`DATABASE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_URL` | string | — | DSN подключения к PostgreSQL (pgx) | +| `DATABASE_POOL_SIZE` | int32 | `10` | Размер пула соединений | + +### S3 (`S3_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_ENDPOINT_URL` | string | `https://storage.yandexcloud.net` | Эндпоинт S3 | +| `S3_ACCESS_KEY_ID` | string | — | Access key | +| `S3_SECRET_ACCESS_KEY` | string | — | Secret key | +| `S3_PARTITION_ID` | string | `yc` | Partition ID (aws-sdk-go-v2) | +| `S3_SIGNING_REGION` | string | `ru-central1` | Регион для подписи запросов | + +### Auth (без префикса) + +Поле-структура `Auth` не имеет префикса, поэтому переменные читаются напрямую. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_HOST` | string | — | Хост сервиса аутентификации (получение токенов пользователя/админа) | +| `USERNAME` | string | — | Логин админ-учётки | +| `PASSWORD` | string | — | Пароль админ-учётки | + +### Flows (`FLOWS_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `FLOWS_URL` | string | — | Базовый URL сервиса flows | +| `FLOWS_INTERNAL_URL` | string | — | Внутренний URL flows (обновление документов review). Есть только в конфигах `copy`/`copyv2` | + +### Workspaces (`WORKSPACES_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKSPACES_URL` | string | — | URL сервиса рабочих областей (используется воркерами copy/copyv2) | + +### Workflows (`WORKFLOWS_*`) + +Используется воркером `split_pdf`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOWS_HOST` | string | — | Хост сервиса workflows | +| `WORKFLOWS_IMAGE_TAG` | string | `latest` | Тег docker-образа задач обработки PDF | + +> Container registry (`cr.yandex/crp3ccidau046kdj8g9q`) и флаг `UploadResultsToS3=true` заданы в коде воркера `split_pdf` (`worker.go`), а не через окружение. + +### System log (`SYSTEM_LOG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SYSTEM_LOG_URL` | string | — | URL сервиса системных логов (copy, copyv2, create_versions, create_versionsv2) | + +### Telegram (`TELEGRAM_*`) + +Клиент алертинга для воркеров. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TELEGRAM_TOKEN` | string | — | Токен бота | +| `TELEGRAM_ALERT_GROUP_ID` | int64 | — | ID группы для алертов | +| `TELEGRAM_DEBUG` | bool | `false` | Debug-режим бота | + +### AMQP / RabbitMQ (`AMQP_*`) + +Используется воркерами `markingsv2` и `copyv2` (маркировка бандлов через RabbitMQ). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AMQP_HOST` | string | — | Хост RabbitMQ | +| `AMQP_PORT` | string | — | Порт RabbitMQ | +| `AMQP_USER` | string | — | Пользователь | +| `AMQP_PASSWORD` | string | — | Пароль | +| `AMQP_PATH_API` | string | — | Vhost / путь API в URL подключения | + +## Матрица «переменная → бинарник» + +| Секция | http | copy | copyv2 | create_versions | create_versionsv2 | flows_callback | markings | markingsv2 | sign | signv2 | split_pdf | update_bundles | +| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| Общие | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `ADDRESS`/`PROCESS_CACHE_*` | ✓ | | | | | | | | | | | | +| `PUBLIC_KEY`,`OPERATE_URL`,`SAREX_BACKEND_BASE_URL`,`CAMUNDA_CLIENT_*`,`CAMUNDA_PROCESS_DEFINITION_ID` | ✓ | | | | | | | | | | | | +| `CAMUNDA_KEYCLOAK_URL`,`ZEEBE_*` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `DATABASE_*` | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | +| `S3_*` | ✓ | ✓ | ✓ | ✓ | ✓ | | | ✓ | ✓ | ✓ | | | +| `AUTH_*`/`USERNAME`/`PASSWORD` | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `FLOWS_URL` | | ✓ | ✓ | | | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | +| `FLOWS_INTERNAL_URL` | | ✓ | ✓ | | | | | | | | | | +| `WORKSPACES_URL` | | ✓ | ✓ | | | | | | | | | | +| `WORKFLOWS_*` | | | | | | | | | | | ✓ | | +| `SYSTEM_LOG_URL` | | ✓ | ✓ | ✓ | ✓ | | | | | | | | +| `AMQP_*` | | | ✓ | | | | | ✓ | | | | | +| `TELEGRAM_*` | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | + +> Матрица построена по структурам `Config` соответствующих бинарников. Наличие поля в структуре не всегда означает, что клиент инициализируется — см. замечания ниже. + +## Переменные в Helm-чарте (`.helm/values.yaml`) + +Обычные значения (блок `envs`): + +| Переменная | Значения по окружениям | +| --- | --- | +| `SAREX_BACKEND_BASE_URL` | stage: `https://stage.sarex.io`, preprod: `https://preprod.sarex.io`, production: `https://lk.sarex.io` | + +Значения из секрета (блок `secretEnvs`, общий для всех сервисов через якорь `*cde_secret_envs`), берутся из k8s-секрета `cde-secret` одноимёнными ключами: `ENVIRONMENT`, `LOG_LEVEL`, `ZEEBE_GATEWAY`, `DATABASE_URL`, `S3_ENDPOINT_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `PUBLIC_KEY`, `PDM_URL`, `FLOWS_URL`, `FLOWS_INTERNAL_URL`, `USERNAME`, `PASSWORD`, `CAMUNDA_PROCESS_DEFINITION_ID`, `OPERATE_URL`, `CAMUNDA_KEYCLOAK_URL`, `CAMUNDA_CLIENT_ID`, `CAMUNDA_CLIENT_SECRET`, `WORKFLOWS_HOST`, `WORKSPACES_URL`, `AUTH_HOST`, `TELEGRAM_ALERT_GROUP_ID`, `TELEGRAM_TOKEN`, `IS_CONTOUR`, `AMQP_HOST`, `AMQP_PORT`, `AMQP_USER`, `AMQP_PASSWORD`, `AMQP_PATH_API`, `SYSTEM_LOG_URL`. + +## Развёртывание через kustomize (этот репозиторий) + +В `iac/apps/cde` секреты доставляются не через `secretEnvs` чарта, а через **Vault Agent Injector**: аннотации подов монтируют секрет `secrets/data/vault/apps/cde` в файл `/vault/secrets/cde-env`, который экспортируется перед запуском (`source /vault/secrets/cde-env`, затем `exec /http` или `/worker`). Дополнительно контейнерам задаётся `S3_IS_CONTOUR=true` (примечание: в коде используется переменная `IS_CONTOUR`). + +Оверлеи: `base` (общие манифесты), `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`. + +## Замечания и потенциальные проблемы + +- **Нет глобального префикса.** Имена переменных короткие (`USERNAME`, `PASSWORD`, `AUTH_HOST`) — легко пересечься с системными; следите за окружением процесса. +- **`PUBLIC_KEY` обязателен для `http`** — при пустом/некорректном PEM сервис паникует на старте (`server.go`). +- **`PDM_URL`** присутствует в `secretEnvs` Helm, но соответствующий клиент (`internal/adapters/http/pdm`) в текущей сборке нигде не инициализируется — переменная фактически не используется кодом. +- **`S3_IS_CONTOUR`** задаётся в kustomize-манифестах, тогда как код читает `IS_CONTOUR` (без префикса `S3_`). Проверьте, что для влияния на поведение выставлен именно `IS_CONTOUR`. +- **`FLOWS_INTERNAL_URL`** объявлен только в конфигах `copy`/`copyv2`; в остальных воркерах поля нет, хотя ключ есть в общем секрете. +- **Значения по умолчанию для секретов Camunda/Zeebe** (`identity-secret-for-components`) подходят для локального стенда, но должны переопределяться в prod. +- Приложение читает `.env` только если файл существует; иначе используются переменные окружения. `make`-целей для генерации `.env` в репозитории нет — используйте этот `.env.example` как шаблон. diff --git a/apps/cde/ENDPOINTS.md b/apps/cde/ENDPOINTS.md new file mode 100644 index 0000000..69a941a --- /dev/null +++ b/apps/cde/ENDPOINTS.md @@ -0,0 +1,130 @@ +# Эндпоинты внешних сервисов, с которыми взаимодействует cde-orchestration-demo + +Документ описывает все HTTP/AMQP/gRPC-эндпоинты внешних сервисов, к которым обращается оркестратор (сервер `cmd/http` и воркеры). Это исходящие вызовы; описание API, который оркестратор **предоставляет**, — в `openapi.yaml`. + +## Как устроено взаимодействие + +Клиенты внешних сервисов лежат в `internal/adapters/http/*` и `internal/adapters/*`. Базовые URL берутся из переменных окружения (см. `CONFIGURATION.md`). Для HTTP используются два клиента: Fiber `client` (camunda, flows, pdm, workspaces, system_log) и `go-resty` (workflows, sarexbackend). Аутентификация — по-разному в зависимости от сервиса (OAuth client_credentials, Bearer-токен пользователя/админа, Basic). + +## Базовые адреса по сервисам + +| Сервис | Переменная базового адреса | Клиент | Назначение | +| --- | --- | --- | --- | +| Camunda Operate / REST | `OPERATE_URL` | Fiber | Управление инстансами процессов, переменными, сообщениями | +| Camunda Keycloak | `CAMUNDA_KEYCLOAK_URL` | Fiber | OAuth-токен для Operate | +| Zeebe Gateway | `ZEEBE_GATEWAY` | gRPC (SDK) | Деплой BPMN, обработка job'ов воркерами | +| Auth (токены) | `AUTH_HOST` | Fiber/resty | Токены пользователя/админа для flows и pdm | +| Flows | `FLOWS_URL`, `FLOWS_INTERNAL_URL` | Fiber | Ревью, действия пользователя, обновление бандлов/документов | +| PDM | `PDM_URL` | Fiber | Маркировка бандлов *(клиент не подключён — см. примечание)* | +| Workflows | `WORKFLOWS_HOST` | resty | Создание workflow обработки PDF | +| Workspaces | `WORKSPACES_URL` | Fiber | Создание рабочих областей | +| System log | `SYSTEM_LOG_URL` | Fiber | Отправка системных логов | +| Sarex backend | `SAREX_BACKEND_BASE_URL` | resty | Получение MRPA по id | +| Telegram Bot API | `TELEGRAM_TOKEN` | tgbotapi | Алертинг воркеров | +| RabbitMQ (AMQP) | `AMQP_*` | amqp091 | Маркировка бандлов (RPC), вычисление хеш-сумм | + +## Эндпоинты по сервисам + +### Camunda Operate / REST (`OPERATE_URL`, `CAMUNDA_KEYCLOAK_URL`) + +`internal/adapters/http/camunda/client.go`. Все запросы (кроме получения токена) идут с заголовком `Authorization: Bearer `. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{CAMUNDA_KEYCLOAK_URL}/auth/realms/camunda-platform/protocol/openid-connect/token` | OAuth-токен (`grant_type=client_credentials`) | +| POST | `{OPERATE_URL}/v2/process-instances` | Создать инстанс процесса | +| GET | `{OPERATE_URL}/api/process-instances/{key}` | Получить инстанс процесса | +| POST | `{OPERATE_URL}/v1/variables/search` | Поиск переменных процесса по имени/значению | +| GET | `{OPERATE_URL}/api/process-instances/{key}/variables/{varId}` | Значение конкретной переменной | +| POST | `{OPERATE_URL}/api/process-instances/{key}/variables` | Список переменных инстанса (`scopeId`) | +| POST | `{OPERATE_URL}/v2/messages/publication` | Публикация сообщения процессу (напр. `signRequest`) | + +### Zeebe Gateway (`ZEEBE_GATEWAY`) + +`internal/adapters/zeebe`. gRPC через официальный SDK `camunda/zeebe/clients/go/v8`. Используется для деплоя определений процессов (`NewProcessDefinition`) и для job-воркеров, которые слушают Service Task типа `ZEEBE_WORKER_JOB_TYPE` и по завершении/ошибке возвращают результат в инстанс процесса. + +### Auth — токены (`AUTH_HOST`) + +Используется адаптерами flows и pdm для получения Bearer-токенов. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `{AUTH_HOST}/token/user/{userID}/` | Токен от имени пользователя | +| POST | `{AUTH_HOST}/token/` | Токен админа (`username`/`password`) | + +### Flows (`FLOWS_URL`, `FLOWS_INTERNAL_URL`) + +`internal/adapters/http/flows/adapter.go`. Запросы (кроме `update-documents`) идут с `Authorization: Bearer `; часть операций — с ретраями (экспоненциальный backoff). + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{FLOWS_URL}/user-actions/` | Добавить действие в историю | +| PATCH | `{FLOWS_URL}/reviews/{reviewID}/approve/` | Утвердить review (со статусом/комментарием) | +| PATCH | `{FLOWS_URL}/reviews/{reviewID}/update-bundles/` | Обновить бандлы review | +| PATCH | `{FLOWS_INTERNAL_URL}/reviews/{reviewID}/update-documents/` | Обновить документы review (внутренний URL, без авторизации) | +| GET | `{FLOWS_URL}/reviews/{reviewID}/documents/` | История бандлов документов review | + +### PDM (`PDM_URL`, `AUTH_HOST`) — не подключён + +`internal/adapters/http/pdm/client.go`. Клиент реализован, но в текущей сборке нигде не инициализируется (см. примечание в конце). Для полноты — какие вызовы он делает: + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `{AUTH_HOST}/token/user/{userID}/` | Токен пользователя (Basic auth логин/пароль) | +| PUT | `{PDM_URL}/bundles/{bundleID}/marks` | Проставить маркировки бандлу | + +### Workflows (`WORKFLOWS_HOST`, `AUTH_HOST`) + +`internal/adapters/http/workflows/adapter.go` (resty). Используется воркером `split_pdf`. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{WORKFLOWS_HOST}/internal/v1/companies/{companyID}/workflows` | Создать workflow обработки/оптимизации PDF | + +> Базовый URL resty-клиента установлен в `AUTH_HOST`, а адрес workflows подставляется полным (`WORKFLOWS_HOST`). В параметрах задач передаётся `django_host = AUTH_HOST`. + +### Workspaces (`WORKSPACES_URL`) + +`internal/adapters/http/workspaces/client.go`. Используется воркерами `copy`/`copyv2`. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{WORKSPACES_URL}/internal/v2/workspaces` | Создать рабочую область | + +### System log (`SYSTEM_LOG_URL`) + +`internal/adapters/http/system_log/client.go`. Используется воркерами `copy`, `copyv2`, `create_versions`, `create_versionsv2`. + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `{SYSTEM_LOG_URL}/api/v0/system_log` | Отправить пакет системных логов | + +### Sarex backend (`SAREX_BACKEND_BASE_URL`) + +`internal/adapters/sarexbackend/client.go` (resty). Используется HTTP-сервером при подписи (проверка MRPA). Токены проксируются из входящего запроса (`Authorization`, опц. `Identity`). + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `{SAREX_BACKEND_BASE_URL}/api/core/mrpa/{id}/` | Получить MRPA по id | + +### Telegram Bot API (`TELEGRAM_TOKEN`, `TELEGRAM_ALERT_GROUP_ID`) + +`internal/adapters/http/telegram/client.go` через `go-telegram-bot-api`. Отправка алертов в заданную группу при ошибках/паниках в задачах воркеров. + +### RabbitMQ / AMQP (`AMQP_*`) + +Подключение вида `amqp://{USER}:{PASSWORD}@{HOST}:{PORT}/{PATH_API}`. + +| Адаптер | Назначение | +| --- | --- | +| `internal/adapters/amqp/markings` | Маркировка бандла и получение его хеш-суммы (RPC поверх временной очереди, `correlation_id`). Используется воркерами `markingsv2`/`copyv2` | +| `internal/adapters/amqp/rpc` | Общий RPC-клиент RabbitMQ (переподключение, вычисление хеш-сумм объектов) | + +## Обработка ошибок + +Каждый адаптер проверяет `StatusCode()` ответа и оборачивает не-`200 OK` в ошибку с телом ответа (`internal/errors`). Отдельно у sarexbackend маппинг: `404 → ErrNotFound`, `403 → ErrForbidden`, прочие → generic. Адаптеры flows и pdm выполняют ретраи с экспоненциальным backoff (до 5 попыток). + +## Примечания + +- **PDM-клиент не подключён.** `internal/adapters/http/pdm` реализован, а переменная `PDM_URL` присутствует в Helm-секрете, но `pdm.New(...)` не вызывается ни в одном бинарнике. Раздел оставлен для полноты; при фактическом использовании актуализируйте документ. +- Пути даны относительно базовых URL из окружения; итоговый URL = `<базовый адрес>` + `путь`. diff --git a/apps/cde/openapi.yaml b/apps/cde/openapi.yaml new file mode 100644 index 0000000..dca823a --- /dev/null +++ b/apps/cde/openapi.yaml @@ -0,0 +1,298 @@ +openapi: 3.0.3 + +info: + title: CDE Orchestration API + version: "0.0.0" + description: | + HTTP API оркестратора **cde-orchestration-demo** + (`gitlab.com/sarex-team/cde-orchestration-demo`) — управление процессами + согласования/подписи документов в Camunda (Zeebe/Operate). + + Сервис написан на Go (**Fiber v3**), точка входа — `cmd/http/main.go`, + сборка приложения — `internal/app/http/server.go`. Все ручки объявлены в + `internal/controller/http/v0` и смонтированы под префиксом `/api`. + + ### Аутентификация + Все эндпоинты проходят через middleware `pkg/http/middleware/auth.go`. + Токен передаётся заголовком `Authorization: Bearer `. Поддерживаются + два режима: + + 1. **Sarex** (по умолчанию) — подпись JWT проверяется RSA public key из + переменной `PUBLIC_KEY`. + 2. **Zitadel** — если передан дополнительный заголовок + `Identity: Bearer `, полезная нагрузка берётся из метаданных + этого токена (`urn:zitadel:iam:user:metadata`); подпись основным + сервисом не проверяется. + + При отсутствии/некорректности заголовков middleware возвращает `401`. + + ### Замечания + - Ручка `GET /api/process/{instance_key}` может вернуть `425 Too Early`, + если процесс есть в кеше, но ещё не создан в Camunda (идёт обработка). + - Тело ответов на запись (`process`, `sign`, `operate`) обычно пустое — + значим только HTTP-статус. + + contact: + name: cde-orchestration-demo + url: https://gitlab.com/sarex-team/cde-orchestration-demo + +servers: + - url: http://localhost:8080/api + description: Локальный запуск (Fiber, ADDRESS по умолчанию :8080) + - url: http://cde-svc.cde.svc.cluster.local/api + description: Внутрикластерный адрес (ClusterIP) + +tags: + - name: infra + description: Служебные эндпоинты + - name: process + description: Процессы согласования/подписи + - name: sign + description: Отправка подписей в процесс + - name: operate + description: Управление определениями процессов (BPMN) + +security: + - bearerAuth: [] + +paths: + /: + get: + tags: [infra] + summary: Проверка доступности + description: Возвращает 200 OK. Требует валидной авторизации (middleware). + operationId: root + responses: + "200": + description: OK + "401": + description: Не авторизован + + /process/: + post: + tags: [process] + summary: Запустить процесс + description: | + Создаёт инстанс процесса согласования/подписи в Camunda по документам + из `payload`. Если для `flow_id` уже есть активный инстанс — вернётся + `400`. + operationId: createProcess + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessCreateRequest" + responses: + "202": + description: Процесс принят к обработке + "400": + description: Ошибка валидации или активный инстанс уже существует + "401": + description: Не авторизован + "500": + description: Внутренняя ошибка (ошибка создания инстанса в Camunda) + + /process/{instance_key}: + get: + tags: [process] + summary: Получить состояние процесса + description: | + Возвращает текущее состояние процесса по `flow_id` (в пути — числовой + ключ). Логика: если запись есть в кеше, но нет активного инстанса в + Camunda — процесс ещё обрабатывается (`425`). + operationId: getProcess + parameters: + - name: instance_key + in: path + required: true + description: Числовой идентификатор (`flow_id`) + schema: + type: integer + format: uint64 + responses: + "200": + description: Состояние процесса + content: + application/json: + schema: + $ref: "#/components/schemas/GetProcessInstanceResponse" + "401": + description: Не авторизован + "404": + description: Процесс не найден + "425": + description: Too Early — процесс ещё обрабатывается + "500": + description: Внутренняя ошибка + + /sign/: + post: + tags: [sign] + summary: Отправить подписи в процесс + description: | + Публикует сообщение `signRequest` в процесс Camunda с подписями из + `payload`. Для элементов с `mrpa_id` предварительно проверяется доступ + через sarex-backend. + operationId: sign + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SignRequest" + responses: + "200": + description: Подписи приняты, сообщение отправлено в процесс + "401": + description: Отсутствует токен авторизации + "403": + description: Нет прав на MRPA + "422": + description: MRPA не найдена + "500": + description: Внутренняя ошибка + + /operate/processes: + post: + tags: [operate] + summary: Загрузить определение процесса (BPMN) + description: | + Принимает BPMN-файл (multipart, поле `definition`) и деплоит его в + Zeebe. + operationId: deployProcessDefinition + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [definition] + properties: + definition: + type: string + format: binary + description: BPMN-файл определения процесса + responses: + "200": + description: Определение загружено + "400": + description: Файл не передан/некорректен + "401": + description: Не авторизован + "500": + description: Внутренняя ошибка деплоя + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Sarex (проверяется по `PUBLIC_KEY`). Для режима Zitadel + дополнительно передаётся заголовок `Identity: Bearer `. + + schemas: + ProcessCreateRequest: + type: object + required: [flow_id, author_id] + description: Запрос на старт процесса (`internal/dto/http.go`). + properties: + flow_id: + type: integer + format: uint64 + description: Внешний ID процесса (обязателен, != 0) + author_id: + type: integer + format: uint64 + description: ID автора запроса (обязателен, != 0) + company_id: + type: integer + format: uint64 + step_id: + type: integer + format: uint64 + metadata: + type: object + additionalProperties: true + payload: + type: array + description: Документы для обработки (произвольные объекты) + items: + type: object + additionalProperties: true + overwrite_marks: + type: boolean + mode: + type: string + description: Режим работы ("original", "copy", "both") + use_signature: + type: boolean + create_copy_on_finish: + type: boolean + comment: + type: string + is_last_signer: + type: boolean + + GetProcessInstanceResponse: + type: object + description: Состояние инстанса процесса (`internal/dto/http.go`). + properties: + status: + type: string + use_signature: + type: boolean + is_finished: + type: boolean + is_ready_for_sign: + type: boolean + is_last_signer: + type: boolean + create_copy_on_finish: + type: boolean + comment: + type: string + payload: + description: Полезная нагрузка процесса (структура зависит от процесса) + nullable: true + instance_key: + type: integer + format: uint64 + + SignRequest: + type: object + required: [flow_id, payload] + description: Запрос на подпись документов в процессе (`internal/dto/http.go`). + properties: + flow_id: + type: integer + format: uint64 + metadata: + type: object + additionalProperties: true + payload: + type: array + items: + $ref: "#/components/schemas/SignRequestPayloadElem" + + SignRequestPayloadElem: + type: object + properties: + bundle_id: + type: string + format: uuid + author_id: + type: integer + format: uint64 + signature: + type: string + description: Сгенерированная подпись (помещается в p7s) + algorithm: + type: string + mrpa_id: + type: string + format: uuid + nullable: true + description: Если задан — проверяется доступ через sarex-backend diff --git a/apps/checklists/.env.example b/apps/checklists/.env.example new file mode 100644 index 0000000..ae0042a --- /dev/null +++ b/apps/checklists/.env.example @@ -0,0 +1,31 @@ +# Piccolo (обязательно для запуска) +PYTHONPATH=src +PICCOLO_CONF=db.config + +# App +DEBUG=true + +# HTTP app +HTTP_APP_HOST=0.0.0.0 +HTTP_APP_PORT=8000 +HTTP_APP_ROOT_PATH="" +HTTP_APP_WORKERS=1 +HTTP_APP_ADMIN_ENABLE=true + +# Database +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres + +# OpenTelemetry +OTEL_ENABLE=false +OTEL_URL=http://signoz-otel-collector-external.signoz.svc.cluster.local:4317 +OTEL_SERVICE_NAME=checklists-backend.checklists-stage +OTEL_INSECURE=true + +# Auth (JWT) +# При JWT_AUTH_ENABLE=false middleware отключён и используется дефолтный пользователь +JWT_AUTH_ENABLE=false +JWT_AUTH_PUBLIC_KEY=key diff --git a/apps/checklists/CONFIGURATION.md b/apps/checklists/CONFIGURATION.md new file mode 100644 index 0000000..1f60db9 --- /dev/null +++ b/apps/checklists/CONFIGURATION.md @@ -0,0 +1,171 @@ +# Конфигурация проекта checklists-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). + +В отличие от единого класса настроек, конфигурация разбита на несколько независимых классов `BaseSettings`, у каждого — **свой** `env_prefix` (плоские имена, без вложенного разделителя): + +| Класс | `env_prefix` | Раздел | +| --- | --- | --- | +| `Config` | *(нет префикса)* | `debug` | +| `HTTPAppConfig` | `HTTP_APP_` | Параметры HTTP-приложения/uvicorn | +| `DatabaseConfig` | `DATABASE_` | Подключение к PostgreSQL | +| `OTELConfig` | `OTEL_` | Трейсинг/логи OpenTelemetry | +| `JWTAuthConfig` | `JWT_AUTH_` | Аутентификация по JWT | + +Подклассы подключаются к корневому `Config` как поля со значениями по умолчанию (`http_app`, `database`, `otel`, `jwt_auth`) и читают окружение в момент импорта. Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Особенности: + +- **`.env` не загружается автоматически** — в `config.py` не задан `env_file`, зависимости `python-dotenv` нет. Файл `.env.template` — это шаблон; переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s. +- Помимо переменных приложения, для запуска нужны две инфраструктурные переменные Piccolo: `PYTHONPATH=src` и `PICCOLO_CONF=db.config` (заданы в `.env.template` и в `Dockerfile`). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (`make run`) | Переменные окружения процесса. `.env.template` — шаблон, приложение его **не подхватывает** автоматически | +| Контейнер | `docker/http/Dockerfile` задаёт `PYTHONPATH`/`PICCOLO_CONF`; прочие переменные пробрасываются при запуске | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и `HELM_SET_ARGS` | + +Способы запуска (`Makefile`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `make run` | `src/cmd/http/main.py` → `uvicorn` (factory `app.http:create_app`) | HTTP API | +| `make migrate` | `piccolo migrations forwards all` | Применение миграций БД | +| `make migrations` | `piccolo migrations new checklists --auto` | Генерация новой миграции | +| `make format` / `make format-check` | `ruff` | Форматирование/линт | + +Порядок запуска в контейнере (`docker/http/entrypoint.sh`): сначала выполняются миграции (`piccolo migrations forwards all`), затем стартует приложение (`src/cmd/http/main.py`). Uvicorn запускается в режиме фабрики; `reload` включается при `DEBUG=true`, число воркеров — из `HTTP_APP_WORKERS`. + +## Переменные приложения + +В столбце «Переменная» указано полное имя (префикс + поле). Дефолт `—` означает, что значение обязательно. + +### App (`Config`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DEBUG` | bool | `True` | Режим отладки. Влияет на `reload` uvicorn, логирование SQL-запросов Piccolo (`log_queries`/`log_responses`), а также на `production`-флаг и `debug` piccolo-admin | + +### HTTP-приложение (`HTTP_APP_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_APP_HOST` | string | `0.0.0.0` | Адрес прослушивания | +| `HTTP_APP_PORT` | int | `8000` | Порт | +| `HTTP_APP_ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси; в k8s — `/checklists`) | +| `HTTP_APP_WORKERS` | int | `1` | Число воркеров uvicorn | +| `HTTP_APP_ADMIN_ENABLE` | bool | `True` | Монтировать ли piccolo-admin по пути `/admin/` | + +### Database (`DATABASE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_HOST` | string | `postgres` | Хост PostgreSQL | +| `DATABASE_PORT` | int | `5432` | Порт PostgreSQL | +| `DATABASE_NAME` | string | `postgres` | Имя базы данных | +| `DATABASE_USER` | string | `postgres` | Пользователь БД | +| `DATABASE_PASSWORD` | string | `postgres` | Пароль пользователя БД | + +Подключение собирается в `src/db/config.py` (`PostgresEngine`). SSL-параметров в настройках нет; в prod TLS обеспечивается на уровне подключения/CA-сертификата (см. `docker/http/ca.crt`). + +### OpenTelemetry (`OTEL_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `OTEL_ENABLE` | bool | `False` | Включить трейсинг/логи OTEL. При `True` подключаются `fastapi-otel-tools` и инструментирование FastAPI | +| `OTEL_URL` | string | `http://signoz-otel-collector-external.signoz.svc.cluster.local:4317` | Адрес OTLP-коллектора | +| `OTEL_SERVICE_NAME` | string | `checklists-backend.checklists-stage` | Имя сервиса в трейсах | +| `OTEL_INSECURE` | bool | `True` | Небезопасное (без TLS) подключение к коллектору | + +> При `OTEL_ENABLE=true` `access_log` uvicorn отключается (логи идут через OTEL-обработчик). + +### Auth (`JWT_AUTH_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_AUTH_ENABLE` | bool | `False` | Включить `JWTAuthMiddleware`. При `False` middleware не подключается, и в контекст подставляется дефолтный пользователь (для локальной разработки) | +| `JWT_AUTH_PUBLIC_KEY` | string | `key` | Публичный RSA-ключ для JWT (алгоритм `RS512`) | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в запуске/сборке. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `PYTHONPATH` | `.env.template`, `Dockerfile` | Путь к исходникам (`src`) | +| `PICCOLO_CONF` | `.env.template`, `Dockerfile` | Путь к конфигу Piccolo (`db.config`) | +| `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`checklists-backend`) | +| `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile (`./docker/http/Dockerfile`) | +| `IMAGE_NAME` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Имя собираемого образа | +| `CHART_NAME` / `CHART_VERSION` / `RELEASE_NAME` | `.gitlab-ci.yml` | Параметры релиза Helm | + +Базовый образ — `python:3.13-slim-bookworm`; менеджер зависимостей — `uv` (`uv sync --locked`). В образ добавляется CA-сертификат Yandex (`docker/http/ca.crt`). + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Сервис деплоится подключаемым чартом `universal-chart` (OCI-зависимость). Окружение выбирается ключом `universal-chart.global.env` (`stage`/`preprod`/`production`); для каждой переменной значение берётся из блока с ключом текущего окружения либо из `_default`. + +Обычные значения (блок `envs`) переопределяют дефолты кода, в частности: + +| Переменная | Значение в чарте | +| --- | --- | +| `HTTP_APP_ROOT_PATH` | `/checklists` | +| `HTTP_APP_WORKERS` | `3` | +| `HTTP_APP_ADMIN_ENABLE` | `true` | +| `DATABASE_PORT` | `6432` (PgBouncer) | +| `DATABASE_NAME` | `checklists_db` (stage), `checklists` (preprod/production) | +| `OTEL_ENABLE` | `true` | +| `OTEL_URL` | `http://otel-collector.opentelemetry-collector.svc.cluster.local:4317` | +| `OTEL_SERVICE_NAME` | `checklists-backend.proc` (stage), `…checklists-preprod`, `…checklists-prod` | +| `JWT_AUTH_ENABLE` | `true` | +| `DEBUG` | `false` | + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `DATABASE_USER` | `checklists-postgresql-secret` (stage) / `ya-pg-secret` (preprod, production) | `user` | +| `DATABASE_PASSWORD` | `checklists-postgresql-secret` / `ya-pg-secret` | `password` | +| `DATABASE_HOST` | `checklists-postgresql-secret` / `ya-pg-secret` | `host` | +| `JWT_AUTH_PUBLIC_KEY` | `jwt-secret` | `public-key` | + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики — 1 по умолчанию, 2 в production, ресурсы), `image.*`, `service.*` (ClusterIP, порт `80` → `8000`). Проверки `liveness`/`readiness` отключены. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline.yaml`, `common-security-scan.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | +| --- | --- | --- | +| ветка `stage` | `stage` | `proc` | +| ветка `master` | `preprod` | `checklists-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `checklists-prod` | + +Ключевые переменные пайплайна: `SERVICE_NAME`, `DOCKERFILE_PATH`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (проброс `image.name`, `global.env`, метаданных коммита). Job `lint` прогоняет `ruff check`/`ruff format --check` на образе `uv`. + +## Замечания и потенциальные проблемы + +- Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение вручную либо задавать в манифестах. +- Аутентификация JWT в коде выполняет разбор токена с `options={"verify_signature": False}` в обоих режимах (sarex-backend и Zitadel) — подпись фактически не проверяется на уровне приложения, доверие обеспечивается сетевым слоем (Istio). При `JWT_AUTH_ENABLE=false` middleware не подключается и используется дефолтный пользователь из `entity/context.py`. +- Внутренние эндпоинты (`/internal/*`) аутентификации на уровне приложения не требуют. +- SSL-настроек подключения к БД в коде нет; в prod используется PgBouncer (`DATABASE_PORT=6432`) и CA-сертификат, вшитый в образ. +- Piccolo-admin доступен по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`; в режиме `DEBUG=false` он поднимается в `production`-режиме. + +## Минимальный набор для локального запуска + +Нужен доступный PostgreSQL. Помимо `PYTHONPATH=src` и `PICCOLO_CONF=db.config`, для запуска достаточно значений по умолчанию — обязательных переменных без дефолта нет. Практически стоит задать: + +- `DEBUG` (`true` локально) +- `HTTP_APP_HOST`, `HTTP_APP_PORT` +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `JWT_AUTH_ENABLE` (`false` для локальной разработки — тогда используется дефолтный пользователь) +- `OTEL_ENABLE` (`false` локально) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/checklists/openapi.yaml b/apps/checklists/openapi.yaml new file mode 100644 index 0000000..8761b1a --- /dev/null +++ b/apps/checklists/openapi.yaml @@ -0,0 +1,963 @@ +openapi: 3.0.3 + +info: + title: Checklists + version: "0.1.0" + description: | + REST API сервиса **checklists-backend** — управление чек-листами + (`Checklist`) и их результатами (`ChecklistResult`). + + Сервис написан на Python (**FastAPI** + ORM **Piccolo**). Приложение + собирается фабрикой `create_app` в `src/app/http.py`. Роутинг состоит из + двух групп: + + - публичный API — префикс `/api/v1` (`controller/http/api`); + - внутренний API — префикс `/internal/v1` (`controller/http/internal_api`), + предназначен для вызовов внутри кластера (через ingress не публикуется). + + Интерактивная документация (ReDoc) доступна по `/docs/`, схема — + по `/openapi.json/` (с учётом `root_path`). Админ-панель Piccolo монтируется + по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`. + + ### Аутентификация + Аутентификация включается флагом `JWT_AUTH_ENABLE`. При включённом + `JWTAuthMiddleware` (`controller/http/middlewares.py`) публичные эндпоинты + требуют заголовок `Authorization: Bearer `. Поддерживаются два режима: + + 1. **Zitadel** — если передан дополнительный заголовок `identity` + (`Identity `), полезная нагрузка (`user_id`, `company_ids`) берётся + из этого токена (`urn:zitadel:iam:user:metadata`). + 2. **sarex-backend** — если заголовка `identity` нет, разбирается основной + токен (алгоритм `RS512`, ключ `JWT_AUTH_PUBLIC_KEY`). + + В обоих режимах разбор выполняется с `verify_signature=False` — подпись на + уровне приложения не проверяется, доверие обеспечивается сетевым слоем. + Внутренние эндпоинты (`/internal/*`) и пути `/docs/`, `/openapi.json/`, + `/admin/*` аутентификацию пропускают. При `JWT_AUTH_ENABLE=false` middleware + не подключается и используется дефолтный пользователь. + + ### Пагинация + Списочные ответы используют пагинацию limit/offset и оборачиваются в + `PaginatedResponse` — `{ count, result }`, где `count` — число объектов в + текущем ответе, `result` — сами объекты. Параметры: `limit` (по умолчанию + `100`), `offset` (по умолчанию `0`), сортировка — `order_by`/`ascending`. + + ### Обработка ошибок + Доменные ошибки (`controller/http/errors.py`) возвращаются как + `application/json` с телом `{ "detail": "<текст>" }`. Маппинг: + + - `ResourceNotPermittedError` → **403**; + - `ResourceNotFoundError` → **404**; + - `StateConflictError` → **409**; + - `ChecklistResultValidationError` → **422**; + - прочее → **500**. + + Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в + стандартном формате `422` (`HTTPValidationError`). Все публичные эндпоинты + (`/api/*`) при невалидном/отсутствующем токене возвращают `401`. + + contact: + name: checklists-backend + url: https://gitlab/proc/checklists-backend + +servers: + - url: https://api.sarex.io/checklists + description: Production (ingress, root_path=/checklists) + - url: https://stage-api.sarex.io/checklists + description: Stage (ingress, root_path=/checklists) + - url: http://checklists-backend-service.proc.svc.cluster.local + description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000). Единственный способ достучаться до /internal/v1 + - url: http://localhost:8000 + description: Локальный запуск (Uvicorn, порт по умолчанию 8000) + +tags: + - name: Checklists + description: Чек-листы — создание, просмотр, поиск, удаление + - name: Checklist results + description: Результаты чек-листов — создание, просмотр, обновление, удаление + - name: internal + description: Внутренние эндпоинты (только внутри кластера) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Checklists + # ========================================================================== + /api/v1/checklists/: + post: + tags: [Checklists] + summary: Создать чек-лист + operationId: createChecklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistCreate' + responses: + '201': + description: Созданный чек-лист + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistReadFull' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + get: + tags: [Checklists] + summary: Список чек-листов + operationId: listChecklists + parameters: + - { $ref: '#/components/parameters/OrderByChecklist' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - name: company_id + in: query + required: false + description: Фильтр по ID компаний + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + responses: + '200': + description: Страница чек-листов (компактное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/checklists/filter/: + post: + tags: [Checklists] + summary: Список чек-листов (фильтры в теле) + description: Аналог `GET /api/v1/checklists/`, но фильтры и пагинация передаются в теле запроса. + operationId: filterChecklists + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistFiltersWithPagination' + responses: + '200': + description: Страница чек-листов (компактное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/checklists/{instance_id}/: + get: + tags: [Checklists] + summary: Чек-лист по id + operationId: getChecklist + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '200': + description: Чек-лист (полное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistReadFull' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Checklists] + summary: Удалить чек-лист + operationId: deleteChecklist + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '204': + description: Чек-лист удалён + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Checklist results + # ========================================================================== + /api/v1/results/: + post: + tags: [Checklist results] + summary: Создать результат чек-листа + description: | + Создаёт результат по `checklist_id`. Данные чек-листа переносятся бэком + автоматически; в теле передаются метаданные и список значений инпутов + (`input_values`). + operationId: createChecklistResult + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultCreate' + responses: + '201': + description: Созданный результат + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + get: + tags: [Checklist results] + summary: Список результатов чек-листов + operationId: listChecklistResults + parameters: + - { $ref: '#/components/parameters/OrderByChecklistResult' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { $ref: '#/components/parameters/FilterResultId' } + - { $ref: '#/components/parameters/FilterChecklistId' } + - { $ref: '#/components/parameters/FilterCreatorId' } + - { $ref: '#/components/parameters/FilterIsDraft' } + - { $ref: '#/components/parameters/FilterIsLocked' } + responses: + '200': + description: Страница результатов + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/results/{instance_id}/: + get: + tags: [Checklist results] + summary: Результат чек-листа по id + operationId: getChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '200': + description: Результат чек-листа + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [Checklist results] + summary: Обновить результат чек-листа + description: Частичное обновление значений инпутов и флага черновика. + operationId: updateChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultPartialUpdate' + responses: + '200': + description: Обновлённый результат + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Checklist results] + summary: Удалить результат чек-листа + operationId: deleteChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '204': + description: Результат удалён + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Internal + # ========================================================================== + /internal/v1/results/: + get: + tags: [internal] + summary: Список результатов (внутренний) + description: Внутрикластерный эндпоинт. Аутентификация на уровне приложения не выполняется. + operationId: internalListChecklistResults + security: [] + parameters: + - { $ref: '#/components/parameters/OrderByChecklistResult' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { $ref: '#/components/parameters/FilterResultId' } + - { $ref: '#/components/parameters/FilterChecklistId' } + - { $ref: '#/components/parameters/FilterCreatorId' } + - { $ref: '#/components/parameters/FilterIsDraft' } + - { $ref: '#/components/parameters/FilterIsLocked' } + responses: + '200': + description: Страница результатов + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead' + '422': { $ref: '#/components/responses/ValidationError' } + + /internal/v1/results/lock: + patch: + tags: [internal] + summary: Массовое обновление блокировки результатов + description: Устанавливает флаг `is_locked` для списка результатов по их id. Внутрикластерный эндпоинт. + operationId: internalBulkUpdateLock + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultBulkLockingPartialUpdate' + responses: + '204': + description: Флаги блокировки обновлены + '422': { $ref: '#/components/responses/ValidationError' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. Алгоритм `RS512`, + ключ `JWT_AUTH_PUBLIC_KEY`. Разбор выполняется без проверки подписи + (`verify_signature=False`). + identityToken: + type: apiKey + in: header + name: identity + description: | + Опциональный заголовок `identity` (`Identity `) для режима Zitadel. + При его наличии полезная нагрузка (`user_id`, `company_ids`) берётся из + этого токена. + + parameters: + InstanceId: + name: instance_id + in: path + required: true + schema: + type: integer + Limit: + name: limit + in: query + required: false + description: Максимальное количество объектов + schema: + type: integer + default: 100 + Offset: + name: offset + in: query + required: false + description: Количество пропущенных объектов + schema: + type: integer + default: 0 + Ascending: + name: ascending + in: query + required: false + description: Сортировка по возрастанию + schema: + type: boolean + default: true + OrderByChecklist: + name: order_by + in: query + required: false + description: Поле для сортировки + schema: + type: string + enum: [id] + default: id + OrderByChecklistResult: + name: order_by + in: query + required: false + description: Поле для сортировки + schema: + type: string + enum: [id] + default: id + FilterResultId: + name: id + in: query + required: false + description: Фильтр по ID результата + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterChecklistId: + name: checklist_id + in: query + required: false + description: Фильтр по ID чек-листа + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterCreatorId: + name: creator_id + in: query + required: false + description: Фильтр по ID создателя результата + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterIsDraft: + name: is_draft + in: query + required: false + description: Признак «чернового» результата + schema: + type: boolean + nullable: true + FilterIsLocked: + name: is_locked + in: query + required: false + description: Признак блокировки результата + schema: + type: boolean + nullable: true + + responses: + Unauthorized: + description: Токен не предоставлен или невалиден + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + NotFound: + description: Запрошенный ресурс не найден + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + Conflict: + description: Конфликт состояния + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + ValidationError: + description: | + Ошибка валидации тела/параметров запроса (FastAPI/Pydantic) либо + доменная ошибка валидации результата (`{ detail }`). + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + + schemas: + # ---- Общие ---- + HTTPError: + type: object + properties: + detail: + type: string + description: Описание ошибки + required: [detail] + + HTTPValidationError: + type: object + properties: + detail: + type: array + items: + $ref: '#/components/schemas/ValidationError' + + ValidationError: + type: object + properties: + loc: + type: array + items: + anyOf: + - type: string + - type: integer + msg: + type: string + type: + type: string + required: [loc, msg, type] + + # ---- Ограничения инпутов ---- + InputChoiceOption: + type: object + properties: + id: + type: string + format: uuid + description: Уникальный идентификатор опции + name: + type: string + description: Название опции + example: "Да" + order: + type: integer + description: Порядковый номер опции + example: 1 + color: + type: string + nullable: true + description: Цвет опции (hex или именованный html-цвет) + example: "#00aa00" + selected_tip: + type: string + nullable: true + description: Заметка при выборе опции + example: 'Рекомендуется добавить комментарий при ответе "Нет"' + alt_name: + type: string + nullable: true + description: Название опции для записи в историю + example: "Одобрено" + required: [id, name, order, selected_tip] + + InputChoiceConstraints: + type: object + properties: + type: + type: string + enum: [choice] + options: + type: array + nullable: true + description: Список опций для выбора + items: + $ref: '#/components/schemas/InputChoiceOption' + required: [type, options] + + InputStringConstraints: + type: object + properties: + type: + type: string + enum: [string] + min_length: + type: integer + minimum: 0 + default: 0 + description: Минимально допустимое количество символов + max_length: + type: integer + minimum: 1 + default: 1000 + description: Максимально допустимое количество символов + required: [type] + + InputConstraints: + oneOf: + - $ref: '#/components/schemas/InputChoiceConstraints' + - $ref: '#/components/schemas/InputStringConstraints' + discriminator: + propertyName: type + mapping: + choice: '#/components/schemas/InputChoiceConstraints' + string: '#/components/schemas/InputStringConstraints' + + # ---- Чек-лист (создание) ---- + ChecklistInputCreate: + type: object + properties: + name: + type: string + maxLength: 1024 + description: Название + example: "Комментарий" + order: + type: integer + description: Порядковый номер + is_required: + type: boolean + description: Обязательное ли поле для заполнения + constraints: + $ref: '#/components/schemas/InputConstraints' + required: [name, order, is_required, constraints] + + ChecklistItemCreate: + type: object + properties: + description: + type: string + maxLength: 8192 + description: Описание шага + order: + type: integer + description: Порядковый номер + inputs: + type: array + description: Список элементов ввода + items: + $ref: '#/components/schemas/ChecklistInputCreate' + required: [description, order, inputs] + + ChecklistCreate: + type: object + properties: + name: + type: string + maxLength: 250 + description: Название + example: "Чек-лист проверки документов" + description: + type: string + maxLength: 8192 + description: Описание чек-листа + company_id: + type: integer + minimum: 1 + description: ID компании + example: 1 + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistItemCreate' + required: [name, description, company_id, items] + + # ---- Чек-лист (чтение) ---- + ChecklistInputRead: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + name: + type: string + maxLength: 1024 + order: + type: integer + is_required: + type: boolean + constraints: + $ref: '#/components/schemas/InputConstraints' + required: [id, created_at, updated_at, name, order, is_required, constraints] + + ChecklistItemRead: + type: object + properties: + id: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + description: + type: string + maxLength: 8192 + order: + type: integer + inputs: + type: array + description: Список элементов ввода + items: + $ref: '#/components/schemas/ChecklistInputRead' + required: [id, created_at, updated_at, description, order, inputs] + + ChecklistReadCompact: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + name: + type: string + description: + type: string + company_id: + type: integer + minimum: 1 + required: [id, created_at, updated_at, name, description, company_id] + + ChecklistReadFull: + allOf: + - $ref: '#/components/schemas/ChecklistReadCompact' + - type: object + properties: + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistItemRead' + required: [items] + + ChecklistFiltersWithPagination: + type: object + properties: + order_by: + type: string + enum: [id] + default: id + ascending: + type: boolean + default: true + limit: + type: integer + default: 100 + offset: + type: integer + default: 0 + company_id: + type: array + nullable: true + description: Фильтр по ID компаний + items: + type: integer + minimum: 1 + + # ---- Результаты чек-листов ---- + ChecklistResultInputCreate: + type: object + properties: + input_id: + type: integer + description: ID инпута, для которого устанавливается значение + example: 1 + value: + description: 'Значение (тип зависит от инпута: id опции для choice, строка для string)' + nullable: true + example: "Да" + required: [input_id, value] + + ChecklistResultCreate: + type: object + properties: + entity_type: + type: string + description: Сущность, для которой создан результат + example: "review" + entity_id: + type: string + description: ID сущности, для которой создан результат + example: "1" + checklist_id: + type: integer + description: ID чек-листа + example: 1 + accessible_by: + type: array + description: SA ID роли/места/пользователя, которым доступен результат + items: + type: string + format: uuid + is_draft: + type: boolean + description: Является ли результат черновым + input_values: + type: array + description: Список устанавливаемых значений + items: + $ref: '#/components/schemas/ChecklistResultInputCreate' + required: [entity_type, entity_id, checklist_id, accessible_by, is_draft, input_values] + + ChecklistResultPartialUpdate: + type: object + properties: + input_values: + type: array + description: Список устанавливаемых значений + items: + $ref: '#/components/schemas/ChecklistResultInputCreate' + is_draft: + type: boolean + description: Является ли результат черновым + required: [input_values, is_draft] + + ChecklistResultInput: + type: object + properties: + input_id: + type: integer + description: ID инпута, для которого создан результат + name: + type: string + description: Название + order: + type: integer + description: Порядковый номер + value: + type: string + nullable: true + description: Введённое значение (строка или UUID выбранной опции) + value_text: + type: string + nullable: true + description: Текстовое представление введённого значения + color: + type: string + nullable: true + description: Цвет значения + required: [input_id, name, order, value, value_text, color] + + ChecklistResultItem: + type: object + properties: + description: + type: string + description: Описание шага + order: + type: integer + description: Порядковый номер + inputs: + type: array + description: Список введённых значений + items: + $ref: '#/components/schemas/ChecklistResultInput' + required: [description, order, inputs] + + ChecklistResultRead: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + entity_type: + type: string + example: "review" + entity_id: + type: string + example: "1" + checklist_id: + type: integer + accessible_by: + type: array + items: + type: string + format: uuid + is_draft: + type: boolean + company_id: + type: integer + description: ID компании + is_locked: + type: boolean + description: Заблокирован ли результат для изменений + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistResultItem' + required: + - id + - created_at + - updated_at + - entity_type + - entity_id + - checklist_id + - accessible_by + - is_draft + - company_id + - is_locked + - items + + ChecklistResultBulkLockingPartialUpdate: + type: object + properties: + ids: + type: array + description: Список id результатов, которым нужно обновить флаг + items: + type: integer + example: [1, 2, 3] + is_locked: + type: boolean + description: Заблокирован ли результат для изменений + required: [ids, is_locked] + + # ---- Пагинация ---- + PaginatedResponse_ChecklistReadCompact: + type: object + properties: + count: + type: integer + description: Количество объектов + example: 1 + result: + type: array + description: Объекты + items: + $ref: '#/components/schemas/ChecklistReadCompact' + required: [count, result] + + PaginatedResponse_ChecklistResultRead: + type: object + properties: + count: + type: integer + description: Количество объектов + example: 1 + result: + type: array + description: Объекты + items: + $ref: '#/components/schemas/ChecklistResultRead' + required: [count, result] diff --git a/apps/comparisons/.env.example b/apps/comparisons/.env.example new file mode 100644 index 0000000..5cb48b1 --- /dev/null +++ b/apps/comparisons/.env.example @@ -0,0 +1,54 @@ +# Пример переменных окружения для comparisons-backend. +# Значения разбираются пакетом kelseyhightower/envconfig (config/config.go, FromEnv). +# Префикса нет — имена переменных используются как есть. +# Приложение НЕ загружает .env автоматически: переменные нужно экспортировать +# в окружение (для локального запуска см. .docker/.env и docker-compose). + +# API +API_ADDRESS=0.0.0.0:8080 + +# Database (PostgreSQL) +POSTGRES_ADDRESS=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=password +POSTGRES_DB=comparisons +POSTGRES_POOL_SIZE=10 +# TLS-подключение к БД. При ENABLE_SSL=1 используется YC-PG-CERTIFICATE как CA. +ENABLE_SSL=0 +DB_CERT_PATH=/home/user/.postgresql/root.crt +YC-PG-CERTIFICATE= + +# Внешние сервисы (внутрикластерные адреса) +DOCUMENTATION_URL=http://documentations-service.documentations-stage/ +EXTERNAL_DOCUMENTATION_URL=https://stage-api.sarex.io/documentations +# Хранилище файлов PDM (multistorage, config/storage.go). Без него сервис +# стартует, но с предупреждением — PDM-хранилище будет недоступно. +DOCUMENTATION_FILESTREAM_URL=http://documentations-filestream-service.documentations-stage/ +WORKFLOW_URL=http://workflows-service.processing-stage/ +WORKSPACE_URL=http://workspaces-service.workspaces-stage/ +EXTERNAL_WORKSPACE_URL= +COMPARISON_URL=http://comparisons-backend-service.comparisons-stage/ +WORKFLOW_IMAGES_VERSION=develop +BIM_V2_INTERNAL_URL=http://bim-backend-v2-service.bim-api-stage/ +# Используется клиентом workflow (pdf2pdf) как django_host в параметрах задачи. +DJANGO_HOST=https://stage.sarex.io + +# Comparisons +# Ограничение параллелизма ABAP-сравнения (0 — без ограничения). +ABAP_FIXED_CONC=0 + +# Sentry / окружение +ENVIRONMENT=stage +SENTRY_DSN= +SENTRY_DEBUG=0 + +# Отладка +# Логировать SQL-запросы (go-pg query hook). +ENABLE_SQL_QUERY=0 + +# --- Дополнительно (используются только при локальном запуске / внешними +# --- библиотеками, кодом config.Config напрямую не читаются) --- +# S3_SERVICE_ACCOUNT=/etc/sarex/yc_s3_doc_account.json +# DJANGO_ORIGINATOR=docs_local +# NAMESPACE=local diff --git a/apps/comparisons/CONFIGURATION.md b/apps/comparisons/CONFIGURATION.md new file mode 100644 index 0000000..6cf383b --- /dev/null +++ b/apps/comparisons/CONFIGURATION.md @@ -0,0 +1,148 @@ +# Конфигурация проекта comparisons-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса `comparisons-backend` (Go). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` функцией `FromEnv()` через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (структура `Config`). + +Особенности разбора: + +- **префикса нет** — `envconfig.Process("", &cfg)` вызывается с пустым префиксом, поэтому имена переменных совпадают с тегами `envconfig:"..."` (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`); +- **вложенности нет** — все переменные плоские (без разделителя секций); +- отдельные значения читаются напрямую через `os.Getenv` в обход структуры `Config`: `DOCUMENTATION_FILESTREAM_URL` (`config/storage.go`) и `DJANGO_HOST` (`clients/workflow_cli/pdf2pdf.go`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение самому либо задавать через `--env`/`env_file` (docker-compose). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. Собирается через `make api` (`go install ./cmd/api`, `./cmd/migrations`) | +| Локально (контейнеры) | `.docker/.env` + `.docker/docker-compose.yml` (`make docker`). Postgres поднимается из этого же compose | +| Kubernetes (Helm, репозиторий бэкенда) | `.helm/values-.yaml`: блок `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблон `.helm/templates/api.yaml` | +| Kubernetes (Kustomize, этот репозиторий infra) | `apps/comparisons/base` + оверлеи; env задаются в `base/backend-deployment.yaml` и патчах оверлеев. **Схема переменных здесь отличается — см. раздел ниже** | +| CI/CD (GitLab) | `.gitlab-ci.yml`: подключает шаблоны `generic/common-ci` (stage/preprod/prod), job `unit-tests` (образ `golang:1.21`) | + +Способы запуска процессов (`cmd/*`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `api` (`make api`) | `cmd/api/main.go` | HTTP API (gorilla/mux). Перед стартом настраивает Sentry и подключение к Postgres | +| `migrations` | `cmd/migrations/main.go` | Миграции БД (`robinjoseph08/go-pg-migrations`). Создание: `go run ./cmd/migrations/main.go create ` | + +## Переменные приложения + +Дефолт `—` означает, что явного значения по умолчанию нет (пустая строка / нулевое значение типа Go). Обязательность отдельных переменных проверяется в рантайме при создании клиентов. + +### API + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес и порт прослушивания HTTP-сервера (напр. `0.0.0.0:8080`) | + +### Database (PostgreSQL) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | +| `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (в коде `main.go` пул жёстко равен `30`; переменная читается, но фактически используется значение из кода) | +| `ENABLE_SSL` | bool | `false` | Подключение к БД по TLS. При `true` используется `YC-PG-CERTIFICATE` как корневой сертификат, `ServerName` = `POSTGRES_ADDRESS` | +| `DB_CERT_PATH` | string | — | Путь к CA-сертификату PostgreSQL (используется инфраструктурой; в Helm — смонтированный файл) | +| `YC-PG-CERTIFICATE` | string | — | Содержимое CA-сертификата для TLS-подключения к БД. Имя с дефисами не соответствует остальным (`envconfig` допускает произвольный тег) | + +### Внешние сервисы + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATION_URL` | string | — | Внутренний URL сервиса документаций (обязателен для `documentation_cli`) | +| `EXTERNAL_DOCUMENTATION_URL` | string | — | Внешний URL сервиса документаций (обязателен для `documentation_cli`) | +| `DOCUMENTATION_FILESTREAM_URL` | string | — | URL PDM-хранилища файлов (`config/storage.go`). Если не задан — сервис стартует, но PDM-хранилище недоступно (лог-warning) | +| `WORKFLOW_URL` | string | — | Внутренний URL сервиса workflow (обязателен для `workflow_cli`) | +| `WORKSPACE_URL` | string | — | Внутренний URL сервиса workspace (обязателен для `workspace_cli`) | +| `EXTERNAL_WORKSPACE_URL` | string | — | Внешний URL сервиса workspace | +| `COMPARISON_URL` | string | — | URL самого сервиса сравнений (обязателен для `workflow_cli`) | +| `WORKFLOW_IMAGES_VERSION` | string | — | Версия/тег образов задач workflow (обязателен для `workflow_cli`) | +| `BIM_V2_INTERNAL_URL` | string | — | Внутренний URL BIM API v2 | +| `DJANGO_HOST` | string | — | Хост Django (LK). Передаётся как `django_host` в параметры задачи pdf2pdf (`clients/workflow_cli/pdf2pdf.go`) | + +### Comparisons + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ABAP_FIXED_CONC` | uint64 | `0` | Ограничение параллелизма ABAP-сравнения (`0` — без ограничения) | + +### Sentry и окружение + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENVIRONMENT` | string | — | Окружение развёртывания (`stage`/`preprod`/`prod`); передаётся в Sentry | +| `SENTRY_DSN` | string | — | DSN Sentry | +| `SENTRY_DEBUG` | bool | `false` | Debug-режим Sentry | + +### Отладка + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы (query hook go-pg) | + +### Дополнительные переменные (не читаются `config.Config`) + +Присутствуют в `.docker/.env` для локального запуска или используются внешними библиотеками (`gotools`), но напрямую структурой `Config` не разбираются: + +| Переменная | Где встречается | Назначение | +| --- | --- | --- | +| `S3_SERVICE_ACCOUNT` | `.docker/.env` | Путь к JSON сервисного аккаунта S3 | +| `DJANGO_ORIGINATOR` | `.docker/.env` | Ориджинатор для интеграции с Django | +| `NAMESPACE` | `.docker/.env` | Логическое пространство имён для локального запуска | +| `API_ADDRESS_FILE` | `.helm/values-*.yaml` | Адрес file-варианта API (задан в Helm, кодом не используется) | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `envs` для каждого окружения (`stage`/`preprod`/`production`) и содержат те же переменные приложения, что описаны выше (различаются адресами БД/сервисов, `WORKFLOW_IMAGES_VERSION`, доменом Django и т.п.). + +Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `POSTGRES_USER` | `ya-pg-secret` | `username` | +| `POSTGRES_PASSWORD` | `ya-pg-secret` | `password` | +| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `certificate` | + +Прочие значения чарта (не переменные приложения): `api.*` (имя, образ, порт, реплики, ресурсы, ingress `api_host`/`api_host_prefix`/`api_path`/`internal_path`, `permitted_ns`), `version`, `imagePullSecrets`. + +## Развёртывание через Kustomize (этот репозиторий, `apps/comparisons`) + +Структура: `base` (общие манифесты) и оверлеи `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`. Секреты БД и публичный JWT-ключ подтягиваются из **HashiCorp Vault** (аннотации `vault.hashicorp.com/*` в `base/backend-deployment.yaml`), а не из k8s-секретов. + +> **Важно: расхождение схем переменных.** Манифест `base/backend-deployment.yaml` использует другой (более новый) набор имён переменных, чем Go-код из `comparisons-backend` (`config/config.go`): напр. `HTTP_PORT`, `LOGGER_LOG_LEVEL`, `DATABASE_NAME`, `DOCUMENTATIONS_INTERNAL_HOST`, `DOCUMENTATIONS_EXTERNAL_HOST`, `WORKFLOWS_HOST`, `WORKFLOWS_DJANGO_HOST`, `WORKFLOWS_BIMV2_INTERNAL_HOST`, `WORKSPACES_HOST`, `EAV_HOST`, `APP_NAME`, `AUTH_PUBLIC_KEY`, `WORKFLOWS_CONFIG_FILEPATH` и др., а также `/ping` в health-проверках и образ `comparisons_backend_prod`. Такой схемы нет в Go-репозитории. Перед использованием этих файлов стоит убедиться, какой именно образ бэкенда деплоится оверлеем: если это Go-сервис из `comparisons-backend`, набор env нужно привести к именам из таблиц выше. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` и переключает окружение по ветке/тегу: + +| Условие | Шаблон | Окружение | +| --- | --- | --- | +| ветка `stage` | `gitlab-ci/comparisons-backend/.gitlab-ci-stage.yml` | stage | +| ветка `master` | `gitlab-ci/comparisons-backend/.gitlab-ci-preprod.yml` | preprod | +| тег (`CI_COMMIT_TAG`) | `gitlab-ci/comparisons-backend/.gitlab-ci-prod.yml` | prod | + +Стадии: `prebuild-secscan`, `dependencies-build`, `unittest`, `build`, `state-update`, `deploy`. Job `unit-tests` (образ `golang:1.21`) выполняет `make unit-tests`. + +## Минимальный набор для локального запуска + +Postgres поднимается через `.docker/docker-compose.yml` (`make docker`). Минимально необходимо задать: + +- `API_ADDRESS` +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL` (`0` локально) +- `DOCUMENTATION_URL`, `EXTERNAL_DOCUMENTATION_URL`, `DOCUMENTATION_FILESTREAM_URL` +- `WORKFLOW_URL`, `WORKSPACE_URL`, `COMPARISON_URL`, `WORKFLOW_IMAGES_VERSION` +- `BIM_V2_INTERNAL_URL`, `DJANGO_HOST` +- `ENVIRONMENT`; при использовании Sentry — `SENTRY_DSN` +- по желанию: `ENABLE_SQL_QUERY` (`1` для отладки SQL), `ABAP_FIXED_CONC` + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/comparisons/ENDPOINTS.md b/apps/comparisons/ENDPOINTS.md new file mode 100644 index 0000000..a0925b2 --- /dev/null +++ b/apps/comparisons/ENDPOINTS.md @@ -0,0 +1,77 @@ +# Эндпоинты, с которыми взаимодействует comparisons-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `comparisons-frontend`). + +## Как устроено взаимодействие + +Все запросы описаны декларативно в реестре `module/networking/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`: + +- `service` — логическое имя сервиса (`EServices`, см. таблицу хостов ниже); +- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`); +- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query); +- `body(args)` — опционально, формирование тела запроса. + +Запрос выполняется единой функцией `fetch(endpoint, params)`, которая через `httpService` (`module/httpService/httpService.ts`, поверх `@sarex-team/sdk-js` + `axios`) отправляет запрос на базовый хост сервиса. Базовый хост подставляется `resolveHost(service)` из `module/httpService/hosts.ts` в зависимости от `buildEnv` (`__BUILD_ENV__`, задаётся сборкой; по умолчанию `prod`). Результат возвращается как `{ resp }` либо `{ errMessage }`. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/httpService/hosts.ts` (`allHosts`). Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. Хосты берутся из `@sarex-team/sdk-js` (`resolveHost`). + +| Сервис (`EServices`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `comparisons` | Сервис сравнений (comparisons-backend) | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `documentations` | Сервис документации (диски, документы) | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `sarexApi` | Gateway/API Sarex (`/gateway`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `sarex` | Локальный сервис данных (`/api/core`) | `""` (относительные пути) | `""` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `bimv2` | BIM API v2 | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local`, `preprod` и `contour`. В `local` сервисы проксируются на `https://localhost:9000/sarex-backend` и `https://localhost:9000/sarex-api-backend/*`. В `contour` используются относительные пути (`/comparisons`, `/documentations`, `/bimv2`, `/workflows`) для изолированного контура. В `preprod` — `https://api.preprod.sarex.io/*`. + +## Эндпоинты по сервисам + +### `comparisons` — Сервис сравнений + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getTypes` | GET | `/api/v1/types` | Справочник типов сравнений и их параметров | +| `createComparison` | POST | `/api/v1/comparisons` | Создать сравнение (тело — параметры сравнения) | +| `getComparisons` | GET | `/api/v1/comparisons?workspace_id={workspaceId}` | Список сравнений рабочей области | +| `deleteComparison` | DELETE | `/api/v1/comparisons/{id}` | Удалить сравнение | + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDisks` | GET | `/api/v1/disks` | Список дисков | +| `getDocuments` | GET | `/api/v1/disks/{id}/documents` | Документы диска | +| `getNearestNameTemplate` | GET | `/api/v1/documents/{documentId}/name_template` | Ближайший шаблон имени документа | + +### `sarexApi` — Gateway/API Sarex + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDocumentsV2` | GET | `/gateway/api/v1/disks/{id}/documents?parent_id=&child_id=&search=&{attributeValue}` | Документы диска с фильтрами (родитель/ребёнок/поиск/атрибуты) | + +### `sarex` — Локальный сервис данных + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getUsersByIDs` | GET | `/api/core/users/?id={ids}` | Пользователи по списку id | + +### `workflows` — Сервис обработки документов + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getWorkflow` | GET | `/api/v1/workflows/{workflowId}` | Workflow по id | + +### `bimv2` — BIM API v2 + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getBim` | GET | `/api/v1/bims/{bimId}` | BIM-модель по id | + +## Обработка ошибок + +`fetch` перехватывает исключения запроса и возвращает `{ errMessage }` (строка ошибки) вместо данных; успешный ответ приходит как `{ resp: }`. Явного маппинга кодов ответа в реестре нет — обработка и отображение ошибок выполняются на уровне репозиториев/вью-моделей, использующих `fetch`. diff --git a/apps/comparisons/openapi.yaml b/apps/comparisons/openapi.yaml new file mode 100644 index 0000000..0a800a7 --- /dev/null +++ b/apps/comparisons/openapi.yaml @@ -0,0 +1,793 @@ +openapi: 3.0.3 + +info: + title: Comparisons Service API + version: "1.0.0" + description: | + REST API сервиса **comparisons-backend** (`pdm/comparisons-backend`) — создание и + просмотр сравнений (облако-BIM отклонение/статусы, облако-облако, облако-поверхность, + pdf-pdf), их элементов, изменений и фильтров. + + Сервис написан на Go (роутер **gorilla/mux**). Сервер собирается функцией + `bootstrapAPI` в `cmd/api/bootstrap.go`. Роутинг состоит из двух групп: + + - публичный API — префикс `/api/v1` (`cmd/api/routes_api.go`); + - внутренний API — префикс `/internal/v1` (`cmd/api/routes_internal.go`), + предназначен для вызовов внутри кластера (через ingress не публикуется). + + ### Аутентификация + Публичные эндпоинты (`/api/v1/*`) требуют JWT: middleware `auth.JWTToCtx` + + `auth.JWTUserExtractorFromCtx` (`gitlab.com/sarex-team/gotools/auth`). Токен + передаётся заголовком `Authorization: Bearer `; middleware + `auth.DeleteJWTFromQueryMiddleware` дополнительно позволяет передать токен + query-параметром `jwt` (он удаляется из запроса после разбора). + + Внутренние эндпоинты (`/internal/v1/*`) аутентификации на уровне приложения + не требуют — ограничение доступа обеспечивается сетевым слоем. + + ### Обработка ошибок + Ошибки возвращаются функцией `httperror.Write` (`gotools/httperror`). Основные + статусы: `400` — некорректный запрос/параметры, `404` — сравнение не найдено, + `500` — внутренняя ошибка. Идентификатор запроса пробрасывается middleware + `reqid`. + + ### Замечания (расхождения кода) + - `GET /api/v1/comparisons` возвращает данные разной формы в зависимости от + query-параметра: `workspace_id` → `{ comparisons: [] }`, `document_id` → + `{ results: [] }`, `bundle_id` → одиночный объект сравнения. + - `GET /api/v1/filter_fields` и `GET /api/v1/elements` работают только со + сравнениями типа `deviation`; для других типов вернётся `400`. + - Схема сравнения полиморфна по полю `type` (`deviation`/`c2c`/`c2s`/`abap`/`pdf2pdf`). + + contact: + name: comparisons-backend + url: https://gitlab.com/sarex-team/comparisons-backend + +servers: + - url: https://api.sarex.io/comparisons + description: Production (ingress) + - url: https://stage-api.sarex.io/comparisons + description: Stage (ingress) + - url: https://api.preprod.sarex.io/comparisons + description: Preprod (ingress) + - url: http://comparisons-backend-service.comparisons-stage + description: Внутрикластерный адрес (ClusterIP). Единственный способ достучаться до /internal/v1 + - url: http://localhost:8080 + description: Локальный запуск (API_ADDRESS по умолчанию 0.0.0.0:8080) + +tags: + - name: types + description: Справочник типов сравнений + - name: comparisons + description: Сравнения — создание, просмотр, удаление + - name: elements + description: Элементы сравнения и их изменения + - name: internal + description: Внутренние эндпоинты (только внутри кластера) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Types + # ========================================================================== + /api/v1/types: + get: + tags: [types] + summary: Справочник типов сравнений + description: Возвращает доступные типы сравнений и их параметры (для построения форм). + operationId: getTypes + responses: + '200': + description: Список типов сравнений + content: + application/json: + schema: + type: object + properties: + types: + type: array + items: + $ref: '#/components/schemas/CompareType' + + # ========================================================================== + # Comparisons + # ========================================================================== + /api/v1/comparisons: + post: + tags: [comparisons] + summary: Создать сравнение + description: | + Создаёт сравнение указанного типа. Автор берётся из JWT. Если `parent_doc_id` + и `parent_disk_id` не заданы — вычисляются по рабочей области. + operationId: createComparison + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRequest' + responses: + '200': + description: Созданное сравнение + content: + application/json: + schema: + $ref: '#/components/schemas/Comparison' + '400': + description: Некорректный запрос / неизвестный тип сравнения + '500': + description: Внутренняя ошибка + get: + tags: [comparisons] + summary: Список сравнений + description: | + Возвращает сравнения по одному из query-параметров. Форма ответа зависит + от параметра (см. описание в `info`). Должен быть задан ровно один параметр. + operationId: getComparisons + parameters: + - name: workspace_id + in: query + required: false + schema: + type: string + format: uuid + description: "Сравнения рабочей области → ответ `{ comparisons: [] }`" + - name: bundle_id + in: query + required: false + schema: + type: string + format: uuid + description: Сравнение по бандлу → ответ — одиночный объект + - name: document_id + in: query + required: false + schema: + type: integer + format: int64 + description: "Сравнения документа → ответ `{ results: [] }`" + responses: + '200': + description: Результат (форма зависит от параметра запроса) + content: + application/json: + schema: + oneOf: + - type: object + properties: + comparisons: + type: array + items: + $ref: '#/components/schemas/Comparison' + - type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/Comparison' + - $ref: '#/components/schemas/Comparison' + '400': + description: Не задан workspace_id / bundle_id / document_id + '500': + description: Внутренняя ошибка + + /api/v1/comparisons/{comparison_id}: + parameters: + - name: comparison_id + in: path + required: true + schema: + type: integer + format: int64 + get: + tags: [comparisons] + summary: Сравнение по id + operationId: getComparisonById + responses: + '200': + description: Сравнение + content: + application/json: + schema: + $ref: '#/components/schemas/Comparison' + '404': + description: Сравнение не найдено + '500': + description: Внутренняя ошибка + delete: + tags: [comparisons] + summary: Удалить сравнение + description: Мягкое удаление сравнения. Автор берётся из JWT. + operationId: deleteComparisonById + responses: + '200': + description: Успешно удалено + '400': + description: Некорректный id + '500': + description: Внутренняя ошибка + + /api/v1/filter_fields: + get: + tags: [comparisons] + summary: Поля фильтрации сравнения + description: | + Возвращает набор полей фильтрации для сравнения типа `deviation` по документу. + Для полей отклонения (`deviation`, `deviation_x/y/z`) заполняются `min`/`max`. + Если сравнение по документу не найдено — вернётся пустой `results`. + operationId: getFilterFields + parameters: + - name: doc_id + in: query + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: Поля фильтрации + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/FilterFields' + '400': + description: Не задан doc_id / сравнение не типа deviation + '500': + description: Внутренняя ошибка + + /api/v1/tolerance: + get: + tags: [comparisons] + summary: Допуск (tolerance) сравнения + description: Возвращает значение допустимого отклонения для сравнения типа `deviation` по бандлу. + operationId: getTolerance + parameters: + - name: bundle_id + in: query + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Допуск + content: + application/json: + schema: + type: object + properties: + tolerance: + type: number + format: double + '400': + description: Не задан / некорректный bundle_id + '404': + description: Сравнение не найдено + '500': + description: Внутренняя ошибка + + # ========================================================================== + # Elements + # ========================================================================== + /api/v1/elements: + get: + tags: [elements] + summary: Список элементов сравнения + description: | + Постранично возвращает элементы сравнения типа `deviation` по документу. + Параметры отклонения задаются строкой вида `min:max`. Если сравнение не + найдено — вернётся пустой `results` с типами по умолчанию. + operationId: getListElements + parameters: + - name: doc_id + in: query + required: true + schema: + type: integer + format: int64 + - name: limit + in: query + required: false + schema: + type: integer + default: 10 + minimum: 0 + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + minimum: 0 + - name: order_by + in: query + required: false + schema: + type: string + enum: [deviation, deviation_x, deviation_y, deviation_z, sarex_id, name] + default: sarex_id + - name: order + in: query + required: false + schema: + type: string + enum: [asc, desc] + default: asc + - name: deviation + in: query + required: false + schema: + type: string + description: Диапазон общего отклонения в формате `min:max` + - name: deviation_x + in: query + required: false + schema: + type: string + description: Диапазон отклонения по X в формате `min:max` + - name: deviation_y + in: query + required: false + schema: + type: string + description: Диапазон отклонения по Y в формате `min:max` + - name: deviation_z + in: query + required: false + schema: + type: string + description: Диапазон отклонения по Z в формате `min:max` + - name: deviation_status + in: query + required: false + schema: + type: string + enum: [in_tolerance, out_of_tolerance] + - name: view_status + in: query + required: false + schema: + type: string + enum: [viewed, not_viewed] + - name: only_with_comment + in: query + required: false + schema: + type: boolean + responses: + '200': + description: Страница элементов + content: + application/json: + schema: + $ref: '#/components/schemas/ListElementsResponse' + '400': + description: Некорректные параметры запроса + '500': + description: Внутренняя ошибка + + /api/v1/elements/{element_id}: + parameters: + - name: element_id + in: path + required: true + schema: + type: integer + format: int64 + patch: + tags: [elements] + summary: Обновить элемент + description: | + Обновляет один из атрибутов элемента: комментарий, статус отклонения или + статус просмотра. Должно быть задано ровно одно из полей. Изменение + фиксируется записью в истории изменений. + operationId: updateElement + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateElementRequest' + responses: + '200': + description: Успешно обновлено (тело не возвращается) + '400': + description: Некорректный запрос + '500': + description: Внутренняя ошибка + + /api/v1/elements/{element_id}/changes: + get: + tags: [elements] + summary: История изменений элемента + operationId: getElementChanges + parameters: + - name: element_id + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: Список изменений + content: + application/json: + schema: + type: object + properties: + changes: + type: array + items: + $ref: '#/components/schemas/Change' + '400': + description: Некорректный element_id + '500': + description: Внутренняя ошибка + + # ========================================================================== + # Internal + # ========================================================================== + /internal/v1/comparisons/{comparison_id}/webhook: + post: + tags: [internal] + summary: Webhook результата сравнения (внутренний) + description: | + Вызывается после завершения обработки сравнения типа `deviation`. Скачивает + `deviation_json` из PDM-хранилища по бандлу, создаёт элементы сравнения и + возвращает содержимое `deviation_json`. Аутентификация на уровне приложения + не требуется. + operationId: comparisonWebhook + security: [] + parameters: + - name: comparison_id + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: Содержимое deviation_json + content: + application/json: + schema: + $ref: '#/components/schemas/DeviationJSON' + '404': + description: Сравнение не найдено + '500': + description: Внутренняя ошибка + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. Допускается передача + query-параметром `jwt` (удаляется middleware после разбора). + + schemas: + ComparisonType: + type: string + enum: [c2c, c2s, deviation, abap, pdf2pdf] + + CompareType: + type: object + description: Тип сравнения и набор его параметров (для формы создания). + properties: + name: + $ref: '#/components/schemas/ComparisonType' + verbose_name: + type: string + params: + type: array + items: + $ref: '#/components/schemas/CompareFormParam' + + CompareFormParam: + type: object + properties: + name: + type: string + verbose_name: + type: string + hint: + type: string + source: + type: string + enum: [form, viewer] + type: + type: string + description: Тип поля (напр. cloud, bimv2, number, multichoice, choice, array, pdf, surface) + options: + type: array + items: + type: object + additionalProperties: true + default: {} + min: + type: number + format: double + max: + type: number + format: double + + CreateRequest: + type: object + required: [name, workspace_id, type, company_id, params] + properties: + name: + type: string + workspace_id: + type: string + format: uuid + parent_doc_id: + type: integer + format: int64 + nullable: true + parent_disk_id: + type: string + format: uuid + nullable: true + type: + $ref: '#/components/schemas/ComparisonType' + company_id: + type: integer + format: int64 + params: + type: object + description: Параметры сравнения, зависят от type (см. /api/v1/types). + additionalProperties: true + + Comparison: + type: object + description: | + Полиморфное сравнение. Поле `type` определяет набор `params`. Ниже приведён + пример для типа `deviation`; для других типов набор params отличается. + properties: + id: + type: integer + format: int64 + type: + $ref: '#/components/schemas/ComparisonType' + created_by: + type: integer + format: int64 + name: + type: string + workspace_id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + nullable: true + document_id: + type: integer + format: int64 + nullable: true + bundle_id: + type: string + format: uuid + nullable: true + params: + $ref: '#/components/schemas/DeviationParams' + + DeviationParams: + type: object + properties: + cloud_bundle_id: + type: string + format: uuid + model_bundle_id: + type: string + format: uuid + down_sample_size: + type: number + format: double + resolution: + type: number + format: double + linear_margin: + type: number + format: double + tolerance: + type: number + format: double + sarex_ids: + type: array + items: + type: integer + format: int64 + transformation: + type: array + items: + type: number + format: double + applied_statuses: + type: array + items: + type: string + crop_margin: + type: number + format: double + nullable: true + + Element: + type: object + properties: + id: + type: integer + format: int64 + sarex_id: + type: integer + format: int64 + name: + type: string + deviation_status: + type: string + enum: [in_tolerance, out_of_tolerance] + view_status: + type: string + enum: [viewed, not_viewed] + deviation: + type: number + format: double + deviation_x: + type: number + format: double + deviation_y: + type: number + format: double + deviation_z: + type: number + format: double + axis_and_angle: + type: object + description: Ось и угол поворота (utils.AxisAndAngle) + additionalProperties: true + comment: + type: string + nullable: true + bbox_matrix: + type: array + items: + type: number + format: double + reg_matrix: + type: array + items: + type: number + format: double + + ListElementsResponse: + type: object + properties: + count: + type: integer + next: + type: string + nullable: true + previous: + type: string + nullable: true + types: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Option' + crop_margin: + type: number + format: double + nullable: true + results: + type: array + items: + $ref: '#/components/schemas/Element' + + UpdateElementRequest: + type: object + description: | + Должно быть задано ровно одно из полей (`comment`, `deviation_status`, + `view_status`) — валидатор `required_without_all`. + properties: + comment: + type: string + deviation_status: + type: string + enum: [in_tolerance, out_of_tolerance] + view_status: + type: string + enum: [viewed, not_viewed] + + Change: + type: object + properties: + id: + type: integer + format: int64 + element_id: + type: integer + format: int64 + created_at: + type: string + format: date-time + author: + type: integer + format: int64 + changed_field: + type: string + enum: [deviation_status, view_status, comment] + old_value: + type: string + new_value: + type: string + + FilterFields: + type: object + properties: + name: + type: string + enum: [deviation, deviation_x, deviation_y, deviation_z, deviation_status, view_status, only_with_comment] + verbose_name: + type: string + type: + type: string + enum: [checkbox, selector, slider] + options: + type: array + items: + $ref: '#/components/schemas/Option' + min: + type: number + format: double + nullable: true + max: + type: number + format: double + nullable: true + + Option: + type: object + properties: + name: + type: string + verbose_name: + type: string + + DeviationJSON: + type: object + properties: + crop_margin: + type: number + format: double + nodes: + type: object + additionalProperties: + $ref: '#/components/schemas/Node' + + Node: + type: object + properties: + bbox_matrix: + type: array + items: + type: number + format: double + reg_matrix: + type: array + items: + type: number + format: double + node_name: + type: string diff --git a/apps/contracts/.env.example b/apps/contracts/.env.example new file mode 100644 index 0000000..7657626 --- /dev/null +++ b/apps/contracts/.env.example @@ -0,0 +1,19 @@ +# App +LOG_LEVEL=debug +ADDRESS=:8080 + +# Auth +# Публичный RSA-ключ (PEM) для проверки JWT. +# Читается напрямую через os.Getenv("PUBLIC_KEY") в cmd/http/main.go. +PUBLIC_KEY= + +# Database +# DSN подключения к PostgreSQL (pgx), напр. postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable +DB_URL=postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable +DB_POOL_SIZE=10 + +# CLI (миграции) — cmd/cli +# Путь к каталогу с миграциями (по умолчанию migrations) +DB_MIGRATIONS_PATH=migrations +# Необязательно: путь к .env-файлу для cli (эквивалент флага -env-file) +# ENV_FILE=.env diff --git a/apps/contracts/CONFIGURATION.md b/apps/contracts/CONFIGURATION.md new file mode 100644 index 0000000..a812483 --- /dev/null +++ b/apps/contracts/CONFIGURATION.md @@ -0,0 +1,135 @@ +# Конфигурация проекта contracts + +Документ описывает все переменные окружения и способы конфигурирования сервиса `contracts` (Go, HTTP API + CLI миграций). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig) по структуре `Config` в `internal/app/http/config.go`. Дополнительно `.env`-файл автоматически подгружается через [`github.com/joho/godotenv`](https://github.com/joho/godotenv): + +- HTTP-процесс (`cmd/http/main.go`) вызывает `godotenv.Load(".env")` перед разбором конфигурации — если файл `.env` есть в рабочем каталоге, его переменные попадают в окружение; +- CLI-процесс (`cmd/cli/main.go`) загружает файл из `ENV_FILE` (или `.env` по умолчанию), путь можно задать флагом `-env-file`. + +Особенности разбора (`go-envconfig`): + +- глобального префикса нет — верхнеуровневые поля читаются по своим именам (`LOG_LEVEL`, `ADDRESS`); +- вложенные секции задаются префиксом на уровне структуры: `Database` → `env:", prefix=DB_"`, `Auth` → `env:", prefix=AUTH_"`; +- значения по умолчанию заданы в тегах через `default=…`; поля без `default` при отсутствии переменной остаются пустыми (нулевым значением типа), а не приводят к панике на этапе разбора — ошибки всплывают позже (например, невалидный `DB_URL` или пустой `PUBLIC_KEY`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | `.env` в рабочем каталоге (авто-загрузка `godotenv`) + переменные окружения процесса | +| Локально (docker-compose) | `docker-compose.yml`: сервис `contracts` берёт переменные из `env_file: .env`; поднимается вместе с `postgres` | +| Kubernetes (Helm) | `.helm/values-.yaml`: блоки `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблон `.helm/templates/deployment.yaml` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` и переменные `workflow.rules` (namespace, release, chart) | + +Способы запуска процессов: + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `http` | `cmd/http/main.go` | HTTP API (Fiber v3), слушает `ADDRESS` | +| `cli migrate` | `cmd/cli/main.go` | Применение миграций БД (`golang-migrate`), каталог `DB_MIGRATIONS_PATH` | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала `./cli migrate`, затем `./http`. + +## Переменные приложения + +В столбце «Переменная» указано полное имя (с учётом префикса секции). Дефолт `—` означает, что значения по умолчанию нет. + +### App + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `debug` | Уровень логирования (zap): `debug`/`info`/`warn`/`error` и т.п. | +| `ADDRESS` | string | `:8080` | Адрес и порт прослушивания HTTP-сервера (Fiber) | + +### Database (`DB_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_URL` | string | — | DSN подключения к PostgreSQL (`pgxpool.ParseConfig`), напр. `postgres://user:pass@host:5432/db?sslmode=verify-full` | +| `DB_POOL_SIZE` | int32 | `10` | Максимальный размер пула соединений (`pgxpool.Config.MaxConns`) | + +### Auth (`AUTH_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM) для проверки JWT. См. замечание ниже — фактически используется `PUBLIC_KEY` | +| `PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM). Читается напрямую в `cmd/http/main.go` через `os.Getenv("PUBLIC_KEY")` и записывается в `config.Auth.PublicKey`, перекрывая `AUTH_PUBLIC_KEY` | + +> При старте `AuthProvider` парсит ключ (`pem.Decode` + `x509.ParsePKIXPublicKey`). Если `PUBLIC_KEY` пустой или невалидный — приложение падает с `panic` ещё до старта HTTP-сервера. + +## Переменные CLI (миграции) + +Читаются в `cmd/cli/main.go` (структура `cliConfig`, префикс `DB_`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_URL` | string | — | DSN подключения к PostgreSQL для применения миграций (обязателен, иначе ошибка `DB_URL is required`) | +| `DB_MIGRATIONS_PATH` | string | `migrations` | Путь к каталогу с SQL-миграциями (`golang-migrate`) | +| `ENV_FILE` | string | `.env` | Путь к `.env`-файлу, из которого CLI загружает переменные (можно задать флагом `-env-file=PATH`) | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная / параметр | Где используется | Назначение | +| --- | --- | --- | +| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | `docker-compose.yml` | Параметры локального контейнера PostgreSQL (`postgres`/`admin`/`postgres`) | +| build-stage `golang:1.24` | `Dockerfile` | Базовый образ для сборки бинарников `http` и `cli` | +| runtime `alpine:latest` | `Dockerfile` | Финальный образ; копируются `http`, `cli`, `migrations/`, `entrypoint.sh`; открыт порт `8080` | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `envs` (в текущих values он пуст: `envs: []`). Значения из секретов (блок `secrets`) монтируются как env через `secretKeyRef` в `.helm/templates/deployment.yaml`: + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `DB_URL` | `ya-pg-secret` | `db_url` | +| `PUBLIC_KEY` | `public-key` | `key` | + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы, `service_name`/`service_port`), `api.*` (host/prefix/path ingress), `imagePullSecrets`. + +Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `ya-pg-secret` (ключ `certificate`) как файл `/opt/.postgresql/root.crt` (см. `deployment.yaml`). Секрет `ya-pg-secret` при отсутствии создаётся шаблоном `ya-pg-secret.yaml` со случайными значениями и политикой `helm.sh/resource-policy: keep`. + +Параметры окружений (`.helm/values-.yaml`): + +| Окружение | `api.host` | `deployment.service_port` | +| --- | --- | --- | +| stage | `stage-api.sarex.io` | `8080` | +| preprod | `api.preprod.sarex.io` | `80` | +| production | `api.sarex.io` | `8080` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | Chart version | +| --- | --- | --- | --- | +| ветка `master` | `preprod` | `contracts-preprod` | `0.0.1-preprod` | +| ветка `stage` | `stage` | `contracts-stage` | `0.0.1-stage` | +| тег (`CI_COMMIT_TAG`) | `prod` | `contracts-prod` | `0.0.1-prod` | + +Общие переменные пайплайна: `RELEASE_NAME=contracts`, `CHART_NAME=contracts`, `IMAGE_PATH=deployment.image`, `HELM_SET_ARGS="--set deployment.image=${IMAGE_NAME}"`, `DOCKERFILE_PATH=Dockerfile`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY` (`true`), `ENABLE_LINTER` (`false`). Для merge request-ов пайплайн запускается без деплоя. + +## Замечания и потенциальные проблемы + +- **Дублирование ключа авторизации.** В `Config` объявлено поле `Auth.PublicKey` с тегом `AUTH_PUBLIC_KEY`, но `cmd/http/main.go` дополнительно читает `os.Getenv("PUBLIC_KEY")` и перезаписывает им значение. В Helm секрет прокидывается как `PUBLIC_KEY`. Практически используется именно `PUBLIC_KEY`; `AUTH_PUBLIC_KEY` в текущем деплое не задаётся. +- **`.env` загружается автоматически** (в отличие от Python-сервисов): `godotenv.Load(".env")` в HTTP-процессе и `godotenv.Load(ENV_FILE|.env)` в CLI. Файл `.env` при этом попадает под `.gitignore` (`*.env`) и в репозиторий не коммитится. +- **Пустой `PUBLIC_KEY` — фатально.** `auth.New` делает `panic`, если ключ не удаётся распарсить как PEM/PKIX. Для локального запуска нужен валидный публичный ключ. +- **`DB_URL` обязателен и для http, и для cli.** Невалидный DSN приводит к ошибке `pgxpool.ParseConfig`/подключения; в CLI пустой `DB_URL` даёт явную ошибку `DB_URL is required`. +- **`envs: []` в values.** Все прикладные переменные в k8s сейчас приходят только из секретов (`DB_URL`, `PUBLIC_KEY`); `LOG_LEVEL`/`ADDRESS` используют дефолты (`debug`, `:8080`). + +## Минимальный набор для локального запуска + +PostgreSQL поднимается через `docker-compose up postgres`, приложение — сборкой `cmd/http` (или целиком через docker-compose). Минимально необходимо задать: + +- `DB_URL` — DSN до PostgreSQL (для локали обычно `?sslmode=disable`) +- `PUBLIC_KEY` — валидный публичный RSA-ключ (PEM) для проверки JWT +- при необходимости: `LOG_LEVEL`, `ADDRESS`, `DB_POOL_SIZE` (иначе применяются дефолты) +- для миграций (`cli migrate`): `DB_URL` и, при нестандартном расположении, `DB_MIGRATIONS_PATH` + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/contracts/ENDPOINTS.md b/apps/contracts/ENDPOINTS.md new file mode 100644 index 0000000..1c5a35d --- /dev/null +++ b/apps/contracts/ENDPOINTS.md @@ -0,0 +1,64 @@ +# Эндпоинты, с которыми взаимодействует contracts-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `contracts-frontend`). + +## Как устроено взаимодействие + +Запросы сгруппированы по доменам в каталоге `src/shared/api/fetch/*.api.ts`. Каждая функция вызывает соответствующий метод `httpService` (`src/shared/api/http-service.ts`), который создаётся фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). Для запроса указываются: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (добавляется к базовому хосту сервиса); +- `data` — тело запроса (для `post`/`put`); +- `axiosConfig.params` — query-параметры; +- `cache`, `queryKey` — опции кеширования (react-query-подобные ключи из `src/shared/api/keys/*`); +- `isCSRF` — включение CSRF-обработки (для `departments`). + +Базовый хост подставляется по значению `service` и текущему окружению `__BUILD_ENV__` (`local`/`stage`/`preprod`/`prod`, по умолчанию `prod`; см. `http-service.ts`). В режиме `local` для http-сервиса устанавливается тип `zitadel` (`setTypeOfHttpService("zitadel")`). Итоговый URL = `<базовый хост сервиса>` + `url`. + +## Базовые хосты по сервисам и окружениям + +Значения из `src/shared/api/hosts.ts`. Ниже перечислены сервисы, **фактически используемые** запросами модуля; в реестре хостов определены и другие сервисы (`bim`, `bimv2`, `workflows`, `workspaces`, `documentations`, `comparisons`, `remarks`, `projects`, `eavV1`, `notifications`, `google`, `sarexApi`, `zitadel`), но обращений к ним в `fetch/*` нет. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `contracts` | Сервис договоров (contracts-backend) | `https://stage-api.sarex.io/contracts` | `https://api.sarex.io/contracts` | +| `sarex` | Локальный backend Sarex (core/admin) | `""` (относительные пути) | `""` | +| `gateway` | Gateway/API Sarex (ресурсы/проекты) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | + +> Также определены окружения `local` и `preprod`. В `local` сервисы проксируются на относительные пути (`contracts` → `/sarex-contracts`, `sarex` → `/sarex-backend`, `gateway` → `/sarex-gateway`, `zitadel` → `/zitadel`). Значения `preprod` используют домен `api.preprod.sarex.io`. + +## Эндпоинты по сервисам + +### `contracts` — Сервис договоров + +Определены в `src/shared/api/fetch/contract.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchContractsByResourceId` | GET | `/api/v0/contracts` | Список договоров (query: `limit`, `offset`, `resource_id`, `tenant_id`) | +| `fetchCreateContractByResourceId` | POST | `/api/v0/contracts` | Создать договор | +| `fetchUpdateContract` | PUT | `/api/v0/contracts/{contract.id}` | Обновить договор по id | + +> Функция удаления `fetchDeleteContract` (`DELETE /api/v0/contracts/{contractId}`) присутствует в коде, но закомментирована. + +### `sarex` — Локальный backend Sarex (core/admin) + +Определены в `company.api.ts`, `contractor.api.ts`, `department.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCompanies` | GET | `/api/core/admin/companies/` | Список компаний (кешируется, ключ `companies`) | +| `fetchContractors` | GET | `/api/core/admin/contractors/?company_id={companyId}` | Контрагенты компании (кешируется, ключ `contractors`) | +| `fetchDepartments` | GET | `/api/core/admin/departments/` | Отделы (кешируется, ключ `departments`, `isCSRF: true`) | + +### `gateway` — Gateway/API Sarex + +Определён в `project.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchProjects` | GET | `/api/v1/resources/?company_id={companyId}` | Список ресурсов/проектов компании (кешируется, ключ `projects`) | + +## Обработка запросов и кеширование + +Кеширование включается флагом `cache: true` с ключом `queryKey` (значения ключей — в `src/shared/api/keys/*.ts`: `companies`, `projects`, `contractors`, `departments`). Обработка ошибок и авторизация (в т.ч. режим `zitadel` для `local`) выполняются внутри `httpService` из `@sarex-team/sdk-js`. diff --git a/apps/contracts/openapi.yaml b/apps/contracts/openapi.yaml new file mode 100644 index 0000000..81bca62 --- /dev/null +++ b/apps/contracts/openapi.yaml @@ -0,0 +1,404 @@ +openapi: 3.0.3 + +info: + title: Contracts Service API + version: "1.0.0" + description: | + REST API сервиса **contracts** (`platform/contracts`) — управление + договорами (контрактами): создание (в т.ч. массовое), получение по id, + списочный вывод с фильтрами и пагинацией, обновление. + + Сервис написан на Go (**Fiber v3**). Приложение собирается в + `internal/app/http/app.go` (`New`). Роутинг вложен под общий префикс + `/api/v0` (`app.server.Group("/api/v0")`), внутри — группа + `/contracts` (`internal/controller/http/v0`). + + ### Аутентификация + Все эндпоинты группы `/api/v0/contracts` защищены middleware + (`internal/adapter/auth/middleware.go`). Поддерживаются два режима: + + 1. **Zitadel** — если передан заголовок `Identity: Bearer `, + пользователь берётся из полезной нагрузки этого токена + (`urn:zitadel:iam:user:metadata`). Подпись на уровне приложения + не проверяется (валидность обеспечивается сетевым слоем/Istio). + 2. **sarex-backend** — если заголовка `Identity` нет, подпись основного + токена `Authorization: Bearer ` проверяется публичным RSA-ключом + (`PUBLIC_KEY`). + + Корневой эндпоинт `GET /api/v0/` (health/ping) аутентификации не требует. + + ### Идентификаторы + Идентификатор договора — **ULID** (строка), парсится через + `ulid.Parse`. `resource_id` — **UUID**. + + ### Пагинация + Списочный вывод использует `limit`/`offset` (query-параметры, по умолчанию + `limit=100`, `offset=0`). + + ### Обработка ошибок + Ошибки возвращаются с соответствующим HTTP-статусом; тело — либо строка + с описанием, либо `{"error": "..."}` (при внутренней панике). Коды: + `400` — некорректный запрос/невалидный id, `401` — проблемы аутентификации, + `403` — пользователь не состоит в компании (`tenant_id`), `404` — договор + не найден, `500` — внутренняя ошибка, `501` — метод не реализован. + + ### Замечания (расхождения кода) + - `PATCH` и `DELETE` (как по коллекции, так и по id) возвращают + **`501 Not Implemented`** — обработчики-заглушки. + - `POST /api/v0/contracts` принимает **как одиночный объект, так и массив**: + тип создания выбирается по форме тела (объект → создание одного договора, + массив → пакетное создание). + - `GET /api/v0/contracts` (список) требует query-параметр `tenant_id` + (middleware `UserInCompanyMiddleware` проверяет, что пользователь состоит + в этой компании; иначе `400`/`403`). + +servers: + - url: https://api.sarex.io/contracts + description: production + - url: https://stage-api.sarex.io/contracts + description: stage + - url: https://api.preprod.sarex.io/contracts + description: preprod + +security: + - bearerAuth: [] + +tags: + - name: contracts + description: Договоры + - name: service + description: Служебные эндпоинты + +paths: + /api/v0/: + get: + tags: [service] + summary: Health / ping + description: Возвращает `200 OK` без тела. Аутентификация не требуется. + security: [] + responses: + "200": + description: OK + + /api/v0/contracts: + post: + tags: [contracts] + summary: Создать договор или несколько договоров + description: | + Принимает либо одиночный объект `CreateContractRequest`, либо массив + таких объектов. Форма тела определяет режим создания. + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/CreateContractRequest" + - type: array + items: + $ref: "#/components/schemas/CreateContractRequest" + responses: + "201": + description: Договор(ы) создан(ы) + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ContractResponse" + - type: array + items: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Некорректное тело запроса / ошибка валидации + "401": + description: Ошибка аутентификации + "500": + description: Внутренняя ошибка + get: + tags: [contracts] + summary: Список договоров + description: | + Возвращает договоры с фильтрацией и пагинацией. Требует `tenant_id` + (проверяется принадлежность пользователя к компании). + parameters: + - name: tenant_id + in: query + required: true + schema: + type: integer + format: int64 + description: Идентификатор компании/арендатора + - name: limit + in: query + required: false + schema: + type: integer + format: int64 + default: 100 + - name: offset + in: query + required: false + schema: + type: integer + format: int64 + default: 0 + - name: resource_id + in: query + required: false + schema: + type: string + format: uuid + - name: contractor_id + in: query + required: false + schema: + type: integer + format: int64 + responses: + "200": + description: Список договоров + content: + application/json: + schema: + $ref: "#/components/schemas/ContractPaginatedResponse" + "400": + description: Некорректные параметры запроса / отсутствует tenant_id + "401": + description: Ошибка аутентификации + "403": + description: Пользователь не состоит в указанной компании + "500": + description: Внутренняя ошибка + put: + tags: [contracts] + summary: Пакетное обновление договоров + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UpdateContractRequest" + responses: + "200": + description: Договоры обновлены + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Некорректное тело запроса + "401": + description: Ошибка аутентификации + "500": + description: Внутренняя ошибка + patch: + tags: [contracts] + summary: Пакетное частичное обновление (не реализовано) + responses: + "501": + description: Not Implemented + delete: + tags: [contracts] + summary: Пакетное удаление (не реализовано) + responses: + "501": + description: Not Implemented + + /api/v0/contracts/{id}: + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор договора (ULID) + get: + tags: [contracts] + summary: Получить договор по id + responses: + "200": + description: Договор + content: + application/json: + schema: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Невалидный id + "401": + description: Ошибка аутентификации + "404": + description: Договор не найден + "500": + description: Внутренняя ошибка + put: + tags: [contracts] + summary: Обновить договор + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateContractRequest" + responses: + "200": + description: Договор обновлён + content: + application/json: + schema: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Невалидный id / некорректное тело + "401": + description: Ошибка аутентификации + "404": + description: Договор не найден + "500": + description: Внутренняя ошибка + patch: + tags: [contracts] + summary: Частичное обновление (не реализовано) + responses: + "501": + description: Not Implemented + delete: + tags: [contracts] + summary: Удалить договор (не реализовано) + responses: + "501": + description: Not Implemented + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Основной токен `Authorization: Bearer ` (проверяется по RSA-ключу). + Опционально может передаваться заголовок `Identity: Bearer ` + (режим Zitadel), который имеет приоритет. + + schemas: + Contractor: + type: object + description: Произвольный JSON-объект с данными контрагента (в БД — JSONB). + additionalProperties: true + + CreateContractRequest: + type: object + required: [number, tenant_id, started_at, deadline_at] + properties: + number: + type: string + minLength: 1 + description: Номер договора + tenant_id: + type: integer + format: int64 + description: Идентификатор компании/арендатора + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + minimum: 0 + description: + type: string + + UpdateContractRequest: + type: object + required: [number, tenant_id, started_at, deadline_at] + properties: + id: + type: string + description: ULID договора + number: + type: string + minLength: 1 + tenant_id: + type: integer + format: int64 + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + minimum: 0 + description: + type: string + + ContractResponse: + type: object + properties: + id: + type: string + description: ULID договора + number: + type: string + tenant_id: + type: integer + format: int64 + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + description: + type: string + + ContractPaginatedResponse: + type: object + properties: + count: + type: integer + format: int64 + limit: + type: integer + format: int64 + offset: + type: integer + format: int64 + results: + type: array + items: + $ref: "#/components/schemas/ContractResponse" diff --git a/apps/control-interface/ENDPOINTS.md b/apps/control-interface/ENDPOINTS.md new file mode 100644 index 0000000..cd653e1 --- /dev/null +++ b/apps/control-interface/ENDPOINTS.md @@ -0,0 +1,162 @@ +# Эндпоинты, с которыми взаимодействует srx-admin + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается приложение `srx-admin` (панель администрирования, деплой `control-interface`). + +## Как устроено взаимодействие + +`srx-admin` — это монорепозиторий (`admin-monorepo`) c двумя фронтенд-сервисами и общим пакетом: + +- `services/admin` — хост-приложение (основной админ-интерфейс); +- `services/assets` — федеративный модуль (Module Federation), встраиваемый в хост; +- `packages/app-kit` — общий пакет с реестром API-функций и таблицей хостов. + +Запросы описаны не единым реестром, а по доменам — в файлах `shared/api/fetch/*.api.ts`. Каждый домен экспортирует фабрику (например `UserApi`, `AssetApi`, `ProjectApi`), которая принимает `httpService` и возвращает набор методов. Внутри метода вызывается `httpService.getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest` со структурой: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (с подстановкой параметров прямо в строку или через `axiosConfig.params`); +- `data` — тело запроса (для POST/PUT/PATCH); +- `axiosConfig`, `cache`, `queryKey`, `controller`, `isCSRF` — опции axios, кеширования, ключа запроса, отмены и CSRF-токена. + +`httpService` создаётся в `shared/api/http-service.ts` через `createHttpService` из `@sarex-team/sdk-js`. Базовый хост подставляется по логическому имени `service` из `packages/app-kit/src/shared/api/hosts.ts` в зависимости от окружения сборки `__ENDPOINT__` (`BUILD_ENV`, по умолчанию `prod`). Итоговый URL = `<базовый хост сервиса>` + `url`. + +## Базовые хосты по сервисам и окружениям + +Значения из `packages/app-kit/src/shared/api/hosts.ts`. Ниже приведены `stage` и `prod`; дополнительно определены окружения `local`, `contour` и `preprod` (см. примечание). Сервисы, к которым `srx-admin` реально обращается, отмечены значком «●» в колонке «Используется». + +| Сервис (`service`) | Назначение | Используется | `stage` | `prod` | +| --- | --- | --- | --- | --- | +| `iam` | IAM: пользователи, отделы, должности, группы, права | ● | `https://stage-api.sarex.io/iam` | `https://api.sarex.io/iam` | +| `eavV1` | EAV: ассеты, атрибуты, права на ассеты, модули | ● | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` | +| `gateway` | Gateway: ресурсы (проекты) и права на ресурсы | ● | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/pm`, `/api/commons`) | ● | `""` (относительные пути) | `""` | +| `bimv2` | BIM v2: модели статусов | ● | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` | +| `premises` | Сервис помещений | ● | `https://stage-api.sarex.io/premises` | `https://api.sarex.io/premises` | +| `notifications` | Лямбда уведомлений (email) | ● | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` | +| `documentations` | Сервис документации | | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `workspaces` | Сервис рабочих областей | | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `workflows` | Сервис обработки документов | | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `comparisons` | Сервис сравнений | | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `remarks` | Сервис замечаний | | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `projects` | Сервис проектов | | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` | +| `bim` | BIM-API | | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` | +| `sarexApi` | Gateway/API Sarex (корень) | | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `google` | Временное хранилище (GCS) | | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `zitadel` | IdP (аутентификация) | | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> В окружении `local` сервисы проксируются на относительные пути (`sarex` → `/sarex-backend`, `gateway` → `/sarex-gateway`, `eavV1` → `/sarex-eav-v1`, `notifications` → `/sarex-notifications`, `iam` → `/iam`, `premises` → `/premises` и т. д.). Окружение `contour` использует относительные пути для изолированного контура. Подключаемые удалённые модули (Module Federation) описаны отдельно в `services/*/config/endpoints.ts` (см. раздел «Удалённые модули»). + +## Эндпоинты по сервисам + +### `iam` — IAM (пользователи, отделы, должности, группы, права) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchUsers` | GET | `/api/admin/v0/users/?{query}` | Список пользователей компании (пагинация, поиск, фильтры) | +| `fetchCreateUser` | POST | `/api/admin/v0/users/` | Создать пользователя | +| `fetchUpdateUser` | PATCH | `/api/admin/v0/users/{id}/` | Обновить пользователя | +| `fetchBulkUpdateUsers` | PATCH | `/api/admin/v0/users/` | Массовое обновление пользователей | +| `fetchBulkUpdateUsersActivation` | POST | `/api/admin/v0/users/activation/` | Массовая активация/деактивация пользователей | +| `fetchDepartments` | GET | `/api/admin/v0/departments/` | Список отделов (пагинация, поиск, фильтр по компании) | +| `fetchCreateDepartment` | POST | `/api/admin/v0/departments/` | Создать отдел (CSRF) | +| `fetchUpdateDepartment` | PUT | `/api/admin/v0/departments/{id}/` | Обновить отдел (CSRF) | +| `fetchDeleteDepartment` | DELETE | `/api/admin/v0/departments/{id}/` | Удалить отдел (CSRF) | +| `fetchPositions` | GET | `/api/admin/v0/positions` | Список должностей (пагинация, поиск, фильтр по компании) | +| `fetchCreatePosition` | POST | `/api/admin/v0/positions/` | Создать должность (CSRF) | +| `fetchUpdatePosition` | PUT | `/api/admin/v0/positions/{id}/` | Обновить должность (CSRF) | +| `fetchDeletePosition` | DELETE | `/api/admin/v0/positions/{id}/` | Удалить должность (CSRF) | +| `fetchGroups` | POST | `/api/admin/v0/groups/search/` | Поиск функциональных групп (фильтры, пагинация, сортировка) | +| `createGroup` | POST | `/api/admin/v0/groups` | Создать группу | +| `updateGroup` | PATCH | `/api/admin/v0/groups/{id}` | Обновить группу | +| `deleteGroup` | DELETE | `/api/admin/v0/groups/{id}` | Удалить группу | +| `fetchPermissions` | POST | `/api/admin/v0/permissions/search/` | Поиск прав (фильтры, пагинация, сортировка) | +| `createPermission` | POST | `/api/admin/v0/permissions` | Создать право | +| `deletePermission` | DELETE | `/api/admin/v0/permissions/{id}` | Удалить право | + +### `eavV1` — EAV (ассеты, атрибуты, права на ассеты, модули) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchGetAssetsV4` | GET | `/api/v4/assets/` | Список ассетов (v4, параметры фильтрации) | +| `fetchGetAssetsV5` | GET | `/api/v2/assets/` | Список ассетов (v5) | +| `fetchCreateBulkAssetsV4` | POST | `/api/v4/assets/` | Массовое создание ассетов (v4) | +| `fetchCreateBulkAssetsV5` | POST | `/api/v2/assets/` | Массовое создание ассетов (v5) | +| `fetchUpdateBulkAssetsV4` | PATCH | `/api/v4/assets/` | Массовое обновление ассетов (v4) | +| `fetchUpdateBulkAssetsV5` | PATCH | `/api/v2/assets/` | Массовое обновление ассетов (v5) | +| `fetchDeleteAssetV4` | DELETE | `/api/v4/assets/{assetId}/` | Удалить ассет (v4) | +| `fetchDeleteAssetV5` | DELETE | `/api/v2/assets/{assetId}/` | Удалить ассет (v5) | +| `fetchCopyRootAsset` | POST | `/api/v4/assets/{asset_id}/copy/` | Копировать корневой ассет | +| `fetchCopyAssets` | POST | `/api/v2/assets/copy-to-destination-bulk/` | Массовое копирование ассетов в назначения | +| `fecthGetAssetPermissions` | GET | `/api/v4/permissions/?asset_id={id}&service_account_id={id}` | Права доступа ассета | +| `fetchPostCreateAssetPermissions` | POST | `/api/v4/permissions/` | Создать права на ассет | +| `fetchPostUpdateAssetPermissions` | PATCH | `/api/v4/permissions/` | Обновить права на ассет | +| `fetchDeleteAssetPermissions` | DELETE | `/api/v4/permissions/{permissionId}/` | Удалить права на ассет | +| `fetchGetAssetPermissionsTree` | GET | `/api/v4/permissions/relative/?asset_id={id}` | Дерево наследуемых прав ассета | +| `fetchAttributes` | GET | `/api/v1/attribute/` | Список атрибутов компании (пагинация, поиск, фильтр по id) | +| `createAttribute` | POST | `/api/v1/attribute/` | Создать атрибут | +| `updateAttribute` | PUT | `/api/v1/attribute/{id}/` | Обновить атрибут | +| `deleteAttribute` | DELETE | `/api/v1/attribute/{attributeId}/` | Удалить атрибут | +| `fetchModules` | GET | `/api/v1/modules/` | Список модулей (CSRF) | + +### `gateway` — Gateway (ресурсы/проекты, права на ресурсы) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchProjects` | GET | `/api/v2/resources?show_all=true&limit=10000&company_id={id}` | Список проектов компании (кешируется) | +| `fetchGetProjectByProjectId` | GET | `/api/v2/resources/{projectId}` | Проект по id (кешируется) | +| `fetchCreateProject` | POST | `/api/v2/resources` | Создать проект/ресурс | +| `fetchUpdateProjectByProjectId` | PATCH | `/api/v2/resources/{projectId}` | Обновить проект | +| `fetchDeleteProjectByProjectId` | DELETE | `/api/v2/resources/{projectId}` | Удалить проект | +| `fetchParentDocumentByResourceId` | GET | `/api/v1/resources-rpc/parent-document-by-resource-id/{resourceId}` | Родительский документ по resource id | +| `fetchResources` | GET | `/api/v1/resources/?company_id={id}` | Список ресурсов компании (кешируется) | +| `fetchCreatePermission` | POST | `/api/v1/resource-permissions/` | Выдать права на ресурсы сервисному аккаунту | +| `fetchResourcesByUsersId` | POST | `/api/v1/resources/users-with-resources/` | Ресурсы по набору пользователей | +| `fetchBulkUpdateUsersPermissions` | PATCH | `/api/v1/resources/permissions-bulk/` | Массовое обновление прав на ресурсы | +| `fetchBulkUpdateUsersCompanyResourcesPermission` | POST | `/api/v1/company-resource-permissions/bulk/` | Массовая выдача прав на ресурсы компании | + +### `sarex` — Локальный сервис данных + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCoordinates` | GET | `/api/commons/cs/` | Справочник систем координат (кешируется) | +| `fetchLinksToPlanningByProjectId` | GET | `/api/pm/msp/projects/?resource_id={projectId}&strict=true` | Связи проекта с планированием (кешируется) | +| `fetchBulkUpdateUserNotifications` | PATCH | `/api/core/users/bulk/notifications/` | Массовое переключение уведомлений пользователей | +| `getMrpas` | POST | `/api/core/mrpa/list/` | Список МРПА (пагинация, фильтры, агрегации) | +| `createMrpa` | POST | `/api/core/mrpa/` | Загрузить МРПА (multipart/form-data) | +| `deleteMrpa` | DELETE | `/api/core/mrpa/{id}/` | Удалить МРПА | + +### `bimv2` — BIM v2 (модели статусов) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCreateCompanyStatusModel` | POST | `/api/v1/companies/{companyId}/status_model` | Создать модель статусов компании | +| `fetchGetCompanyStatusModels` | GET | `/api/v1/companies/{companyId}/status_model` | Модели статусов компании | +| `fetchGetBIMStatusModels` | GET | `/api/v1/bims/{bimId}/status_models` | Модели статусов BIM | +| `fetchUpdateBIMStatusModel` | POST | `/api/v1/bims/{bimId}/status_model` | Обновить модель статусов BIM | +| `fetchGetBIMStatuses` | POST | `/api/v1/bims/{bimId}/statuses?{search}` | Статусы BIM (с фильтром) | + +### `premises` — Сервис помещений + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getPremises` | POST | `/api/v1/premises/filter/` | Помещения по локациям и ресурсу | + +### `notifications` — Лямбда уведомлений + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchSendEmail` | POST | `/` | Отправить email-уведомление (from `hello@sarex.io`) | + +## Удалённые модули (Module Federation) + +Помимо HTTP-API, `srx-admin` подгружает удалённые микрофронтенды через `remoteEntry.js`. Адреса заданы в `services/admin/config/endpoints.ts` и `services/assets/config/endpoints.ts` (объект `moduleEndpoints`). + +| Модуль | `stage` / `local` | `prod` | `contour` | +| --- | --- | --- | --- | +| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | `/documentations/static/module/remoteEntry.js` | +| `assets` | `https://stage-modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `https://modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `/control-interface/modules/assets/remoteEntry.js` | + +> В `preprod` используются хосты вида `https://modules.preprod.sarex.io/...`. + +## Обработка ошибок и авторизация + +Запросы выполняются через `httpService` (`@sarex-team/sdk-js` поверх `axios`). Для части эндпоинтов (`iam`: отделы, должности, создание пользователей/групп; `eavV1`: модули) передаётся флаг `isCSRF: true` — добавляется CSRF-токен. Ошибки обрабатываются на уровне SDK и сторов приложения; человекочитаемые сообщения задаются в сторах (`errorMessage`), например «Произошла ошибка при запросе пользователей» / «мест работы» / «ролей» / «функциональных групп». diff --git a/apps/cross-section/ENDPOINTS.md b/apps/cross-section/ENDPOINTS.md new file mode 100644 index 0000000..f82be13 --- /dev/null +++ b/apps/cross-section/ENDPOINTS.md @@ -0,0 +1,50 @@ +# Эндпоинты, с которыми взаимодействует cross-section + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `cross-section`). + +## Как устроено взаимодействие + +Запросы описаны в двух API-объектах в `module/api/endpoints.ts`: `crossSectionApi` (поперечные сечения) и `exportsApi` (экспорт и скачивание вложений). Каждый метод вызывает соответствующий хелпер `httpService` (`getRequest`/`postRequest`/`deleteRequest`) со структурой: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (с подстановкой параметров/query); +- `data` — опционально, тело запроса (для `POST`/`PUT`). + +`httpService` создаётся функцией `createHttpService` из `@sarex-team/sdk-js` в `module/api/http-service.ts`. Базовый хост подставляется по ключу `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV` (по умолчанию `prod`). В окружении `local` тип сервиса переключается на `original` (`setTypeOfHttpService("original")`). Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Определены окружения `local`, `stage`, `prod`, `preprod`. + +| Сервис (`service`) | Назначение | `local` | `stage` | `prod` | `preprod` | +| --- | --- | --- | --- | --- | --- | +| `gateway` | Gateway/API Sarex (используется всеми эндпоинтами модуля) | `https://stage-api.sarex.io/gateway/` | `https://stage-api.sarex.io/gateway/` | `https://api.sarex.io/gateway/` | `https://api.preprod.sarex.io/gateway/` | +| `drawings` | Сервис чертежей | `https://stage-api.sarex.io/drawings/` | `https://stage-api.sarex.io/drawings/` | `https://api.sarex.io/drawings/` | `https://api.preprod.sarex.io/drawings/` | +| `sarex` | Локальный сервис данных (относительные пути) | `https://stage.sarex.io/` | `""` | `""` | `""` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | `https://login.preprod.sarex.io` | + +> Фактически все эндпоинты модуля обращаются к сервису `gateway`. Сервисы `drawings`, `sarex` и `zitadel` объявлены в реестре хостов, но напрямую в `endpoints.ts` не используются. + +## Эндпоинты по сервисам + +### `gateway` — Gateway/API Sarex + +#### `crossSectionApi` — поперечные сечения + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getCrossSections` | GET | `api/v1/drawings/cross-sections?instance_id={uuid}` | Список поперечных сечений по инстансу | +| `getCrossSectionData` | GET | `api/v1/drawings/cross-sections/{uuid}/data` | Данные поперечного сечения по uuid | +| `createCrossSections` | POST | `api/v1/drawings/cross-sections` | Создать поперечное сечение (тело — `model`) | +| `removeCrossSections` | DELETE | `api/v1/drawings/cross-sections/{uuid}/` | Удалить поперечное сечение | + +#### `exportsApi` — экспорт + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `createExport` | POST | `api/v1/drawings/exports` | Создать экспорт (тело: `company_id`, `cross_section_id`, `file_type`; по умолчанию `file_type = "dwg"`) | +| `downloadExport` | GET | `api/v1/attachments/{attachment_id}` | Скачать вложение экспорта по id | + +## Обработка ошибок + +В модуле нет отдельного слоя маппинга ошибок (аналога `module/api/errors.ts`): обработка HTTP-ошибок выполняется на уровне `httpService` из `@sarex-team/sdk-js`. Каждый метод возвращает `response.data` (для `createExport` — весь ответ). diff --git a/apps/django/.env.example b/apps/django/.env.example new file mode 100644 index 0000000..eda0503 --- /dev/null +++ b/apps/django/.env.example @@ -0,0 +1,259 @@ +# ============================================================================= +# Пример конфигурации sarex-backend (Django) + sarex-frontend +# Значения читаются кодом через django-environ (env(...)) и pydantic-settings +# (классы *Settings с env_prefix) в config/settings/*.py. +# В кластере переменные приходят из Vault (файлы /vault/secrets/*) и из блока +# env Deployment-манифестов (см. base/backend-deployment.yaml, celery-deployment.yaml). +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Django core +# ----------------------------------------------------------------------------- +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_DEBUG=False +DJANGO_ISOLATED=False +ALLOWED_HOSTS='*' +APPEND_SLASH=True +FZ152_COMPLIANCE=False +PDM_SYNC=1 +OBJECT_STORAGE_SYNC=True +# Секретный ключ Django. В production.py по умолчанию задан хардкодом, +# при необходимости переопределяется переменной SECRET_KEY. +SECRET_KEY= +# STATIC_ROOT / MEDIA_ROOT нужны только если раскомментированы в production.py +# STATIC_ROOT=/opt/sarex/static +# MEDIA_ROOT=/opt/sarex/media +DISK_USAGE_ROOT=/ +USE_SSL_FOR_URL_SERIALIZATION=True +WEB_APP_AUTH_MODE=jwt-session-based + +# ----------------------------------------------------------------------------- +# База данных (PostgreSQL) — читается в config/settings/production.py +# В кластере приходит из Vault-секрета secrets/data/postgresql/apps/django +# ----------------------------------------------------------------------------- +DJANGO_POSTGRES_HOST=postgresql.django.svc.cluster.local +DJANGO_POSTGRES_PORTS=5432 +DJANGO_POSTGRES_DATABASE=sarex_db +DJANGO_POSTGRES_USER=sarex +DJANGO_POSTGRES_PASSWORD=password + +# ----------------------------------------------------------------------------- +# JWT (RS512). Ключи в кластере приходят из Vault (rsa_keys), \n экранируются. +# ----------------------------------------------------------------------------- +JWT_PRIVATE_KEY= +JWT_PUBLIC_KEY= +JWT_KID=1 +DJANGO_JWT_SECRET='Froom too much love of living' +SIMPLE_JWT_ISSUER=django + +# ----------------------------------------------------------------------------- +# Celery — брокер (RabbitMQ) и backend результатов (Redis или Postgres) +# CELERY_RABBITMQ_* приходит из Vault-секрета secrets/data/rabbitmq/apps/django +# ----------------------------------------------------------------------------- +CELERY_USE_POSTGRES=False +CELERY_RABBITMQ_HOST=rabbitmq.rabbitmq.svc.cluster.local +CELERY_RABBITMQ_PORT=5672 +CELERY_RABBITMQ_USER=rabbit +CELERY_RABBITMQ_PASSWORD=rabbit +CELERY_RABBITMQ_VHOST=api +CELERY_REDIS_HOST=redis +CELERY_REDIS_PORT=6379 +CELERY_REDIS_DATABASE=0 +# Backend результатов на Postgres (используется при CELERY_USE_POSTGRES=True) +CELERY_POSTGRES_DATABASE=celery_db +CELERY_POSTGRES_USER=sarex +CELERY_POSTGRES_PASSWORD=sarex +CELERY_POSTGRES_HOST=localhost +CELERY_POSTGRES_PORT=5432 + +# Дублирующий набор RabbitMQ (django), прокидывается Vault-шаблоном +DJANGO_RABBIT_HOSTNAME=rabbitmq.rabbitmq.svc.cluster.local +DJANGO_RABBIT_USER=rabbit +DJANGO_RABBIT_PASS=rabbit +DJANGO_RABBIT_VHOST=api + +# Redis для Django-кеша/сервисов +DJANGO_REDIS_HOST=redis +DJANGO_REDIS_PORT=6379 + +# ----------------------------------------------------------------------------- +# Redis-кеш (CacheSettings) +# ----------------------------------------------------------------------------- +CACHE_HOST=localhost +CACHE_PORT=6379 +# CACHE_PASSWORD= +CACHE_SSL=False +# CACHE_SSL_CA_CERTS= + +# ----------------------------------------------------------------------------- +# S3 / объектное хранилище (S3Settings, env_prefix S3_) +# S3_* приходит из Vault-секрета secrets/data/minio/apps/django +# ----------------------------------------------------------------------------- +S3_HOST=https://storage.yandexcloud.net +AWS_S3_ENDPOINT_URL=https://storage.yandexcloud.net +S3_LOGIN= +S3_PASSWORD= +S3_BUCKET=sarex-media-storage +S3_REGION=ru-central1 +# AWS_DEFAULT_REGION=ru-central1 +# Нативная библиотека загрузки в S3 (см. Dockerfile) +S3TOOLS_LIB_PATH=/opt/sarex/lib/s3tools.so +S3TOOLS_WORKERS=10 + +# ----------------------------------------------------------------------------- +# Kafka (KafkaSettings, env_prefix KAFKA_) +# Аутентификация приходит из Vault-секрета secrets/data/kafka/apps/django +# ----------------------------------------------------------------------------- +KAFKA_BOOTSTRAP_SERVERS='["localhost:9092"]' +KAFKA_SECURITY_PROTOCOL= +KAFKA_SASL_MECHANISM= +KAFKA_SASL_PLAIN_USERNAME=user +KAFKA_SASL_PLAIN_PASSWORD=password +KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/kafka.crt +KAFKA_TOPICS='{"planning": "message-hub-stage", "ams-sync": "ams-sync"}' + +# ----------------------------------------------------------------------------- +# Sentry (SentrySettings, env_prefix SENTRY_) — читается из .env.base/.env +# ----------------------------------------------------------------------------- +SENTRY_USE=0 +SENTRY_HOST="https://sentry.sarex.io/" +SENTRY_ENVIRONMENT="production" +SENTRY_TRACES_SAMPLE_RATE=1.0 +SENTRY_PROFILES_SAMPLE_RATE=0.1 + +# ----------------------------------------------------------------------------- +# Server-флаги приложения (ServerSettings, env_prefix SERVER_) +# Ниже — переменные, реально задаваемые в манифестах кластера. +# ----------------------------------------------------------------------------- +SERVER_HOST=https://lk.sarex.io +SERVER_API_HOST=https://api.sarex.io +SERVER_ZITADEL_ENABLED=True +SERVER_KAFKA_ENABLED=False +SERVER_USE_METASHAPE=0 +SERVER_USE_CLICKHOUSE=0 +SERVER_USE_CHANGELOG=0 +SERVER_CHANGELOG_MODE=0 +SERVER_CHANGELOG_MODE_SYSTEM_LOG=1 +SERVER_SAVE_DIFF_DEM=1 +SERVER_S3_STREAM_IMPORT=1 +SERVER_USE_DJANGO_STORAGE=1 +SERVER_DJANGO_URLS=1 +SERVER_CHECK_IMPORT_HASH=1 +SERVER_USE_WRORKFLOW_STATUS=1 +SERVER_HIDE_USER_SCROLL_PERMISSIONS=0 +SERVER_EXTERNAL_FIND_BY_USERNAME_ENABLED=True +SERVER_EXTERNAL_FIND_BY_EMAIL_ENABLED=True +SERVER_CHUNKED_PATH=/tmp/chunked_uploads/%Y/%m/%d +CHECK_IMPORT_HASH=1 + +# ----------------------------------------------------------------------------- +# Workflows / processing (WorkFlowsSettings, env_prefix WORKFLOWS_) +# ----------------------------------------------------------------------------- +WORKFLOWS_USE=1 +WORKFLOWS_HOST=https://api.sarex.io +WORKFLOWS_BASE_HOST=https://lk.sarex.io +WORKFLOWS_PREFIX=/internal/v1 +# WORKFLOWS_TIMEOUT=120 +# WORKFLOWS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q +# WORKFLOWS_TAG=stable + +# ----------------------------------------------------------------------------- +# Внешние API-сервисы (наследуют BaseApiServiceMixin: +# host / api_prefix / internal_host / internal_prefix / timeout / enable) +# ----------------------------------------------------------------------------- +# Gateway (env_prefix GATEWAY_) +# GATEWAY_HOST=https://api.sarex.io +# Gatekeeper (env_prefix GK_) +# GK_ENCRYPTION_KEY= +# BIM v2 (env_prefix BIMV2_) +BIMV2_INTERNAL_HOST=http://bim-backend-v2-service.bim-api +BIMV2_TIMEOUT=60 +# EAV (env_prefix EAV_) +EAV_ENABLE=1 +# Documentation (env_prefix DOCUMENTATION_) +# DOCUMENTATION_HOST=https://api.sarex.io +# Analytics (env_prefix ANALYTICS_) +# ANALYTICS_HOST=https://lk.sarex.io +# Users (env_prefix USERS_) +# USERS_HOST=https://lk.sarex.io +# System log (env_prefix SYSTEM_LOG_) +# SYSTEM_LOG_INTERNAL_HOST= +# Resources (env_prefix RESOURCES_) +# RESOURCES_INTERNAL_HOST=http://localhost:8001 + +# ----------------------------------------------------------------------------- +# Measurements (MeasurementSettings, env_prefix MEASUREMENTS_) +# ----------------------------------------------------------------------------- +MEASUREMENTS_HOST=https://api.sarex.io/measurements/ +# MEASUREMENTS_TIMEOUT=180 +# MEASUREMENTS_WINDOW_SIZE=1000 + +# ----------------------------------------------------------------------------- +# ClickHouse (ClickHouseSettings, env_prefix CLICKHOUSE_) +# ----------------------------------------------------------------------------- +# CLICKHOUSE_HOST= +# CLICKHOUSE_PORT=9000 +# CLICKHOUSE_USER= +# CLICKHOUSE_PASSWORD= +# CLICKHOUSE_DATABASE=values_db +# CLICKHOUSE_TABLE=values +# CLICKHOUSE_SECURE=False +# CLICKHOUSE_VERIFY=False +# CLICKHOUSE_CERT= + +# ----------------------------------------------------------------------------- +# Zitadel (ZitadelSettings, env_prefix ZITADEL_) +# ZITADEL_ACCESS_TOKEN приходит из Vault-секрета secrets/data/vault/common/django_auth +# ----------------------------------------------------------------------------- +ZITADEL_HOST=https://zitadel.contour.infra.sarex.tech +ZITADEL_ACCESS_TOKEN= +# ZITADEL_USERS_ENDPOINT=/v2/users + +# ----------------------------------------------------------------------------- +# Keycloak (KeyCloakSettings env_prefix KC_, KeyCloakSyncSettings env_prefix KC_SYNC) +# ----------------------------------------------------------------------------- +KC_SYNC_ENABLE=0 +KC_USE_REDIRECT_LOGOUT=False +# KC_CLIENT_ID= +# KC_CLIENT_SECRET= +# KC_DISCOVERY_URL= +# KC_REALM=sarex + +# ----------------------------------------------------------------------------- +# Трейсинг (TracingConfig, env_prefix TRACING_) + OpenTelemetry +# ----------------------------------------------------------------------------- +# TRACING_SERVICE_NAME=backend.sarex-stage +# TRACING_ENDPOINT=localhost:4317 +# TRACING_INSECURE=False +# TRACING_ENVIRONMENT=prod + +# ----------------------------------------------------------------------------- +# Comparator / прочее +# ----------------------------------------------------------------------------- +COMPARATOR_URL=https://wb.sarex.io/comparator +COMPARATOR_SECTION=sarex-production-storage +# COMPARATOR_JWT= +# COMPARATOR_BASIC_TOKEN= +# WORKFLOWSSETTINGS_HOST=https://api.sarex.io # используется в configmap production.py +# WORKFLOWSSETTINGS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q + +# ----------------------------------------------------------------------------- +# Легаси/интеграции (значения по умолчанию есть в base.py) +# ----------------------------------------------------------------------------- +# PG_NODE_HOST=127.0.0.1:5000 +# PG_API_KEY= +# PG_MONGO_HOST=localhost +# PG_MONGO_PORT=27017 +# PG_IMPORT_PATH=/home/sarex/pg/import +# WIKIMAPIA_API_KEY= +# ANALYTICS_IMPORT_METRICS_SHEET_NAME=SRX + +# ============================================================================= +# Frontend (sarex-frontend) — сборочные переменные (webpack DefinePlugin) +# Базовые хосты сервисов берутся из endpoints.js по ключу ENDPOINT. +# ============================================================================= +# ENDPOINT=prod # stage | prod | preprod | contour | local +# SAREX_BACKEND=https://stage.sarex.io/ # прокси-таргет backend при npm start (env.js) +# TYPE_OF_HTTP_SERVICE=original +# CLIENT_ID= +# AMPLITUDE_ENABLED=false diff --git a/apps/django/CONFIGURATION.md b/apps/django/CONFIGURATION.md new file mode 100644 index 0000000..4dfb722 --- /dev/null +++ b/apps/django/CONFIGURATION.md @@ -0,0 +1,357 @@ +# Конфигурация проекта sarex-backend (Django) + +Документ описывает способы конфигурирования backend-сервиса `sarex` (Django) и +основные переменные окружения. Фронтенд-приложение `sarex-frontend` (шелл на +Module Federation) конфигурируется отдельно на этапе сборки — см. раздел в конце +и `ENDPOINTS.md`. + +## Способы конфигурирования + +Сервис — это Django-приложение (проект `config`, бизнес-логика в пакете `sarex`). +Конфигурация складывается из двух механизмов: + +1. **Модуль настроек Django** выбирается переменной `DJANGO_SETTINGS_MODULE`. + Модули лежат в `config/settings/` и наследуются друг от друга через + `from .base import *`. +2. **Переменные окружения** читаются двумя способами: + - `django-environ` — объект `env = environ.Env()` в `config/settings/base.py`, + вызовы `env('NAME', default=...)`, `env.bool(...)`, `env.list(...)`, + `env.str(...)`; + - `pydantic-settings` — классы-наследники `BaseSettings` с `env_prefix` + (напр. `ServerSettings` → префикс `SERVER_`), инстанцируются как синглтоны + (`SERVERSETTINGS = ServerSettings()` и т.п.). + +Приложение **не загружает `.env` автоматически** в основном конфиге +(`DJANGO_READ_DOT_ENV_FILE` закомментирован). Исключение — pydantic-классы +`SentrySettings` (читает `.env.base`, `.env`), `ZitadelSettings`, `KafkaSettings` +(читают `.env`). В остальном переменные нужно экспортировать в окружение процесса. + +### Модули настроек (`config/settings/*.py`) + +| Модуль | Назначение | +| --- | --- | +| `base.py` | Базовые настройки, все классы `*Settings`, INSTALLED_APPS, DRF, Celery-очереди | +| `production.py` | Продакшн: `DEBUG=False`, БД из `DJANGO_POSTGRES_*`, SimpleJWT (RS512), логирование | +| `docker.py` | Наследует `test.py`, `ALLOWED_HOSTS=["*"]`, БД на хосте `postgres` | +| `test.py` / `test_ksg.py` | Прогон тестов | +| `example.local.py` / `example.ldap.local.py` | Шаблоны для локального `local.py` (копируются вручную) | + +По умолчанию `manage.py` и `config/celery.py` используют `config.settings.local`. +В кластере задаётся `DJANGO_SETTINGS_MODULE=config.settings.production`, при этом +файл `production.py` **подменяется** ConfigMap-ом `django-configmap` (монтируется в +`/opt/sarex/config/settings/production.py`) — см. раздел про деплой. + +### Способы запуска процессов + +| Процесс | Команда | Назначение | +| --- | --- | --- | +| Web/API (uWSGI) | `uwsgi --plugin python3 --ini uwsgi.ini` | HTTP API на `0.0.0.0:8000`, модуль `config.wsgi:application` | +| Web/API (dev) | `python manage.py runserver` | Локальный запуск | +| Celery worker+beat | `celery -A config worker -B -l info -E -Q default -n default_worker.%h` | Фоновые задачи и периодические таски | +| Миграции | `python manage.py migrate` | Выполняются в `entrypoint.sh` перед стартом uWSGI | + +Порядок запуска контейнера backend (`entrypoint.sh`): сначала +`opentelemetry-instrument python manage.py migrate`, затем +`opentelemetry-instrument uwsgi --plugin python3 --ini uwsgi.ini`. В кластере +перед `entrypoint.sh` секреты из Vault экспортируются в окружение (`set -a; . /vault/secrets/...`). + +## Переменные приложения + +Ниже перечислены основные переменные. Дефолт `—` означает, что значение +обязательно (в `production.py` без него будет ошибка старта). + +### Django core + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.local` | Модуль настроек Django | +| `DJANGO_DEBUG` | bool | `False` | Режим отладки | +| `DJANGO_ISOLATED` | bool | `False` | Изолированный режим (в configmap отключает Sentry) | +| `ALLOWED_HOSTS` | list/str | — (в prod из env) | Разрешённые хосты; в кластере `*` | +| `APPEND_SLASH` | bool | `True` | Автодобавление слеша в URL | +| `FZ152_COMPLIANCE` | bool | `False` | Режим соответствия 152-ФЗ | +| `PDM_SYNC` | bool | `False` | Синхронизация с PDM | +| `OBJECT_STORAGE_SYNC` | bool | `True` | Синхронизация с объектным хранилищем | +| `SECRET_KEY` | string | хардкод в `production.py` | Секретный ключ Django | +| `SIMPLE_JWT_ISSUER` | string | `django` | Issuer для JWT | +| `DISK_USAGE_ROOT` | string | `/` | Корень для расчёта занятого места | +| `USE_SSL_FOR_URL_SERIALIZATION` | bool | `True` | Использовать https при сериализации URL | +| `WEB_APP_AUTH_MODE` | string | `JWTDefault` | Режим авторизации веб-приложения | + +### База данных (PostgreSQL) + +Читаются в `config/settings/production.py`. В кластере приходят из Vault-секрета +`secrets/data/postgresql/apps/django` (файл `/vault/secrets/django-postgresql`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_POSTGRES_HOST` | string | — | Хост PostgreSQL | +| `DJANGO_POSTGRES_PORTS` | string | `5432` | Порт PostgreSQL | +| `DJANGO_POSTGRES_DATABASE` | string | — | Имя базы | +| `DJANGO_POSTGRES_USER` | string | — | Пользователь | +| `DJANGO_POSTGRES_PASSWORD` | string | — | Пароль | + +Движок БД — `django_prometheus.db.backends.postgresql`. + +### JWT (SimpleJWT, RS512) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_PRIVATE_KEY` | string | — | Приватный RSA-ключ подписи (`\n` заменяются на переводы строк). В кластере — из Vault `rsa_keys` | +| `JWT_PUBLIC_KEY` | string | — | Публичный RSA-ключ проверки | +| `JWT_KID` | string | `None` | `kid` в заголовке токена (используется для межсервисных вызовов) | +| `DJANGO_JWT_SECRET` | string | `Froom too much love of living` | Легаси-секрет | + +### Celery (`CELERY_*`) + +Брокер — RabbitMQ; backend результатов — Redis (по умолчанию) или Postgres +(`CELERY_USE_POSTGRES=True`). `CELERY_RABBITMQ_*` в кластере из Vault-секрета +`secrets/data/rabbitmq/apps/django`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CELERY_USE_POSTGRES` | bool | `False` | Использовать Postgres как result backend | +| `CELERY_RABBITMQ_HOST` | string | `localhost` | Хост RabbitMQ | +| `CELERY_RABBITMQ_PORT` | int | `5672` | Порт | +| `CELERY_RABBITMQ_USER` | string | `rabbit` | Пользователь | +| `CELERY_RABBITMQ_PASSWORD` | string | `rabbit` | Пароль | +| `CELERY_RABBITMQ_VHOST` | string | `api` | Виртуальный хост | +| `CELERY_REDIS_HOST` | string | `localhost` | Хост Redis (result backend) | +| `CELERY_REDIS_PORT` | int | `6379` | Порт Redis | +| `CELERY_REDIS_DATABASE` | int | `0` | Номер БД Redis | +| `CELERY_POSTGRES_DATABASE` | string | `celery_db` | БД для result backend на Postgres | +| `CELERY_POSTGRES_USER` | string | `sarex` | Пользователь | +| `CELERY_POSTGRES_PASSWORD` | string | `sarex` | Пароль | +| `CELERY_POSTGRES_HOST` | string | `localhost` | Хост | +| `CELERY_POSTGRES_PORT` | string | `5432` | Порт | + +Дополнительно из Vault-шаблона прокидываются дублирующие `DJANGO_RABBIT_HOSTNAME`, +`DJANGO_RABBIT_USER`, `DJANGO_RABBIT_PASS`, `DJANGO_RABBIT_VHOST`, а также +`DJANGO_REDIS_HOST` / `DJANGO_REDIS_PORT`. + +### Кеш Redis (`CACHE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CACHE_HOST` | string | `localhost` | Хост Redis | +| `CACHE_PORT` | int | `6379` | Порт | +| `CACHE_PASSWORD` | string \| null | `None` | Пароль | +| `CACHE_SSL` | bool | `False` | TLS | +| `CACHE_SSL_CA_CERTS` | string \| null | `None` | CA-сертификат | + +### S3 / объектное хранилище (`S3_*`) + +`S3_*` в кластере из Vault-секрета `secrets/data/minio/apps/django`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_HOST` | string | `https://storage.yandexcloud.net` | Эндпоинт S3 | +| `S3_LOGIN` | string | `""` | Access key | +| `S3_PASSWORD` | string | `""` | Secret key | +| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию | +| `S3_REGION` | string | `""` | Регион (fallback: `AWS_DEFAULT_REGION`) | +| `AWS_S3_ENDPOINT_URL` | string | `https://storage.yandexcloud.net` | Эндпоинт (легаси-переменная) | +| `S3TOOLS_LIB_PATH` | string | `/opt/sarex/lib/s3tools.so` | Путь к нативной библиотеке загрузки (Dockerfile) | +| `S3TOOLS_WORKERS` | int | `10` | Число воркеров загрузки | + +### Kafka (`KAFKA_*`) + +Аутентификация в кластере из Vault-секрета `secrets/data/kafka/apps/django`. +Продюсер создаётся только при `SERVER_KAFKA_ENABLED=True`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | `["localhost:9092"]` | Список брокеров | +| `KAFKA_SECURITY_PROTOCOL` | string | `""` | Протокол безопасности | +| `KAFKA_SASL_MECHANISM` | string | `""` | SASL-механизм | +| `KAFKA_SASL_PLAIN_USERNAME` | string | `user` | Логин | +| `KAFKA_SASL_PLAIN_PASSWORD` | string | `password` | Пароль | +| `KAFKA_SSL_CAFILE` | string | `""` | Путь к CA-сертификату | +| `KAFKA_TOPICS` | dict (JSON) | `{}` | Маппинг логических имён на топики | + +### Sentry (`SENTRY_*`) + +Читается классом `SentrySettings` из `.env.base` / `.env`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SENTRY_USE` | bool | `True` | Включить Sentry | +| `SENTRY_HOST` | string | `""` | DSN/хост Sentry | +| `SENTRY_ENVIRONMENT` | string | `""` | Окружение | +| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов | +| `SENTRY_PROFILES_SAMPLE_RATE` | float | `0.1` | Доля профилей | + +### Флаги приложения (`SERVER_*`, класс `ServerSettings`) + +Класс содержит десятки булевых флагов и параметров. Наиболее значимые (реально +задаются в манифестах): + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SERVER_HOST` | string | `https://lk.sarex.io` | Внешний хост ЛК | +| `SERVER_API_HOST` | string | `https://api.sarex.io` | Внешний хост API | +| `SERVER_ZITADEL_ENABLED` | bool | `True` | Включить Zitadel-аутентификацию | +| `SERVER_KAFKA_ENABLED` | bool | `False` | Включить Kafka-продюсер | +| `SERVER_USE_METASHAPE` | bool | `True` | Использовать Metashape | +| `SERVER_USE_CLICKHOUSE` | bool | `False` | Использовать ClickHouse | +| `SERVER_CACHE_ENABLED` | bool | `False` | Включить кеш (в configmap выставляется `True`) | +| `SERVER_USE_NOTIFICATIONS` | bool | `True` | Уведомления | +| `SERVER_TIMEOUT` | int | `60` | Таймаут по умолчанию | +| `SERVER_CHUNKED_PATH` | string | — | Путь для чанкованных загрузок | + +Полный список полей — в `ServerSettings` (`config/settings/base.py`). Любое поле +переопределяется переменной `SERVER_` в верхнем регистре. + +### Workflows / processing (`WORKFLOWS_*`, класс `WorkFlowsSettings`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOWS_USE` | bool | `False` | Включить интеграцию с processing | +| `WORKFLOWS_HOST` | string | `https://api.sarex.io` | Хост сервиса processing | +| `WORKFLOWS_BASE_HOST` | string | `https://lk.sarex.io` | Базовый хост | +| `WORKFLOWS_PREFIX` | string | `/internal/v1` | Префикс внутреннего API | +| `WORKFLOWS_TIMEOUT` | int | `120` | Таймаут | +| `WORKFLOWS_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов задач | +| `WORKFLOWS_TAG` | string | `stable` | Тег образов | + +### Внешние API-сервисы (`BaseApiServiceMixin`) + +Классы `GateWaySetttings` (`GATEWAY_`), `BimV2ApiSettings` (`BIMV2_`), +`EAVSettings` (`EAV_`), `AnalyticsSettings` (`ANALYTICS_`), +`DocumentationSettings` (`DOCUMENTATION_`), `UsersSettings` (`USERS_`), +`SystemLogSettings` (`SYSTEM_LOG_`), `ResourceSettings` (`RESOURCES_`) наследуют +общий набор полей: + +| Поле (переменная `_`) | Тип | Назначение | +| --- | --- | --- | +| `HOST` | string | Внешний хост сервиса | +| `API_PREFIX` | string | Префикс публичного API | +| `INTERNAL_HOST` | string | Внутренний хост (внутрикластерный) | +| `INTERNAL_PREFIX` | string | Префикс внутреннего API | +| `TIMEOUT` | int | Таймаут запроса | +| `ENABLE` | bool | Включён ли сервис | + +Реально задаваемые в манифестах: `BIMV2_INTERNAL_HOST`, `BIMV2_TIMEOUT`, +`EAV_ENABLE`. Отдельно — `GK_ENCRYPTION_KEY` (класс `GatekeeperSettings`). + +### Measurements (`MEASUREMENTS_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MEASUREMENTS_HOST` | string | `https://api.sarex.io/measurements/` | Хост сервиса измерений | +| `MEASUREMENTS_TIMEOUT` | int | `180` | Таймаут | +| `MEASUREMENTS_WINDOW_SIZE` | int | `1000` | Размер окна | + +### ClickHouse (`CLICKHOUSE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CLICKHOUSE_HOST` | string | `rc1d-...yandexcloud.net` | Хост | +| `CLICKHOUSE_PORT` | int | `9000` | Порт | +| `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` | string | `""` | Учётные данные | +| `CLICKHOUSE_DATABASE` | string | `values_db` | База | +| `CLICKHOUSE_TABLE` | string | `values` | Таблица | +| `CLICKHOUSE_SECURE` / `CLICKHOUSE_VERIFY` | bool | `False` | TLS и проверка сертификата | +| `CLICKHOUSE_CERT` | string | `""` | CA-сертификат | + +### Zitadel (`ZITADEL_*`) и Keycloak (`KC_*`, `KC_SYNC*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ZITADEL_HOST` | string | `""` | Хост Zitadel (IdP) | +| `ZITADEL_ACCESS_TOKEN` | string | `""` | Сервисный токен (из Vault `django_auth`) | +| `ZITADEL_USERS_ENDPOINT` | string | `/v2/users` | Эндпоинт пользователей | +| `KC_SYNC_ENABLE` | bool | `False` | Включить синхронизацию с Keycloak | +| `KC_USE_REDIRECT_LOGOUT` | bool | `False` | Redirect при logout | +| `KC_CLIENT_ID` / `KC_CLIENT_SECRET` / `KC_DISCOVERY_URL` / `KC_REALM` | string | см. `KeyCloakSettings` | Параметры клиента Keycloak | + +### Трейсинг (`TRACING_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACING_SERVICE_NAME` | string | `backend.sarex-stage` | Имя сервиса в трейсах | +| `TRACING_ENDPOINT` | string | `localhost:4317` | OTLP-коллектор | +| `TRACING_INSECURE` | bool | `False` | Без TLS | +| `TRACING_ENVIRONMENT` | string | `prod` | Окружение | + +### Comparator и прочее + +| Переменная | Значение по умолчанию | Назначение | +| --- | --- | --- | +| `COMPARATOR_URL` | `https://wb.sarex.io/comparator` | URL сервиса сравнения | +| `COMPARATOR_SECTION` | `sarex-production-storage` | Секция хранилища | +| `COMPARATOR_JWT` | `default_jwt` | Токен сравнения | +| `WORKFLOWSSETTINGS_HOST` / `WORKFLOWSSETTINGS_REGISTRY` | — | Используются напрямую в configmap `production.py` | +| `PG_NODE_HOST`, `PG_API_KEY`, `PG_MONGO_HOST`, `PG_MONGO_PORT`, `PG_IMPORT_PATH` | см. `base.py` | Легаси-интеграции PG | + +## Конфигурация в кластере (Kubernetes) + +Манифесты приложения — в этом же каталоге (`base/`, оверлеи `brusnika-stage`, +`brusnika-prod`, `yc-k8s-test`). Секреты монтируются через **Vault Agent Injector** +(аннотации `vault.hashicorp.com/*` на Deployment `backend` и `celery`). Файлы +секретов в контейнере и их содержимое: + +| Файл `/vault/secrets/...` | Секрет Vault | Переменные | +| --- | --- | --- | +| `django-postgresql` | `secrets/data/postgresql/apps/django` | `DJANGO_POSTGRES_HOST/PORTS/DATABASE/USER/PASSWORD` | +| `django-rabbitmq` | `secrets/data/rabbitmq/apps/django` | `CELERY_RABBITMQ_*`, `DJANGO_RABBIT_*` | +| `django-s3` | `secrets/data/minio/apps/django` | `AWS_S3_ENDPOINT_URL`, `S3_HOST/BUCKET/LOGIN/PASSWORD` | +| `django-kafka` | `secrets/data/kafka/apps/django` | `KAFKA_BOOTSTRAP_SERVERS/SECURITY_PROTOCOL/SASL_*` | +| `django-jwt-private` / `django-jwt-public` | `secrets/data/vault/common/rsa_keys` | `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` | +| `django-common` | `secrets/data/vault/common/django_auth` | `ZITADEL_ACCESS_TOKEN` | + +Контейнер экспортирует эти файлы в окружение до запуска (`set -a; . /vault/secrets/...`). +Кроме того, `production.py` из ConfigMap содержит функцию `_load_env_file`, которая +подхватывает те же файлы при запуске `manage.py` через `kubectl exec` вне entrypoint. + +Остальные (несекретные) переменные задаются в блоке `env` контейнеров +`backend`/`celery` (`SERVER_*`, `WORKFLOWS_*`, `BIMV2_*`, `MEASUREMENTS_*`, +`ZITADEL_HOST`, `KAFKA_TOPICS`, `EAV_ENABLE`, `PDM_SYNC`, `JWT_KID` и др.). + +ConfigMap `django-configmap` подменяет `config/settings/production.py` +(смонтирован в `/opt/sarex/config/settings/production.py`) и переопределяет +`ALLOWED_HOSTS`, CORS, `DATABASES`, `SIMPLE_JWT`, `REST_FRAMEWORK`, `MIDDLEWARE`, +`KeyCloakSettings`, `SAREX_MODULES`, а также включает Sentry (если не `ISOLATED`). +ConfigMap `zitadel-configmap` содержит `config.json` с `client_id`/`host` Zitadel. +`uwsgi-configmap` монтирует `uwsgi.ini`. + +## CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` +(`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение +по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | +| --- | --- | --- | +| ветка `stage` | `stage` | `aero` | +| ветка `master` | `preprod` | (см. правила) | +| тег | `prod` | (см. правила) | + +Ключевые переменные: `SERVICE_NAME=backend`, `DOCKERFILE_PATH=./Dockerfile`, +`RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `HELM_SET_ARGS` +(`universal-chart.services.backend.image.name…`, `…celery.image.name…`). + +## Замечания и потенциальные проблемы + +- В `production.py` `SECRET_KEY` задан хардкодом (закомментированный `env('SECRET_KEY')`). + Для реального прод-развёртывания ключ желательно вынести в секрет. +- Основной конфиг не читает `.env` автоматически; переменные нужно экспортировать + в окружение (в кластере это делает Vault + `set -a`). Только `SENTRY_*`, + `ZITADEL_*`, `KAFKA_*` читаются из файлов `.env.base`/`.env` их pydantic-классами. +- `production.py` в репозитории backend и `production.py` из ConfigMap `django-configmap` + — **разные** файлы. В кластере используется версия из ConfigMap (в ней, в частности, + выставлено `DEBUG=True` в конце и включён `corsheaders`). +- `DATABASES['default']['ENGINE']` — `django_prometheus.db.backends.postgresql` + (обёртка для метрик Prometheus). +- Значения `SERVER_*`-флагов у `backend` и `celery` местами различаются + (напр. `SERVER_ZITADEL_ENABLED`, `SERVER_API_HOST`) — это ожидаемо. + +## Минимальный набор для локального запуска + +Согласно `README.md` backend: поднять Postgres/Redis/RabbitMQ (docker-compose), +скопировать шаблон настроек `cp config/settings/example.local.py config/settings/local.py`, +применить миграции (`python manage.py migrate`) и создать суперпользователя. +Минимально требуются переменные БД (`DJANGO_POSTGRES_*` или значения в `local.py`), +брокера Celery (`CELERY_RABBITMQ_*`) и, при использовании соответствующих функций, +`S3_*`, `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`. Примеры значений — в `.env.example` +рядом с этим файлом. diff --git a/apps/django/ENDPOINTS.md b/apps/django/ENDPOINTS.md new file mode 100644 index 0000000..c24c36e --- /dev/null +++ b/apps/django/ENDPOINTS.md @@ -0,0 +1,127 @@ +# Эндпоинты и хосты сервисов для sarex-frontend + +Документ описывает базовые хосты сервисов, к которым обращается фронтенд-шелл +`sarex-frontend`, и удалённые модули (Module Federation), которые он подгружает. +Backend для этого приложения — `sarex-backend` (Django), его конфигурация и +переменные описаны в `CONFIGURATION.md`, а серверное REST API — в `openapi.yaml`. + +## Как устроено взаимодействие + +`sarex-frontend` — это host-приложение на **Webpack Module Federation**. Реестр +хостов задаётся декларативно в `endpoints.js` (корень репозитория): объект вида +`<сервис>.<тип>.<окружение>` → URL. Возможные типы: + +- `api` / `apiV1` / `apiV2` / … — базовый URL REST API сервиса; +- `module` — URL `remoteEntry.js` удалённого микрофронтенда; +- `gateway` — базовый URL gateway. + +Окружение выбирается сборочной переменной `ENDPOINT` (`process.env.ENDPOINT`, +по умолчанию `prod`) в `webpack.common.js`. Значения `endpoints.js[сервис][тип][ENDPOINT]` +пробрасываются в код как константы `process.env.` через `DefinePlugin` +(объект `processEnvByEndpoint`), напр. `endpoints.bim.api[ENDPOINT]` → `BIM_API`. + +Для локального запуска backend-таргет задаётся отдельно в `env.js` +(`SAREX_BACKEND`, по умолчанию `https://stage.sarex.io/`) и используется +dev-сервером (`webpack.dev.js`) как прокси; собственный Django-backend в окружении +`contour` доступен по относительным путям (пустой хост). + +## Окружения (`ENDPOINT`) + +| Значение | Назначение | +| --- | --- | +| `prod` | Продакшн (`https://api.sarex.io`, `https://modules.sarex.io`) | +| `stage` | Stage (`https://stage-api.sarex.io`, `https://stage-modules.sarex.io`) | +| `preprod` | Preprod (`https://api.preprod.sarex.io`, `https://modules.preprod.sarex.io`) | +| `contour` | Изолированный контур — относительные пути (пустой хост) | +| `local` | Локальная разработка (задан не у всех сервисов) | + +> У части сервисов `gateway` определены также специальные ключи `contour_local` +> (`https://stage.sarex.io`) и `contour_prod` (`https://lk.sarex.io`). + +## Базовые хосты API-сервисов + +Итоговый URL = `<базовый хост сервиса>` + путь запроса. + +| Сервис (`endpoints.*`) | Константа | `stage` | `prod` | `contour` | +| --- | --- | --- | --- | --- | +| `api.api` | `SAREX_API` | `https://stage-api.sarex.io` | `https://api.sarex.io` | `""` | +| `bim.api` | `BIM_API` | `https://stage-api.sarex.io/bim/api/v1` | `https://api.sarex.io/bim/api/v1` | `""` | +| `bim.apiV2` | `BIM_API_V2` | `…/bim/api/v2` | `…/bim/api/v2` | `""` | +| `bim.files` | `BIM_FILES` | `…/bim` | `…/bim` | `""` | +| `workspaces.api` / `workspacesV2.api` | `WORKSPACESV2_API` | `…/workspaces/` | `…/workspaces/` | `/workspaces/` | +| `workflows.api` | `WORKFLOWS_API` | `…/workflows` | `…/workflows` | `/workflows` | +| `remarks.api` | `REMARKS_API` | `…/remarks/api/v1` | `…/remarks/api/v1` | `/remarks/api/v1` | +| `issues.api` | `ISSUES_API` | `…/issues/api/v1` | `…/issues/api/v1` | `/issues/api/v1` | +| `issuesBase.api` | `ISSUES_BASE_API` | `…/issues/api` | `…/issues/api` | `/issues/api` | +| `flows.api` | — | `…/issues/api/v1` | `…/issues/api/v1` | `/issues/api/v1` | +| `inspections.api` | — | `…/inspections/api/v1` | `…/inspections/api/v1` | `/inspections/api` | +| `documentations.api` | `DOCUMENTATIONS_API` | `…/documentations/api/v1` | `…/documentations/api/v1` | `/documentations/api/v1` | +| `pm.api` | — | `…/documentations/api/v1` | `…/documentations/api/v1` | `/documentations/api/v1` | +| `analyticsV2.api` | — | `…/analytics-v2/api/v1` | `…/analytics-v2/api/v1` | `/analytics-v2/api/v1` | +| `analytics.api` | `ANALYTICS_API` | `…/analytics` | `…/analytics` | `/analytics` | +| `processes.api` | `PROCESSES_API` | `…/flows/api/v1` | `…/flows/api/v1` | `/flows/api/v1` | +| `gateway.gateway` | `GATEWAY` | `…/gateway` | `…/gateway` | (local: `http://localhost:9000/gateway`) | +| `gateway.api` | `GATEWAY_API` | `…/gateway/api/v1` | `…/gateway/api/v1` | `/gateway/api/v1` | +| `gateway.apiV2` | `GATEWAY_API_V2` | `…/gateway/api/v2` | `…/gateway/api/v2` | `/gateway/api/v2` | +| `eav.api` | `EAV_API` | `…/eav/api/v0` | `…/eav/api/v0` | `/eav/api/v0` | +| `eav.apiV1…apiV4` | `EAV_API_V1…V4` | `…/eav/api/v1…v4` | `…/eav/api/v1…v4` | `/eav/api/v1…v4` | +| `notifications.api` | `NOTIFICATIONS_API` | `…/lambdas/notification/` | `…/lambdas/notification/` | `""` | +| `lambdas.api` | `LAMBDAS_API` | `…/lambdas` | `…/lambdas` | `/lambdas` | +| `automations.api` | `AUTOMATIONS_API` | `…/automation/api/v1` | `…/automation/api/v1` | `/automation/api/v1` | +| `orchestrator.api` | `ORCHESTRATOR_API` | `…/orchestrator` | `…/orchestrator/api` | `/orchestrator/api` | + +> `…` = `https://stage-api.sarex.io` (stage) или `https://api.sarex.io` (prod). +> Собственный Django-backend (`sarex-backend`) в контуре обслуживается по +> относительным путям `/api/...` — см. `openapi.yaml`. + +## Удалённые модули (Module Federation, `remoteEntry.js`) + +Хост подгружает микрофронтенды по URL из `endpoints.<сервис>.module[ENDPOINT]`. +Базовый хост модулей: `https://stage-modules.sarex.io` (stage) / +`https://modules.sarex.io` (prod); в контуре — относительные пути. + +| Сервис | Константа | Путь `module` (относительный, контур) | +| --- | --- | --- | +| `workspaces` | `WORKSPACES_MODULE` | `/workspaces/module/remoteEntry.js` | +| `workspacesV2` | `WORKSPACESV2_MODULE` | `/workspaces-v2/module/remoteEntry.js` | +| `workflows` | `WORKFLOWS_MODULE` | `/workflows/module/remoteEntry.js` | +| `remarks` | `REMARKS_MODULE` | `/remarks/static/module/remoteEntry.js` | +| `issues` | `ISSUES_MODULE` | `/issues/static/module/remoteEntry.js` | +| `flows` | `FLOWS_MODULE` | `/flows/static/module/remoteEntry.js` | +| `inspections` | `INSPECTIONS_MODULE` | `/inspections/static/module/remoteEntry.js` | +| `documentations` | `DOCUMENTATIONS_MODULE` | `/documentations/static/module/remoteEntry.js` | +| `pm` | `PM_MODULE` | `/pm/module/remoteEntry.js` | +| `projects` | `PROJECTS_MODULE` | `/projects/static/module/remoteEntry.js` | +| `analyticsV2` | `ANALYTICS_MODULE` | `/analytics-v2/static/module/remoteEntry.js` | +| `reviews` | `REVIEWS_MODULE` | `/reviews/static/module/remoteEntry.js` | +| `administration` | `ADMINISTRATION_MODULE` | `/control-interface/modules/admin/remoteEntry.js` | +| `adminProc` | `ADMIN_PROC_MODULE` | `/admin-frontend/static/module/remoteEntry.js` | +| `assets` | `ASSETS_MODULE` | `/control-interface/modules/assets/remoteEntry.js` | +| `premises` | `PREMISES_MODULE` | `/premises/static/module/remoteEntry.js` | +| `contracts` | `CONTRACTS_MODULE` | `/cotracts/static/module/remoteEntry.js` | +| `transmittal` | `TRANSMITTAL_MODULE` | `/transmittal/static/module/remoteEntry.js` | +| `prescriptions` | `PRESCRIPTIONS_MODULE` | `/prescriptions/static/module/remoteEntry.js` | +| `rfi` | `RFI_MODULE` | `/rfi/static/module/remoteEntry.js` | +| `assistant` | — | `/assistant/static/module/remoteEntry.js` | + +Список подключаемых в ЛК модулей (пункты меню) дублируется на стороне backend в +`SAREX_MODULES` (ConfigMap `django-configmap`): `remarks`, `issues`, +`documentations`, `reviews`, `processes`, `rfi`, `transmittal`. + +## Backend, обслуживающий шелл + +Основной API самого шелла (аутентификация, пользователи, настройки приложения, +проекты/цели/миссии, аналитика) — это `sarex-backend` под префиксом `/api/...` +(и `/internal/...` для внутрикластерных вызовов). Полное описание серверных +эндпоинтов приведено в `openapi.yaml`. Ключевые группы: + +| Префикс | Назначение | +| --- | --- | +| `/api/token…`, `/api/auth/…`, `/api/login`, `/api/logout` | Аутентификация и JWT | +| `/api/app-settings/` | Настройки приложения | +| `/api/core/…` | Пользователи, компании, цели, миссии, ортофото, облака точек и т.д. | +| `/api/client/…` | Клиентский дашборд, загрузки, self-сервис | +| `/api/analytics/…` | Аналитические дашборды, метрики, виджеты | +| `/api/map/…` | Кадастр и заметки на карте | +| `/api/pg/…` | Облака точек, экспорт, измерения | +| `/internal/client/…` | Внутренние вызовы (настройки, токены) | diff --git a/apps/django/FRONTEND_REQUESTS.md b/apps/django/FRONTEND_REQUESTS.md new file mode 100644 index 0000000..124fa99 --- /dev/null +++ b/apps/django/FRONTEND_REQUESTS.md @@ -0,0 +1,417 @@ +# Полный перечень запросов sarex-frontend + +Извлечено из исходников `src/` и `modules/`: обёртки `httpService.{get,post,put,patch,delete}Request`, вызовы `fetch`, эндпоинты RTK Query (`builder.query/mutation`). Базовый хост подставляется по ключу `service` (для `httpService`, см. `src/Model/api/hosts.ts`) либо по `env.*` из `endpoints.js`; выбор окружения — переменной сборки `ENDPOINT`. В путях `{param}` — подстановки, `?a=&b=` — query-параметры, заданные в коде. Хосты приведены для `prod`. + +**Всего вызовов: 392** — httpService: 296, fetch: 76, RTK Query: 20. + + +## `sarex` — Django backend (`sarex-backend`), host `/` (в контуре относительные пути) + +Уникальных запросов: 206 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+2) | +| POST | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+1) | +| PUT | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+3) | +| PATCH | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts | +| DELETE | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts | +| POST | `/api/analytics/attachments/` | src/Components/ImageDropZone/ImageDropZone.jsx | +| GET | `/api/analytics/attachments/?dashboard={dashboardId}` | src/Components/ImageDropZone/hook.js | +| DELETE | `/api/analytics/attachments/{id}/` | src/Components/ImageDropZone/ImageDropZone.jsx | +| DELETE | `/api/analytics/attributes-options/{id}/` | src/Model/api.js | +| DELETE | `/api/analytics/attributes/{id}/` | src/Model/api.js | +| PUT | `/api/analytics/dashboards/{id}/` | src/Model/api.js | +| DELETE | `/api/analytics/dashboards/{id}/` | src/Model/api.js | +| GET | `/api/analytics/expressions/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts | +| POST | `/api/analytics/expressions/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts | +| DELETE | `/api/analytics/expressions/{id}/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts | +| PUT | `/api/analytics/expressions/{newComputationValue.id}` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts | +| PATCH | `/api/analytics/folders/{body.id}/` | modules/analytics/store/api/api.ts | +| DELETE | `/api/analytics/folders/{id}/` | modules/analytics/store/api/api.ts (+1) | +| PATCH | `/api/analytics/groups/{body.id}/` | src/Model/api.js | +| DELETE | `/api/analytics/groups/{id}/` | src/Model/api.js | +| DELETE | `/api/analytics/metric2widget/{id}/` | src/Model/api.js | +| GET | `/api/analytics/metrics/` | modules/analytics/entities/Metric/MetricEntity.ts | +| DELETE | `/api/analytics/metrics/{id}/` | modules/analytics/store/api/api.ts | +| PUT | `/api/analytics/metrics/{metric.id}/` | modules/analytics/store/api/api.ts | +| GET | `/api/analytics/metrics/{params \|\|` | modules/analytics/store/api/api.ts | +| GET | `/api/analytics/texts/?dashboard={id}` | modules/widget/store/widgets.ts | +| PATCH | `/api/analytics/texts/{body.id}/` | src/Model/api.js | +| DELETE | `/api/analytics/texts/{id}/` | src/Model/api.js | +| POST | `/api/analytics/values/import_xlsx/` | modules/analytics/store/api/api.ts | +| PATCH | `/api/analytics/values/{id}/` | src/Model/api.js | +| DELETE | `/api/analytics/values/{valueId}/` | src/Model/api.js | +| PATCH | `/api/analytics/widgets/{id}/` | src/Model/api.js | +| DELETE | `/api/analytics/widgets/{id}/` | src/Model/api.js | +| GET | `/api/client/dashboard/feed/` | src/Model/actions.js | +| GET | `/api/client/dashboard/targets/{orderId}/feed/` | src/Model/actions.js | +| POST | `/api/client/dashboard/targets/{orderId}/feed/` | src/Model/actions.js | +| POST | `/api/client/dashboard/targets/{targetId}/request_survey/` | src/Model/actions.js | +| GET | `/api/client/dashboard/webcams/` | src/Model/api.js | +| GET | `/api/client/folders/?target={targetId}&limit=10000` | src/Model/api.js | +| PATCH | `/api/client/folders/{id}/` | src/Model/api.js | +| DELETE | `/api/client/folders/{id}/` | src/Model/api.js | +| GET | `/api/client/settings/` | src/Model/actions.js | +| PUT | `/api/client/settings/` | src/Model/actions.js | +| POST | `/api/client/uploads/` | src/Model/actions.js | +| GET | `/api/client/uploads/?target={targetId}&limit=10000` | src/Model/actions.js | +| GET | `/api/client/uploads/{doc}/layers/` | src/Model/actions.js | +| DELETE | `/api/client/uploads/{fileId}/` | src/Model/actions.js | +| POST | `/api/client/uploads/{fileId}/translate_to_svf/` | src/Model/actions.js | +| PATCH | `/api/client/uploads/{id}/` | src/Model/actions.js | +| GET | `/api/commons/cs/` | src/Model/actions.js (+1) | +| GET | `/api/core/admin/companies/` | src/shared/api/fetch/company.api.ts | +| POST | `/api/core/c2s-comparisons/` | src/Components/Viewer/Panels/ComparisonsPanel/store/api/api.ts | +| GET | `/api/core/companies/` | modules/targetsTree/repositories/CompanyRepository/RESTCompanyRepository.ts (+1) | +| POST | `/api/core/contour/export/` | src/Model/api.js | +| GET | `/api/core/contour/export/?pointcloud={pointcloudId}` | src/Model/api.js | +| GET | `/api/core/materials/` | modules/measurements/components/volume/store/materials.ts | +| POST | `/api/core/media-folders/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| DELETE | `/api/core/media-folders/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| PATCH | `/api/core/media-folders/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| POST | `/api/core/mesh/` | src/Model/api.js | +| DELETE | `/api/core/mesh/{id}/` | src/Model/api.js | +| POST | `/api/core/missions/import/create/` | src/Model/actions.js | +| GET | `/api/core/missions/{id}/` | src/Model/api.js | +| POST | `/api/core/mrpa/` | src/shared/api/fetch/mrpa.api.ts | +| POST | `/api/core/mrpa/list/` | src/shared/api/fetch/mrpa.api.ts | +| DELETE | `/api/core/mrpa/{id}/` | src/shared/api/fetch/mrpa.api.ts | +| GET | `/api/core/orthophotos/{id}/` | src/Model/api.js | +| GET | `/api/core/orthophotos/{orthoId}/altitude_and_temperature/?points={lng},{lat}` | src/Model/api.js | +| GET | `/api/core/panoramas/{id}/` | src/Model/api.js | +| DELETE | `/api/core/panoramas/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| PATCH | `/api/core/panoramas/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| DELETE | `/api/core/photos/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| PATCH | `/api/core/photos/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| GET | `/api/core/pointclouds/?mission={mission}&comparison_type={type}` | src/Components/Viewer/Panels/ComparisonsPanel/store/api/api.ts | +| POST | `/api/core/polygons/` | src/Model/actions.js | +| GET | `/api/core/polygons/?with_links_folder{targetIdQuery}` | src/Model/api.js | +| POST | `/api/core/state/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts | +| GET | `/api/core/state/?pointcloud_id={pointCloudId}` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts | +| PATCH | `/api/core/state/{id}/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts | +| DELETE | `/api/core/state/{id}/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts | +| GET | `/api/core/targets/{id}/` | src/Model/api.js | +| GET | `/api/core/users/` | modules/dashboards/models/entities/Users/UsersEntity.ts | +| DELETE | `/api/core/videos/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| PATCH | `/api/core/videos/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| GET | `/api/map/cadastre/point/?point={point}` | src/Model/actions.js | +| GET | `/api/map/notes/folders/?orthophoto={targetId}` | src/Model/api.js | +| PATCH | `/api/map/notes/folders/{id}/` | src/Model/api.js | +| DELETE | `/api/map/notes/folders/{id}/` | src/Model/api.js | +| PATCH | `/api/mar/tasks/{taskId}/` | src/Model/api.js | +| DELETE | `/api/mar/tasks_attachments/{id}/` | src/Model/api.js | +| DELETE | `/api/mar/tasks_groups_attachments/{id}/` | src/Model/api.js | +| GET | `/api/notifications` | modules/targetsTree/repositories/TargetRepository/RESTTragetRepository.ts (+1) | +| POST | `/api/pg/attachments/` | src/Model/actions.js (+1) | +| DELETE | `/api/pg/attachments/{id}/` | src/Model/api.js | +| POST | `/api/pg/compare/c2c/` | src/Model/api.js | +| GET | `/api/pg/compare/c2c/?pointcloud={pointcloud}` | src/Model/api.js | +| POST | `/api/pg/compare/c2s/` | src/Model/actions.js | +| POST | `/api/pg/compare/t2t/` | src/Model/api.js | +| GET | `/api/pg/compare/t2t/?pointcloud={pointcloud}` | src/Model/api.js | +| POST | `/api/pg/export/pointcloud/` | src/Model/api.js | +| GET | `/api/pg/export/pointcloud/?pointcloud={pointcloudId}` | src/Model/api.js | +| POST | `/api/pg/measurements/` | src/Model/Store/voxelVolumeStore.js (+1) | +| GET | `/api/pg/measurements/?target={targetId}` | src/Model/actions.js | +| PUT | `/api/pg/measurements/{idOnServer}/` | src/Model/Store/voxelVolumeStore.js | +| DELETE | `/api/pg/measurements/{idOnServer}/` | src/Model/Store/voxelVolumeStore.js | +| PUT | `/api/pg/measurements/{measurement.idOnServer}/` | src/Model/actions.js | +| DELETE | `/api/pg/measurements/{measurementId}/` | src/Model/actions.js | +| GET | `/api/pg/orthomosaicexport/?mission_id={missionId}` | src/Model/api.js | +| POST | `/api/pg/pdf-overlays/` | src/Model/api.js | +| DELETE | `/api/pg/pdf-overlays/{id}/` | src/Model/api.js | +| GET | `/api/pg/pointclouds/{id}/comparisons/` | src/Model/api.js | +| GET | `/api/pg/pointclouds/{pointCloudId}/` | src/Model/actions.js | +| GET | `/api/pg/pointclouds/{pointCloudId}/altitude/?points={formattedPoints}` | src/Model/actions.js | +| POST | `/api/pg/pointclouds/{pointCloudId}/baseplane_volume/` | src/Model/actions.js | +| POST | `/api/pg/pointclouds/{pointCloudId}/measurements/` | src/Model/actions.js | +| GET | `/api/pg/pointclouds/{pointCloudId}/measurements/delete_measurements/` | src/Model/actions.js | +| DELETE | `/api/pg/pointclouds/{pointCloudId}/measurements/{idOnServer}/` | src/Model/actions.js | +| PATCH | `/api/pg/pointclouds/{pointCloudId}/measurements/{measurementId}/` | src/Model/actions.js | +| POST | `/api/pg/pointclouds/{pointCloudId}/project_volume/` | src/Model/actions.js | +| POST | `/api/pg/pointclouds/{pointCloudId}/volume/` | src/Model/actions.js | +| GET | `/api/pg/pointclouds/{pointcloud}/comparisons/` | src/Model/api.js | +| POST | `/api/pg/pointclouds/{targetId}/measurements/{measurementId}/calculate/` | src/Model/api.js | +| GET | `/api/pg/projects/` | src/Model/actions.js | +| POST | `/api/pg/projects/` | src/Model/actions.js | +| POST | `/api/pg/projects/{id}/images/` | src/Model/actions.js | +| PATCH | `/api/pg/projects/{id}/images/{photo.id}/` | src/Model/actions.js | +| GET | `/api/pg/projects/{id}/status/` | src/Model/actions.js | +| POST | `/api/pg/projects/{projectId}/build/` | src/Model/actions.js | +| GET | `/api/pg/projects/{projectId}/images/{GCPName ? `?marker={GCPName}` :` | src/Model/actions.js | +| POST | `/api/pg/projects/{projectId}/init/` | src/Model/actions.js | +| GET | `/api/pg/projects/{projectId}/markers/` | src/Model/actions.js | +| POST | `/api/pg/projects/{projectId}/optimize_cameras/` | src/Model/actions.js | +| POST | `/api/pg/projects/{projectId}/update_marker/` | src/Model/actions.js | +| GET | `/api/pm/dms/?target={targetId}` | src/Model/actions.js | +| GET | `/api/pm/msp/projects/?bundle_id={bundleID}&schema=tiny` | modules/pm/gate/repositories/SelectedKSGProjectRepository/WorkspaceSelectedKSGProjectRepository.ts | +| DELETE | `/api/pm/msp/projects/{id}/` | src/Model/api.js | +| GET | `/api/pm/msp/projects/{id}/profiles/` | modules/pm/gate/repositories/TaskResourceConnectionBaseRepository/TaskResourceConnectionBaseRepository.ts | +| GET | `/api/pm/msp/resources-tasks/?projects={project}` | modules/pm/gate/repositories/TaskResourcesConnectionsRepository/TaskResourcesConnectionsRepository.ts | +| GET | `/api/pm/msp/tasks/?project={project}{query ? `&{query}` :` | modules/pm/gate/repositories/TasksRepository/TasksRepository.ts | +| GET | `/api/pm/msp/tasks/{id}/` | modules/pm/gate/repositories/TasksRepository/TasksRepository.ts | +| GET | `/api/workflows/?target={id}` | modules/targetsV2_OLD_DEPRECATED/services/TargetTreeService/TargetTreeService.ts | +| GET | `/api/workflows/?target={this.targetId}` | modules/missions/controllers/MissionsCardList/MissionsCardListController.ts | +| POST | `Analytic.baseUrl` | src/Model/api.js | +| POST | `ClientFolders.baseUrl` | src/Model/api.js | +| GET | `DashboardsAPI.baseUrl` | src/Model/api.js | +| POST | `DashboardsAPI.baseUrl` | src/Model/api.js | +| POST | `GroupsApi.baseUrl` | src/Model/api.js | +| POST | `MetricsApi.baseUrl` | modules/analytics/store/api/api.ts | +| GET | `MetricsFoldersAPI.baseUrl` | modules/analytics/store/api/api.ts | +| POST | `MetricsFoldersAPI.baseUrl` | modules/analytics/store/api/api.ts | +| POST | `NotesFolders.baseUrl` | src/Model/api.js | +| POST | `ProjectsAPI.taskUrl` | src/Model/api.js | +| PUT | `StreamFile.baseUrl` | src/Model/api.js | +| GET | `host` | modules/control/entities/Storage/StorageRESTEntity.ts | +| POST | `host` | modules/control/entities/Storage/StorageRESTEntity.ts | +| GET | `hostGetInfo` | modules/control/entities/Storage/StorageRESTEntity.ts | +| GET | `hostInfo` | modules/account/entities/Storage/StorageRESTEntity.ts | +| POST | `hostInfo` | modules/account/entities/Storage/StorageRESTEntity.ts | +| POST | `hostUpdatePassword` | modules/account/entities/Storage/StorageRESTEntity.ts | +| GET | `hosts` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| POST | `hosts` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| POST | `hosts.expression2widget` | modules/widget/entities/Expressions/ExpressionEntity.ts | +| GET | `hosts.expressions` | modules/widget/entities/Expressions/ExpressionEntity.ts | +| GET | `link` | src/Model/actions.js | +| POST | `mediaEndpoint` | src/Model/Media/api.ts | +| GET | `nextUrl` | src/Model/MediaSidebar/Api/MediaModelApi.ts | +| POST | `panoramasEndpoint` | src/Model/Media/api.ts | +| POST | `photosEndpoint` | src/Model/Media/api.ts | +| GET | `this.path` | modules/analytics/entities/Folder/FolderEntity.ts | +| GET | `this.pathname` | modules/missions/entity/Mission/MissionRESTEntity.ts | +| GET | `this.props.url` | modules/auth/TokenIssuer.ts | +| POST | `this.uploadUrl` | src/Model/streamFile.ts | +| GET | `url` | src/Model/api.js | +| GET | `url.toString()` | modules/targetsTree/repositories/TargetRepository/RESTTragetRepository.ts (+2) | +| POST | `videosEndpoint` | src/Model/Media/api.ts | +| POST | `{Export.baseUrlHorizontal}` | src/Model/api.js | +| POST | `{Export.urlTablePoints}` | src/Model/api.js | +| POST | `{Export.urlTablePoints}pointcloud/` | src/Model/api.js | +| PATCH | `{ProjectsAPI.taskUrl}{body.id}/` | src/Model/api.js | +| DELETE | `{ProjectsAPI.taskUrl}{id}/` | src/Model/api.js | +| GET | `{TrackingMetaData.trackerUrl}events/` | src/Model/api.js | +| GET | `{hosts.expression2widget}?widget={id}` | modules/widget/entities/Expressions/ExpressionEntity.ts | +| PUT | `{hosts.expression2widget}{body.id}/` | modules/widget/entities/Expressions/ExpressionEntity.ts | +| DELETE | `{hosts.expression2widget}{id}/` | modules/widget/entities/Expressions/ExpressionEntity.ts | +| GET | `{hosts}aggregates/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| POST | `{hosts}{body.dashboboardId}/copy/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| PUT | `{hosts}{body.id}/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| DELETE | `{hosts}{id}/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts | +| GET | `{host}api/tracking/meta/contractor/` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}api/tracking/meta/vehicle/` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}api/tracking/meta/vehicletracker/` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}api/tracking/meta/zones/` | modules/vehicle-tracking/methods/inner/api.ts | +| POST | `{host}api/tracking/meta/zones/` | modules/vehicle-tracking/methods/inner/api.ts | +| PUT | `{host}api/tracking/meta/zones/{id}` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}api/tracking/stats-zoned/?{queryParams.toString()}` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}buffer/` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{host}entries-log/?{queryParams.toString()}` | modules/vehicle-tracking/methods/inner/api.ts | +| GET | `{panoramasEndpoint}?mission={mission}` | src/Model/Media/api.ts | +| PATCH | `{panoramasEndpoint}{panoramaId}` | src/Model/Media/api.ts | +| GET | `{photosEndpoint}?mission={mission}` | src/Model/Media/api.ts | +| PATCH | `{photosEndpoint}{photoId}/` | src/Model/Media/api.ts | +| GET | `{this.pathname}?{searchParams.toString()}` | modules/targets/entities/Target/TargetRESTEntity.ts | +| PATCH | `{this.pathname}{params.id}/` | modules/missions/entity/Mission/MissionRESTEntity.ts | +| DELETE | `{this.pathname}{params.id}/` | modules/missions/entity/Mission/MissionRESTEntity.ts | +| GET | `{videosEndpoint}?mission={mission}` | src/Model/Media/api.ts | +| PATCH | `{videosEndpoint}{videoId}` | src/Model/Media/api.ts | + +## `sarexApi` — `https://api.sarex.io` (stage `https://stage-api.sarex.io`) + +Уникальных запросов: 23 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `(динамический URL — передаётся переменной)` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+3) | +| GET | `/flows/api/v1/{entity}/tasks-count/` | src/Model/actions.js | +| GET | `/transmittals/api/v1/transmittals/count` | src/Model/actions.js | +| GET | `pulse/api/core/check_admin/` | src/Model/Pulse/api.ts | +| GET | `pulse/api/core/user/` | src/Model/Pulse/api.ts | +| GET | `{baseUrl}/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts (+1) | +| POST | `{baseUrl}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+2) | +| DELETE | `{baseUrl}/bulk_delete/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) | +| GET | `{baseUrl}/check_new/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts | +| GET | `{baseUrl}/last-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts | +| POST | `{baseUrl}/mark-all-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts | +| POST | `{baseUrl}/mark-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts | +| GET | `{baseUrl}/unread-count` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts | +| POST | `{baseUrl}/upload` | modules/sarexPulse/SarexPulse/shared/api/mediaApi.ts | +| POST | `{baseUrl}/viewed/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts (+1) | +| GET | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) | +| PUT | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) | +| DELETE | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+2) | +| POST | `{baseUrl}/{id}/close/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts | +| POST | `{baseUrl}/{id}/dislike/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts | +| POST | `{baseUrl}/{id}/like/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts | +| POST | `{baseUrl}/{id}/publish/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) | +| POST | `{baseUrl}/{id}/unpublish/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) | + +## `bim` — `https://api.sarex.io/bim` + +Уникальных запросов: 17 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| PATCH | `/api/v1/bims/{bimId}` | src/Model/BIM/api.js | +| POST | `/api/v1/bims/{bimId}/archive` | src/Model/BIM/api.js | +| GET | `/api/v1/bims/{bimId}/changes` | src/Model/BIM/api.js | +| POST | `/api/v1/bims/{bimId}/changes` | src/Model/BIM/api.js | +| GET | `/api/v1/bims/{bimId}/deviations` | src/Model/BIM/api.js | +| GET | `/api/v1/bims/{bimId}/elements` | src/Model/BIM/api.js | +| GET | `/api/v1/bims/{bimId}/sarexid/{sarexIds}` | src/Model/BIM/api.js | +| POST | `/api/v1/bims/{bimId}/unarchive` | src/Model/BIM/api.js | +| POST | `/api/v1/bims/{srcBIMId}/merge/{dstBIMId}` | src/Model/BIM/api.js | +| POST | `/api/v1/changes` | src/Model/BIM/api.js | +| POST | `/api/v1/comparisons` | src/Model/BIM/api.js | +| GET | `/api/v1/elements/{elementId}/properties` | src/Model/BIM/api.js | +| PUT | `/api/v1/elements/{elementId}/properties` | src/Model/BIM/api.js | +| GET | `/api/v1/elements/{elementId}/stats` | src/Model/BIM/api.js | +| POST | `/api/v1/targets/{targetId}/bims` | src/Model/BIM/api.js | +| GET | `/api/v1/targets/{targetId}/bims?archived=1` | src/Model/BIM/api.js | +| POST | `/api/v2/deviations` | src/Model/BIM/api.js | + +## `workflows` — `https://api.sarex.io/workflows` + +Уникальных запросов: 2 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `/api/v1/workflows/{comparison.c2c_workflow},{comparison.t2t_workflow}/state` | src/Components/Viewer/Panels/ComparisonsPanel/components/MissionComparison/MissionComparison.tsx | +| GET | `/api/v1/workflows/{workflowId}` | src/Components/Viewer/Panels/FilesPanel/hooks/useGetWorkflowStatus.ts | + +## `workspaces` — `https://api.sarex.io/workspaces` + +Уникальных запросов: 1 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `/api/v1/workspaces/{workspaceID}` | modules/dashboards/view-model/WorkspacePreviewSelectorViewModel/WorkspacePreviewSelectorViewModel.ts | + +## `gateway` — `https://api.sarex.io/gateway` + +Уникальных запросов: 1 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `/api/v1/resources` | src/Model/actions.js | + +## `comparisons` — `https://api.sarex.io/comparisons` + +Уникальных запросов: 1 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `/{id}/` | src/Components/Viewer/Panels/LayersPanel/store/comparisonsLayers.ts | + +## `notifications` — `https://api.sarex.io/lambdas/notification` + +Уникальных запросов: 1 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| POST | `(динамический URL — передаётся переменной)` | modules/notifications/email-notification/email-notifier.ts | + +## `processes` (fetch) — `https://api.sarex.io/flows/api/v1` + +Уникальных запросов: 32 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| POST | `/documents/` | modules/reviews/api/index.ts | +| GET | `/documents/?full=true{document_ids ? `&document_ids={document_ids}` :` | modules/reviews/api/index.ts | +| PATCH | `/documents/set-status/?document_ids={ids.join(",")}` | modules/reviews/api/index.ts | +| PUT | `/documents/{id}/` | modules/reviews/api/index.ts | +| POST | `/flows/` | modules/processes/store/api/index.ts | +| GET | `/flows/count_flows_by_resource/?{params}` | modules/processes/store/api/index.ts | +| PUT | `/flows/{body.id}/?full=true` | modules/processes/store/api/index.ts | +| POST | `/flows/{id}/copy/?full=true` | modules/processes/store/api/index.ts | +| POST | `/reviewers/` | modules/processes/store/api/index.ts | +| PUT | `/reviewers/{body.id}/` | modules/processes/store/api/index.ts | +| DELETE | `/reviewers/{id}/` | modules/processes/store/api/index.ts | +| POST | `/reviews/` | modules/reviews/api/index.ts | +| GET | `/reviews/count_by_resource_id/?{params}` | modules/reviews/api/index.ts | +| GET | `/reviews/count_by_reviewer_id/?current_reviewers={query}` | modules/reviews/api/index.ts | +| PUT | `/reviews/{body.id}/` | modules/reviews/api/index.ts | +| DELETE | `/reviews/{id}/` | modules/reviews/api/index.ts | +| PATCH | `/reviews/{id}/approve/` | modules/reviews/api/index.ts | +| GET | `/reviews/{id}/documents/` | modules/reviews/api/index.ts | +| PATCH | `/reviews/{id}/update-bundles/` | modules/reviews/api/index.ts | +| PATCH | `/reviews/{reviewId}/change_reviewers/` | modules/reviews/api/index.ts | +| PUT | `/reviews/{reviewId}/documents/` | modules/reviews/api/index.ts | +| POST | `/statuses/` | modules/processes/store/api/index.ts | +| PUT | `/statuses/{body.id}/` | modules/processes/store/api/index.ts | +| DELETE | `/statuses/{id}/` | modules/processes/store/api/index.ts | +| POST | `/steps/` | modules/processes/store/api/index.ts | +| PUT | `/steps/{body.id}/?full=true` | modules/processes/store/api/index.ts | +| GET | `/steps/{stepId}/active_reviews/` | modules/processes/store/api/index.ts | +| GET | `/steps/{stepId}/get_reviewers/?review_id={reviewId}` | modules/reviews/api/index.ts | +| PATCH | `/steps/{stepId}/update_reviewers/` | modules/processes/store/api/index.ts | +| GET | `/tasks/reviewers-max-end-dates/?{query}` | modules/reviews/api/index.ts | +| PATCH | `/tasks/{id}/change-duration/` | modules/reviews/api/index.ts | +| PATCH | `/tasks/{id}/change-priority/` | modules/reviews/api/index.ts | + +## `orchestrator` (fetch) — `https://api.sarex.io/orchestrator/api` + +Уникальных запросов: 3 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| POST | `/process` | modules/reviews/api/marks.ts | +| GET | `/process/{id}` | modules/reviews/api/marks.ts | +| POST | `/sign` | modules/reviews/api/marks.ts | + +## `automation` (fetch) — `https://api.sarex.io/automation/api/v1` + +Уникальных запросов: 1 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| POST | `/automations` | modules/automation/store/automation.ts | + +## `analytics-v2` (RTK Query) — `https://api.sarex.io/analytics-v2/api/v1` + +Уникальных запросов: 19 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `/api/commons/wiki.get` | src/Components/Wiki/apiWiki/api.ts | +| POST | `/api/commons/wiki.media.save` | src/Components/Wiki/apiWiki/api.ts | +| POST | `/api/commons/wiki.update` | src/Components/Wiki/apiWiki/api.ts | +| GET | `/api/core/media-notes-attachments/` | src/Model/MediaNotes/api.ts | +| POST | `/api/core/media-notes-attachments/` | src/Model/MediaNotes/api.ts | +| DELETE | `/api/core/media-notes-attachments/{attachmentId}` | src/Model/MediaNotes/api.ts | +| GET | `/api/core/media-notes-comments/` | src/Model/MediaNotes/api.ts | +| POST | `/api/core/media-notes-comments/` | src/Model/MediaNotes/api.ts | +| DELETE | `/api/core/media-notes-comments/{body.commentId}/` | src/Model/MediaNotes/api.ts | +| GET | `/api/core/media-notes/` | src/Model/MediaNotes/api.ts | +| POST | `/api/core/media-notes/` | src/Model/MediaNotes/api.ts | +| PATCH | `/api/core/media-notes/{body.id}/` | src/Model/MediaNotes/api.ts | +| DELETE | `/api/core/media-notes/{params.noteId}/` | src/Model/MediaNotes/api.ts | +| POST | `/api/core/targets/v2/{target_id}/calculate_panoramas_relative_orientation/` | src/Components/MediaViewer/MediaPanorama/panoramaApi.ts | +| GET | `/api/core/targets/{target}/missions/` | src/Components/Viewer/Panels/Media/MediaApi/missionsApi.ts | +| GET | `/api/notifications/?page={page}` | src/Model/AeroNoty/api.ts | +| POST | `/api/notifications/update/` | src/Model/AeroNoty/api.ts | +| GET | `/workflows/get_items_by_generic_key/?model=target&object_id={target_id}` | src/Components/MediaViewer/MediaPanorama/panoramaApi.ts | +| GET | `wfs` | src/Model/GIS/api.ts | + +## Прочее (static-конфиг, внешние сервисы, wrappers с динамическим URL) + +Уникальных запросов: 13 + +| Метод | Путь | Файл (источник) | +| --- | --- | --- | +| GET | `(динамический URL — передаётся переменной)` | src/Components/Viewer/Panels/Media/MediaRedux.tsx | +| GET | `/static/config.json` | src/shared/config/config.ts | +| GET | `/tasks/?` + params.toString()` | modules/reviews/api/index.ts | +| GET | `file.attachment` | src/Components/Viewer/Panels/MeasurementDetails/MediaFiles_old.jsx | +| GET | `http://localhost:8000{path}` | src/Model/fakeServer/fakeServer.js | +| GET | `https://blooming-cove-51473.herokuapp.com/track/list` | src/Model/Store/trackingStore.js | +| GET | `https://blooming-cove-51473.herokuapp.com/track/notifications` | src/Model/Store/trackingStore.js | +| GET | `image` | src/Components/Viewer/Services/States/Stores/ViewerStates.ts | +| GET | `link` | src/Model/actions.js | +| GET | `url` | modules/account/ui/StorageCard/lib/file-utils.ts (+2) | +| POST | `url` | modules/reviews/api/index.ts | +| PUT | `url` | modules/reviews/api/index.ts | +| PATCH | `url` | modules/reviews/api/index.ts | \ No newline at end of file diff --git a/apps/django/openapi.yaml b/apps/django/openapi.yaml new file mode 100644 index 0000000..0d34553 --- /dev/null +++ b/apps/django/openapi.yaml @@ -0,0 +1,589 @@ +openapi: 3.0.3 + +info: + title: Sarex Backend API + version: "1.1.12" + description: | + REST API сервиса **sarex-backend** — монолитное Django-приложение (проект + `config`, бизнес-логика в пакете `sarex`) на Django REST Framework. Отдаётся + через uWSGI (`config.wsgi:application`) на порту `8000`. + + Документ описывает основную поверхность публичного API под префиксом `/api/` + и внутреннего API под префиксом `/internal/`. Маршрутизация собирается в + `config/urls.py` и включаемых `sarex/*/api/urls.py`. Многие ресурсы + зарегистрированы через DRF-роутеры (`SimpleRouter`/`DefaultRouter`), поэтому + поддерживают стандартный набор действий (list/create/retrieve/update/ + partial_update/destroy). Конкретные схемы запросов/ответов в коде не + объявлены декларативно (используется `rest_framework.schemas.coreapi.AutoSchema`), + поэтому тела здесь описаны обобщённо. + + ### Аутентификация + Большинство эндпоинтов требуют аутентификации (DRF + `DEFAULT_PERMISSION_CLASSES = [IsAuthenticated]`). Поддерживаются несколько + механизмов (`DEFAULT_AUTHENTICATION_CLASSES`): Zitadel JWT, SimpleJWT + (`Authorization: Bearer `, алгоритм `RS512`), Basic, Session, + RemoteUser. Токены выпускаются эндпоинтами `/api/token…`. + + ### Пагинация + По умолчанию используется `LimitOffsetPagination` (`PAGE_SIZE = 1000`). + Списочные ответы содержат `count`, `next`, `previous`, `results`. + + ### Замечание о полноте + Перечислены основные маршруты. Часть включаемых подмодулей + (`sarex/pg/api/*`, `sarex/mar/*`, `sarex/base/api/commons`) представлена + группами; детальные под-пути см. в соответствующих `urls.py`. + +servers: + - url: https://api.sarex.io + description: prod + - url: https://stage-api.sarex.io + description: stage + - url: / + description: contour (относительные пути) + +security: + - bearerAuth: [] + +tags: + - name: auth + description: Аутентификация и JWT-токены + - name: base + description: Уведомления, workflows, модули, health + - name: core + description: Пользователи, компании, цели, миссии, медиа + - name: client + description: Клиентский дашборд и self-сервис + - name: analytics + description: Дашборды, метрики, виджеты + - name: map + description: Кадастр и заметки на карте + - name: pg + description: Облака точек, экспорт, измерения + - name: internal + description: Внутрикластерные вызовы + - name: system + description: Метрики и служебные эндпоинты + +paths: + + # --------------------------------------------------------------------------- + # Auth / tokens + # --------------------------------------------------------------------------- + /api/login/: + post: + tags: [auth] + summary: Вход пользователя (сессия) + security: [] + responses: + "200": { $ref: "#/components/responses/Ok" } + "401": { $ref: "#/components/responses/Unauthorized" } + /api/logout/: + post: + tags: [auth] + summary: Выход пользователя + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/app-settings/: + get: + tags: [auth] + summary: Настройки приложения + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/token/: + post: + tags: [auth] + summary: Получить пару access/refresh токенов + security: [] + requestBody: { $ref: "#/components/requestBodies/Generic" } + responses: + "200": { $ref: "#/components/responses/TokenPair" } + "401": { $ref: "#/components/responses/Unauthorized" } + /api/token/me: + post: + tags: [auth] + summary: Токен для текущего пользователя + responses: + "200": { $ref: "#/components/responses/TokenPair" } + /api/token/user/{pk}/: + post: + tags: [auth] + summary: Токен для пользователя по id (из админки) + parameters: [ { $ref: "#/components/parameters/PkPath" } ] + responses: + "200": { $ref: "#/components/responses/TokenPair" } + /api/token/jwks: + get: + tags: [auth] + summary: JWKS (набор публичных ключей) + security: [] + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/token/refresh/: + post: + tags: [auth] + summary: Обновить access-токен (ротация) + security: [] + requestBody: { $ref: "#/components/requestBodies/Generic" } + responses: + "200": { $ref: "#/components/responses/TokenPair" } + /api/token/public/: + get: + tags: [auth] + summary: Публичный ключ проверки JWT + security: [] + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/auth/obtain/: + post: + tags: [auth] + summary: Получить refresh-токен + security: [] + requestBody: { $ref: "#/components/requestBodies/Generic" } + responses: + "200": { $ref: "#/components/responses/TokenPair" } + /api/auth/refresh/: + post: + tags: [auth] + summary: Обновить access-токен + security: [] + requestBody: { $ref: "#/components/requestBodies/Generic" } + responses: + "200": { $ref: "#/components/responses/TokenPair" } + + # --------------------------------------------------------------------------- + # System + # --------------------------------------------------------------------------- + /metrics: + get: + tags: [system] + summary: Метрики Prometheus + security: [] + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/health/: + get: + tags: [base] + summary: Health check + security: [] + responses: + "200": { $ref: "#/components/responses/Ok" } + + # --------------------------------------------------------------------------- + # Base + # --------------------------------------------------------------------------- + /api/modules/: + get: + tags: [base] + summary: Список доступных модулей + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/update/notifications/: + post: + tags: [base] + summary: Массовое обновление уведомлений + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/workflows/: + get: + tags: [base] + summary: Список workflow + responses: + "200": { $ref: "#/components/responses/List" } + /api/workflows/{id}/: + parameters: [ { $ref: "#/components/parameters/IdPath" } ] + get: + tags: [base] + summary: Workflow по id + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/notifications/: + get: + tags: [base] + summary: Список уведомлений + responses: + "200": { $ref: "#/components/responses/List" } + /api/notifications/{id}/: + parameters: [ { $ref: "#/components/parameters/IdPath" } ] + get: + tags: [base] + summary: Уведомление по id + responses: + "200": { $ref: "#/components/responses/Ok" } + /api/usernotifications/: + get: + tags: [base] + summary: Пользовательские уведомления + responses: + "200": { $ref: "#/components/responses/List" } + /api/commons/cs/: + get: + tags: [base] + summary: Справочник систем координат + responses: + "200": { $ref: "#/components/responses/List" } + + # --------------------------------------------------------------------------- + # Core — ресурсы DRF-роутера (CRUD) + # --------------------------------------------------------------------------- + /api/core/users/: + get: + tags: [core] + summary: Список пользователей + responses: { "200": { $ref: "#/components/responses/List" } } + post: + tags: [core] + summary: Создать пользователя + requestBody: { $ref: "#/components/requestBodies/Generic" } + responses: { "201": { $ref: "#/components/responses/Ok" } } + /api/core/users/{id}/: + parameters: [ { $ref: "#/components/parameters/IdPath" } ] + get: { tags: [core], summary: Пользователь по id, responses: { "200": { $ref: "#/components/responses/Ok" } } } + put: { tags: [core], summary: Обновить пользователя, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "200": { $ref: "#/components/responses/Ok" } } } + patch: { tags: [core], summary: Частично обновить пользователя, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "200": { $ref: "#/components/responses/Ok" } } } + delete: { tags: [core], summary: Удалить пользователя, responses: { "204": { description: No Content } } } + /api/core/users/introspect: + get: + tags: [core] + summary: Интроспекция текущего пользователя + responses: { "200": { $ref: "#/components/responses/Ok" } } + /api/core/users/{pk}/introspect: + parameters: [ { $ref: "#/components/parameters/PkPath" } ] + get: + tags: [core] + summary: Интроспекция пользователя (админ) + responses: { "200": { $ref: "#/components/responses/Ok" } } + /api/core/users/bulk/notifications/: + post: + tags: [core] + summary: Массовое обновление уведомлений пользователей + responses: { "200": { $ref: "#/components/responses/Ok" } } + /api/core/users_by_sa/: + get: + tags: [core] + summary: Пользователи по сервисному аккаунту + responses: { "200": { $ref: "#/components/responses/List" } } + /api/core/v2/users/: + get: + tags: [core] + summary: Упрощённый список пользователей (v2) + responses: { "200": { $ref: "#/components/responses/List" } } + /api/core/v3/users/: + get: + tags: [core] + summary: Оптимизированный список пользователей (v3) + responses: { "200": { $ref: "#/components/responses/List" } } + /api/core/companies/: + get: { tags: [core], summary: Список компаний, responses: { "200": { $ref: "#/components/responses/List" } } } + post: { tags: [core], summary: Создать компанию, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/companies/{id}/: + parameters: [ { $ref: "#/components/parameters/IdPath" } ] + get: { tags: [core], summary: Компания по id, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/surfaces/: + get: { tags: [core], summary: Список поверхностей, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/mesh/: + get: { tags: [core], summary: Список mesh, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/multiple-surface/: + get: { tags: [core], summary: Множественные поверхности, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/state/: + get: { tags: [core], summary: Состояние приложения, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/polygons/: + get: { tags: [core], summary: Полигоны, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/media-notes/: + get: { tags: [core], summary: Медиа-заметки, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/media-notes-comments/: + get: { tags: [core], summary: Комментарии медиа-заметок, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/media-notes-attachments/: + get: { tags: [core], summary: Вложения медиа-заметок, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/media-folders/: + get: { tags: [core], summary: Папки медиа, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/target-links/: + get: { tags: [core], summary: Ссылки на цели, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/materials/: + get: { tags: [core], summary: Материалы, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/pointclouds/: + get: { tags: [core], summary: Облака точек, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/orthophotos/: + get: { tags: [core], summary: Ортофотопланы, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/videos/: + get: { tags: [core], summary: Видео, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/photos/: + get: { tags: [core], summary: Фото, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/panoramas/: + get: { tags: [core], summary: Панорамы, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/color-presets/: + get: { tags: [core], summary: Цветовые пресеты компании, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/missions/: + get: { tags: [core], summary: Список миссий, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/missions/import/: + get: { tags: [core], summary: Задачи импорта миссий, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/missions/import/create/: + post: { tags: [core], summary: Создать задачу импорта миссии, responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/targets/: + get: { tags: [core], summary: Список целей, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/targets/v2/: + get: { tags: [core], summary: Список целей (v2), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/targets/v3/: + get: { tags: [core], summary: Список целей (v3), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/targets/tree/: + get: { tags: [core], summary: Дерево целей, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/targets/pdm/: + get: { tags: [core], summary: Цели PDM, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/targets/{id}/missions/: + parameters: [ { $ref: "#/components/parameters/IdPath" } ] + get: { tags: [core], summary: Миссии цели, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/export/: + get: { tags: [core], summary: Экспорт данных, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/export/pointcloud/: + post: { tags: [core], summary: Экспорт региона из облака точек, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/contour/export/: + post: { tags: [core], summary: Экспорт контура, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/contour/exports/: + get: { tags: [core], summary: Список экспортов контура, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/uploads/streaming/: + post: { tags: [core], summary: Потоковая загрузка (создание), responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/uploads/streaming/{pk}/: + parameters: [ { $ref: "#/components/parameters/PkStrPath" } ] + put: { tags: [core], summary: Догрузка чанка, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/storage-cleanup/: + post: { tags: [core], summary: Очистка хранилища, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/celery/stats: + get: { tags: [core], summary: Статистика Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/celery/active: + get: { tags: [core], summary: Активные задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/celery/scheduled: + get: { tags: [core], summary: Запланированные задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/celery/{uuid}/details/: + parameters: [ { name: uuid, in: path, required: true, schema: { type: string } } ] + get: { tags: [core], summary: Детали задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/admin/users/: + get: { tags: [core], summary: Админ — пользователи, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/groups/: + get: { tags: [core], summary: Админ — группы, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/companies/: + get: { tags: [core], summary: Админ — компании, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/positions/: + get: { tags: [core], summary: Админ — должности, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/departments/: + get: { tags: [core], summary: Админ — подразделения, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/contractors/: + get: { tags: [core], summary: Админ — подрядчики, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/admin/permissions/: + get: { tags: [core], summary: Админ — права, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/service_accounts/: + get: { tags: [core], summary: Сервисные аккаунты, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/service-accounts/personalized/: + get: { tags: [core], summary: Персонализированный сервисный аккаунт, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/service-accounts/user/{user_id}/: + parameters: [ { name: user_id, in: path, required: true, schema: { type: integer } } ] + get: { tags: [core], summary: Сервисный аккаунт пользователя (v2), responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/core/c2s-comparisons/: + post: { tags: [core], summary: Создать c2s-сравнение, responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/permissions/: + get: { tags: [core], summary: Права (только чтение), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/units/: + get: { tags: [core], summary: Единицы измерения, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/upload/media-file/: + post: { tags: [core], summary: Загрузка медиа-файла, responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/mrpa/list/: + get: { tags: [core], summary: Список MRPA, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/core/mrpa/: + post: { tags: [core], summary: Создать MRPA, responses: { "201": { $ref: "#/components/responses/Ok" } } } + /api/core/mrpa/{pk}/: + parameters: [ { name: pk, in: path, required: true, schema: { type: string, format: uuid } } ] + get: { tags: [core], summary: MRPA по id, responses: { "200": { $ref: "#/components/responses/Ok" } } } + + # --------------------------------------------------------------------------- + # Client + # --------------------------------------------------------------------------- + /api/client/dashboard/targets/: + get: { tags: [client], summary: Дашборд — цели, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/client/dashboard/missions/: + get: { tags: [client], summary: Дашборд — миссии, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/client/dashboard/webcams/: + get: { tags: [client], summary: Дашборд — веб-камеры, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/client/settings/: + get: { tags: [client], summary: Настройки пользователя, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/client/settings/user/{user_id}/: + parameters: [ { name: user_id, in: path, required: true, schema: { type: integer } } ] + get: { tags: [client], summary: Настройки пользователя по id, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/client/self/: + get: { tags: [client], summary: Информация о себе, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/client/self/set_password/: + post: { tags: [client], summary: Смена пароля, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/client/uploads/: + get: { tags: [client], summary: Загрузки клиента, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/client/folders/: + get: { tags: [client], summary: Папки загрузок клиента, responses: { "200": { $ref: "#/components/responses/List" } } } + + # --------------------------------------------------------------------------- + # Analytics + # --------------------------------------------------------------------------- + /api/analytics/dashboards/: + get: { tags: [analytics], summary: Дашборды, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/widgets/: + get: { tags: [analytics], summary: Виджеты, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/metrics/: + get: { tags: [analytics], summary: Метрики, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/expressions/: + get: { tags: [analytics], summary: Выражения, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/folders/: + get: { tags: [analytics], summary: Папки метрик, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/filters/: + get: { tags: [analytics], summary: Фильтры, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/groups/: + get: { tags: [analytics], summary: Группы, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/values/: + get: { tags: [analytics], summary: Значения метрик, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/attributes/: + get: { tags: [analytics], summary: Атрибуты, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/attachments/: + get: { tags: [analytics], summary: Вложения дашбордов, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/analytics/reviews-service-feed/: + post: { tags: [analytics], summary: Приём событий из сервиса reviews, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/analytics/remarks-service-feed/: + post: { tags: [analytics], summary: Приём событий из сервиса remarks, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/analytics/tracking-service-feed/: + post: { tags: [analytics], summary: Приём событий трекинга, responses: { "200": { $ref: "#/components/responses/Ok" } } } + + # --------------------------------------------------------------------------- + # Map + # --------------------------------------------------------------------------- + /api/map/cadastre/point/: + get: { tags: [map], summary: Данные кадастра по точке, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/map/cadastre/export/: + post: { tags: [map], summary: Экспорт кадастровых данных, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/map/cadastre/wikimapia/redirect/: + get: { tags: [map], summary: Редирект Wikimapia, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/map/notes/: + get: { tags: [map], summary: Заметки на ортофото, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/map/notes/folders/: + get: { tags: [map], summary: Папки заметок на ортофото, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/ds/telecom/cadastre/: + get: { tags: [map], summary: Изображение кадастра (telecom), responses: { "200": { $ref: "#/components/responses/Ok" } } } + + # --------------------------------------------------------------------------- + # PG (облака точек / экспорт / измерения) + # --------------------------------------------------------------------------- + /api/pg/pointclouds/: + get: { tags: [pg], summary: Облака точек, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/pointclouds/{pointcloud}/measurements/: + parameters: [ { name: pointcloud, in: path, required: true, schema: { type: string } } ] + get: { tags: [pg], summary: Измерения облака точек, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/volume-dynamic/: + get: { tags: [pg], summary: Динамика объёмов, responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/pg/orthomosaicexport/: + get: { tags: [pg], summary: Экспорт ортомозаики (кастомный), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/exports/pointcloud/: + get: { tags: [pg], summary: Экспорты облаков точек, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/exports/orthomosaic/: + get: { tags: [pg], summary: Экспорты ортомозаики, responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/measurements/: + get: { tags: [pg], summary: Измерения (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/attachments/: + get: { tags: [pg], summary: Вложения (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/compare/: + get: { tags: [pg], summary: Сравнение (подмодуль), responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/pg/export/: + get: { tags: [pg], summary: Экспорт (подмодуль), responses: { "200": { $ref: "#/components/responses/Ok" } } } + /api/pg/pdf-overlays/: + get: { tags: [pg], summary: PDF-оверлеи (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } } + /api/pg/projects/: + get: { tags: [pg], summary: Проекты Metashape (при SERVER_USE_METASHAPE), responses: { "200": { $ref: "#/components/responses/List" } } } + + # --------------------------------------------------------------------------- + # Internal + # --------------------------------------------------------------------------- + /internal/client/settings/{pk}/: + parameters: [ { $ref: "#/components/parameters/PkPath" } ] + get: + tags: [internal] + summary: Внутренние настройки клиента + responses: { "200": { $ref: "#/components/responses/Ok" } } + /internal/client/token/{pk}/: + parameters: [ { $ref: "#/components/parameters/PkPath" } ] + get: + tags: [internal] + summary: Внутренний токен клиента + responses: { "200": { $ref: "#/components/responses/TokenPair" } } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + parameters: + IdPath: + name: id + in: path + required: true + schema: { type: integer } + PkPath: + name: pk + in: path + required: true + schema: { type: integer } + PkStrPath: + name: pk + in: path + required: true + schema: { type: string } + + requestBodies: + Generic: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + + responses: + Ok: + description: Успешный ответ + content: + application/json: + schema: + type: object + additionalProperties: true + List: + description: Списочный ответ с пагинацией (LimitOffset) + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedList" + TokenPair: + description: Пара токенов + content: + application/json: + schema: + $ref: "#/components/schemas/TokenPair" + Unauthorized: + description: Не аутентифицирован + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + schemas: + PaginatedList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true, format: uri } + previous: { type: string, nullable: true, format: uri } + results: + type: array + items: + type: object + additionalProperties: true + TokenPair: + type: object + properties: + access: { type: string } + refresh: { type: string } + Error: + type: object + properties: + detail: { type: string } diff --git a/apps/document-link/.env.example b/apps/document-link/.env.example new file mode 100644 index 0000000..2b08905 --- /dev/null +++ b/apps/document-link/.env.example @@ -0,0 +1,16 @@ +# document-link-frontend (Next.js) +# +# Внимание: текущий код фронтенда НЕ читает переменные окружения напрямую +# (в src/ нет обращений к process.env / NEXT_PUBLIC_*). Базовый URL API +# выбирается по window.location.hostname, а JWT зашит в коде (см. CONFIGURATION.md). +# Перечисленные ниже переменные — это конфигурационная поверхность, объявленная +# в Helm (.helm/values.yaml), и их следует использовать вместо хардкода. + +# Публичный базовый хост API (сервис documentations), без схемы. +# Итоговый запрос: https:///documentations/api/v1/public/documents/public_link/ +# stage: stage-api.sarex.io, production: api.sarex.io +NEXT_PUBLIC_API_BASE_URL=stage-api.sarex.io + +# JWT сервисного аккаунта для запроса публичной ссылки (Authorization: Bearer ). +# В инфраструктуре берётся из секрета documentations-publiclink-jwt-secret (ключ jwt). +NEXT_PUBLIC_API_TOKEN= diff --git a/apps/document-link/CONFIGURATION.md b/apps/document-link/CONFIGURATION.md new file mode 100644 index 0000000..6a6ea7d --- /dev/null +++ b/apps/document-link/CONFIGURATION.md @@ -0,0 +1,95 @@ +# Конфигурация проекта document-link (document-link-frontend) + +Документ описывает способы конфигурирования и все переменные окружения сервиса публичных ссылок на документы. + +## Что это за сервис + +`document-link-frontend` — микрофронтенд на **Next.js 13** (App Router, `src/app`), отдающий публичную страницу-карточку документа по ссылке вида `https://document-link..sarex.io/`. По `uuid` фронтенд запрашивает метаданные документа у сервиса `documentations` и показывает название, автора, версию, размер, срок действия ссылки и кнопку скачивания (см. `ENDPOINTS.md`). Собственного бэкенда у сервиса нет — это чистый фронтенд, поэтому файлы вида `openapi.yaml` для него неприменимы. + +## Способы конфигурирования + +В отличие от бэкенд-сервисов, у фронтенда **нет разбора переменных окружения в коде**. На текущий момент: + +- Базовый хост API выбирается в рантайме по `window.location.hostname` в `src/app/[uuid]/components/modal.tsx` (жёстко заданный `switch`), а не из переменной окружения; +- JWT для авторизации запроса **зашит в коде** (константа `fixedToken` в том же файле) — временное решение; +- Обращений к `process.env` / `NEXT_PUBLIC_*` в `src/` нет. + +При этом конфигурационная поверхность **объявлена в инфраструктуре** (Helm-чарт репозитория и CI) в виде переменных `NEXT_PUBLIC_*` — их предполагается использовать вместо хардкода. Ниже описаны и код, и инфраструктурные объявления. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся значения | +| --- | --- | +| Локально (`npm run dev`) | Значения зашиты в коде; `apiBaseUrl` для `localhost` → `stage-api.sarex.io` | +| Локально (Docker / `docker-compose`) | `Dockerfile` (`node:18-alpine`, `next build`, `ENTRYPOINT npm start`, порт `3000`); `docker-compose.yaml` пробрасывает `8000:3000` | +| Kubernetes — репозиторный чарт | `.helm/values.yaml`: блок `services.frontend.envs` и `secretEnvs` (см. ниже) | +| Kubernetes — infra (этот репозиторий) | `apps/document-link/base` (kustomize) и `apps/document-link/{brusnika-stage,brusnika-prod}` (FluxCD `HelmRelease` поверх `universal-chart`) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: `generic/common-ci` (`universal-pipeline`, ref `apps-business`), `SERVICE_NAME=document-link` | + +## Переменные приложения + +Объявлены в `.helm/values.yaml` исходного репозитория. **Важно:** текущий код фронтенда их не читает (см. раздел «Замечания»). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `NEXT_PUBLIC_API_BASE_URL` | string | `stage-api.sarex.io` (stage), `api.sarex.io` (production) | Базовый хост API (сервис `documentations`), без схемы | +| `NEXT_PUBLIC_API_TOKEN` | string (secret) | — | JWT сервисного аккаунта для `Authorization: Bearer `. Берётся из секрета `documentations-publiclink-jwt-secret` (ключ `jwt`) | + +## Базовые хосты API по окружениям + +Логика `src/app/[uuid]/components/modal.tsx` (`switch` по `window.location.hostname`): + +| Hostname фронтенда | `apiBaseUrl` | +| --- | --- | +| `localhost` | `stage-api.sarex.io` | +| `document-link.stage.sarex.io` | `stage-api.sarex.io` | +| `document-link.sarex.io` | `api.sarex.io` | +| прочее | не определён (ошибка в консоль, fallback `stage-api.sarex.io`) | + +## Сборка и контейнер + +| Параметр | Значение | Где задано | +| --- | --- | --- | +| Базовый образ | `node:18-alpine` | `Dockerfile` | +| Команда сборки | `npm i` → `npm run build` (`next build`) | `Dockerfile` | +| Entrypoint | `npm start` (`next start`) | `Dockerfile` | +| Порт приложения | `3000` | `Dockerfile` (`EXPOSE 3000`), `.helm/values.yaml` (`port._default: 3000`) | +| Образ (registry) | `cr.yandex/crp3ccidau046kdj8g9q/document-link-frontend` | `.helm/values.yaml`, infra `HelmRelease` | + +## Деплой (infra: `apps/document-link`) + +| Оверлей | Механизм | Особенности | +| --- | --- | --- | +| `base` | kustomize (`Deployment` + `Service` + `Namespace`) | namespace `document-link` c `istio-injection: enabled`; `Deployment/frontend` | +| `brusnika-stage` | FluxCD `HelmRelease` → `universal-chart` `0.1.7` | `replicaCount`: stage `1`; probes выключены | +| `brusnika-prod` | FluxCD `HelmRelease` → `universal-chart` `0.1.7` | `replicaCount`: preprod/production `3`; probes выключены | +| `yc-k8s-test` | kustomize (`../base`) | тестовый контур | + +Порты сервиса: в `universal-chart` — `service.port 8080` → `targetPort 3000` (`portName: http`); в `base/service.yaml` — `port 80` → `targetPort 80`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Ключевые переменные: `SERVICE_NAME=document-link`, `DOCKERFILE_PATH=./Dockerfile`, `CI_TRIGGER_SOURCE=app`. Окружение переключается по ветке/тегу: + +| Условие | STAND | Namespace | Chart | +| --- | --- | --- | --- | +| `merge_request_event` | — | — | `ENABLE_BUILD_IMAGE=false` (только проверки) | +| ветка `stage` | `stage` | `documentations` | `document-link` `0.1.7`, `--build-arg ENV=stage` | +| ветка `master` | `preprod` | `document-link-preprod` | `document-link` `0.1.7`, `--build-arg ENV=preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `document-link-prod` | `document-link` `0.1.7`, `--build-arg ENV=prod` | + +`HELM_SET_ARGS` пробрасывает в `universal-chart` образ (`services.frontend.image.name.`), `global.env`, а также `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner`. + +## Замечания и потенциальные проблемы + +- **Env-переменные не используются кодом.** `NEXT_PUBLIC_API_BASE_URL` и `NEXT_PUBLIC_API_TOKEN` объявлены в `.helm/values.yaml`, но `src/` их не читает: базовый хост берётся из `switch` по `hostname`, а токен зашит константой `fixedToken`. Для корректной работы на разных стендах логику стоит перевести на `process.env.NEXT_PUBLIC_*` (и тогда `.env.example` станет рабочим шаблоном). +- **Зашитый JWT.** Константа `fixedToken` в `modal.tsx` — секрет в исходниках и с ограниченным сроком действия (`exp`). Должен приходить из секрета `documentations-publiclink-jwt-secret` через `NEXT_PUBLIC_API_TOKEN`. +- **Неизвестный hostname.** При домене, не входящем в `switch`, `apiBaseUrl` не задаётся явно (используется дефолт `stage-api.sarex.io`) — для новых стендов список нужно расширять. +- **Расхождение портов.** Приложение слушает `3000` (Dockerfile/helm `targetPort`), но `apps/document-link/base/deployment.yaml` объявляет `containerPort: 80`, а `base/service.yaml` — `port/targetPort 80`. В `universal-chart` (`HelmRelease`) — корректный `targetPort 3000`. Kustomize-`base` стоит выровнять на `3000`. +- **Многоступенчатый Dockerfile закомментирован.** Финальный `runner`-stage отключён — образ запускается из `builder` с полным `node_modules`; для прод-образа стоит включить slim-runner. + +## Минимальный набор для запуска + +- Локально: `npm i && npm run dev`, открыть `http://localhost:3000/` (API — `stage-api.sarex.io`). +- Docker: `docker compose up` (порт `8000` → контейнер `3000`). +- В кластере фактически требуется рабочий JWT для сервиса `documentations` (сейчас — `fixedToken`; целевое — секрет `documentations-publiclink-jwt-secret`). diff --git a/apps/document-link/ENDPOINTS.md b/apps/document-link/ENDPOINTS.md new file mode 100644 index 0000000..8c5f5d6 --- /dev/null +++ b/apps/document-link/ENDPOINTS.md @@ -0,0 +1,66 @@ +# Эндпоинты, с которыми взаимодействует document-link-frontend + +Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается фронтенд публичных ссылок (`document-link-frontend`). + +## Как устроено взаимодействие + +Фронтенд загружает карточку документа по `uuid` из URL (`/`). Запрос выполняется хуком `useSWR` в `src/app/[uuid]/components/modal.tsx` через нативный `fetch`. Базовый хост API выбирается в рантайме по `window.location.hostname`, итоговый URL = `https://` + путь эндпоинта. Скачивание файлов выполняется переходом браузера по ссылкам, которые возвращает сам API (`download_link`, `download_mrpas_link`). + +Авторизация: заголовок `Authorization: Bearer ` (сейчас — зашитая константа `fixedToken`; целевое — `NEXT_PUBLIC_API_TOKEN` из секрета `documentations-publiclink-jwt-secret`). Запрос идёт с `credentials: "include"`. + +## Базовые хосты по окружениям + +Значения из `switch` по `window.location.hostname` в `modal.tsx`: + +| Hostname фронтенда | `apiBaseUrl` (базовый хост API) | +| --- | --- | +| `localhost` | `https://stage-api.sarex.io` | +| `document-link.stage.sarex.io` | `https://stage-api.sarex.io` | +| `document-link.sarex.io` | `https://api.sarex.io` | +| прочее | не определён (fallback `https://stage-api.sarex.io`) | + +## Эндпоинты по сервисам + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| Публичная ссылка | GET | `/documentations/api/v1/public/documents/public_link/{uuid}` | Метаданные документа по публичной ссылке (`uuid`) | + +Пример итогового URL: `https://stage-api.sarex.io/documentations/api/v1/public/documents/public_link/e602b98d-58a1-4ba3-8a89-84e5617b5aac`. + +Ожидаемые поля ответа (используются во фронтенде): + +| Поле ответа | Тип | Использование | +| --- | --- | --- | +| `name` | string | Название документа | +| `document_type` | string | Тип (иконка): `bim`/`bimv2`/`cloud`/`surface`/`workspace`/`pdf`/`deviation`/`c2s`/`c2c`/`abap`/`ksg`/`docx`/`xlsx`/`dxf`/`dwg`/… | +| `author` | string | Автор | +| `version` | string | Версия документа (скрывается для `workspace`/`folder`/`project`) | +| `size` | number | Размер в байтах (форматируется библиотекой `bytes`) | +| `download_token` | string | Токен скачивания | +| `download_link` | string (URL) | Прямая ссылка на скачивание файла | +| `download_mrpas_link` | string (URL) \| null | Ссылка на скачивание МЧД (опционально) | +| `expires_at` | string (datetime) \| null | Срок действия ссылки; `null` → «Неограничено» | +| `is_connector` | bool | Признак «файл > 5 Гб, требуется Sarex-коннектор» | + +### Скачивание файлов (динамические ссылки) + +Не отдельные эндпоинты реестра, а переход браузера по URL из ответа: + +| Действие | Источник URL | +| --- | --- | +| Скачать файл/папку | `download_link` из ответа `public_link` | +| Скачать МЧД | `download_mrpas_link` из ответа `public_link` (если не `null`) | + +При `is_connector = true` вместо прямого скачивания показывается предупреждение со ссылкой на [Sarex-коннектор](https://support.sarex.io/knowledge_base/item/347946). + +## Обработка ошибок + +Статус ответа маппится в человекочитаемое сообщение (`modal.tsx`): + +| Статус | Сообщение | +| --- | --- | +| `400`, `404` | «Ссылка не найдена» | +| `410` | «Время действия вашей ссылки истекло» | +| `500`, `503` | «Что-то пошло не так» | diff --git a/apps/documentations/api-v2.CONFIGURATION.md b/apps/documentations/api-v2.CONFIGURATION.md new file mode 100644 index 0000000..e98d81c --- /dev/null +++ b/apps/documentations/api-v2.CONFIGURATION.md @@ -0,0 +1,229 @@ +# Конфигурация проекта documentations-api-v2 + +Документ описывает все переменные окружения и способы конфигурирования сервиса **documentation-api-v2** (`pdm/documentation-api-v2`) — Go-сервис домена «documentations» (v2), отвечающий за диски, документы, бандлы, data source, страницы, публичные ссылки, workflow-обработку и подписи. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`github.com/kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.New()` → `envconfig.Process("", cfg)`). + +Особенности разбора: + +- **Префикса нет** — переменные читаются под своими именами (напр. `API_ADDRESS`, `POSTGRES_ADDRESS`), имя задаётся тегом `envconfig:"..."`. +- **Вложенность не используется** — конфиг плоский. Параметры БД вынесены в встроенную структуру `gopg.Config` (`pkg/postgres/gopg/postgres.go`), но остаются на верхнем уровне переменных. +- **Дефолты** заданы тегом `default:"..."` только у части полей (см. таблицы). Поле без дефолта, которое не передали, получает нулевое значение Go (`""`, `0`, `false`) — жёсткой валидации «обязательности» у envconfig в этом коде нет, сервис стартует и с пустыми значениями. +- Переменная БД-сертификата имеет имя с дефисами `YC-PG-CERTIFICATE` (тег `envconfig:"YC-PG-CERTIFICATE"`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Единственный внешний файл — JSON-описание workflow-задач (`WORKFLOWS_CONFIG_FILEPATH`), разбираемый отдельно в `config/workflows.go` (`NewTasksExecutionConfigFromFilepath`); при ошибке чтения/парсинга сервис **паникует**. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `.env.template` — только шаблон; приложение **не загружает `.env` автоматически** (в коде нет dotenv) | +| Локально (docker-compose) | `make docker-compose` → `.docker/docker-compose.yml` с `env_file: .docker/.docker.env` | +| Kubernetes (Helm) | `.helm/values.yaml`, чарт-зависимость `universal-chart`: блоки `envs` (обычные значения) и `secretEnvs` (из k8s-секретов) сервиса `api` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) — переключение окружения/namespace, `HELM_SET_ARGS` | + +Способы запуска процессов (`cmd/`): + +| Бинарник | Точка входа | Назначение | +| --- | --- | --- | +| `api_server` | `cmd/api_server/main.go` | Основной HTTP API (Fiber). Точка входа контейнера (`CMD ["./api_server"]`) | +| `migrate` (`migrations`) | `cmd/migrate/main.go` | Миграции БД (`go-pg-migrations`): `migrate` / `rollback`. Собирается как `./migrations` | +| `filestream_server` | `cmd/filestream_server/main.go` | Отдельный сервер потоковой отдачи файлов (в основном Dockerfile **не собирается**) | + +Сборка образа (`.docker/api.Dockerfile`, тег `-tags migrate`) кладёт `api_server`, `migrations` и `.example.tasks_execution_config.json`. Миграции применяются автоматически при старте приложения (см. README), либо вручную через бинарник `migrations`. + +## Переменные приложения + +Дефолт `—` означает, что значение в коде по умолчанию не задано (используется нулевое значение Go, если переменную не передать). + +### App / окружение + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `APP_NAME` | string | `documentations-backend` | Имя приложения (Sentry `ServerName`) | +| `APP_VERSION` | string | `v1` | Версия (Sentry `Release`) | +| `ENVIRONMENT` | string | — | Окружение: `stage`/`preprod`/`production` (Sentry `Environment`) | +| `NAMESPACE` | string | — | k8s namespace, используется в логике дисков | + +### HTTP-сервер + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания Fiber. Читается **обоими** бинарниками (`api_server` и `filestream_server`) | + +> `api_server` устанавливает большой `BodyLimit` (5 ТБ) и `ReadBufferSize` 96×4096; `filestream_server` — `BodyLimit` 64 МБ. Оба отдают `GET /ping` для проб k8s; `api_server` дополнительно отдаёт `GET /swagger/*`. + +### Database (`gopg.Config`, `pkg/postgres/gopg/postgres.go`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | +| `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений | +| `ENABLE_SSL` | bool | — | Подключение к БД по TLS (сертификат из `YC-PG-CERTIFICATE`) | +| `ENABLE_SQL_QUERY` | bool | — | Логирование SQL-запросов | +| `YC-PG-CERTIFICATE` | string (PEM) | — | CA-сертификат PostgreSQL. Обязателен при `ENABLE_SSL=1` | + +### Аутентификация и публичные ссылки + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PUBLIC_KEY` | string (PEM) | — | Публичный RSA-ключ (PKIX) для проверки JWT sarex-backend | +| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | string | — | HMAC-секрет JWT для публичных/временных ссылок на документы | +| `DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES` | uint8 | — | TTL публичной ссылки, минуты | +| `PUBLIC_LINK_HOST` | string | — | Базовый хост генерируемых публичных ссылок | + +### Django / IAM (`pkg/django`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_HOST` | string | — | Базовый URL Django/монолита (пользователи, компании, сервис-аккаунты) | +| `DJANGO_BASIC_AUTH` | string (base64) | — | Basic-auth `base64(login:password)` для Django и клиента flows | +| `DJANGO_ORIGINATOR` | string | — | Идентификатор источника (`docs_stage`/`docs_preprod`/`docs_prod`) | + +### Внешние сервисы (базовые URL клиентов) + +Каждый клиент (`pkg/clients/*`) создаётся с `SetBaseURL()` и ретраями. Подробнее по путям — см. `api-v2.ENDPOINTS.md`. + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `DOCUMENTATION_URL` | string | Self-URL сервиса, подставляется в workflow-задачи | +| `WORKFLOW_URL` | string | Сервис workflows (запуск обработки) | +| `WORKSPACE_URL` | string | Сервис workspaces | +| `WORKSPACE_V2_EXTERNAL_URL` | string | Внешний URL workspaces v2 | +| `WORKSPACE_BUNDLE_VERSION` | string | Версия бандла для интеграции с workspaces | +| `MARKS_PROCESSING_URL` | string | Сервис PDF-маркировок (marks) | +| `BIM_API_URL` | string | BIM API v1 | +| `BIM_API_V2_URL` | string | BIM API v2 (bim-core) | +| `BIM_API_URL_EXTERNAL` | string | Внешний URL BIM API | +| `SYSTEM_LOG_URL` | string | Сервис журналирования (system-log) | +| `FLOWS_URL` | string | Сервис flows | +| `FILE_URL_EXTERNAL` | string | Внешний URL для отдачи файлов | + +### S3 / MinIO (`pkg/s3/minio`) + +Клиент инициализируется только при `ENABLE_S3=1` (иначе `api_server` работает без S3). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_S3` | bool | — | Включить инициализацию S3-клиента | +| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта Yandex S3 | +| `S3_SERVICE_ACCOUNT_STR` | string | — | Альтернатива: JSON сервис-аккаунта строкой | + +### BIM-логика + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_BIMV1_FOR_BIMV2` | bool | — | Использовать BIM v1 API вместо v2 в пайплайне документов | +| `LAST_MASTER_BIM` | int | — | Граничный id для маршрутизации BIM master | +| `LAST_SLAVE_1_BIM` | int | — | Граничный id для маршрутизации BIM slave 1 | + +### Кеш и файловый стример + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `READ_WRITE_TIMEOUT_FILE_STREAM` | duration | — | Таймаут чтения/записи файлового стримера (напр. `6h`) | +| `CACHE_DEFAULT_EXPIRATION` | duration | — | TTL кеша (напр. `60s`) | +| `CACHE_CLEANUP_INTERVAL` | duration | — | Интервал очистки кеша | +| `USE_CACHE_IN_FILE_STREAMER` | bool | — | Включить кеш в файловом стримере | + +### Workflow-задачи + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOWS_CONFIG_FILEPATH` | string | `.example.tasks_execution_config.json` | Путь к JSON-описанию задач (`config/workflows.go`); при ошибке — паника | +| `WORKFLOWS_IMAGES_VERSION` | string | — | Тег образов задач (`develop`/`master`) | +| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов задач | +| `IS_CONVERTED_PDF_UPLOADING_TO_S3` | bool | `true` | Загружать сконвертированный PDF в S3 | + +### Наблюдаемость: Sentry и OpenTelemetry + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SENTRY_DSN` | string | — | DSN Sentry | +| `SENTRY_DEBUG` | bool | — | Debug-режим Sentry | +| `ENABLE_OBSERVABILITY` | bool | — | Включить slog-хендлер observability и трассировку запросов БД | +| `OBSERVABILITY_COLLECTOR_ENDPOINT` | string | — | Endpoint OTLP-коллектора логов | +| `TRACER_USE` | bool | `false` | Включить OTEL-трейсинг Fiber | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора трейсов | +| `TRACER_USE_INSECURE` | bool | `true` | Подключение к коллектору без TLS | +| `SERVICE_NAME` | string | `documentations-api-v2` | Имя сервиса в трейсах | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя OTEL-логгера | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_FILESTREAM_ADDRESS` | `.env.template`, `.docker.env` | Присутствует в шаблонах, но **кодом не читается** (см. Замечания) | +| `POSTGRES_EXTERNAL_PORT` | `.docker/docker-compose.yml` | Внешний порт проброса контейнера Postgres | +| `API_VERSION` | `.docker/docker-compose.yml` | Тег образа `api` (по умолчанию `local`) | +| `GITLAB_CREDENTIALS` | `.docker/api.Dockerfile` (build-arg) | Доступ к приватному GitLab при `go build` | +| `CI_COMMIT_SHORT_SHA` | `.docker/api.Dockerfile` (build-arg), CI | Идентификатор сборки | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — обёртка над зависимостью `universal-chart` (`oci://.../charts`, версия `0.1.7`). Сервис `api`: `deployment` (реплики, ресурсы, пробы `GET /ping:8080`), `service` (ClusterIP `80 → 8080`), `image`, `volumes`. + +Смонтированные тома: + +- Секрет `documentations-yc-s3` → `/etc/sarex/yc-s3-storage` (на него указывает `S3_SERVICE_ACCOUNT`); +- ConfigMap `tasks-execution-config-documentation-v2` → `/etc/app/tasks_execution_config.json` (на него указывает `WORKFLOWS_CONFIG_FILEPATH` в k8s). + +Обычные значения (`envs`) задают те же переменные `APP_NAME`, `API_ADDRESS` (`0.0.0.0:8080`), `ENVIRONMENT`, `NAMESPACE`, URL внешних сервисов, `ENABLE_SSL=1`, `ENABLE_S3=1`, флаги трассировки и т.п. — с разбивкой по окружениям (`_default`/`stage`/`preprod`/`production`). + +Значения из секретов (`secretEnvs`, монтируются через `secretKeyRef`): + +| Переменная | Секрет (`_default`) | Секрет (`preprod`) | Ключ | +| --- | --- | --- | --- | +| `POSTGRES_USER` | `documentations-postgresql-secret` | `ya-pg-secret` | `username` | +| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `ya-pg-secret` | `host` | +| `POSTGRES_PORT` | `documentations-postgresql-secret` | `ya-pg-secret` | `port` | +| `POSTGRES_DB` | `documentations-postgresql-secret` | `ya-pg-secret` | `database` | +| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `ya-pg-secret` | `password` | +| `YC-PG-CERTIFICATE` | `documentations-postgresql-secret` (`ca.crt`) | `yc-pg-certificate` (`certificate`) | см. столбцы | +| `DJANGO_BASIC_AUTH` | `django-auth` | — | `key` | +| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | `yc-jwt-secret` | — | `secret` | +| `PUBLIC_KEY` | `public-key` | — | `key` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | Chart version | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | +| ветка `master` | `preprod` | `documentations-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `0.0.1-prod` | +| `merge_request_event` | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные: `SERVICE_NAME=documentations-v2`, `RELEASE_NAME`/`CHART_NAME=documentations-v2`, `DOCKERFILE_PATH=.docker/api.Dockerfile`, `HELM_SET_ARGS` (`--set universal-chart.global.env=…`, `--set universal-chart.services.api.image.name.=${IMAGE_NAME}`), флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_DEPLOY`. + +## Замечания и потенциальные проблемы + +- **Нет обязательности полей.** `envconfig` в этом коде не помечает поля как required — при отсутствии переменной берётся нулевое значение Go. Пустые критичные значения (адрес БД, `PUBLIC_KEY`) приведут к ошибке уже в рантайме (падение при `db.Ping`, отказ проверки JWT), а не на этапе разбора конфига. +- **`API_FILESTREAM_ADDRESS` не читается.** Оба сервера слушают `API_ADDRESS`; отдельной переменной для порта файлового стримера в коде нет. +- **Опечатка `POSTGRES_POLL_SIZE`.** В `.env.template`/`.docker.env` встречается `POSTGRES_POLL_SIZE`; код читает `POSTGRES_POOL_SIZE` (в helm имя корректное). Значение с опечаткой не подхватывается. +- **`YC-PG-CERTIFICATE`** — имя с дефисами, читается через явный тег `envconfig`. В `.helm` для preprod монтируется из отдельного секрета `yc-pg-certificate` (ключ `certificate`). +- **Файл workflow-задач обязателен по существу.** Если файл по пути `WORKFLOWS_CONFIG_FILEPATH` отсутствует или не парсится — `config.NewTasksExecutionConfigFromFilepath` вызывает `panic`. Локально нужен `.example.tasks_execution_config.json`, в k8s — том ConfigMap. +- **S3 опционален.** При `ENABLE_S3=0` S3-клиент не создаётся; хендлеры, работающие с хранилищем, будут получать `nil`-хранилище. +- **Автомиграции при старте.** Приложение накатывает миграции автоматически (см. README); ручной прогон — бинарником `migrations migrate`/`migrations rollback`. + +## Минимальный набор для локального запуска + +Postgres поднимается через `make docker-compose` (образ `timescale/timescaledb-ha:pg13`, инициализация расширений uuid/ltree из `.docker/install-uuid-ltree.sql`). Приложение — бинарник `./api_server`. Минимально задать: + +- `API_ADDRESS` (напр. `localhost:8000`); +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_PASSWORD`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL=0`, `ENABLE_SQL_QUERY`; +- `PUBLIC_KEY` (для проверки JWT sarex-backend), `DOCUMENT_PUBLIC_LINK_JWT_SECRET`; +- `WORKFLOWS_CONFIG_FILEPATH` с существующим JSON (по умолчанию `.example.tasks_execution_config.json`); +- `ENABLE_S3=0`, `TRACER_USE=false`, `ENABLE_OBSERVABILITY=0` — чтобы не поднимать S3/OTEL локально; +- URL внешних сервисов (`DJANGO_HOST`, `WORKFLOW_URL`, `WORKSPACE_URL`, `BIM_API_URL`, `BIM_API_V2_URL`, `MARKS_PROCESSING_URL`, `SYSTEM_LOG_URL`, `FLOWS_URL`) — по мере необходимости для соответствующих сценариев. + +Готовые значения-примеры приведены в `api-v2.env.example` (с учётом замечаний выше). diff --git a/apps/documentations/api-v2.ENDPOINTS.md b/apps/documentations/api-v2.ENDPOINTS.md new file mode 100644 index 0000000..a7a0440 --- /dev/null +++ b/apps/documentations/api-v2.ENDPOINTS.md @@ -0,0 +1,88 @@ +# Эндпоинты, с которыми взаимодействует documentations-api-v2 + +Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается Go-сервис **documentation-api-v2** (`pdm/documentation-api-v2`). + +## Как устроено взаимодействие + +Внешние вызовы выполняются типизированными клиентами в `pkg/clients/*` и `pkg/django`. Каждый клиент строится на [`github.com/go-resty/resty/v2`](https://github.com/go-resty/resty) с `SetBaseURL()`, ретраями и (опционально) OTEL-трассировкой. Базовый URL берётся из переменной окружения (см. `api-v2.CONFIGURATION.md`); итоговый URL = `<базовый URL>` + путь, указанный в коде метода клиента. + +Клиенты создаются в `internal/api/httpserver/server.go` (`App.Run`) и передаются в репозитории/юзкейсы. Аутентификация к внешним сервисам: + +- **Django** и **flows** используют Basic-auth из `DJANGO_BASIC_AUTH`; +- **workspace** (`Archive`) пробрасывает пользовательский Bearer-токен (`SetAuthToken`); +- прочие внутренние сервисы вызываются по кластерным адресам без явной авторизации на уровне клиента. + +## Базовые адреса по сервисам + +| Клиент (`pkg/...`) | Переменная базового URL | Назначение | +| --- | --- | --- | +| `django` | `DJANGO_HOST` | Монолит/IAM: пользователи, компании, сервис-аккаунты, настройки | +| `clients/workspace` | `WORKSPACE_URL` | Сервис рабочих областей | +| `clients/workflows` | `WORKFLOW_URL` | Запуск workflow-обработки | +| `clients/bimv1` | `BIM_API_URL` | BIM API v1 | +| `clients/bimv2` | `BIM_API_V2_URL` | BIM API v2 (bim-core) | +| `clients/flows` | `FLOWS_URL` | Сервис flows (процессы) | +| `clients/marks` | `MARKS_PROCESSING_URL` | Сервис PDF-маркировок | +| `clients/system_log` | `SYSTEM_LOG_URL` | Сервис журналирования | + +## Эндпоинты по сервисам + +### `django` — монолит/IAM (`pkg/django`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `api/client/settings/` | Клиентские настройки | +| GET | `/api/core/companies/` | Список компаний | +| GET | `/api/core/users/` | Список пользователей | +| GET | `api/core/users/{user_id}/introspect` | Интроспекция пользователя | +| GET | `/api/core/service_accounts/` | Сервис-аккаунты | +| GET | `/api/core/service-accounts/personalized/` | Персонализированные сервис-аккаунты | + +### `workspace` — рабочие области (`pkg/clients/workspace`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `internal/v2/workspaces` | Создать рабочую область | +| DELETE | `internal/v2/documents/{document_ids}` | Удалить документы из рабочих областей; возвращает id опустевших областей | +| POST | `api/v1/workspaces/{workspace_id}/archive` | Архивировать рабочую область (с пользовательским Bearer-токеном) | + +### `workflows` — workflow-обработка (`pkg/clients/workflows`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `internal/v1/companies/{company_id}/workflows` | Создать/запустить workflow для компании | + +### `bimv1` — BIM API v1 (`pkg/clients/bimv1`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/internal/v1/targets/{target_id}/bims-pdm` | Зарегистрировать новый BIM (v1) | +| POST | `/internal/v1/targets/{target_id}/bims-v2-pdm` | Зарегистрировать новый BIM (v2) | + +### `bimv2` — BIM API v2 (`pkg/clients/bimv2`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/internal/v1/projects/{project_id}/bims` | Создать BIM в проекте | + +### `flows` — процессы (`pkg/clients/flows`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `api/v1/documents/?full=true&document_ids={id}` | Документы процессов по id | + +### `marks` — PDF-маркировки (`pkg/clients/marks`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/api/v1/marks/{bundle_id}` | Массовое создание маркировок для бандла | + +### `system_log` — журналирование (`pkg/clients/system_log`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/api/v0/system_log` | Отправка пакета записей журнала | + +## Обработка ошибок + +Клиенты проверяют HTTP-статус ответа и при коде, отличном от ожидаемого (обычно `200`), оборачивают ошибку через `github.com/rotisserie/eris` с указанием имени метода клиента (напр. `bimapi.addNewBim: invalid response code %d expected 200`). Транспортные ошибки resty также оборачиваются `eris.Wrap`. Настроены ретраи (напр. клиент workspace — 5 попыток с паузой 1 c, таймаут 10 c). diff --git a/apps/documentations/api-v2.env.example b/apps/documentations/api-v2.env.example new file mode 100644 index 0000000..3cd222b --- /dev/null +++ b/apps/documentations/api-v2.env.example @@ -0,0 +1,87 @@ +# ============================================================================= +# documentations-api-v2 (documentation-api-v2) — пример переменных окружения +# Разбор: github.com/kelseyhightower/envconfig, config/config.go + pkg/postgres/gopg/postgres.go +# Префикса у переменных нет; вложенность не используется. Дефолт указан там, где он задан в коде. +# ============================================================================= + +# --- App --------------------------------------------------------------------- +APP_NAME=documentations-backend # default: documentations-backend +APP_VERSION=v1 # default: v1; используется как Sentry Release +ENVIRONMENT=stage # stage/preprod/production; идёт в Sentry Environment +NAMESPACE=documentations # k8s namespace, используется в бизнес-логике дисков + +# --- HTTP-сервер ------------------------------------------------------------- +API_ADDRESS=localhost:8000 # адрес прослушивания Fiber (и api_server, и filestream_server) +# API_FILESTREAM_ADDRESS=localhost:8050 # присутствует в шаблоне, но КОДОМ НЕ ЧИТАЕТСЯ (см. Замечания) + +# --- PostgreSQL (pkg/postgres/gopg) ------------------------------------------ +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=postgres +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_POOL_SIZE=1 # в helm: POSTGRES_POOL_SIZE (в шаблоне встречается опечатка POSTGRES_POLL_SIZE — не читается) +ENABLE_SSL=0 # 1 → TLS к БД по сертификату из YC-PG-CERTIFICATE +ENABLE_SQL_QUERY=1 # логирование SQL-запросов +YC-PG-CERTIFICATE="" # PEM CA-сертификат PostgreSQL (имя с дефисами, читается через envconfig-тег) +# POSTGRES_EXTERNAL_PORT=6432 # только для docker-compose, приложением не читается + +# --- Django / IAM (pkg/django) ----------------------------------------------- +DJANGO_HOST=https://stage.sarex.io +DJANGO_BASIC_AUTH= # base64(login:password) для Basic-auth в Django и flows +DJANGO_ORIGINATOR=docs_stage # идентификатор источника для system-log/Django + +# --- Внешние сервисы (base URL клиентов) ------------------------------------- +DOCUMENTATION_URL=http://api-v2-service.documentations/ # self-URL, подставляется в workflow-задачи +WORKFLOW_URL=http://workflows-api-service.platform:8000/ +WORKSPACE_URL=http://workspaces-backend-service.platform:8000/ +WORKSPACE_V2_EXTERNAL_URL=https://stage.sarex.io/workspaces-v2/ +WORKSPACE_BUNDLE_VERSION=v1 +MARKS_PROCESSING_URL=http://marks-service.documentations:8000 +BIM_API_URL=http://bim-api-service.bim-api-stage/ +BIM_API_V2_URL=http://bim-core-api.platform.svc.cluster.local:8000/ +BIM_API_URL_EXTERNAL=https://stage-api.sarex.io/bim +SYSTEM_LOG_URL=http://system-log-api-service.platform:80 +FLOWS_URL= +FILE_URL_EXTERNAL= # внешний URL для отдачи файлов + +# --- S3 / MinIO (pkg/s3/minio) ----------------------------------------------- +ENABLE_S3=0 # 1 → инициализировать S3-клиент +S3_SERVICE_ACCOUNT=/etc/sarex/yc-s3-storage/yc-s3-service-account.json # путь к JSON сервис-аккаунта +S3_SERVICE_ACCOUNT_STR= # альтернатива: JSON сервис-аккаунта строкой + +# --- Публичные ссылки на документы ------------------------------------------- +PUBLIC_LINK_HOST= # хост генерируемых публичных ссылок +DOCUMENT_PUBLIC_LINK_JWT_SECRET="" # HMAC-секрет JWT публичной ссылки +DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES=5 # TTL публичной ссылки (uint8) + +# --- Аутентификация ---------------------------------------------------------- +PUBLIC_KEY="" # PKIX RSA public key (PEM) для проверки JWT sarex-backend + +# --- BIM-логика -------------------------------------------------------------- +USE_BIMV1_FOR_BIMV2=0 # использовать BIM v1 API вместо v2 +LAST_MASTER_BIM=0 # граничные id для маршрутизации BIM-запросов +LAST_SLAVE_1_BIM=0 + +# --- Кеш / файловый стример -------------------------------------------------- +READ_WRITE_TIMEOUT_FILE_STREAM=6h # Go duration +CACHE_DEFAULT_EXPIRATION=60s # Go duration +CACHE_CLEANUP_INTERVAL=60s # Go duration +USE_CACHE_IN_FILE_STREAMER=1 # bool + +# --- Workflow-задачи --------------------------------------------------------- +WORKFLOWS_IMAGES_VERSION=develop # тег образов задач (develop/master) +WORKFLOWS_CONFIG_FILEPATH=.example.tasks_execution_config.json # default; в k8s — /etc/app/tasks_execution_config.json +CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q # default; реестр образов задач +IS_CONVERTED_PDF_UPLOADING_TO_S3=true # default: true + +# --- Наблюдаемость: Sentry + OpenTelemetry ----------------------------------- +SENTRY_DSN="" +SENTRY_DEBUG=false +ENABLE_OBSERVABILITY=0 # bool; включает slog-хендлер и трассировку в БД +OBSERVABILITY_COLLECTOR_ENDPOINT=0 # endpoint OTLP-коллектора логов +TRACER_USE=false # default: false; включить OTEL-трейсинг Fiber +TRACER_HOST=localhost:4317 # default: localhost:4317 +TRACER_USE_INSECURE=true # default: true +SERVICE_NAME=documentations-api-v2 # default: documentations-api-v2 +TRACER_LOGGER_NAME=tracer_logger # default: tracer_logger diff --git a/apps/documentations/api-v2.openapi.yaml b/apps/documentations/api-v2.openapi.yaml new file mode 100644 index 0000000..5bf26dc --- /dev/null +++ b/apps/documentations/api-v2.openapi.yaml @@ -0,0 +1,1258 @@ +openapi: 3.0.3 + +info: + title: Documentation API v2 + version: "1.0" + description: | + REST API сервиса **documentation-api-v2** (`pdm/documentation-api-v2`) — + Go-сервис домена «documentations» (v2): управление дисками, документами, + бандлами, data source, страницами, публичными ссылками, workflow-обработкой + и загрузкой файлов (в т.ч. multipart). + + Сервис написан на Go (**Fiber v2**, `github.com/gofiber/fiber/v2`). HTTP-сервер + собирается в `internal/api/httpserver/server.go`. Спецификация ниже получена + конвертацией сгенерированной `swag`-схемы (`docs/swagger.yaml`, Swagger 2.0) + в OpenAPI 3.0.3 и нормализацией путей под реальные маршруты Fiber. + + Роутинг состоит из двух групп (`server.go`, `App.Run`): + + - публичный API — префикс `/api/v1`; + - внутренний API — префикс `/internal/v1` (для вызовов внутри кластера). + + Интерактивная документация Swagger UI доступна по `/swagger/*`. + Служебный эндпоинт проб k8s — `GET /ping` (вне групп, без аутентификации). + + ### Аутентификация + JWT-middleware (`pkg/middleware/jwt_auth`) подключён **только к группе + `/api/v1`**. Токен передаётся заголовком `Authorization: Bearer ` + либо query-параметром `?auth_jwt=` (`JWTToCtx`). Поддерживаются два + режима (`jwtauth.New`): + + 1. **Zitadel** — если передан заголовок `Identity: Bearer `, полезная + нагрузка (`urn:zitadel:iam:user:metadata`) берётся из identity-токена + без проверки подписи (доверие обеспечивает Istio). + 2. **sarex-backend** — если заголовка `Identity` нет, подпись основного + токена проверяется публичным RSA-ключом `PUBLIC_KEY` (PKIX). + + Для маршрутов публичных/временных ссылок (`/api/v1/public/*` и + `download_type=temporary`) токен проверяется HMAC-секретом + `DOCUMENT_PUBLIC_LINK_JWT_SECRET`. + + Эндпоинты `/internal/v1/*` middleware аутентификации на уровне приложения + **не проходят** — доступ ограничивается сетевым слоем кластера. (В исходной + swag-схеме параметр `Authorization` у них помечен как обязательный — + это артефакт аннотаций, а не рантайм-поведение.) + + ### Пагинация + Явной пагинации у списочных ответов нет: коллекции возвращаются целиком + (массивом или объектом-обёрткой, напр. `GetDiskListResponse.disks`, + `ServiceAccountsResponse.service_account`). Часть выборок ограничивается + параметрами запроса (`company_id`, `extend`, `upload_path`). + + ### Обработка ошибок + Ошибки возвращаются как JSON `application/json` со схемой `AppError` — + `{ message, error_code }` (`internal/api/apperrors`). Код `error_code` + определяет HTTP-статус (`apperrors/error_response.go`): + + | `error_code` | Константа | HTTP-статус | + | --- | --- | --- | + | `PDM-0000` | SystemErrorCode | 500 | + | `PDM-0001` | ErrNotFoundCode | 404 | + | `PDM-0002` | ErrNoAuthCode | (ошибки авторизации → 401) | + | `PDM-0003` | ErrNoAccessCode | 403 | + | `PDM-0004` | ErrInvalidCode | 400 | + | `PDM-0005` | ErrGoneCode | 410 | + + ### Замечания (расхождения кода/схемы) + - Ошибки аутентификации middleware отдаёт статусом **401** (в swag-схеме + эти ответы не описаны — там только `400`/`404`/`500`). + - Ошибка «истёкшая публичная ссылка» маппится на **`410 Gone`** + (`ErrGoneCode`), а не на `404`. + - Путь загрузки части файла содержит двойной слэш — + `POST /api/v1/uploads//multipart/{upload_id}/{part_num}` (так в схеме и + маршруте). + - `api_server` задаёт очень большой `BodyLimit` (5 ТБ) для загрузки файлов. + + contact: + name: Sarex + url: https://gitlab/pdm/documentation-api-v2 + +servers: + - url: http://api-v2-service.documentations + description: Stage (ClusterIP, порт 80 → 8080) + - url: http://api-v2-service.documentations-preprod + description: Preprod (ClusterIP) + - url: http://api-v2-service.documentations-prod + description: Production (ClusterIP) + - url: http://localhost:8000 + description: Локальный запуск (API_ADDRESS) + +tags: + - name: document + description: Документы (создание, изменение, перемещение, скачивание) + - name: disk + description: Диски и их проекты/сервис-аккаунты + - name: bundle + description: Бандлы и загрузка файлов + - name: page + description: Страницы data source + - name: upload + description: Multipart-загрузка + - name: workflow + description: Workflow-обработка и вебхуки + - name: workspace + description: Рабочие области + - name: permission + description: Типы прав доступа + - name: data_source + description: Data source (внутренние проверки) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Documents (public /api/v1) + # ========================================================================== + /api/v1/documents: + post: + tags: [document] + summary: create document + description: Create document with bundle and data_sources. + operationId: createDocument + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDocumentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/types: + get: + tags: [document] + summary: types + description: Descriptions of possible documents. + operationId: getDocumentTypes + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BundleTypeStruct' + + /api/v1/documents/{document_id}: + get: + tags: [document] + summary: get document + description: >- + Get document by document_id and optional "extend". If "extend" is + "bundles", returns bundles with all data_sources and pages. + operationId: getDocument + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + - { name: extend, in: query, required: false, schema: { type: string }, example: bundles } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + patch: + tags: [document] + summary: change document + description: Change document's name. + operationId: changeDocument + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PatchDocument' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + delete: + tags: [document] + summary: delete document + description: Delete document (soft delete). + operationId: deleteDocument + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_id}/update-path: + patch: + tags: [document] + summary: move document to another folder + description: Move document to another folder. + operationId: updateDocumentPath + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PatchDocumentPath' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_id}/move_bundles: + patch: + tags: [document] + summary: move bundles + description: >- + Move bundles to document. The source document is marked as deleted if it + has no bundles left after the move. + operationId: moveBundles + parameters: + - { name: document_id, in: path, required: true, schema: { type: string }, example: "1" } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MoveBundleRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/Bundle' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_id}/add_bundle: + post: + tags: [document] + summary: add bundle to document + description: Add created and uploaded bundle to an existing document. + operationId: addBundleToDocument + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/AddBundleRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Bundle' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_id}/ancestors: + get: + tags: [document] + summary: get ancestors of documents + operationId: getDocumentAncestors + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_id}/download: + get: + tags: [document] + summary: get url for download document + operationId: getDocumentDownloadURL + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/DownloadURLResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/documents/{document_ids}/company: + get: + tags: [document] + summary: get company_ids by document_ids + description: Get map of documents and companies. + operationId: getDocumentsCompanyMap + parameters: + - { name: document_ids, in: path, required: true, schema: { type: string }, example: "1,2,3,4,5" } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/DocumentCompanyMap' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Disks + # ========================================================================== + /api/v1/disks: + get: + tags: [disk] + summary: get disks + description: >- + Get disks. company_id requests disks of a specific company; by default + the user gets disks of all their companies. + operationId: getDisks + parameters: + - { name: company_id, in: query, required: true, schema: { type: integer }, example: 1 } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/GetDiskListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + post: + tags: [disk] + summary: create disk + description: Create disk. Requires admin permissions in Django. + operationId: createDisk + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateDiskRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Disk' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + delete: + tags: [disk] + summary: delete disk + description: >- + Soft delete of a disk (marks the disk, not the document, as deleted). + Requires admin permissions in Django. disk_id passed as path in the + underlying route. + operationId: deleteDisk + parameters: + - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/disks/{disk_id}/projects: + get: + tags: [disk] + summary: get projects + description: Get projects by disk_id. + operationId: getDiskProjects + parameters: + - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/GetDiskListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/disks/{disk_id}/service_accounts: + get: + tags: [disk] + summary: get service accounts + description: Get service accounts by disk_id. + operationId: getDiskServiceAccounts + parameters: + - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/ServiceAccountsResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Bundles + # ========================================================================== + /api/v1/bundles/{bundle_id}/bucket_name: + get: + tags: [bundle] + summary: get bucket name + description: Get bucket name by bundle_id. + operationId: getBucketName + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/BucketNameResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/bundles/{bundle_id}/download: + get: + tags: [bundle] + summary: get url for download bundle + operationId: getBundleDownloadURL + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/DownloadURLResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/bundles/{bundle_id}/{bundle_key}/download: + get: + tags: [bundle] + summary: get url for download bundle (data_source) + description: Get url for download of a specific data_source of a bundle. + operationId: getBundleDataSourceDownloadURL + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + - { name: bundle_key, in: path, required: true, schema: { type: string }, example: las } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/DownloadURLResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/bundles/{bundle_id}/{bundle_key}/upload_multipart: + post: + tags: [bundle] + summary: initial multipart upload + description: Initialize a multipart upload for chunked file upload to storage. + operationId: initMultipartUpload + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree } + - { name: upload_path, in: query, required: false, schema: { type: string }, example: r/1/1/2 } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/MultipartUpload' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/bundles/{bundle_id}/{bundle_key}/upload_single: + post: + tags: [bundle] + summary: upload single file + description: >- + Upload a single file to storage for a specific bundle. Bundle and + DataSource must already exist. + operationId: uploadSingleFile + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree } + - { name: upload_path, in: query, required: false, schema: { type: string }, example: data/r/r.bin } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/bundles/{bundle_id}/upload_finish: + post: + tags: [bundle] + summary: finish upload + description: >- + Finish upload: change bundle status from "created" to "uploaded", update + data_source file sizes, check for files in storage. + operationId: finishUpload + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Bundle' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Pages + # ========================================================================== + /api/v1/pages: + post: + tags: [page] + summary: create page + description: Creates a page for a specified data_source_id. + operationId: createPage + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreatePageRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Page' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/pages/{data_source}/{page_key}/download: + get: + tags: [page] + summary: get download url page + operationId: getPageDownloadURL + parameters: + - { name: data_source, in: path, required: true, schema: { type: string }, example: "1" } + - { name: page_key, in: path, required: true, schema: { type: string }, example: "2" } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/DownloadURLResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Permissions + # ========================================================================== + /api/v1/permissions: + get: + tags: [permission] + summary: permission types + operationId: getPermissionTypes + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/PermissionType' } + + # ========================================================================== + # Uploads (multipart parts) + # ========================================================================== + /api/v1/uploads//multipart/{upload_id}/{part_num}: + post: + tags: [upload] + summary: upload multipart upload part + description: >- + Upload a part of a file to storage. NB: the route contains a double + slash after `uploads` (as registered). + operationId: uploadMultipartPart + parameters: + - { name: upload_id, in: path, required: true, schema: { type: string } } + - { name: part_num, in: path, required: true, schema: { type: integer }, example: 1 } + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + required: [file] + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/uploads/multipart/{upload_id}/abort: + post: + tags: [upload] + summary: abort multipart upload + operationId: abortMultipartUpload + parameters: + - { name: upload_id, in: path, required: true, schema: { type: string } } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/uploads/multipart/{upload_id}/complete: + post: + tags: [upload] + summary: complete multipart upload + description: >- + Complete a multipart upload. When all parts are uploaded, marks parts in + storage and database as "completed". + operationId: completeMultipartUpload + parameters: + - { name: upload_id, in: path, required: true, schema: { type: string } } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Workflows + # ========================================================================== + /api/v1/workflows/{workflow_id}: + get: + tags: [workflow] + summary: get workflow + operationId: getWorkflow + parameters: + - { name: workflow_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Workflow' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + post: + tags: [workflow] + summary: webhook + description: >- + Determines and updates workflow status (and bundle workflow status). On + success both are set to "done"; if bundle has no document_id, status is + set to "No document". + operationId: workflowWebhook + parameters: + - { name: workflow_id, in: path, required: true, schema: { type: string } } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Workspaces + # ========================================================================== + /api/v1/workspaces: + post: + tags: [workspace] + summary: create workspace + description: Creates a workspace document that may include other documents. + operationId: createWorkspace + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateWorkspaceRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /api/v1/workspaces/{ws_id}: + get: + tags: [workspace] + summary: get workspace document + description: Get document with workspace type. + operationId: getWorkspace + parameters: + - { name: ws_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + # ========================================================================== + # Internal (/internal/v1) — без app-аутентификации, только внутри кластера + # ========================================================================== + /internal/v1/bundles/{bundle_id}: + get: + tags: [bundle] + summary: get internal bundle + description: Get internal bundle by bundle_id. + operationId: getInternalBundle + security: [] + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Bundle' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /internal/v1/bundles/{bundle_id}/workflow: + post: + tags: [bundle] + summary: add workflow + description: Add workflow to bundle. + operationId: addBundleWorkflow + security: [] + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Workflow' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /internal/v1/documents/{document_id}: + delete: + tags: [document] + summary: internal delete document + description: Internal delete document (soft delete). + operationId: internalDeleteDocument + security: [] + parameters: + - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + + /internal/v1/is_folder/bundles/{bundle_id}/{bundle_key}: + get: + tags: [data_source] + summary: is data_source folder handler + description: Returns whether the data_source is a folder or not. + operationId: isDataSourceFolder + security: [] + parameters: + - { name: bundle_id, in: path, required: true, schema: { type: string } } + - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/IsDataSourceFolderResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + '500': { $ref: '#/components/responses/ServerError' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + JWT в заголовке `Authorization: Bearer ` или в query-параметре + `auth_jwt`. Режим sarex-backend проверяется RSA-ключом `PUBLIC_KEY`; + режим Zitadel — по заголовку `Identity`. + + responses: + BadRequest: + description: Bad Request + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + NotFound: + description: Not Found + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + ServerError: + description: Internal Server Error + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + + schemas: + ErrCodeType: + type: string + enum: [PDM-0000, PDM-0001, PDM-0002, PDM-0003, PDM-0004, PDM-0005] + description: >- + SystemErrorCode / ErrNotFoundCode / ErrNoAuthCode / ErrNoAccessCode / + ErrInvalidCode / ErrGoneCode + + AppError: + type: object + properties: + message: { type: string } + error_code: { $ref: '#/components/schemas/ErrCodeType' } + + AddBundleRequest: + type: object + required: [bundle_id] + properties: + bundle_id: { type: string } + + BucketNameResponse: + type: object + properties: + bucket_name: { type: string } + + Bundle: + type: object + properties: + attributes: { type: object, additionalProperties: true } + author: { $ref: '#/components/schemas/User' } + bim_id: { type: integer } + bundle_copied_from: { type: string } + created_at: { type: string } + data_sources: + type: array + items: { $ref: '#/components/schemas/DataSource' } + date: { type: string, description: optional field for django integration } + document_id: { type: integer } + has_digital_signature: { type: boolean } + has_qr_code: { type: boolean } + has_stamp: { type: boolean } + id: { type: string } + size: { type: integer } + type: { $ref: '#/components/schemas/BundleType' } + upload_status: { $ref: '#/components/schemas/BundleUploadStatus' } + workflow: { $ref: '#/components/schemas/Workflow' } + workflow_status: { $ref: '#/components/schemas/BundleWorkflowStatus' } + + BundleKey: + type: string + enum: + - las + - potree + - e57 + - clouds + - panoramas_json + - panoramas + - glb + - original + - tiles + - tif + - tif_dem + - metadata + - colored-relief-tif + - topographic_tiles + - geojson + - ap_rasterized_las + - ap_rasterized_potree + - ab_las + - ab_potree + - deviation_json + - bim + - bim_optimized + - bim_optimizedGz + - bim_metadata + - bim_metadataGz + - bim_metadata_static + - bim_metadata_staticGz + - diff_json + - ifc + - nwd + - rvt + - nwc + - abap_json + - cloud_ooc + - debug_abap_json + - pdf + - p7s + - xlsx + - dxf + - docx + - dwg + + BundleKeyField: + type: object + properties: + allowed_extensions: + type: array + items: { type: string } + can_be_downloaded_by_user: { type: boolean } + can_be_uploaded_by_user: { type: boolean } + file: { type: boolean } + folder: { type: boolean } + key: { $ref: '#/components/schemas/BundleKey' } + required: { type: boolean } + title: { type: string } + + BundleType: + type: string + enum: + - cloud + - bim + - bimv2 + - surface + - other_files + - deviation + - dem + - orthophoto + - dxf + - ksg + - docx + - dwg + - pdf + - xlsx + - abap + - c2c + - c2s + + BundleTypeStruct: + type: object + properties: + fields: + type: array + items: { $ref: '#/components/schemas/BundleKeyField' } + name: { $ref: '#/components/schemas/BundleType' } + title: { type: string } + + BundleUploadStatus: + type: string + enum: [created, uploaded] + + BundleWorkflowStatus: + type: string + enum: [created, skipped, done, errored] + + CreateDiskRequest: + type: object + required: [admin_ids, company_id, storage_type] + properties: + admin_ids: + type: array + minItems: 1 + items: { type: integer } + company_id: { type: integer } + storage_type: + type: string + maxLength: 50 + enum: [sarex] + + CreateDocumentRequest: + type: object + required: [disk_id, is_folder, name, parent_id] + properties: + bundle_id: { type: string } + disk_id: { type: string } + is_folder: { type: boolean } + is_project: { type: boolean } + name: { type: string, maxLength: 250 } + parent_id: { type: integer } + target_id: { type: integer } + + CreatePageRequest: + type: object + properties: + data_source: { type: string } + id: { type: string } + page_order: { type: integer } + thumbnail: { type: string } + + CreateWorkspaceRequest: + type: object + required: [disk_id, documents, name, parent_id] + properties: + disk_id: { type: string } + documents: + type: array + items: { type: integer } + name: { type: string, maxLength: 250 } + parent_id: { type: integer } + + DataSource: + type: object + properties: + file_name: { type: string } + format: { $ref: '#/components/schemas/DataSourceFormat' } + id: { type: string } + key: { $ref: '#/components/schemas/BundleKey' } + pages: + type: array + items: { $ref: '#/components/schemas/Page' } + size: { type: integer } + type: { $ref: '#/components/schemas/DataSourceUploadType' } + + DataSourceFormat: + type: string + enum: [glb, json, gz, s3d] + + DataSourceUploadType: + type: string + enum: [file, folder] + + DisableButton: + type: string + enum: [workspace, delete, rename, create, project, download, history, add_doc_to_ws, del_doc_from_ws] + + Disk: + type: object + properties: + admin_ids: + type: array + items: { type: integer } + bucket_name: { type: string } + company_id: { type: integer } + created_at: { type: string } + disable_button: + type: array + items: { $ref: '#/components/schemas/DisableButton' } + document_id: { type: integer } + id: { type: string } + name: { type: string } + permissions_editable: { type: boolean } + storage_type: { type: string } + + Document: + type: object + properties: + author: { $ref: '#/components/schemas/User' } + bundles: + type: array + items: { $ref: '#/components/schemas/Bundle' } + created_at: { type: string } + dashboard_id: { type: integer } + disable_button: + type: array + items: { $ref: '#/components/schemas/DisableButton' } + disk_id: { type: string } + document_copied_from: { type: integer } + files: + type: array + items: { type: integer } + folders: + type: array + items: { type: integer } + has_digital_signature: { type: boolean } + has_public_link: { type: boolean } + has_qr_code: { type: boolean } + has_stamp: { type: boolean } + id: { type: integer } + is_folder: { type: boolean } + name: { type: string } + path: { type: string } + project_id: { type: integer } + public_link: { type: string } + public_link_available: { type: boolean } + public_link_expiration_time: { type: string } + public_link_id: { type: string } + type: { $ref: '#/components/schemas/DocumentType' } + workspace_id: { type: string } + + DocumentCompanyMap: + type: object + properties: + result: + type: object + additionalProperties: { type: integer } + + DocumentType: + type: string + enum: + - workspace + - dashboard + - cloud + - bim + - bimv2 + - project + - folder + - root + - surface + - pdf + - xlsx + - orthophoto + - dem + - dxf + - ksg + - docx + - dwg + + DownloadURLResponse: + type: object + properties: + download_url: { type: string } + + GetDiskListResponse: + type: object + properties: + disks: + type: array + items: { $ref: '#/components/schemas/Disk' } + + IsDataSourceFolderResponse: + type: object + properties: + result: { type: boolean } + + MoveBundleRequest: + type: object + required: [bundle_ids] + properties: + bundle_ids: + type: array + items: { type: string } + + MultipartUpload: + type: object + properties: + upload_id: { type: string } + + Page: + type: object + properties: + id: { type: string } + page_order: { type: integer } + thumbnail: { type: string } + + PatchDocument: + type: object + required: [name] + properties: + name: { type: string } + + PatchDocumentPath: + type: object + required: [parent_id] + properties: + parent_id: { type: integer } + + PermissionType: + type: object + properties: + label: { type: string } + value: { type: string } + + ServiceAccount: + type: object + properties: + id: { type: string } + name: { type: string } + type: { type: string } + username: { type: string } + + ServiceAccountsResponse: + type: object + properties: + service_account: + type: array + items: { $ref: '#/components/schemas/ServiceAccount' } + + User: + type: object + properties: + companies: + type: array + items: { type: integer } + departments: + type: array + items: { $ref: '#/components/schemas/UserDepartment' } + first_name: { type: string } + id: { type: integer } + is_superuser: { type: boolean } + last_name: { type: string } + positions: + type: array + items: { $ref: '#/components/schemas/UserPosition' } + service_account_id: { type: string } + username: { type: string } + + UserDepartment: + type: object + properties: + id: { type: integer } + service_account_id: { type: string } + + UserPosition: + type: object + properties: + id: { type: integer } + service_account_id: { type: string } + + Workflow: + type: object + properties: + company_id: { type: integer } + created_at: { type: string } + id: { type: string } + name: { type: string } + state: { type: string } + task_runs: + type: array + items: { type: object, additionalProperties: true } + tasks: + type: array + items: { type: object, additionalProperties: true } + updated_at: { type: string } + valid_until: { type: string } diff --git a/apps/documentations/api.CONFIGURATION.md b/apps/documentations/api.CONFIGURATION.md new file mode 100644 index 0000000..0f97489 --- /dev/null +++ b/apps/documentations/api.CONFIGURATION.md @@ -0,0 +1,345 @@ +# Конфигурация проекта documentations-api + +Документ описывает все переменные окружения и способы конфигурирования сервиса документаций (`documentation-api`). Репозиторий собирает **два бинарника/образа**, разворачиваемых в неймспейсе `documentations`: + +| Бинарник | Точка входа | Образ | Deployment | Назначение | +| --- | --- | --- | --- | --- | +| **API** | `cmd/api` | `documentations` (`cr.yandex/.../documentations-api`) | `documentations-api` | Основной REST-API: диски, документы, бандлы, права, воркспейсы, штампы, публичные ссылки и т. д. | +| **Filestream** | `cmd/filestreamer` | `documentations-api-files` (`.../documentations-filestream`) | `documentations-filestream` | Потоковая отдача/приём файлов из S3 (скачивание документов и бандлов, догрузка частей, gzip/range). | + +Оба бинарника используют **одну и ту же структуру конфигурации** (`config.Config`) — различия только в том, какие поля реально задействуются (см. раздел «Различия api и filestream»). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`github.com/kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.FromEnv()` → `envconfig.Process("", &cfg)`). + +Особенности разбора: + +- **Префикса нет** (в `envconfig.Process` передаётся пустая строка) — имена переменных плоские, задаются тегом `envconfig:"..."` у каждого поля структуры `Config`; +- **Вложенности нет** — двойных подчёркиваний/секций, как в pydantic, здесь не используется; +- **Значения по умолчанию** задаются тегом `default:"..."` прямо в структуре (например `USE_BIM_INSERTER default:"true"`). Поля без `default` и без значения в окружении получают нулевое значение типа (`""`, `0`, `false`, `nil`) — то есть формально **обязательных полей с ошибкой старта у envconfig нет**; отсутствующая переменная просто становится «пустой», а несостоятельность конфигурации всплывает позже в рантайме (например, невозможность подключиться к БД/S3); +- **Отдельного конфиг-файла (yaml/toml) у приложения нет.** Приложение **не загружает `.env` автоматически** — переменные должны быть в окружении процесса (в контейнере их проставляет Helm, локально — вручную или через `docker-compose --env-file`); +- Единственный внешний файл конфигурации — `WORKFLOWS_CONFIG_FILEPATH` (JSON с параметрами запуска задач обработки, см. `.example.tasks_execution_config.json`), читается `workflow.NewTasksExecutionConfigFromFilepath` при старте api. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. Готовый шаблон — `.docker/.env` | +| Локально (контейнеры) | `.docker/docker-compose.yml` + `make docker`: значения из `.docker/.env` и `.docker/.docker.env` | +| Kubernetes (Helm) | `.helm/values.yaml` (universal-chart): блоки `services.api.envs`/`secretEnvs` и `services.filestream.envs`/`secretEnvs` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` (окружение по ветке/тегу) и `HELM_SET_ARGS` | + +Способы запуска процессов: + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `api` (`make api` / `make run-api-dev`) | `cmd/api` | Основной REST-API | +| `filestreamer` (`make file_api` / `make run-filestreamer-dev`) | `cmd/filestreamer` | Файловый стример | +| `migrations migrate` | `cmd/migrations` | Прогон миграций БД | +| `delete_expired_public_links` | `cmd/scripts/delete_expired_public_links` | CronJob удаления просроченных публичных ссылок | +| `refresh_latest_bundle_filters_view` | `cmd/scripts/refresh_latest_bundle_filters_view` | CronJob обновления материализованного представления | +| `cleanup_failed_s32d_sessions` | `cmd/scripts/cleanup_failed_s32d_sessions` | CronJob очистки зависших s3d→ifc сессий | + +Порядок запуска в контейнере: сначала миграции, затем сервер. + +- `entrypoint.sh` (api): `migrations migrate` → `api`; +- `file_entrypoint.sh` (filestream): `migrations migrate` → `filestreamer`. + +Для локальной разработки предусмотрен hot-reload через `air`: `.air.toml` (api, `./tmp/api`) и `.air.filestreamer.toml` (filestream, `./tmp/filestreamer`). Окружение сборки описано в `flake.nix` (Go 1.22 + `air`), образы собираются с Go 1.24 (`.docker/api.dockerfile`, `.docker/api-filestream.dockerfile`). + +## HTTP-фреймворк, порты, health + +- Роутер — `gorilla/mux`, обёрнутый в `rest.NewCustomRouter` (`gitlab.sarex.io/platform/gotools/rest`). Ответы оборачиваются в JSON (`rest.JSONResponse`), включена gzip-компрессия (`gorilla/handlers.CompressHandler`). +- Пробы `liveness`/`readiness` в Helm ходят на `GET /ping` (эндпоинт предоставляется кастомным роутером `rest`, в коде маршрутов репозитория не объявлен). +- Порт api — из `API_ADDRESS`, порт filestream — из `API_ADDRESS_FILE` (в k8s оба слушают `0.0.0.0:8080`). +- Таймауты: у api `Read/WriteTimeout = 30m` (жёстко в коде), у filestream `Read/WriteTimeout = READ_WRITE_TIMEOUT_FILE_STREAM` (по умолчанию окружения — `6h`). +- Оба сервиса регистрируют `net/http/pprof` (`/debug/pprof/...`). + +## Аутентификация и авторизация + +Разбор описан в `cmd/api/bootstrap.go`, `cmd/filestreamer/main.go`, `pkg/midleware/auth.go`, `pkg/midleware/signature.go`. + +Цепочка middleware для `/api/v1/*`: `sentry` → `JSONResponse` → `reqid` → `logging` → (**только filestream**: `SignatureMiddleware`) → `auth.JWTToCtx` → `JWTUserExtractorFromCtx` → `DjangoToCtx` → `NewAuthMiddleware`/`NewAuthMiddlewareWithZitadel` → `DeleteJWTFromQueryMiddleware` → `sentry.AddUser`. + +- **JWT**: токен из заголовка `Authorization: Bearer ...` проверяется по RSA-публичному ключу (`PUBLIC_KEY`, формат PEM/PKIX). Из claims извлекаются `company_ids` и `service_accounts`. +- **Zitadel** (опционально, `USE_ZITADEL=1`): дополнительная проверка токена через Zitadel и разбор метаданных пользователя (`urn:zitadel:iam:user:metadata`, base64-поля `company_ids`/`service_accounts`). +- **Identity-заголовок**: при наличии `Identity` метаданные берутся из него. +- **Публичные ссылки/временные загрузки**: пути `/api/v1/public/...`, `/api/v1/public_link_mrpas/...` и запросы с `download_type=temporary` проверяются по HMAC-секрету `DOCUMENT_PUBLIC_LINK_JWT_SECRET`. +- **Подписанные ссылки (только filestream)**: при наличии query-параметра `signature` `SignatureMiddleware` убирает `Authorization` и проверяет подпись (`SIGNATURE_SECRET_KEY`) с `expires_at`; включается флагом `ENABLE_SIGNATURE_IN_URL` (при `true` обязателен рабочий Valkey — иначе api/filestream завершается с кодом 2). +- Маршруты `/internal/v1/*` используют облегчённую цепочку (`auth.JWTToCtxIfPossible`) без обязательной проверки. + +## Переменные приложения + +В столбце «Переменная» — точное имя (тег `envconfig`). Дефолт `—` означает, что тег `default` не задан (поле получает нулевое значение типа, если переменная не задана в окружении). + +### PostgreSQL + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | +| `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль БД | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (**только api**; filestreamer использует фиксированное значение 50) | +| `ENABLE_SSL` | bool | — | TLS-подключение к БД; при `true` используется `YC-PG-CERTIFICATE` | +| `YC-PG-CERTIFICATE` | string | — | PEM CA-сертификат PostgreSQL (имя с дефисами; в проде — из секрета `yc-pg-certificate`) | + +### S3 + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_S3` | bool | — | Включить S3. В api при `false` сервис стартует без S3-клиента; в filestream S3 нужен всегда | +| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта S3 (монтируется как файл) | +| `S3_SERVICE_ACCOUNT_STR` | string | — | Альтернатива: JSON сервис-аккаунта строкой | + +### API-адреса + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания основного API (`cmd/api`) | +| `API_ADDRESS_FILE` | string | — | Адрес прослушивания файлового стримера (`cmd/filestreamer`) | + +### Sarex backend (Django) и Zitadel + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_HOST` | string | — | Базовый URL Sarex backend (Django). Используется клиентами `django`, `users`, `sarex_backend`, `accounts` | +| `DJANGO_BASIC_AUTH` | string | — | Basic-auth для системных вызовов Django | +| `DJANGO_BASIC_AUTH_FOR_GET_USER` | string | — | Отдельный basic-auth для запросов пользователей | +| `DJANGO_ORIGINATOR` | string | — | Идентификатор источника запросов | +| `USE_ZITADEL` | bool | — | Включить проверку токенов через Zitadel | +| `ZITADEL_DOMAIN` | string | — | Домен Zitadel (IdP) | +| `ZITADEL_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта Zitadel | + +### Внешние сервисы (базовые URL) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `FILE_URL_EXTERNAL` | string | — | Внешний URL файлового сервиса | +| `DOCUMENTATION_URL` | string | — | URL самого сервиса документаций (для внутренних ссылок) | +| `WORKFLOW_URL` | string | — | URL сервиса workflows (создание/чтение процессов обработки) | +| `WORKSPACE_URL` | string | — | URL сервиса воркспейсов | +| `WORKSPACE_V2_EXTERNAL_URL` | string | — | Внешний URL воркспейсов v2 | +| `WORKSPACE_BUNDLE_VERSION` | string | — | Версия бандла воркспейса (`v1`) | +| `MARKS_PROCESSING_URL` | string | — | URL сервиса штампов/маркировок (при HTTP-режиме) | +| `BIM_API_URL` | string | — | URL BIM-API v1 | +| `BIM_API_V2_URL` | string | — | URL BIM-API v2 (bim-core-api) | +| `BIM_API_URL_EXTERNAL` | string | — | Внешний URL BIM-API | +| `SYSTEM_LOG_URL` | string | — | URL сервиса системного лога | +| `FLOWS_URL` | string | — | URL сервиса flows | +| `AUTOMATION_URL` | string | — | URL сервиса автоматизаций | +| `TRANSMITTALS_BASE_URL` | string (nullable) | `nil` | URL сервиса трансмитталов; клиент создаётся только если переменная задана | + +### Публичные ссылки и JWT + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PUBLIC_LINK_HOST` | string | — | Хост публичных ссылок на документы | +| `PUBLIC_KEY` | string | — | RSA-публичный ключ (PEM/PKIX) для проверки JWT | +| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | string | — | HMAC-секрет для JWT публичных ссылок и временных загрузок | +| `DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES` | uint8 | — | Время жизни JWT публичной ссылки (мин.) | +| `PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED` | bool | `true` | Коннектор публичных ссылок для папок | + +### Подпись ссылок (signature-in-URL) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_SIGNATURE_IN_URL` | bool | `false` | Проверять подпись в URL. При `true` требуется рабочий Valkey (иначе сервис завершается с кодом 2) | +| `SIGNATURE_SECRET_KEY` | string | `""` | Секрет для подписи ссылок скачивания | +| `SIGNATURE_IN_URL_EXPIRATION_SECONDS` | uint64 | `600` | Срок жизни подписи (сек.) | +| `ENABLE_AUTH_JWT_IN_URL` | bool | `true` | Добавлять `auth_jwt` в URL | + +### Valkey (Redis-совместимый) — кэши + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `VALKEY_ADDR` | string | `localhost:6380` | Адрес `host:port`. Пустая строка полностью отключает клиент Valkey | +| `VALKEY_LOGIN` | string | `""` | Логин | +| `VALKEY_HOST` | string | `""` | Хост (доп. поле) | +| `VALKEY_PASSWORD` | string | `""` | Пароль | +| `VALKEY_DB` | int | `0` | Номер БД Redis/Valkey | +| `VALKEY_CACHE_TTL` | duration | `1h` | TTL кэша | +| `VALKEY_SSL` | bool | `false` | TLS-подключение | +| `VALKEY_SSL_CA_CERTS` | string | `""` | CA-сертификат для TLS | + +### Файловый стример и in-memory кэш + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `READ_WRITE_TIMEOUT_FILE_STREAM` | duration | — | Read/Write-таймаут HTTP-сервера filestream (в окружении — `6h`) | +| `USE_CACHE_IN_FILE_STREAMER` | bool | — | Включить in-memory кэш в filestream-хранилище | +| `CACHE_DEFAULT_EXPIRATION` | duration | — | TTL записей кэша | +| `CACHE_CLEANUP_INTERVAL` | duration | — | Интервал очистки кэша | + +### BIM / обработка файлов + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_BIMV1_FOR_BIMV2` | bool | — | Использовать BIM v1 для v2 | +| `USE_BIM_INSERTER` | bool | `true` | Включить BIM-inserter | +| `USE_LEGACY_BIM_FLOW` | bool | `false` | Старый flow BIM | +| `LAST_MASTER_BIM` | uint64 | — | Граница master-BIM | +| `LAST_SLAVE_1_BIM` | uint64 | — | Граница slave-1-BIM | +| `LAST_SLAVE_2_BIM` | uint64 | — | Граница slave-2-BIM | +| `CONVERT_DWG_TO_GEOJSON` | bool | `true` | Конвертация DWG→GeoJSON | +| `CONVERT_DXF_TO_GEOJSON` | bool | `true` | Конвертация DXF→GeoJSON | +| `IS_CONVERTED_PDF_UPLOADING_TO_S3` | bool | `true` | Загружать сконвертированный PDF в S3 | +| `DELETE_S3D_AFTER_MESHOPT` | bool | `false` | Удалять s3d после mesh-оптимизации | +| `WORKFLOW_IMAGES_VERSION` | string | — | Тег образов workflow | +| `WORKFLOWS_IMAGES_VERSION` | string | — | Тег образов задач обработки (используется клиентом `workflow`) | +| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр контейнеров для образов задач | +| `WORKFLOWS_CONFIG_FILEPATH` | string | `.example.tasks_execution_config.json` | Путь к JSON с ресурсами задач обработки | + +### Штампы/маркировки (HTTP или RabbitMQ) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_MARKS_RABBITMQ` | bool | `0` | `0` — ходить в `MARKS_PROCESSING_URL` по HTTP; `1` — через RabbitMQ | +| `MARKS_RABBITMQ_HOST` | string | `""` | Хост RabbitMQ | +| `MARKS_RABBITMQ_PORT` | string | `""` | Порт RabbitMQ | +| `MARKS_RABBITMQ_USER` | string | `""` | Пользователь (из секрета) | +| `MARKS_RABBITMQ_PASSWORD` | string | `""` | Пароль (из секрета) | +| `MARKS_RABBITMQ_API` | string | `""` | Vhost/имя очереди | + +### Rate limit эндпоинта метаданных + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `METADATA_RATE_LIMIT_ENABLED` | bool | `true` | Включить rate-limit для `/documents/metadata` | +| `METADATA_RATE_LIMIT_MAX_REQUESTS` | int | `10` | Макс. число запросов | + +### Почта + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_MAILGUN` | bool | `true` | Флаг использования Mailgun | +| `ENABLE_SMTP` | bool | `false` | Флаг использования SMTP | + +### Наблюдаемость + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENVIRONMENT` | string | — | Окружение (для Sentry/трейсинга) | +| `SENTRY_DSN` | string | — | DSN Sentry | +| `SENTRY_DEBUG` | bool | — | Отладка Sentry | +| `NAMESPACE` | string | — | Неймспейс (для контекста запусков задач) | +| `ENABLE_SQL_QUERY` | bool | — | Логировать SQL-запросы | +| `TRACER_USE` | bool | `false` | Включить OpenTelemetry-трейсинг | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACER_USE_INSECURE` | bool | `true` | Подключение к коллектору без TLS | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя логгера трейсинга | +| `SERVICE_NAME` | string | `documentations-api` | Имя сервиса в трейсах (используется api) | +| `SERVICE_NAME_FILESTREAM` | string | `filestream-api` | Имя сервиса в трейсах (используется filestream) | + +## Различия api и filestream + +Оба процесса читают одну и ту же структуру `config.Config`, но: + +| Аспект | api (`cmd/api`) | filestream (`cmd/filestreamer`) | +| --- | --- | --- | +| Слушает адрес из | `API_ADDRESS` | `API_ADDRESS_FILE` | +| Пул соединений к БД | `POSTGRES_POOL_SIZE` | фиксировано `50` (+ `PoolTimeout=1m`), `POSTGRES_POOL_SIZE` игнорируется | +| Read/Write-таймаут сервера | жёстко `30m` | `READ_WRITE_TIMEOUT_FILE_STREAM` | +| S3 | опционален (`ENABLE_S3`) | обязателен (при отсутствии кредов процесс завершается) | +| In-memory кэш хранилища | выключен | управляется `USE_CACHE_IN_FILE_STREAMER` / `CACHE_*` | +| Имя сервиса в трейсах | `SERVICE_NAME` | `SERVICE_NAME_FILESTREAM` | +| Signature-middleware на `/api/v1` | нет | есть (`SignatureMiddleware`) | +| Набор маршрутов | полный REST CRUD (`cmd/api/routes_api.go`, `routes_internal.go`) | только потоковые скачивания/загрузки файлов (`cmd/filestreamer/routes_api.go`, `routes_internal.go`) | + +**Что делает filestream-бинарник.** Это отдельный HTTP-сервис для тяжёлой потоковой работы с файлами, вынесенный из основного API, чтобы не блокировать его долгими соединениями (отсюда таймаут в часы и увеличенный пул БД). Публичные маршруты (`/api/v1`): + +- `GET /documents/folders` — скачивание нескольких папок архивом (gzip); +- `GET|HEAD|POST /bundles/...` — скачивание файлов бандла; при `?format=gz` отдаётся без повторного сжатия, иначе — `CompressHandler`; поддержаны HEAD (range) и внешний матчер `DownloadMatcherExternal`; +- `GET|HEAD|POST /pages/...` — скачивание страниц (постранично); +- `GET|HEAD /documents/...` — скачивание по документам; `POST /documents/...` — скачивание по списку bundle-id; +- `POST /bundles_mrpas/...` и `GET /public_link_mrpas/...` — выгрузка MRPA (в т. ч. по публичной ссылке). + +Внутренние маршруты (`/internal/v1`): скачивание/загрузка бандлов между сервисами и `POST /upload_finish/bundles/...`. + +## Переменные сборки и запуска (не читаются кодом приложения) + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `APP_VERSION` | `Makefile`, dockerfiles | Версия, зашиваемая в бинарь (`-ldflags -X main.version`) | +| `CI_COMMIT_SHORT_SHA` | dockerfiles | Тег версии образа files | +| `GITLAB_CREDENTIALS` | dockerfiles (build-arg) | Доступ к приватным Go-модулям `gitlab.sarex.io` | +| `API_VERSION` | `.docker/docker-compose.yml` | Тег локально запускаемого образа | +| `POSTGRES_EXTERNAL_PORT` | `.docker/docker-compose.yml` | Внешний порт локального Postgres | +| `KEY_JWT` / `JWT_KEY` | `.docker/.env`, docker-compose | JWT-ключ **только для интеграционных тестов**, кодом приложения не читается | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт основан на `universal-chart` (зависимость из `Chart.yaml`) и описывает два сервиса — `services.api` и `services.filestream`. У каждого свои блоки `envs` (обычные значения, с разбивкой по окружениям `_default`/`stage`/`preprod`/`production`) и `secretEnvs` (значения из k8s-секретов). Наборы переменных у обоих сервисов практически идентичны. + +Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `SIGNATURE_SECRET_KEY` | `documentations-download-secret` | `secret` | +| `VALKEY_ADDR` | `valkey-secret` | `url` | +| `VALKEY_LOGIN` | `valkey-secret` | `login` | +| `VALKEY_PASSWORD` | `valkey-secret` | `password` | +| `VALKEY_HOST` | `valkey-secret` | `host` | +| `VALKEY_PORT` | `valkey-secret` | `port` | +| `VALKEY_CA_CERTS` | `valkey-secret` | `cert` | +| `POSTGRES_USER` | `documentations-postgresql-secret` | `user` | +| `POSTGRES_PORT` | `documentations-postgresql-secret` | `port` | +| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `host` | +| `POSTGRES_DB` | `documentations-postgresql-secret` | `database` | +| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `password` | +| `DJANGO_BASIC_AUTH` | `django-auth` | `key` | +| `DJANGO_BASIC_AUTH_FOR_GET_USER` | `django-auth-get-user` | `key` | +| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | `yc-jwt-secret` | `secret` | +| `PUBLIC_KEY` | `public-key` | `key` | +| `MARKS_RABBITMQ_USER` | `cde-rabbitmq-secret` (в api; prod — `marks-rabbit-secret`) / `marks-rabbit-secret` (в filestream) | `user` | +| `MARKS_RABBITMQ_PASSWORD` | то же | `password` | +| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `certificate` | + +Тома (`volumes`) монтируют секреты как файлы: `documentations-yc-s3` → `/etc/sarex/yc-s3-storage` (на него указывает `S3_SERVICE_ACCOUNT`), `zitadel-account` → `/etc/sarex/zitadel` (на него указывает `ZITADEL_ACCOUNT`). Файл `WORKFLOWS_CONFIG_FILEPATH` в проде — `/etc/app/tasks_execution_config.json`. + +Ingress включён только у `filestream` (`ingress.enabled: false` по умолчанию, path `/files/api/` → rewrite `/api/`); у api ingress-блока в values нет — сервис доступен через `documentations-api-svc`. + +Прочие значения чарта (не переменные приложения): `deployment.*` (реплики stage/preprod/prod = 1/3/6, ресурсы, revisionHistoryLimit), `probes` (`/ping`), `service.*`, `serviceAccount`, `imagePullSecrets: dockerhub`, `affinity` (podAntiAffinity у filestream), а также блок `cronjobs` (`delete_expired_public_links`, `refresh_latest_bundle_filters_view`, `refresh_string_path_materialized_view`). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Окружение переключается по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | universal-chart env | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `documentations` | `stage` | +| ветка `master` | `preprod` | `documentations-preprod` | `preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `production` | +| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Общие для всех окружений: `RELEASE_NAME=documentations`, `CHART_NAME=documentations`, `SERVICE_NAME: documentations`, `DOCKERFILE_PATH: .docker/api.dockerfile`, `IMAGE_PATH: api.deployment.image`. Через `HELM_SET_ARGS` проставляются образы: `services.api.image` (основной), `services.filestream.image` (`IMAGE_NAME_API_FILES`, dockerfile `.docker/api-filestream.dockerfile`), а также образы кронджоб `cronjobs.delete_expired_public_links`, `cronjobs.refresh_latest_bundle_filters_view`, `cronjobs.cleanup_failed_s32d_sessions`. Дополнительные образы собираются отдельными job-ами `build_files`, `build_public_link_autodeletion`, `build_refresh_latest_bundle_filters_view`, `build_cleanup_failed_s32d_sessions` (только на `stage`/`master`/тег). + +## Замечания и потенциальные проблемы + +- **У envconfig нет «обязательных» полей.** Отсутствующая переменная без `default` становится нулевым значением, ошибка старта не выбрасывается. Некорректная конфигурация проявляется в рантайме (не удаётся подключиться к БД/S3, невалидный `PUBLIC_KEY` при первой проверке JWT и т. п.). +- **`.env` не подхватывается автоматически** — приложение читает только окружение процесса. Локально удобнее запускать через `docker-compose --env-file` (`make docker`) или экспортировать `.docker/.env` вручную. +- **`POSTGRES_POOL_SIZE` игнорируется в filestream** — там пул жёстко задан как `50`. В api берётся из переменной. +- **`YC-PG-CERTIFICATE`** — имя с дефисами (не в стиле `SNAKE_CASE`), но envconfig читает его по точному тегу. Значение приходит из секрета и используется как содержимое PEM (`AppendCertsFromPEM`), а не как путь к файлу. +- **Рассинхрон имён Valkey.** В Helm задаются `VALKEY_PORT` и `VALKEY_CA_CERTS`, но код читает `VALKEY_ADDR` (host:port одной строкой) и `VALKEY_SSL_CA_CERTS`. Переменные `VALKEY_PORT`/`VALKEY_CA_CERTS` приложением напрямую не читаются (адрес и CA берутся из `VALKEY_ADDR`/`VALKEY_SSL_CA_CERTS`). +- **`HOST` и `DOCUMENTATION_EXTERNAL_URL` из Helm кодом не читаются** — в `config.Config` таких полей нет (для внутренних ссылок используется `DOCUMENTATION_URL`, `FILE_URL_EXTERNAL`, `PUBLIC_LINK_HOST`). +- **`ENABLE_SIGNATURE_IN_URL=true` требует Valkey.** Если клиент Valkey не инициализировался, а флаг включён, оба процесса завершаются с кодом `2`. +- **Флаги `ENABLE_MAILGUN`/`ENABLE_SMTP`** присутствуют в конфиге и Helm, но собственной отправкой почты сервис не занимается (в отличие от transmittal-api); это флаги для внешних интеграций. +- **`TRANSMITTALS_BASE_URL` — nullable.** Клиент трансмитталов создаётся только если переменная задана; иначе связанные вызовы пропускаются. +- **Штампы: HTTP vs RabbitMQ.** При `USE_MARKS_RABBITMQ=1` используется RPC-клиент через RabbitMQ (`MARKS_RABBITMQ_*`), при `0` — HTTP-клиент на `MARKS_PROCESSING_URL`. В values для prod-секретов имя `marks-rabbit-secret` отличается от stage/preprod (`cde-rabbitmq-secret`). + +## Минимальный набор для локального запуска + +Ориентир — `.docker/.env` (+ `make docker` для запуска в контейнерах вместе с Postgres). Минимально нужно задать: + +- `API_ADDRESS`, `API_ADDRESS_FILE`; +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL=0`; +- `ENABLE_S3` (`0` для api без S3; для filestream — `1` и `S3_SERVICE_ACCOUNT`/`S3_SERVICE_ACCOUNT_STR`); +- `DJANGO_HOST`, `DJANGO_ORIGINATOR`, `NAMESPACE`; +- сервисные URL по необходимости: `WORKFLOW_URL`, `WORKSPACE_URL`, `SYSTEM_LOG_URL`, `FLOWS_URL`, `MARKS_PROCESSING_URL`; +- для filestream: `READ_WRITE_TIMEOUT_FILE_STREAM`, `USE_CACHE_IN_FILE_STREAMER`, `CACHE_DEFAULT_EXPIRATION`, `CACHE_CLEANUP_INTERVAL`; +- `ENABLE_SQL_QUERY=1` (для отладки), `TRACER_USE=false`; +- `PUBLIC_KEY`/`DOCUMENT_PUBLIC_LINK_JWT_SECRET` — для реальной проверки JWT (локально можно оставить пустыми, но защищённые ручки будут отклонять токены). + +Готовый пример со всеми значениями приведён в `api.env.example` (рядом с этим документом) и в `.docker/.env` исходного репозитория. diff --git a/apps/documentations/api.ENDPOINTS.md b/apps/documentations/api.ENDPOINTS.md new file mode 100644 index 0000000..bcdd28a --- /dev/null +++ b/apps/documentations/api.ENDPOINTS.md @@ -0,0 +1,114 @@ +# Эндпоинты внешних сервисов, с которыми взаимодействует documentations-api + +Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается сервис документаций (оба бинарника — `cmd/api` и `cmd/filestreamer`). В отличие от фронтенда, единого декларативного реестра эндпоинтов здесь нет — каждый внешний сервис инкапсулирован в собственном клиенте в каталогах `clients/` и `pkg/`. + +## Как устроено взаимодействие + +Клиенты создаются при старте (`cmd/api/routes_api.go`, `cmd/filestreamer/*`) и используют базовый URL из соответствующей переменной окружения (см. `config/config.go`). Большинство клиентов построены на `go-resty/resty` (метод `SetHostURL`/`SetBaseURL`), часть — на внутренних http-обёртках `gitlab.sarex.io/platform/gotools`. Итоговый URL = `<базовый URL сервиса>` + путь из клиента. + +Аутентификация исходящих запросов: + +- к Sarex backend (Django) — HTTP Basic (`DJANGO_BASIC_AUTH`, а для получения пользователей — `DJANGO_BASIC_AUTH_FOR_GET_USER`); для части ручек проксируется заголовок `Identity`/`Bearer`; +- к остальным сервисам — по внутренней сети кластера, как правило без внешней авторизации. + +## Базовые URL по сервисам + +| Сервис | Переменная окружения | Клиент (каталог) | +| --- | --- | --- | +| Sarex backend (Django) | `DJANGO_HOST` | `clients/django`, `pkg/django`, `pkg/users`, `pkg/sarex_backend`, `clients/accounts` | +| Flows | `FLOWS_URL` | `pkg/flows` | +| Workflows | `WORKFLOW_URL` | `clients/workflow`, `pkg/workflows` | +| Workspaces | `WORKSPACE_URL` | `clients/workspace` | +| Transmittals | `TRANSMITTALS_BASE_URL` (опц.) | `pkg/transmittal` | +| Automation | `AUTOMATION_URL` | `pkg/automation` | +| Marks (штампы, HTTP) | `MARKS_PROCESSING_URL` | `pkg/marks/base` | +| Marks (штампы, RabbitMQ) | `MARKS_RABBITMQ_*` | `pkg/marks/rpc` | +| BIM-API v1 | `BIM_API_URL` | `clients/bim-api` | +| BIM-API v2 (bim-core-api) | `BIM_API_V2_URL` | `clients/bim-api-v2` | +| System log | `SYSTEM_LOG_URL` | `pkg/system_log` | + +Дополнительно сервис работает с S3 (объектное хранилище, креды из `S3_SERVICE_ACCOUNT`/`S3_SERVICE_ACCOUNT_STR`) и PostgreSQL — это не HTTP-сервисы и в таблицах ниже не приводятся. + +## Эндпоинты по сервисам + +### Sarex backend (Django) — `DJANGO_HOST` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| GET | `/api/core/users/` | `clients/django/users.go` | Список пользователей | +| GET | `/api/core/users/{id}/` | `clients/django/users.go`, `pkg/users/client.go` | Пользователь по id | +| GET | `/api/core/users/{id}/introspect` | `clients/django/users.go` | Интроспекция пользователя | +| GET | `/api/core/users/{id}` | `pkg/django/client.go` | Пользователь по id (внутренний клиент) | +| GET | `/api/client/settings/` | `clients/django/settings.go` | Клиентские настройки | +| GET | `/api/core/service-accounts/personalized/` | `clients/django/settings.go` | Персонализированные сервисные аккаунты | +| GET | `/api/core/service_accounts/` | `clients/django/service_accounts.go`, `clients/accounts` | Сервисные аккаунты | +| GET | `/api/core/companies/` | `clients/django/companies.go` | Список компаний | +| GET | `/api/core/mrpa/{id}/` | `pkg/sarex_backend/client.go` | MRPA по id (прокидывается заголовок `Identity`) | + +### Flows — `FLOWS_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| GET | `internal/v1/documents/?full=true&document_ids={id}` | `pkg/flows/client.go` | Документы в процессах (flows) по id | +| GET/POST | `internal/v1/documents/` | `pkg/flows/client.go` | Документы процессов | + +### Workflows — `WORKFLOW_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `internal/v1/companies/{company_id}/workflows` | `clients/workflow/client.go` | Создать workflow обработки (BIM/PDF/DWG/DEM/DOCX и т. д.); образы задач — из `CONTAINER_REGISTRY` + `WORKFLOWS_IMAGES_VERSION` | +| GET | `v1/workflows/{id}` | `pkg/workflows/client.go` | Прочитать workflow по id | + +### Workspaces — `WORKSPACE_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `internal/v2/workspaces` | `clients/workspace/client.go` | Создать воркспейс | +| DELETE | `internal/v2/documents/{ids}` | `clients/workspace/client.go` | Удалить документы воркспейса | +| PATCH | `internal/v2/documents/restore` | `clients/workspace/client.go` | Восстановить документы воркспейса | + +### Transmittals — `TRANSMITTALS_BASE_URL` + +Клиент создаётся только если переменная задана. + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `/internal/v1/transmittals/by_bundle_ids` | `pkg/transmittal/client.go` | Трансмитталы по списку bundle-id | + +### Automation — `AUTOMATION_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| — | `/internal/v1/automations/{process_name}` | `pkg/automation/client.go` | Запуск/получение автоматизации по имени процесса | + +### Marks (штампы/маркировки) + +Режим выбирается флагом `USE_MARKS_RABBITMQ`. + +| Метод | Путь / транспорт | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `/api/v1/marks/{bundle_id}` (HTTP, `MARKS_PROCESSING_URL`) | `pkg/marks/base/client.go` | Наложение штампов на бандл (HTTP-режим) | +| — | RabbitMQ (`MARKS_RABBITMQ_*`) | `pkg/marks/rpc` | Наложение штампов через очередь (RPC-режим, не HTTP) | + +### BIM-API v1 — `BIM_API_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `/internal/v1/targets/{target_id}/bims-pdm` | `clients/bim-api/client.go` | Создать BIM для target (PDM) | +| POST | `/internal/v1/targets/{target_id}/bims-v2-pdm` | `clients/bim-api/client.go` | Создать BIM v2 для target (PDM) | + +### BIM-API v2 (bim-core-api) — `BIM_API_V2_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `/internal/v1/projects/{project_id}/bims` | `clients/bim-api-v2/client.go` | Создать BIM для проекта | + +### System log — `SYSTEM_LOG_URL` + +| Метод | Путь | Клиент | Назначение | +| --- | --- | --- | --- | +| POST | `/api/v0/system_log` | `pkg/system_log/client.go` | Отправить запись в системный лог | + +## Обработка ошибок + +Клиенты, как правило, проверяют код ответа и оборачивают ошибку через `github.com/rotisserie/eris` (напр. «invalid response code %d expected 200»). Для случая недоступности исходного сервиса в самом API определён нестандартный статус `523` (`network/consts.go`, `StatusOriginIsUnreachable`). diff --git a/apps/documentations/api.env.example b/apps/documentations/api.env.example new file mode 100644 index 0000000..2ccc24b --- /dev/null +++ b/apps/documentations/api.env.example @@ -0,0 +1,131 @@ +# ============================================================================= +# documentations-api / documentations-filestream — пример переменных окружения +# ============================================================================= +# Все переменные читаются напрямую из окружения процесса библиотекой +# kelseyhightower/envconfig (config/config.go). Префикса и вложенности НЕТ — +# имена плоские. Значения ниже — ориентир для локального запуска (аналог +# .docker/.env). Оба бинарника (api и filestreamer) используют один и тот же +# набор переменных. + +# --- Адреса прослушивания ---------------------------------------------------- +API_ADDRESS=0.0.0.0:6666 # порт основного API (cmd/api) +API_ADDRESS_FILE=0.0.0.0:7777 # порт файлового стримера (cmd/filestreamer) + +# --- PostgreSQL -------------------------------------------------------------- +POSTGRES_ADDRESS=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_DB=documentations +POSTGRES_POOL_SIZE=10 # учитывается только в api; filestreamer жёстко использует 50 +ENABLE_SSL=0 # 1 — TLS к БД, тогда нужен YC-PG-CERTIFICATE +# YC-PG-CERTIFICATE= # PEM CA-сертификат PostgreSQL (в проде — из секрета) + +# --- S3 (объектное хранилище) ------------------------------------------------ +ENABLE_S3=0 # в api при 0 сервис стартует без S3; в filestreamer S3 обязателен +S3_SERVICE_ACCOUNT=/etc/sarex/yc_s3_doc_account.json # путь к JSON сервис-аккаунта +# S3_SERVICE_ACCOUNT_STR= # альтернатива: сам JSON строкой + +# --- Sarex backend (Django) -------------------------------------------------- +DJANGO_HOST=https://stage.sarex.io +DJANGO_BASIC_AUTH= # basic-auth для системных вызовов Django +DJANGO_BASIC_AUTH_FOR_GET_USER= # отдельный basic-auth для получения пользователей +DJANGO_ORIGINATOR=docs_local + +# --- Zitadel (опциональная аутентификация) ----------------------------------- +USE_ZITADEL=0 +ZITADEL_DOMAIN=idp.dev.stage.sarex.io +ZITADEL_ACCOUNT=/etc/sarex/zitadel/zitadel-account.json + +# --- Внешние сервисы (базовые URL) ------------------------------------------- +FILE_URL_EXTERNAL=https://stage-api.sarex.io/files +DOCUMENTATION_URL=http://localhost:8000/ +WORKFLOW_URL=https://stage-api.sarex.io/workflows/api +WORKSPACE_URL=https://stage-api.sarex.io/worspace/api +WORKSPACE_V2_EXTERNAL_URL=https://stage.sarex.io/workspaces-v2/ +WORKSPACE_BUNDLE_VERSION=v1 +MARKS_PROCESSING_URL=http://marks-service.documentations:8000 +BIM_API_URL=http://bim-api-service.bim-api-stage/ +BIM_API_V2_URL=http://bim-core-api.platform.svc.cluster.local:8000/ +BIM_API_URL_EXTERNAL=https://stage-api.sarex.io/bim +SYSTEM_LOG_URL=http://localhost:8888 +FLOWS_URL=http://backend-service.proc.svc.cluster.local:8000 +AUTOMATION_URL=http://automation-api-service.automation-stage.svc.cluster.local:8000 +# TRANSMITTALS_BASE_URL=http://transmittal-service.documentations:8000 # опционально + +# --- Публичные ссылки на документы ------------------------------------------- +PUBLIC_LINK_HOST=https://document-link.stage.sarex.io +PUBLIC_KEY= # PEM RSA public key (PKIX) для проверки JWT +DOCUMENT_PUBLIC_LINK_JWT_SECRET= # секрет HMAC для JWT публичных ссылок/временных загрузок +DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES=5 +PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED=1 + +# --- Подпись ссылок скачивания (signature-in-URL) ---------------------------- +ENABLE_SIGNATURE_IN_URL=false # при true обязателен рабочий Valkey и SIGNATURE_SECRET_KEY +SIGNATURE_SECRET_KEY= +SIGNATURE_IN_URL_EXPIRATION_SECONDS=600 +ENABLE_AUTH_JWT_IN_URL=true + +# --- Valkey / Redis (кэши, кэш подписей) ------------------------------------- +VALKEY_ADDR=localhost:6380 # host:port; пустая строка полностью отключает клиент +VALKEY_LOGIN= +VALKEY_PASSWORD= +VALKEY_HOST= +VALKEY_DB=0 +VALKEY_CACHE_TTL=1h +VALKEY_SSL=false +VALKEY_SSL_CA_CERTS= + +# --- Файловый стример / кэш -------------------------------------------------- +READ_WRITE_TIMEOUT_FILE_STREAM=6h +USE_CACHE_IN_FILE_STREAMER=true +CACHE_DEFAULT_EXPIRATION=30s +CACHE_CLEANUP_INTERVAL=35s + +# --- BIM / обработка --------------------------------------------------------- +USE_BIMV1_FOR_BIMV2=1 +USE_BIM_INSERTER=1 +USE_LEGACY_BIM_FLOW=0 +LAST_MASTER_BIM=1541 +LAST_SLAVE_1_BIM=5000 +LAST_SLAVE_2_BIM=15000 +CONVERT_DWG_TO_GEOJSON=true +CONVERT_DXF_TO_GEOJSON=true +IS_CONVERTED_PDF_UPLOADING_TO_S3=true +DELETE_S3D_AFTER_MESHOPT=false +WORKFLOW_IMAGES_VERSION=develop +WORKFLOWS_IMAGES_VERSION=develop +CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q +WORKFLOWS_CONFIG_FILEPATH=.example.tasks_execution_config.json + +# --- Штампы/маркировки: HTTP или RabbitMQ ------------------------------------ +USE_MARKS_RABBITMQ=0 # 0 — ходить в MARKS_PROCESSING_URL по HTTP; 1 — через RabbitMQ +MARKS_RABBITMQ_HOST= +MARKS_RABBITMQ_PORT= +MARKS_RABBITMQ_USER= +MARKS_RABBITMQ_PASSWORD= +MARKS_RABBITMQ_API= + +# --- Rate limit для /documents/metadata -------------------------------------- +METADATA_RATE_LIMIT_ENABLED=true +METADATA_RATE_LIMIT_MAX_REQUESTS=10 + +# --- Почта (флаги; сама отправка идёт через внешние сервисы) ------------------ +ENABLE_MAILGUN=true +ENABLE_SMTP=false + +# --- Наблюдаемость ----------------------------------------------------------- +ENVIRONMENT=stage +SENTRY_DSN= +SENTRY_DEBUG=0 +NAMESPACE=local +ENABLE_SQL_QUERY=1 # логировать SQL-запросы (для локальной отладки) +TRACER_USE=false +TRACER_HOST=localhost:4317 +TRACER_USE_INSECURE=true +TRACER_LOGGER_NAME=tracer_logger +SERVICE_NAME=documentations-api +SERVICE_NAME_FILESTREAM=filestream-api + +# --- Только для интеграционных тестов (кодом приложения не читается) ---------- +# KEY_JWT=example_jwt diff --git a/apps/documentations/api.openapi.yaml b/apps/documentations/api.openapi.yaml new file mode 100644 index 0000000..da9a545 --- /dev/null +++ b/apps/documentations/api.openapi.yaml @@ -0,0 +1,785 @@ +openapi: 3.0.3 +info: + title: Documentations API + version: "1.0" + description: | + REST-API сервиса документаций (`documentation-api`, бинарник `cmd/api`, образ + `documentations`, deployment `documentations-api` в неймспейсе `documentations`). + + Спецификация реконструирована из исходного кода маршрутов + (`cmd/api/routes_api.go`, `cmd/api/routes_internal.go`) и middleware + (`cmd/api/bootstrap.go`, `pkg/midleware/*`). В репозитории **нет сгенерированного + swagger/openapi**, поэтому схемы тел запросов/ответов приведены обобщённо + (в коде они не описаны декларативно). Пути, методы и параметры пути — + достоверные, из роутера `gorilla/mux`. + + ## Базовые пути + - Публичный API: `/api/v1` (описан ниже). + - Внутренний API: `/internal/v1` (сервис-к-сервису, облегчённая авторизация; + здесь не детализируется — см. `cmd/api/routes_internal.go`). + - Потоковая отдача/приём файлов вынесены в **отдельный сервис `filestream`** + (`cmd/filestreamer`, образ `documentations-api-files`): `/api/v1/bundles/...`, + `/api/v1/documents/...`, `/api/v1/pages/...`, `/api/v1/documents/folders`, + `/api/v1/bundles_mrpas/...`, `/api/v1/public_link_mrpas/...`. + - Health-check: `GET /ping` (предоставляется каркасом роутера `rest`). + - Профилирование: `GET /debug/pprof/...` (net/http/pprof). + + ## Аутентификация + Основной способ — JWT в заголовке `Authorization: Bearer `, проверяемый по + RSA-публичному ключу (`PUBLIC_KEY`, PEM/PKIX). Из claims извлекаются `company_ids` + и `service_accounts`. Опционально включается проверка через Zitadel (`USE_ZITADEL`), + а также разбор заголовка `Identity`. Часть ручек (пути `/public/...`, + `/public_link_mrpas/...` и запросы с `download_type=temporary`) авторизуются по + HMAC-JWT публичных ссылок (`DOCUMENT_PUBLIC_LINK_JWT_SECRET`). В сервисе filestream + ссылки скачивания дополнительно подписываются (`signature` + `expires_at` в query, + секрет `SIGNATURE_SECRET_KEY`). + + ## Формат ошибок + Ответы оборачиваются middleware `rest.JSONResponse`; ошибки возвращаются в JSON. + Нестандартный код `523` (`StatusOriginIsUnreachable`, `network/consts.go`) + используется, когда исходный сервис недоступен. + + ## Пагинация + Единого декларативного механизма пагинации в роутере нет; списки, где она нужна, + принимают параметры фильтрации в теле POST-запроса (напр. `/documents/metadata`, + `/documents/batch`). Эндпоинт `/documents/metadata` дополнительно ограничивается + rate-limit (`METADATA_RATE_LIMIT_*`). + +servers: + - url: https://api.sarex.io/documentations/api/v1 + description: production + - url: https://api.preprod.sarex.io/documentations/api/v1 + description: preprod + - url: https://stage-api.sarex.io/documentations/api/v1 + description: stage + +security: + - bearerAuth: [] + +tags: + - name: disks + - name: documents + - name: bundles + - name: uploads + - name: permissions + - name: workspaces + - name: dashboards + - name: workflows + - name: pages + - name: marks + - name: public-links + - name: related-documents + - name: changelogs + - name: favorite-documents + - name: name-templates + - name: misc + +paths: + /conversion: + post: + tags: [misc] + summary: Запустить конвертацию документа + responses: + "200": { $ref: "#/components/responses/Ok" } + + /disks: + get: + tags: [disks] + summary: Список дисков (доступных пользователю) + responses: + "200": { $ref: "#/components/responses/Ok" } + post: + tags: [disks] + summary: Создать диск (требуются права администратора) + responses: + "200": { $ref: "#/components/responses/Ok" } + "403": { $ref: "#/components/responses/Forbidden" } + /disks/{disk_id}: + delete: + tags: [disks] + summary: Удалить диск (требуются права администратора) + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + "403": { $ref: "#/components/responses/Forbidden" } + /disks/{disk_id}/documents: + get: + tags: [disks, documents] + summary: Документы диска + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + post: + tags: [disks, documents] + summary: Документы диска по списку id + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /disks/{disk_id}/projects: + get: + tags: [disks] + summary: Проекты диска + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /disks/{disk_id}/service_accounts: + get: + tags: [disks, permissions] + summary: Сервисные аккаунты диска + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /disks/{disk_id}/size_migration: + get: + tags: [misc] + summary: Миграция размеров (служебное) + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /disks/{disk_id}/delete_documents_from_ws_migration: + get: + tags: [misc] + summary: Удаление документов при миграции воркспейса (служебное) + parameters: [{ $ref: "#/components/parameters/DiskId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + + /documents: + post: + tags: [documents] + summary: Создать документ/папку + responses: + "200": { $ref: "#/components/responses/Ok" } + delete: + tags: [documents] + summary: Массовое удаление документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/create_report: + post: + tags: [documents] + summary: Сформировать отчёт по метаданным документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/metadata: + post: + tags: [documents] + summary: Список метаданных документов (rate-limited) + responses: + "200": { $ref: "#/components/responses/Ok" } + "429": { $ref: "#/components/responses/TooManyRequests" } + /documents/batch: + post: + tags: [documents] + summary: Пакетное получение документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/flows: + post: + tags: [documents] + summary: Документы в трансмиттале/ревью + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/public_link: + post: + tags: [public-links] + summary: Создать публичную ссылку на документ + responses: + "200": { $ref: "#/components/responses/Ok" } + /public/documents/public_link/{id}: + get: + tags: [public-links] + summary: Прочитать публичную ссылку (публичный доступ по HMAC-JWT) + security: [] + parameters: [{ $ref: "#/components/parameters/StrId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/public_link/{id}: + patch: + tags: [public-links] + summary: Обновить публичную ссылку + parameters: [{ $ref: "#/components/parameters/StrId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + delete: + tags: [public-links] + summary: Удалить публичную ссылку + parameters: [{ $ref: "#/components/parameters/StrId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/update-path: + patch: + tags: [documents] + summary: Сменить родителя документа + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/update-path: + patch: + tags: [documents] + summary: Массовая смена родителя документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/super_create: + post: + tags: [documents] + summary: Создание документа суперпользователем (требуются права администратора) + responses: + "200": { $ref: "#/components/responses/Ok" } + "403": { $ref: "#/components/responses/Forbidden" } + /documents/types: + get: + tags: [documents] + summary: Справочник типов документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/get_folders_download_url: + get: + tags: [documents] + summary: Ссылка на скачивание папок (подписанная) + responses: + "200": { $ref: "#/components/responses/Ok" } + /download_url/documents: + get: + tags: [documents] + summary: Ссылка на скачивание документа + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/name_template: + get: + tags: [documents, name-templates] + summary: Шаблон имени документа + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}: + get: + tags: [documents] + summary: Документ по id + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [documents] + summary: Переименовать/изменить документ (числовой id) + parameters: [{ $ref: "#/components/parameters/DocumentIdNum" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + delete: + tags: [documents] + summary: Удалить документ (числовой id) + parameters: [{ $ref: "#/components/parameters/DocumentIdNum" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/bundles: + get: + tags: [documents, bundles] + summary: Бандлы документа + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/add_bundle: + post: + tags: [documents, bundles] + summary: Привязать бандл к документу + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/move_bundles: + patch: + tags: [documents, bundles] + summary: Переместить бандлы + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/ancestors: + get: + tags: [documents] + summary: Предки документа + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/filetypes_by_extension: + post: + tags: [documents] + summary: Определить тип файла по расширению + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/copy: + post: + tags: [documents] + summary: Копировать документы (долгая операция, таймаут 120 мин) + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/copy_structure: + post: + tags: [documents] + summary: Копировать структуру папок + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/bin: + delete: + tags: [documents] + summary: Окончательно удалить документы из корзины + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/bin/restore: + patch: + tags: [documents] + summary: Восстановить документы из корзины + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_ids}/company: + get: + tags: [documents] + summary: Компания документов + parameters: + - name: document_ids + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/permissions: + get: + tags: [permissions] + summary: Права доступа документа + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + post: + tags: [permissions] + summary: Выдать права на документ + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /documents/{document_id}/download: + get: + tags: [documents] + summary: Скачать документ (отдаётся сервисом filestream) + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + + /permissions: + get: + tags: [permissions] + summary: Справочник прав доступа + responses: + "200": { $ref: "#/components/responses/Ok" } + + /bundles: + post: + tags: [bundles] + summary: Создать бандл + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}: + get: + tags: [bundles] + summary: Бандл по id + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + patch: + tags: [bundles] + summary: Изменить бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + delete: + tags: [bundles] + summary: Удалить бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/download: + get: + tags: [bundles] + summary: Скачать бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/{bundle_key}/download: + get: + tags: [bundles] + summary: Скачать файл бандла по ключу + parameters: + - { $ref: "#/components/parameters/BundleId" } + - { $ref: "#/components/parameters/BundleKey" } + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/{bundle_key}/upload_single: + post: + tags: [bundles, uploads] + summary: Загрузить файл целиком (single upload) + parameters: + - { $ref: "#/components/parameters/BundleId" } + - { $ref: "#/components/parameters/BundleKey" } + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/{bundle_key}/upload_multipart: + post: + tags: [bundles, uploads] + summary: Начать multipart-загрузку файла бандла + parameters: + - { $ref: "#/components/parameters/BundleId" } + - { $ref: "#/components/parameters/BundleKey" } + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/upload_finish: + post: + tags: [bundles, uploads] + summary: Завершить загрузку бандла + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/marks: + put: + tags: [marks] + summary: Добавить штампы/QR/подписи в бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/sign: + post: + tags: [bundles, marks] + summary: Подписать бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/restart: + post: + tags: [bundles, workflows] + summary: Перезапустить workflow бандла + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/cancel_qr: + patch: + tags: [marks] + summary: Отменить QR-код + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/comment: + patch: + tags: [bundles] + summary: Обновить комментарий бандла + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/presigned_url: + get: + tags: [bundles] + summary: Presigned URL бандла + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/mrpas: + get: + tags: [bundles] + summary: MRPA бандла + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundles/{bundle_id}/copy: + post: + tags: [bundles] + summary: Копировать бандл + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /bundle/version: + get: + tags: [bundles] + summary: Версии бандла + responses: + "200": { $ref: "#/components/responses/Ok" } + + /uploads/multipart/{upload_id}/complete: + post: + tags: [uploads] + summary: Завершить multipart-загрузку + parameters: [{ $ref: "#/components/parameters/UploadId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /uploads/multipart/{upload_id}/abort: + post: + tags: [uploads] + summary: Прервать multipart-загрузку + parameters: [{ $ref: "#/components/parameters/UploadId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + /uploads/multipart/{upload_id}/{part_num}: + post: + tags: [uploads] + summary: Загрузить часть (part) файла + parameters: + - { $ref: "#/components/parameters/UploadId" } + - name: part_num + in: path + required: true + schema: { type: integer } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /workspaces: + post: + tags: [workspaces] + summary: Создать воркспейс + responses: + "200": { $ref: "#/components/responses/Ok" } + /workspaces/{ws_id}: + get: + tags: [workspaces] + summary: Документ воркспейса + parameters: + - name: ws_id + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /dashboards: + post: + tags: [dashboards] + summary: Создать дашборд + responses: + "200": { $ref: "#/components/responses/Ok" } + /dashboards/{db_id}: + get: + tags: [dashboards] + summary: Документ дашборда + parameters: + - name: db_id + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /workflows/{workflow_id}: + get: + tags: [workflows] + summary: Workflow по id + parameters: + - name: workflow_id + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /pages: + post: + tags: [pages] + summary: Создать страницу + responses: + "200": { $ref: "#/components/responses/Ok" } + /pages/{data_source}/{page_key}/download: + get: + tags: [pages] + summary: Скачать страницу + parameters: + - name: data_source + in: path + required: true + schema: { type: string } + - name: page_key + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /public/qr/{public_uuid}/document_info: + get: + tags: [marks] + summary: Публичная информация о документе по QR + security: [] + parameters: + - name: public_uuid + in: path + required: true + schema: { type: string, format: uuid } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /related_documents: + post: + tags: [related-documents] + summary: Создать связь документов + responses: + "200": { $ref: "#/components/responses/Ok" } + get: + tags: [related-documents] + summary: Получить связанные документы + responses: + "200": { $ref: "#/components/responses/Ok" } + /related_documents/bulk_delete: + post: + tags: [related-documents] + summary: Массово удалить связи документов + responses: + "200": { $ref: "#/components/responses/Ok" } + + /templates/{bundle_id}: + get: + tags: [misc] + summary: Шаблон по бандлу + parameters: [{ $ref: "#/components/parameters/BundleId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + + /changelogs/create: + post: + tags: [changelogs] + summary: Создать changelog + responses: + "200": { $ref: "#/components/responses/Ok" } + /changelogs/{changelog_id}: + patch: + tags: [changelogs] + summary: Обновить changelog + parameters: + - name: changelog_id + in: path + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/Ok" } + + /links: + post: + tags: [misc] + summary: Создать ссылку + responses: + "200": { $ref: "#/components/responses/Ok" } + + /favorite_documents: + post: + tags: [favorite-documents] + summary: Добавить документ в избранное + responses: + "200": { $ref: "#/components/responses/Ok" } + get: + tags: [favorite-documents] + summary: Список избранных документов + responses: + "200": { $ref: "#/components/responses/Ok" } + /favorite_documents/{document_id}: + delete: + tags: [favorite-documents] + summary: Убрать документ из избранного + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + + /name_templates/create: + post: + tags: [name-templates] + summary: Создать шаблон имени + responses: + "200": { $ref: "#/components/responses/Ok" } + /name_templates/{document_id}: + get: + tags: [name-templates] + summary: Шаблон имени по документу + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + patch: + tags: [name-templates] + summary: Обновить шаблон имени + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + delete: + tags: [name-templates] + summary: Удалить шаблон имени + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + responses: + "200": { $ref: "#/components/responses/Ok" } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + JWT, подписанный ключом, соответствующим `PUBLIC_KEY` (RSA/PKIX). + Для публичных ссылок используется HMAC-JWT (`DOCUMENT_PUBLIC_LINK_JWT_SECRET`). + + parameters: + DiskId: + name: disk_id + in: path + required: true + schema: { type: string } + DocumentId: + name: document_id + in: path + required: true + schema: { type: string } + DocumentIdNum: + name: document_id + in: path + required: true + description: Числовой идентификатор документа (маршрут ограничен regex `[0-9]+`) + schema: { type: integer } + BundleId: + name: bundle_id + in: path + required: true + schema: { type: string, format: uuid } + BundleKey: + name: bundle_key + in: path + required: true + schema: { type: string } + UploadId: + name: upload_id + in: path + required: true + schema: { type: string } + StrId: + name: id + in: path + required: true + schema: { type: string } + + responses: + Ok: + description: Успешный ответ (тело зависит от ручки; в JSON) + content: + application/json: + schema: { type: object, additionalProperties: true } + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + TooManyRequests: + description: Превышен лимит запросов (rate limit) + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + + schemas: + Error: + type: object + properties: + error: + type: string + message: + type: string + additionalProperties: true diff --git a/apps/documentations/dps-message-hub.CONFIGURATION.md b/apps/documentations/dps-message-hub.CONFIGURATION.md new file mode 100644 index 0000000..4e37cb0 --- /dev/null +++ b/apps/documentations/dps-message-hub.CONFIGURATION.md @@ -0,0 +1,156 @@ +# Конфигурация проекта dps-message-hub +# Версия: 0.1.0 + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +`dps-message-hub` (`dps_message_hub`) — это Kafka-воркер на базе [FastStream](https://faststream.airt.ai/), потребляющий сообщения об изменении ассетов и обновляющий данные разметки (`markup_event`) в PostgreSQL домена «documentations». HTTP API у сервиса нет. + +> Это отдельный сервис, не путать с приложением `message-hub` (домен `planning`): у них разные префиксы переменных, набор интеграций и назначение. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/dps_message_hub/infra/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `AppSettings(BaseSettings)`). + +Особенности разбора (`SettingsConfigDict`): + +- `env_prefix="DPS_MESSAGE_HUB_"` — все переменные приложения начинаются с этого префикса; +- `env_nested_delimiter="__"` — вложенные секции задаются двойным подчёркиванием, напр. `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST` → `documentation_db.host`; +- `env_ignore_empty=False` — пустая строка считается заданным значением (не игнорируется); +- `frozen=True` — объект настроек неизменяем после инициализации. + +Настройки разбиты на три вложенные секции (модели `pydantic.BaseModel`), читаемые одним классом `AppSettings`: + +- `app` (`App`) — префикс `DPS_MESSAGE_HUB_APP__`; +- `documentation_db` (`Database`) — префикс `DPS_MESSAGE_HUB_DOCUMENTATION_DB__`; +- `kafka` (`Kafka`) — префикс `DPS_MESSAGE_HUB_KAFKA__`. + +Класс `Settings` (`src/dps_message_hub/infra/config.py`) — синглтон (`wiring.SingletonMeta`) поверх `AppSettings`, отдаёт настройки через свойство `.settings`. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. У классов настроек **не задан** `env_file`, поэтому файл `.env` автоматически не загружается — переменные нужно экспортировать в окружение процесса самостоятельно (`make config` лишь копирует `.example.env` → `.env` как шаблон), либо пробрасывать их в контейнер через `--env-file` (см. `Makefile`, цель `container-run`). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `make config` копирует `.example.env` → `.env`, но приложение **не загружает `.env` автоматически** — экспортируйте сами, напр. `set -a && . ./.env && set +a` | +| Локально (контейнер) | `Makefile`: цель `container-run` пробрасывает переменные через `--env-file .env` | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `universal-chart.services.api.envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов); базовый чарт — `universal-chart` (`oci://.../charts`, версия `0.1.7`) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и `HELM_SET_ARGS` | + +Запуск процесса (`scripts/entrypoint.sh`): единый FastStream-процесс с брокером Kafka: + +``` +faststream run \ + --factory \ + --workers ${DPS_MESSAGE_HUB_NUM_WORKERS} \ + 'dps_message_hub.infra.app:get_app' +``` + +Фабрика `dps_message_hub.infra.app:get_app` (`src/dps_message_hub/infra/app.py`) собирает `FastStream`-приложение: создаёт `KafkaBroker`, подключает роутер-потребитель топика `assets` и открывает пул соединений PostgreSQL в lifespan. Отдельных точек входа для воркеров/крон-задач нет — `pyproject.toml` не содержит `[project.scripts]`. + +## Переменные приложения + +Дефолт `—` означает, что значение обязательно (иначе ошибка старта настроек). + +### Приложение (`DPS_MESSAGE_HUB_APP__*`) — класс `App` + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DPS_MESSAGE_HUB_APP__LOG_LEVEL` | enum (`LogLevel`) | `INFO` | Уровень логирования. Допустимо: `CRITICAL`/`FATAL`/`ERROR`/`WARNING`/`WARN`/`INFO`/`DEBUG`/`NOTSET` | +| `DPS_MESSAGE_HUB_APP__IS_DEV` | bool | `False` | Признак dev-режима. Поле объявлено в настройках, но в текущем коде не используется | +| `DPS_MESSAGE_HUB_APP__BROKER_TYPE` | enum (`BrokerType`) | `—` (обязательно) | Тип брокера сообщений. Поддерживается только значение `kafka` | + +### База данных PostgreSQL (`DPS_MESSAGE_HUB_DOCUMENTATION_DB__*`) — класс `Database` + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST` | string | `—` | Хост PostgreSQL | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PORT` | int | `—` | Порт PostgreSQL | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER` | string | `—` | Пользователь БД | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD` | string | `—` | Пароль пользователя БД | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__NAME` | string | `—` | Имя базы данных | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL` | bool | `—` | Включить TLS-подключение к БД | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_MODE` | enum (`verify-full`/`verify-ca`/`""`) | `—` | Режим проверки TLS (параметр `sslmode` DSN). При `ENABLE_SSL=true` не должно быть пустым | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH` | string | `—` | Путь к CA-сертификату (параметр `sslrootcert` DSN). При `ENABLE_SSL=true` не должно быть пустым | + +Валидатор `check_ssl_options_configured_when_ssl_enabled`: если `ENABLE_SSL=true`, то `SSL_MODE` и `SSL_ROOT_CERT_PATH` обязаны быть непустыми, иначе ошибка старта. Итоговый DSN собирается в вычисляемом поле `documentation_db.uri` (`postgresql://user:password@host:port/name`, при SSL добавляются `?sslmode=...&sslrootcert=...`). + +### Kafka (`DPS_MESSAGE_HUB_KAFKA__*`) — класс `Kafka` + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DPS_MESSAGE_HUB_KAFKA__HOST` | string | `—` | Хост брокера Kafka | +| `DPS_MESSAGE_HUB_KAFKA__PORT` | int | `—` | Порт брокера Kafka | +| `DPS_MESSAGE_HUB_KAFKA__USERNAME` | string | `—` | Логин SASL (`SCRAM-SHA-512`) | +| `DPS_MESSAGE_HUB_KAFKA__PASSWORD` | string | `—` | Пароль SASL (`SCRAM-SHA-512`) | +| `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` | string \| null | `None` | Путь к CA-сертификату для SSL-контекста. Если задан — используется `SASL_SSL`, иначе SASL без TLS | +| `DPS_MESSAGE_HUB_KAFKA__TOPICS` | dict (JSON) → `KafkaTopics` | `—` | Соответствие логического топика реальному имени в Kafka. Обязателен ключ `assets`, напр. `{"assets": "assets_broadcast"}` | + +Формирование параметров подключения (`src/dps_message_hub/infra/app.py`): адрес брокера — вычисляемое поле `kafka.uri` (`host:port`). Безопасность через `faststream.security.SASLScram512`: + +- если заданы `USERNAME` и `PASSWORD` и `SSL_CAFILE` пуст — `SASLScram512(..., use_ssl=False)`; +- если задан `SSL_CAFILE` — `SASLScram512(..., ssl_context=create_ssl_context(cafile=...))`. + +## Переменные инфраструктуры, сборки и запуска + +Не читаются классами настроек приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `DPS_MESSAGE_HUB_NUM_WORKERS` | `scripts/entrypoint.sh`, Helm `envs` | Число воркеров FastStream (`faststream run --workers`). В Helm `_default: '2'` | +| `DPS_MESSAGE_HUB_PYTHON_IMAGE_NAME` | `Dockerfile` (ARG) | Базовый образ Python (по умолчанию `python`) | +| `DPS_MESSAGE_HUB_PYTHON_IMAGE_TAG` | `Dockerfile` (ARG) | Тег базового образа (по умолчанию `3.13-slim`) | +| `PIP_INDEX_URL`, `PIP_TRUSTED_HOST` | `Dockerfile` (ARG) | Индекс/доверенный хост pip при сборке | + +Сборка (`Dockerfile`): многостадийная — стадия `builder` собирает wheel'ы из `requirements/requirements.txt`, стадия `runner` ставит их, копирует `src/` и устанавливает пакет (`pip install . --no-deps`); процесс запускается непривилегированным пользователем `dps_message_hub`. Целевая версия Python — `3.13` (`.python-version`, `requires-python >=3.13`). + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Сервис деплоится через зависимость `universal-chart` (`.helm/Chart.yaml`, версия `0.1.7`). Настраивается один сервис — `services.api` (тип нагрузки — deployment, `replicaCount._default: 1`). HTTP-`service` и `ingress` отключены, health-пробы (`liveness`/`readiness`, путь `/ping`) — `enabled: false` (у сервиса нет HTTP-эндпоинтов). + +Обычные значения (блок `services.api.envs`) различаются по окружениям (`_default`/`stage`/`preprod`/`production`) адресами БД и Kafka, именем БД и именами топиков. В Helm дополнительно заданы (отсутствуют в `.example.env`): `DPS_MESSAGE_HUB_KAFKA__HOST`, `DPS_MESSAGE_HUB_KAFKA__PORT` (`9091`), `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` (`/opt/config/ca.crt`), `DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL=true`, `SSL_MODE=verify-full`, `SSL_ROOT_CERT_PATH=/opt/config/ca.crt`. + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default` / `stage`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER` | `ya-pg-secret` / `documentations-postgresql-secret` | `username` | +| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD` | `ya-pg-secret` / `documentations-postgresql-secret` | `password` | +| `DPS_MESSAGE_HUB_KAFKA__USERNAME` | `kafka-secret` | `username` | +| `DPS_MESSAGE_HUB_KAFKA__PASSWORD` | `kafka-secret` | `password` | + +Помимо env, чарт монтирует CA-сертификат Яндекса из `ConfigMap` `ya-ca-cert` (ключ `ca.crt`) как файл `/opt/config/ca.crt` (том `cm-ya-ca-cert`, `readOnly`) — на него указывают `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` и `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | +| MR (`merge_request_event`) | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | +| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | `stage` | +| ветка `master` | `preprod` | `documentations-preprod` | `0.0.1-preprod` | `preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `0.0.1-prod` | `master` | + +Ключевые переменные пайплайна: `SERVICE_NAME=dps-message-hub`, `DOCKERFILE_PATH=Dockerfile`, `CI_TRIGGER_SOURCE=app`, `RELEASE_NAME=dps-message-hub`, `CHART_NAME=dps-message-hub`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_DEPLOY`, `HELM_SET_ARGS` (`--set universal-chart...`). Отдельные job'ы `format` (`ruff format --diff`) и `lint` (`ruff check`) на образе `python:3.13-slim`. + +## Замечания и потенциальные проблемы + +- Приложение **не читает `.env`** автоматически (в `SettingsConfigDict` нет `env_file`). `make config` только создаёт `.env` из шаблона — переменные нужно экспортировать самому либо передавать контейнеру через `--env-file`. +- Большинство полей БД и Kafka **обязательны** (без дефолтов): при пустом окружении настройки не пройдут валидацию и сервис не стартует. Единственные необязательные — `APP__LOG_LEVEL`, `APP__IS_DEV`, `KAFKA__SSL_CAFILE`. +- `DPS_MESSAGE_HUB_APP__BROKER_TYPE` обязателен; поддерживается только `kafka` (иных веток в `match` нет). При другом значении брокер не будет создан. +- При `ENABLE_SSL=true` обязательно задавать `SSL_MODE` и `SSL_ROOT_CERT_PATH`, иначе валидатор настроек прервёт старт. +- `DPS_MESSAGE_HUB_KAFKA__TOPICS` обязан содержать ключ `assets` (модель `KafkaTopics`); прочие ключи игнорируются, отсутствие `assets` — ошибка старта. +- Поле `APP__IS_DEV` присутствует в настройках, но в текущем коде нигде не задействовано. +- Обработка Kafka-сообщений идёт с `auto_commit=False` и middleware `Retry` (`src/dps_message_hub/interface/middleware.py`), которая повторяет обработку **бесконечно** с экспоненциальной задержкой (до `1<<10 = 1024` сек) — «отравленное» сообщение может заблокировать партицию. + +## Минимальный набор для локального запуска + +Kafka и Zookeeper поднимаются через `Makefile` (цели `run-deps`/`run-zookeeper`/`run-kafka`); PostgreSQL — внешний. Минимально нужно задать: + +- `DPS_MESSAGE_HUB_APP__BROKER_TYPE=kafka`; +- `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST`, `__PORT`, `__USER`, `__PASSWORD`, `__NAME`, `__ENABLE_SSL` (для локали `false`), `__SSL_MODE`, `__SSL_ROOT_CERT_PATH` (при `ENABLE_SSL=false` можно пустыми); +- `DPS_MESSAGE_HUB_KAFKA__HOST`, `__PORT`, `__USERNAME`, `__PASSWORD`, `__TOPICS` (JSON с ключом `assets`); +- `DPS_MESSAGE_HUB_NUM_WORKERS` (для `entrypoint.sh`). + +Локальный запуск: `make run` (через `entrypoint.sh`) либо `make run-dev` (`faststream run --factory --reload dps_message_hub.infra.app:get_app`). Готовые значения-примеры приведены в `dps-message-hub.env.example` рядом с этим документом. diff --git a/apps/documentations/dps-message-hub.ENDPOINTS.md b/apps/documentations/dps-message-hub.ENDPOINTS.md new file mode 100644 index 0000000..127be57 --- /dev/null +++ b/apps/documentations/dps-message-hub.ENDPOINTS.md @@ -0,0 +1,45 @@ +# Интерфейсы сервиса dps-message-hub +# Версия: 0.1.0 + +Документ описывает интерфейсную поверхность сервиса: потребляемые топики Kafka и обращения к внешним зависимостям (PostgreSQL). + +> `dps-message-hub` — это чистый Kafka-воркер на [FastStream](https://faststream.airt.ai/). У него **нет HTTP/REST API** (в коде нет FastAPI/Flask/aiohttp, health-роутов и т.п.), поэтому файла `openapi.yaml` для сервиса нет. Исходящих HTTP-запросов к другим сервисам он тоже не выполняет — единственный получатель данных — база PostgreSQL. + +## Как устроено взаимодействие + +Единое FastStream-приложение (`dps_message_hub.infra.app:get_app`, `src/dps_message_hub/infra/app.py`) объединяет: + +- **Kafka** — потребитель сообщений (`KafkaBroker` + `KafkaRouter`), топик `assets` (`src/dps_message_hub/features/assets/interface/kafka.py`); +- **PostgreSQL** — пул соединений `psycopg` (`AsyncConnectionPool`), открывается/закрывается в lifespan (`src/dps_message_hub/infra/lifespan.py`, `infra/database.py`). + +Каждое сообщение проходит через middleware `Retry` (`src/dps_message_hub/interface/middleware.py`): при исключении обработка повторяется бесконечно с экспоненциальной задержкой (`1 << min(10, retry_count)` секунд). Коммит оффсета ручной — у потребителя `auto_commit=False`. + +## Kafka-потребители (входящие сообщения) + +Реальное имя топика задаётся переменной `DPS_MESSAGE_HUB_KAFKA__TOPICS` (маппинг логического имени `assets` в имя топика Kafka). Формат сообщения — `BrokerMessageDto` (`src/dps_message_hub/infra/dto.py`): поля `schema_version`, `model`, `sender`, `type`, `body`, `diff`, `timestamp`, `trace_id` (alias `xtraceId`), `user_id`, `tenants`, `tags`. + +| Топик (логич.) | `group_id` | Offset reset | `auto_commit` | Обработчик | Назначение | +| --- | --- | --- | --- | --- | --- | +| `assets` | `dps_assets_consumer` | earliest | `false` | `assets` | Обновление событий разметки (`markup_event`) по изменению ассета | + +Диспетчеризация внутри обработчика `assets` по полю `type` (`src/dps_message_hub/features/assets/interface/kafka.py`); `body` разбирается в модель `AssetUpdate` (поля `id`, `attributes[]`), `diff` — произвольный `dict`: + +| `type` сообщения | Условие (`diff`) | Действие | Назначение | +| --- | --- | --- | --- | +| `model_updated` | `diff is None` | — | Пропуск (нет изменений) | +| `model_updated` | в `diff` есть ключ `resource_id` | `unlink_asset(asset_id)` | Отвязка ассета: деактивация его событий разметки | +| `model_updated` | в `diff` есть ключ `attributes` | `update_markup_events(asset_id, attributes)` | Пересоздание событий разметки по обновлённым атрибутам | +| `model_deleted` | — | `unlink_asset(asset_id)` | Отвязка ассета при удалении модели | + +Бизнес-логика (`src/dps_message_hub/features/assets/application/asset.py`): для обновляемых атрибутов существующие активные `markup_event` деактивируются (`inactivation_time`), затем в транзакции вставляются новые записи. Тип атрибута (поле `type`) определяет целевую колонку значения (`value_int`/`value_float`/`value_string`/`value_option_id`/`unit_option_id`/`value_dt`/`value_date`) — маппинг в `features/assets/domain/entities.py`. + +## Внешние зависимости (инфраструктура) + +| Зависимость | Назначение | +| --- | --- | +| Kafka | Источник сообщений (топик `assets`). Подключение `SASLScram512`, при заданном `KAFKA__SSL_CAFILE` — по `SASL_SSL` | +| PostgreSQL | Хранилище данных (`psycopg` async, таблица `markup_event`). Операции: `SELECT`, `UPDATE`, массовая вставка `COPY ... FROM STDIN` (`features/assets/infra/database.py`) | + +## Исходящие HTTP-запросы к внешним сервисам + +Отсутствуют. Сервис не содержит HTTP-клиентов (`httpx`/`aiohttp`/`requests`) и не обращается к другим сервисам по HTTP; все побочные эффекты — запись в PostgreSQL. diff --git a/apps/documentations/dps-message-hub.env.example b/apps/documentations/dps-message-hub.env.example new file mode 100644 index 0000000..69f0f01 --- /dev/null +++ b/apps/documentations/dps-message-hub.env.example @@ -0,0 +1,42 @@ +# ============================================================================= +# dps-message-hub — пример конфигурации (.env) +# Версия: 0.1.0 +# Скопируйте в .env (`make config`) и заполните значения. +# ВНИМАНИЕ: приложение НЕ загружает .env автоматически (в классах настроек нет +# env_file/dotenv). Экспортируйте переменные сами, напр.: +# set -a && . ./.env && set +a +# либо запускайте контейнер через `--env-file .env` (см. Makefile). +# ============================================================================= + +# --- Инфраструктура запуска (не читается классом настроек) --- +# Число воркеров faststream (scripts/entrypoint.sh: faststream run --workers) +DPS_MESSAGE_HUB_NUM_WORKERS=1 + +# --- Приложение (префикс DPS_MESSAGE_HUB_APP__) --- +# CRITICAL | FATAL | ERROR | WARNING | WARN | INFO | DEBUG | NOTSET +DPS_MESSAGE_HUB_APP__LOG_LEVEL=INFO +DPS_MESSAGE_HUB_APP__IS_DEV=true +# Тип брокера сообщений (поддерживается только kafka) +DPS_MESSAGE_HUB_APP__BROKER_TYPE=kafka + +# --- База данных PostgreSQL (префикс DPS_MESSAGE_HUB_DOCUMENTATION_DB__) --- +DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST= +DPS_MESSAGE_HUB_DOCUMENTATION_DB__PORT=6432 +DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER= +DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD= +DPS_MESSAGE_HUB_DOCUMENTATION_DB__NAME= +DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL=false +# verify-full | verify-ca | "" (обязателен при ENABLE_SSL=true) +DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_MODE= +# Путь к CA-сертификату (обязателен при ENABLE_SSL=true) +DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH= + +# --- Kafka (префикс DPS_MESSAGE_HUB_KAFKA__) --- +DPS_MESSAGE_HUB_KAFKA__HOST=localhost +DPS_MESSAGE_HUB_KAFKA__PORT=9092 +DPS_MESSAGE_HUB_KAFKA__USERNAME= +DPS_MESSAGE_HUB_KAFKA__PASSWORD= +# Путь к CA-сертификату для SSL-контекста (SASL_SSL). Если пусто — SASL без SSL +# DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE=/opt/config/ca.crt +# Соответствие логического топика `assets` реальному имени топика Kafka (JSON) +DPS_MESSAGE_HUB_KAFKA__TOPICS='{"assets": "assets_broadcast_test"}' diff --git a/apps/documentations/frontend.CONFIGURATION.md b/apps/documentations/frontend.CONFIGURATION.md new file mode 100644 index 0000000..9de1736 --- /dev/null +++ b/apps/documentations/frontend.CONFIGURATION.md @@ -0,0 +1,66 @@ +# Конфигурация проекта documentation-frontend + +Документ описывает сборку, рантайм-конфигурацию и деплой микрофронтенда `documentation-frontend` (образ `documentation-frontend-app`, деплой `frontend` в домене `documentations`). + +## Способ конфигурирования + +Это фронтенд-модуль (Module Federation remote), поэтому в отличие от backend-сервисов он **не** читает переменные окружения в рантайме. Единственный параметр конфигурации, влияющий на поведение, — окружение сборки `BUILD_ENV`, которое webpack «зашивает» в бандл на этапе сборки. + +- Значение берётся из `process.env.BUILD_ENV` при запуске webpack. +- Webpack подставляет его как глобальную константу `BUILD_ENV` через `DefinePlugin` (`webpack.config.js`). +- Допустимые значения проверяются в `env.js`: `local`/`stage`/`preprod`/`prod`/`contour`. Если значение не входит в набор — сборка падает с ошибкой. +- `build.config.js` по `BUILD_ENV` выбирает режим webpack (`mode`/`devtool`): `local`/`stage` → `development` + `eval-source-map`, `prod`/`preprod`/`contour` → `production` + `source-map`. +- В рантайме `BUILD_ENV` определяет базовые хосты сервисов (`module/api/hosts.ts`, `resolveHost`) и тип http-сервиса (`module/api/http-service.ts`: при `local` — `setTypeOfHttpService("original")`). Подробности по хостам и эндпоинтам — в `frontend.ENDPOINTS.md`. + +## Переменные сборки + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `BUILD_ENV` | `env.js`, `build.config.js`, `webpack.config.js` (`DefinePlugin`), `Dockerfile` (`ARG`), `.gitlab-ci.yml` (`--build-arg`) | Окружение сборки: `local`/`stage`/`preprod`/`prod`/`contour`. Определяет режим сборки и базовые хосты API | +| `NPM_NEXUS_TOKEN` | `Dockerfile` (`ARG`), `.npmrc`, `.gitlab-ci.yml` (`--build-arg`) | Токен доступа к приватному npm-реестру (Nexus) при `npm i` | + +Отдельного `.env`-файла в репозитории нет; переменные передаются как build-arg'и Docker/CI. + +## Сборка (`package.json`, `Dockerfile`) + +Скрипты npm: + +| Скрипт | Команда | Назначение | +| --- | --- | --- | +| `build-module` | `webpack --config webpack.config.js` | Сборка модуля в `dist` (используется в образе) | +| `serve-module` | `webpack serve --config webpack.config.js` | Dev-сервер (порт `9002`, https) | +| `lint` | `eslint ./module/**/*.ts(x) --fix` | Линтинг | +| `start` | `BUILD_ENV=local run-p serve-module storybook` | Локальный запуск (dev-сервер + Storybook) | +| `storybook` / `build-storybook` | `start-storybook` / `build-storybook` | Storybook | + +Сборка образа (`Dockerfile`, multi-stage): + +1. Стадия `static` (`node:16`): `npm i --legacy-peer-deps` с `NPM_NEXUS_TOKEN`, затем `npm run lint` и `BUILD_ENV=$BUILD_ENV npm run build-module` → артефакты в `/app/dist`. +2. Финальная стадия (`nginx:1.19.6`): копирует `dist` в `/dist` и `nginx/nginx.conf` в `/etc/nginx/nginx.conf`. + +Module Federation (`webpack.config.js`, `ModuleFederationPlugin`): имя remote — `srx_documentations`, `filename: module/remoteEntry.js`. Экспонируемые модули: `./DocumentationsPage`, `./DocumentSelect`, `./CreateDocDialog`, `./DownloadFilesDialog`, `./FileBindingsDialog`. Shared-зависимости (singleton): `react`, `react-dom`, `@material-ui/core`, `@material-ui/styles`, `@sarex-team/sdk-js`, `@sarex-team/translator`, `@sarex-team/ui-kit`, `mobx`, `mobx-react-lite`. + +## Деплой (Helm, `.helm/values.yaml`) + +Чарт использует общий `universal-chart`. Ключевые значения для сервиса `frontend`: + +- `deployment.name._default`: `documentation-frontend-static`; порт контейнера `80`. +- `replicaCount`: `stage` — 1, `preprod` — 2, `production` — 2. +- Пробы `liveness`/`readiness`: `httpGet /ping` на порту `80`. +- `resources.requests`: `memory 100Mi`, `cpu 100m`. +- `image.name._default`: `cr.yandex/crp3ccidau046kdj8g9q/documentation-frontend-static:latest` (в CI переопределяется на собранный `IMAGE_NAME` через `HELM_SET_ARGS`). +- `service`: `ClusterIP`, порт `80` (в `stage` — `8080`), `targetPort 80`, `portName http`. +- `imagePullSecrets.name._default`: `dockerhub`. + +## CI/CD (`.gitlab-ci.yml`) + +Пайплайн подключает шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). `SERVICE_NAME`: `documentation-frontend-app`. Окружение переключается по ветке/тегу (`workflow.rules`): + +| Условие | STAND | Namespace | `BUILD_ENV` | `global.env` | `CHART_VERSION` | +| --- | --- | --- | --- | --- | --- | +| ветка `stage` | `stage` | `documentations` | `stage` | `stage` | `0.0.1-stage` | +| ветка `master` | `preprod` | `documentations-preprod` | `preprod` | `preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `prod` | `production` | `0.0.1-prod` | +| merge request | — | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE: "false"`) | + +Каждая ветка задаёт `BUILD_ARGS` (`--build-arg BUILD_ENV=... --build-arg NPM_NEXUS_TOKEN=...`) и `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`), а также `K8S_HUSTLER_BRANCH` соответствующего окружения (`universal-chart-stage`/`-preprod`/`-production`). diff --git a/apps/documentations/frontend.ENDPOINTS.md b/apps/documentations/frontend.ENDPOINTS.md new file mode 100644 index 0000000..e3c538f --- /dev/null +++ b/apps/documentations/frontend.ENDPOINTS.md @@ -0,0 +1,224 @@ +# Эндпоинты, с которыми взаимодействует documentation-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `documentation-frontend`, образ `documentation-frontend-app`, деплой `frontend`). Модуль публикуется как remote для Module Federation (`webpack.config.js`, имя `srx_documentations`). + +## Как устроено взаимодействие + +Все запросы описаны декларативно в реестре `module/api/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`); +- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query); +- `body(args)` — опционально, формирование тела запроса; +- `responseType` — опционально, тип ответа (напр. `blob`); +- `accessToken(args)` — опционально, явная передача access-токена в заголовки; +- `cache`, `queryOptions` — опции кеширования/повторов; +- `showErrorNotification` — показывать ли уведомление об ошибке (по умолчанию `true`). + +Запрос выполняется единой функцией `fetch(endpoint, params, controller)` (`module/api/endpoints.ts`), которая через `httpService` (`module/api/http-service.ts`, обёртка `createHttpService` из `@sarex-team/sdk-js` поверх `axios`) отправляет запрос на базовый хост сервиса. Базовый хост выбирается по паре «`service` + окружение»: `httpService` создаётся с картой хостов `apiHosts` и текущим `buildEnv`, и разрешает хост внутри себя. Тот же алгоритм продублирован в экспортируемом хелпере `resolveHost(service)` из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. + +Окружение определяется глобальной константой `BUILD_ENV`, которую webpack подставляет в бандл через `DefinePlugin` (`webpack.config.js`) из переменной сборки `process.env.BUILD_ENV`. Допустимые значения проверяются в `env.js`: `local`/`stage`/`preprod`/`prod`/`contour`. В режиме `local` тип http-сервиса переключается на `"original"` (`module/api/http-service.ts`). + +Ошибки маппируются в человекочитаемые сообщения в `module/api/errors.ts` (`resolveNetworkErrorByCode`, `resolveNetworkError`). + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts` (объект `apiHosts`). Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. + +| Сервис (`service`) | Назначение | `local` | `stage` | `preprod` | `prod` | `contour` | +| --- | --- | --- | --- | --- | --- | --- | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `/sarex-backend` | `/` | `/` | `/` | `/` | +| `documentations` | Сервис документации (документы, бандлы, файлы) | `https://stage-api.sarex.io/documentations` | `https://stage-api.sarex.io/documentations` | `https://api.preprod.sarex.io/documentations` | `https://api.sarex.io/documentations` | `/documentations` | +| `sarexApi` | Gateway/API Sarex (`/gateway`, `/eav`, `/cde`, `/transmittals`, `/flows`, `/issues`) | `https://stage-api.sarex.io` | `https://stage-api.sarex.io` | `https://api.preprod.sarex.io` | `https://api.sarex.io` | `/` | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://stage-api.sarex.io/workspaces` | `https://api.preprod.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | `/workspaces` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://stage-api.sarex.io/workflows` | `https://api.preprod.sarex.io/workflows` | `https://api.sarex.io/workflows` | `/workflows` | +| `processes` | Сервис рабочих процессов (flows, reviews) | `https://stage-api.sarex.io/flows` | `https://stage-api.sarex.io/flows` | `https://api.preprod.sarex.io/flows` | `https://api.sarex.io/flows` | `/flows` | +| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://stage-api.sarex.io/remarks` | `https://api.preprod.sarex.io/remarks` | `https://api.sarex.io/remarks` | `/remarks` | +| `files` | Сервис файлов | `https://stage-api.sarex.io/files` | `https://stage-api.sarex.io/files` | `https://api.preprod.sarex.io/files` | `https://api.sarex.io/files` | `/files` | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `bim` | BIM-API | `https://stage-bim-api.sarex.io` | `https://stage-bim-api.sarex.io` | `https://bim-api.preprod.sarex.io` | `https://bim-api.sarex.io` | `""` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.preprod.sarex.io` | `https://login.sarex.io` | `""` | + +> Хосты `google`, `bim` и `zitadel` заданы в карте хостов, но напрямую в реестре `endpoints` не используются — они задействованы через SDK/вьюер (`@sarex-team/sdk-js`) и механизм аутентификации. В окружении `local` сервис `sarex` проксируется на `/sarex-backend`, в `stage`/`preprod`/`prod` — на `/` (относительные пути), в `contour` все сервисы работают по относительным путям изолированного контура. Отдельного `module-hosts.ts` (карты хостов удалённых модулей) в репозитории нет. + +## Эндпоинты по сервисам + +### `sarex` — Локальный сервис данных + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getSettings` | GET | `/api/client/settings/` | Клиентские настройки (кешируется, `stateTime: 5`) | +| `getUser` | GET | `/api/core/users/{userId}/` | Пользователь по id | +| `getUsersByCompanyId` | GET | `/api/core/users/?company={companyId}&limit={limit}&offset={offset}` | Пользователи компании (пагинация) | +| `getTargets` | GET | `/api/core/targets/` | Список таргетов | +| `getCompanies` | GET | `/api/core/companies/` | Список компаний | +| `getDepartmentById` | GET | `/api/core/admin/departments?company={companyId}` | Отделы компании | +| `getUsersPositionById` | GET | `/api/core/admin/positions/?company={companyId}` | Должности компании | +| `getMrpaList` | POST | `/api/core/mrpa/list/` | Список МЧД (фильтр по пользователю/компании) | +| `getByFullUrl` | GET | `{url}` | Запрос по произвольному URL | + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDisks` | GET | `/api/v1/disks` | Список дисков | +| `getDocumentTypes` | GET | `/api/v1/documents/types` | Справочник типов документов | +| `getAllPermmission` | GET | `/api/v1/permissions` | Все права доступа | +| `getDocPermission` | GET | `/api/v1/documents/{id}/permissions` | Права доступа документа | +| `postPermission` | POST | `/api/v1/documents/{id}/permissions` | Выдать права сервисному аккаунту | +| `postBundle` | POST | `/api/v1/bundles` | Создать бандл | +| `postFile` | POST | `/api/v1/bundles/{bundleId}/{fileKey}?single_upload=1` | Загрузить файл (single upload) | +| `uploadFileStart` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_multipart` | Начать multipart-загрузку файла | +| `uploadFolderStart` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_multipart?upload_path={folderPath}` | Начать multipart-загрузку с указанием пути | +| `uploadPart` | PUT | `/api/v1/bundles/{bundleId}/{bundleKey}?part_number={partNumber}` | Загрузить часть файла | +| `bundleComplite` | POST | `/api/v1/bundles/{bundleId}/upload_finish` | Завершить загрузку бандла | +| `bundleCompleteUpload` | POST | `/api/v1/bundles/{bundleId}/upload_finish` | Завершить загрузку бандла (дубль ключа) | +| `getFile` | GET | `/api/v1/bundles/{bundleId}/{bundleKey}` | Получить файл бандла | +| `updateComment` | PATCH | `/api/v1/bundles/{bundleId}/comment` | Обновить комментарий бандла | +| `getBundles` | GET | `/api/v1/documents/{id}/bundles` | Бандлы документа | +| `addBundle` | POST | `/api/v1/documents/{documentId}/add_bundle` | Привязать бандл к документу | +| `moveBundles` | PATCH | `/api/v1/documents/{documentId}/move_bundles` | Переместить бандлы | +| `removeBundle` | DELETE | `/api/v1/bundles/{id}` | Удалить бандл | +| `postWorkspace` | POST | `/api/v1/workspaces` | Создать рабочую область | +| `getDocumentById` | GET | `/api/v1/documents/{id}` | Документ по id | +| `getDocumentWithBundles` | GET | `/api/v1/documents/{id}?extend=bundles` | Документ с бандлами | +| `changeDocument` | PATCH | `/api/v1/documents/{documentId}` | Переименовать документ | +| `changeDocumentName` | PATCH | `/api/v1/documents/{id}` | Переименовать документ | +| `updatePath` | PATCH | `/api/v1/documents/{id}/update-path` | Сменить родителя документа | +| `updateDocumentsPaths` | PATCH | `/api/v1/documents/update-path` | Массовая смена родителя | +| `deleteDocument` | DELETE | `/api/v1/documents/{id}` | Удалить документ | +| `deleteDocuments` | DELETE | `/api/v1/documents?document_ids={ids}` | Удалить несколько документов | +| `getFolderChildrenWithActiveProcesses` | POST | `/api/v1/documents/flows` | Дети папки с активными процессами | +| `downloadFile` | GET | `/api/v1/bundles/{lastBundleId}/{key}/download` | Скачать файл (с флагами `include_original_pdf`/`include_printable_pdf`) | +| `downloadFiles` | GET | `/api/v1/download_url/documents?document_ids={documentIds}` | Получить ссылку на скачивание документов | +| `downloadAllFiles` | GET | `/api/v1/bundles/{lastBundleId}/download` | Скачать все файлы бандла | +| `downloadFolder` | GET | `/api/v1/documents/{docId}/download?depth={depth}` | Скачать папку | +| `getFoldersDownloadUrl` | GET | `/api/v1/documents/get_folders_download_url?document_ids={ids}` | Ссылка на скачивание папок | +| `conversionFile` | POST | `/api/v1/conversion` | Конвертация документа (в IFC) | +| `addMarks` | PUT | `/api/v1/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи | +| `sign` | POST | `/api/v1/bundles/{bundleId}/sign` | Подписать бандл | +| `cancelQrCode` | PATCH | `/api/v1/bundles/{bundleId}/cancel_qr` | Отменить QR-код | +| `restartWorkflow` | POST | `/api/v1/bundles/{bundleId}/restart` | Перезапустить workflow бандла | +| `getPublicLink` | GET | `/api/v1/public/documents/public_link/{public_link_id}` | Получить публичную ссылку | +| `createPublicLink` | POST | `/api/v1/documents/public_link` | Создать публичную ссылку | +| `updatePublicLink` | PATCH | `/api/v1/documents/public_link/{public_link_id}` | Обновить публичную ссылку | +| `deletePublicLink` | DELETE | `/api/v1/documents/public_link/{public_link_id}` | Удалить публичную ссылку | +| `removeDoc` | DELETE | `/api/v1/documents/bin?parent_id={id}` | Переместить в корзину | +| `recoveryDocument` | PATCH | `/api/v1/documents/bin/restore?parent_id={id}` | Восстановить из корзины | +| `copyFolderStructure` | POST | `/api/v1/documents/copy_structure` | Копировать структуру папок | +| `getTemplateJSON` | GET | `/api/v1/templates/{bundleId}` | JSON-шаблон бандла | +| `uploadSingleTemplate` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_single` | Загрузить файл шаблона | +| `createReport` | POST | `/api/v1/documents/create_report` | Сформировать отчёт по документам | +| `updateChangelog` | PATCH | `api/v1/changelogs/{bundleId}` | Обновить запись журнала изменений | +| `createChangelog` | POST | `api/v1/changelogs/create` | Создать запись журнала изменений | +| `getFavorites` | GET | `/api/v1/favorite_documents?company_id={companyId}` | Избранные документы | +| `createFavoriteDocument` | POST | `/api/v1/favorite_documents` | Добавить документ в избранное | +| `deleteFavoriteDocument` | DELETE | `/api/v1/favorite_documents/{documentId}` | Убрать документ из избранного | +| `getNearestNameTemplate` | GET | `/api/v1/documents/{documentId}/name_template` | Ближайший шаблон именования (без уведомления об ошибке) | +| `getDocumentNameTemplate` | GET | `/api/v1/name_templates/{documentId}` | Шаблон именования документа (без уведомления об ошибке) | +| `createNameTemplate` | POST | `/api/v1/name_templates/create` | Создать шаблон именования | +| `updateNameTemplate` | PATCH | `/api/v1/name_templates/{documentId}` | Обновить шаблон именования | +| `deleteNameTemplate` | DELETE | `/api/v1/name_templates/{documentId}` | Удалить шаблон именования | +| `createLink` | POST | `/api/v1/links` | Создать ярлык (ссылку на документ) | +| `getBundleMrpas` | GET | `/api/v1/bundles/{bundleId}/mrpas` | МЧД бандла | + +### `sarexApi` — Gateway/API Sarex + +Через этот сервис проходят запросы к `/gateway`, `/eav`, `/cde`, а также к сабпутям других доменов, доступным через общий шлюз: `/transmittals`, `/flows`, `/issues`. + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDocuments` | GET | `/gateway/api/v1/disks/{id}/documents?parent_id={parentId}&child_id={childId}` | Документы диска (по родителю/потомку) | +| `getFolderChildren` | GET | `/gateway/api/v1/disks/{diskId}/documents?parent_id={documentId}` | Дети папки | +| `getSearch` | GET | `/gateway/api/v4/disks/{diskId}/documents?...` | Поиск/фильтрация документов (root_document, limit, filters, bookmark) | +| `createDocument` | POST | `/gateway/api/v1/documents` | Создать документ/папку/проект | +| `fetchDocumentPaths` | POST | `/gateway/api/v1/documents/ancestors` | Предки документов | +| `getAttributesByDocument` | GET | `/gateway/api/v1/documents/{id}/attributes` | Атрибуты документа | +| `updateAttributes` | PUT | `/gateway/api/v1/documents/{id}/attributes` | Обновить атрибуты документа | +| `addAttributes` | POST | `/gateway/eav/api/v0/entity/` | Создать сущность атрибутов (EAV) | +| `getDefaultAttributes` | GET | `/eav/api/v0/schema/?model=document&company_id={companyId}&type_identifier={docType}` | Схема атрибутов по типу | +| `getAttributes` | GET | `/eav/api/v0/attribute/?company_id={params}` | Атрибуты компании | +| `getAttributesWithParams` | GET | `/eav/api/v0/schema/?model=document&{params}` | Схема атрибутов с параметрами | +| `updateSubscription` | POST | `/gateway/api/v1/subscription/` | Создать/обновить подписку | +| `deleteSubscription` | DELETE | `/gateway/api/v1/documents/{documentId}/subscription/` | Удалить подписку | +| `getActivityLog` | GET | `/gateway/api/v1/system_log/?model_names=document&...` | Журнал активности документа | +| `fetchResourceByDocumentId` | GET | `/gateway/api/v1/resources-rpc/resource-by-document-id/{id}/` | Ресурс по id документа | +| `getUsersWithTransmittalProjectPermissions` | GET | `/gateway/api/v2/users/?limit=5000&offset=0&resource_id={projectId}&permissions={permissions}` | Пользователи с правами в проекте | +| `getRemovedDocuments` | GET | `/gateway/api/v1/documents/bin?parent_id={id}{params}` | Удалённые документы в папке | +| `getRemovedFilteredDocuments` | GET | `/gateway/api/v1/documents/bin{params}` | Удалённые документы (фильтр) | +| `getTemplates` | GET | `/gateway/api/v1/disks/{diskId}/flat_documents?type={type}` | Плоский список документов по типу | +| `getFileSize` | GET | `/gateway/api/v1/documents/size?disk_id={diskId}&document_id={documentId}` | Размер документа | +| `completeUpload` | POST | `{uploadUrl}/complete` | Завершение загрузки (по переданному URL) | +| `createTransmittal` | POST | `/transmittals/api/v1/transmittals/create` | Создать трансмиттал | +| `getTransmittalById` | GET | `/transmittals/api/v1/transmittals/{transmittalId}` | Трансмиттал по id | +| `getTransmittalsByBundleId` | POST | `/transmittals/internal/v1/transmittals/by_bundle_ids` | Трансмитталы по id бандлов | +| `getTemplateListForSelect` | GET | `/transmittals/api/v1/transmittal_templates/select?resource={resourceId}` | Список шаблонов трансмитталов для выбора | +| `getSingleTemplate` | GET | `/transmittals/api/v1/transmittal_templates/{templateId}?resource={resourceId}` | Шаблон трансмиттала по id | +| `createPrescriptionDocument` | POST | `/issues/api/prescriptions/{prescriptionId}/generate/` | Сгенерировать документ по предписанию | +| `loadReviewData` | GET | `/flows/api/v1/documents/?bundle_ids={ids}&limit=10000&full=true` | Данные согласований по бандлам (с явным access-токеном) | +| `searchAssets` | POST | `eav/api/v4/assets/search/` | Поиск активов по id | +| `getProjectAssets` | GET | `eav/api/v4/assets/?linkable_to_project={resourceId}&parentId=null` | Активы проекта | +| `getAssetsList` | GET | `eav/api/v4/assets/?tenant_id={tenantId}&linkable_to_project={resourceId}&depth=0&...` | Список активов (поиск/пагинация) | +| `getAssets` | GET | `eav/api/v4/assets/?{params}` | Активы по произвольным параметрам | +| `getAssetLevels` | GET | `eav/api/v4/assets/?tenant_id={tenantId}&parent_id={parentAssetId}&path_contains={id}` | Уровни активов | +| `getBindings` | GET | `/cde/app/v1/bundles/{bundleId}/bindings` | Привязки бандла | +| `createSession` | POST | `/cde/app/v1/s32d/sessions/` | Создать S32D-сессию | +| `getSessionList` | GET | `/cde/app/v1/s32d/sessions/` | Список S32D-сессий | +| `uploadS32DSingle` | PUT | `/cde/app/v1/s32d/sessions/{sessionId}/archive` | Загрузить архив S32D (single) | +| `s32dMultipartInit` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart` | Начать multipart-загрузку архива S32D | +| `s32dMultipartUploadPart` | PUT | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart/{uploadId}/parts/{partNumber}` | Загрузить часть архива S32D | +| `s32dMultipartComplete` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart/{uploadId}/complete` | Завершить multipart-загрузку S32D | +| `startS32DProcess` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/process` | Запустить обработку S32D | + +### `processes` — Сервис рабочих процессов (flows) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getProcesses` | GET | `/api/v1/flows/?{query}` | Список процессов (flows) | +| `createReview` | POST | `/api/v1/reviews/` | Создать review | +| `deleteReview` | DELETE | `/api/v1/reviews/{id}/` | Удалить review | +| `activateReview` | PATCH | `/api/v1/reviews/{id}/approve/` | Активировать/утвердить review | +| `createReviewDocuments` | POST | `/api/v1/documents/` | Добавить документы в review | + +### `workflows` — Сервис обработки документов + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getWorkflow` | GET | `/api/v1/workflows/{workflowId}` | Workflow по id | +| `getWorkflows` | POST | `/api/v1/workflows/batch` | Пакетное получение workflow | + +### `workspaces` — Сервис рабочих областей + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getWorkspaces` | GET | `/api/v1/workspaces/{uuid}` | Рабочая область по uuid | + +### `remarks` — Сервис замечаний + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getRemarksTotalCount` | GET | `/api/v1/total_count` | Общее число замечаний | + +### `files` — Сервис файлов + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `downloadBundlesMrpas` | POST | `/api/v1/bundles_mrpas/` | Скачать МЧД бандлов (ответ `blob`) | + +## Обработка ошибок + +Маппинг ошибок выполняется в `module/api/errors.ts`. Функция `fetch` (`module/api/endpoints.ts`) перехватывает `AxiosError` и вызывает: + +- `resolveNetworkErrorByCode(service, code, endpoint, method, data)` — при HTTP-ответе с кодом ≥ 400 и при отмене запроса (`CanceledError`/`ERR_CANCELED`, внутренний код `-1` → «Запрос был отменен»); +- `resolveNetworkError(service, error)` — при сетевой ошибке без ответа сервера. + +Базовый маппинг кодов (`httpCodeToError`): `400` — «некорректный формат запроса», `401` — «ошибка авторизации», `403` — «доступ запрещен», `404` — «ресурс не найден», `500` — «ошибка сервера». Для неизвестного кода подбирается ближайший (`4xx` → `400`, иначе → `500`). К сообщению добавляется человекочитаемое имя сервиса из `getServiceToName()` (напр. `documentations`/`sarexApi` → «Сервис документации», `sarex` → «Локальный сервис данных», `processes` → «Сервис рабочих процессов», `workflows` → «Сервис обработки документов», `remarks` → «Сервис замечаний», `workspaces` → «Сервис рабочих областей», `files` → «Сервис файлов»). + +Для части кодов сообщение уточняется по эндпоинту и методу: + +- `401` — набор сообщений `error401Messages` (истёкшая/невалидная сессия, завершённая сессия); по умолчанию — «Ваш токен невалиден, обновите страницу». +- `403` — тип определяется `determine403ErrorType(endpoint, method)` (напр. чтение/создание/редактирование/удаление/перемещение документа, скачивание, загрузка файла, управление доступом, изменение атрибутов, создание review/трансмиттала, доступ к диску/проекту), сообщения — `error403Messages`. +- `400` — тип определяется `determine400ErrorType(endpoint, method, data)` с анализом текста `data.message` (конфликт имени, дубликат, отсутствие/некорректность расширения, некорректный формат), сообщения — `error400Messages`. +- `409` — тип определяется `determine409ErrorType(endpoint, method, data)` (дубликаты имён документов/папок/ярлыков, конфликт при `copy_structure`), сообщения — `error409Messages`. + +По умолчанию у запросов включён показ уведомления об ошибке (`showErrorNotification !== false`); отдельные эндпоинты отключают его (`getNearestNameTemplate`, `getDocumentNameTemplate`). Типы кодов ошибок описаны в `module/api/types.ts`. diff --git a/apps/documentations/pdm.CONFIGURATION.md b/apps/documentations/pdm.CONFIGURATION.md new file mode 100644 index 0000000..bfeb1d5 --- /dev/null +++ b/apps/documentations/pdm.CONFIGURATION.md @@ -0,0 +1,241 @@ +# Конфигурация проекта pdm + +Документ описывает все переменные окружения и способы конфигурирования сервиса **pdm** (Go). Сервис деплоится в namespace `documentations` как деплоймент `pdm` (образ `pdmv2`). Это шлюз/агрегатор поверх Postgres и множества внутренних сервисов Sarex (документации, ресурсы, ремарки, вложения, состояния, подписки, EAV, инспекции, релизы, BIM, трансмитталы и др.). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` (функция `NewConfig`) через библиотеку [`cleanenv`](https://github.com/ilyakaznacheev/cleanenv) — вызовом `cleanenv.ReadEnv(cfg)`. + +Особенности разбора: + +- **Плоские имена переменных без общего префикса** — каждое поле помечено тегом `env:"..."` (напр. `POSTGRES_ADDRESS`, `RESOURCES_URL`). Вложенности/делимитера, как в pydantic-settings, здесь нет. +- **Обязательность** задаётся тегом `env-required:"true"` — при отсутствии такой переменной приложение не стартует (`config error`). В таблицах ниже дефолт `—` означает обязательное поле. +- **Значения по умолчанию** задаются тегом `env-default:"..."`. +- `cleanenv.ReadEnv` читает **только переменные окружения процесса** — конфиг-файла (yaml/toml) и авто-загрузки `.env` нет. Единственный файловый источник — JSON сервисного аккаунта S3 (`S3_SERVICE_ACCOUNT`). + +Отдельно от env читается JSON-файл доступа к S3 — путь берётся из `S3_SERVICE_ACCOUNT`, разбор в `pkg/s3` (`NewFromConfigFile`). Формат файла (`.example.s3config.json`): + +```json +{ "endpoint": "", "access_key_id": "", "secret_access_key": "", "use_ssl": true } +``` + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `make config` копирует `.example.env` → `.env` и `.example.s3config.json` → `.s3config.json` (только если файлов ещё нет), но приложение **не загружает `.env` автоматически** — его нужно экспортировать самому. В репозитории для этого есть `.envrc` (`use flake` + `dotenv`) под direnv | +| Локально (live-reload) | `make run-dev` → `air` (`.air.toml`), пересборка `./cmd/httpserver/main.go` | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `services.api.envs` (обычные значения, ключ `_default` и переопределения по окружениям `stage`/`preprod`/`production`) и `services.api.secretEnvs` (значения из k8s-секретов через `secretKeyRef`). Используется зонтичный `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна в `workflow.rules` (по ветке/тегу), общие шаблоны из `generic/common-ci` | + +Точки входа (`cmd/`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `make run` / `make build` | `cmd/httpserver/main.go` | HTTP API (Fiber). Читает конфиг и вызывает `internal/app/http.New(cfg).Run()` | +| — | `cmd/example/main.go`, `cmd/test/main.go` | Вспомогательные утилиты (не участвуют в деплое) | + +Порядок инициализации в `internal/app/http/httpserver.go` (`App.Run`): трейсер (при `TRACER_USE=true`) → подключение к Postgres → инициализация HTTP-клиентов внешних сервисов → репозитории/usecase/сервисы → опциональный Valkey → сборка Fiber-приложения (`internal/controller/http/v1.Setup`) → `app.Listen(":8080")`. + +## Переменные приложения + +Все переменные читаются `config/config.go`. Дефолт `—` означает, что значение обязательно (`env-required:"true"`) и его отсутствие приводит к ошибке старта. + +### App / Log + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `APP_NAME` | string | — | Имя приложения | +| `APP_VERSION` | string | — | Версия приложения | +| `LOG_LEVEL` | string | — | Уровень логирования (`pkg/logging`, напр. `DEBUG`/`INFO`) | + +### Postgres (`POSTGRES`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_POOL_SIZE` | int32 | — | Размер пула соединений (pgxpool) | +| `ENABLE_OBSERVABILITY` | bool | `false` | Инструментирование пула Postgres трейсингом (`otelpgx`) | + +> DSN собирается в `Config.GetPostgresConnectionUrl()` как `postgres://user:password@address:port/db` — **без параметра `sslmode`**. Отдельный флаг `ENABLE_SSL` из Helm кодом не читается (см. «Замечания»). + +### HTTP (`HTTP`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_PORT` | string | — | Порт HTTP. **Обязателен по тегу, но фактически не используется** — сервер слушает `:8080` (хардкод в `httpserver.go`) | +| `PUBLIC_KEY` | string (PEM) | `""` | Публичный ключ (PKIX) для проверки JWT. Формально необязателен, но при пустом/некорректном значении приложение падает (`panic` в `v1.Setup`) | +| `HTTP_BODY_LIMIT` | int | `268435456` (256 MB) | Максимальный размер тела запроса, байт | +| `HTTP_READ_BUFFER_SIZE` | int | `98304` (96 KB) | Размер буфера чтения запроса, байт | + +### Auth и хосты внешних сервисов + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_BASIC_AUTH` | string | — | Basic-токен для авторизации в бэкенде Sarex/Django (клиенты `targets`, `users`) | +| `DJANGO_HOST` | string | — | Базовый URL Django/бэкенда. Используется сразу двумя секциями — `USERS.UserHost` и `SA.SAHost` (accounts/companies/django-клиенты) | +| `NOTES_URL` | string | — | Сервис заметок (`notes`) | +| `FLOWS_URL` | string | — | Сервис процессов (`flows`) | +| `RESOURCES_URL` | string | — | Сервис ресурсов/IAM (`resources`) | +| `REMARKS_URL` | string | — | Сервис замечаний (`remarks`) | +| `ATTACHMENTS_URL` | string | — | Сервис вложений (`attachments`) | +| `STATES_URL` | string | — | Сервис состояний/рабочих областей (`workspaces`) | +| `SUBSCRIPTIONS_URL` | string | — | Сервис подписок (`subscriptions`) | +| `EAV_URL` | string | — | Сервис атрибутов EAV | +| `INSPECTIONS_URL` | string | — | Сервис инспекций | +| `SYSTEM_LOG_URL` | string | — | Сервис системного лога | +| `TARGET_URL` | string | — | Сервис таргетов | +| `DOCUMENTATION_URL` | string | — | Сервис документаций (documentation-api-v2) | +| `BIM_V2_HOST` | string | — | BIM core API v2 | +| `NOTES_URL` | string | — | (см. выше) | +| `DRAWINGS_INTERNAL_URL` | string | — | Внутренний URL сервиса чертежей | +| `RELEASES_URL` | string | — | URL GitLab для получения релизов | +| `RELEASES_TOKEN` | string | — | Токен доступа к GitLab (`RELEASES_URL`) | + +### Ресурсы и фильтр разрешений (`RESOURCES`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_PERMISSIONS_FILTER` | bool | `false` | Включить фильтрацию по разрешениям на уровне сервиса документов | +| `PERMISSIONS_FILTER_COMPANIES` | string (JSON-массив) | `[133, 256, 247, 219, 248, 194, 242, 260, 252, 255, 239, 125, 116, 92, 311, 170]` | Список ID компаний, к которым применяется фильтр. Парсится `json.Unmarshal` в `[]uint64` | + +### Thumbnails (`ATTACHMENTS`, `STATES`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WIDTH_THUMB_ATTACHMENTS` | int | `100` | Ширина превью вложений | +| `HEIGHT_THUMB_ATTACHMENTS` | int | `100` | Высота превью вложений | +| `WIDTH_THUMB_STATES` | int | `100` | Ширина превью состояний | +| `HEIGHT_THUMB_STATES` | int | `100` | Высота превью состояний | + +### Subscriptions / System log — доп. поля + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_SUBSCRIPTIONS` | bool | `true` | Включить интеграцию с подписками в сервисе документов | +| `API_HOST_PREFIX` | string | `""` | Префикс хоста API (напр. `/gateway`), используется сервисом системного лога | + +### S3 (`S3`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON-файлу с доступом к S3 (`endpoint`, `access_key_id`, `secret_access_key`, `use_ssl`). Разбирается в `pkg/s3` | + +### Transmittals (`Transmittals`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTALS_ENABLE` | bool | `true` | Включить клиент трансмитталов; при `false` используется stub-реализация | +| `TRANSMITTALS_BASE_URL` | string | — | Базовый URL сервиса трансмитталов. Обязателен даже при `TRANSMITTALS_ENABLE=false` | + +### Observability / Tracer + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `OBSERVABILITY_COLLECTOR_ENDPOINT` | string | `""` | Эндпоинт OTLP-коллектора (метрики/наблюдаемость) | +| `TRACER_USE` | bool | `false` | Включить трейсинг OpenTelemetry (`golang-fiber-otel-tools`) | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора трейсов | +| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `SERVICE_NAME` | string | `Pdm` | Имя сервиса в трейсах | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя логгера OTel | + +### Valkey (`VALKEY`) — кэш пользователей + +Подключение опционально: если `VALKEY_ADDR` пуст — клиент не создаётся (кэш пользователей отключён). Ошибки подключения/пинга не фатальны (лог `Warn`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `VALKEY_ADDR` | string | `""` | Адрес Valkey (при пустом — кэш выключен) | +| `VALKEY_LOGIN` | string | `""` | Логин | +| `VALKEY_HOST` | string | `""` | Хост | +| `VALKEY_PASSWORD` | string | `""` | Пароль | +| `VALKEY_DB` | int | `0` | Номер БД | +| `VALKEY_SSL` | bool | `false` | Использовать TLS | +| `VALKEY_SSL_CA_CERTS` | string | `""` | Путь к CA-сертификату для TLS | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения (`config/config.go`), но участвуют в сборке/деплое: + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `GITLAB_CREDENTIALS` | `Dockerfile` (build-arg), `.gitlab-ci.yml` (`BUILD_ARGS`) | Учётные данные для доступа к приватным Go-модулям `gitlab.sarex.io` при сборке | +| `SERVICE_NAME` (CI) | `.gitlab-ci.yml` | `pdmv2` — имя сервиса в пайплайне (не путать с `SERVICE_NAME` трейсера) | +| `DOCKERFILE_PATH`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` | Путь к Dockerfile, источник триггера | +| `ENABLE_LINTER`, `ENABLE_BUILD_CHART`, `ENABLE_BUILD_IMAGE`, `ENABLE_STATE_UPDATE`, `ENABLE_DEPLOY` | `.gitlab-ci.yml` | Флаги стадий пайплайна | +| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME` | `.gitlab-ci.yml` | Параметры окружения/деплоя Helm | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Обычные значения задаются в `services.api.envs` (ключ `_default` + переопределения по `stage`/`preprod`/`production`) и содержат переменные приложения, описанные выше, различаясь адресами БД/сервисов, `LOG_LEVEL`, доменами и т.п. + +Значения из секретов (`services.api.secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`, prod/по умолчанию) | Ключ (`secretKey`) | +| --- | --- | --- | +| `POSTGRES_DB` | `documentations-postgresql-secret` (preprod: `ya-pg-secret`) | `database` | +| `POSTGRES_PORT` | `documentations-postgresql-secret` | `port` | +| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `host` | +| `POSTGRES_USER` | `documentations-postgresql-secret` | `username` | +| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `password` | +| `YC-PG-CERTIFICATE` | `documentations-postgresql-secret` (preprod: `yc-pg-certificate`) | `ca.crt` | +| `DJANGO_BASIC_AUTH` | `django-auth` | `key` | +| `PUBLIC_KEY` | `public-key` | `key` | +| `RELEASES_TOKEN` | `releases-token` | `key` | +| `VALKEY_ADDR` | `valkey-secret` | `url` | +| `VALKEY_LOGIN` | `valkey-secret` | `login` | +| `VALKEY_PASSWORD` | `valkey-secret` | `password` | +| `VALKEY_HOST` | `valkey-secret` | `host` | +| `VALKEY_PORT` | `valkey-secret` | `port` | +| `VALKEY_CA_CERTS` | `valkey-secret` | `cert` | + +Помимо env, чарт монтирует секрет `documentations-yc-s3` как том в `/etc/sarex/yc-s3-storage` (readOnly). Именно на файл `/etc/sarex/yc-s3-storage/yc-s3-service-account.json` указывает `S3_SERVICE_ACCOUNT` в prod-конфигурации. + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя `pdm-api`, реплики `stage=3`/`preprod=2`/`production=8`, ресурсы `cpu=1`, `memory=2Gi`), `image.name` (`cr.yandex/.../pdm_v2`), `service.*` (порт `8080`), `imagePullSecrets` (`dockerhub`), `probes.*` (startup/liveness/readiness по `/internal/healthz/*` на порту `8080`). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | `universal-chart.global.env` | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `documentations` | `stage` | +| ветка `master` | `preprod` | `documentations-preprod` | `preprod` | +| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `production` | +| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Общие переменные: `SERVICE_NAME=pdmv2`, `RELEASE_NAME=pdmv2`, `CHART_NAME=pdmv2`, `CHART_VERSION=0.0.1-`, `DOCKERFILE_PATH=Dockerfile`. + +## Замечания и потенциальные проблемы + +- **`HTTP_PORT` фактически игнорируется.** Поле обязательно (`env-required`), но сервер жёстко слушает `:8080` (`app.Listen(":8080")` в `httpserver.go`). Реальный порт задаётся только этим хардкодом; в Helm `HTTP_PORT` и `service.port` совпадают со `8080`, поэтому расхождение незаметно. +- **`PUBLIC_KEY` де-факто обязателен.** По тегам он необязателен (`env:"PUBLIC_KEY"`), но `v1.Setup` при пустом/битом PEM вызывает `panic` (`failed to parse PEM block...`). Для локального запуска нужен валидный публичный ключ. +- **SSL к Postgres в коде не настраивается.** DSN формируется без `sslmode` (`GetPostgresConnectionUrl`). Helm-переменные `ENABLE_SSL` и секрет `YC-PG-CERTIFICATE` кодом **не читаются** — подключение к БД идёт без TLS-параметров на уровне DSN. +- **Множество Helm-переменных не читается приложением.** В `services.api.envs`/`secretEnvs` присутствуют переменные, отсутствующие в `config/config.go`, — вероятно, унаследованы от `documentation-api`: `API_ADDRESS`, `API_ADDRESS_FILE`, `ENABLE_SSL`, `ENABLE_S3`, `FILE_URL_EXTERNAL`, `WORKFLOW_URL`, `WORKSPACE_URL`, `BIM_API_URL`, `BIM_API_V2_URL`, `BIM_API_URL_EXTERNAL`, `WORKSPACE_BUNDLE_VERSION`, `WORKFLOW_IMAGES_VERSION`/`WORKFLOWS_IMAGES_VERSION`, `NAMESPACE`, `DJANGO_ORIGINATOR`, `USE_EXPERIMENTAL`, `READ_WRITE_TIMEOUT_FILE_STREAM`, `CACHE_DEFAULT_EXPIRATION`, `CACHE_CLEANUP_INTERVAL`, `USE_CACHE_IN_FILE_STREAMER`, `SENTRY_DSN`, `SENTRY_DEBUG`, `ENVIRONMENT`, `YC-PG-CERTIFICATE`. Они не влияют на работу pdm. +- **Несовпадение имён Valkey.** Код ждёт `VALKEY_SSL_CA_CERTS` (`config.go`), а Helm-секрет прокидывает `VALKEY_CA_CERTS`; также Helm задаёт `VALKEY_PORT`, который код не читает (адрес берётся целиком из `VALKEY_ADDR`). В результате CA-сертификат и порт из секрета до приложения не доходят. +- **Секции `USERS`/`SA` используют один и тот же env `DJANGO_HOST`.** Оба поля (`UserHost`, `SAHost`) читают одну переменную. +- **`config.env` в репозитории содержит реальные учётные данные** (пароль Postgres, `DJANGO_BASIC_AUTH`, токены) — это конфигурация для отладки, не шаблон. Для примеров использовать `.example.env`; `config.env` не должен попадать в окружения и подлежит ротации секретов. +- **`ENABLE_SQL_QUERY` из `config.env` кодом не читается** — в `config/config.go` такого поля нет. +- **`cleanenv.ReadEnv` не загружает `.env` автоматически.** `make config` лишь создаёт файлы-шаблоны (`.env`, `.s3config.json`) при их отсутствии; переменные нужно экспортировать вручную (например, через `direnv`/`.envrc`). +- **v0-роутер (echo) не подключён.** В `cmd/httpserver/main.go` используется только `internal/controller/http/v1.Setup` (Fiber). Пакет `internal/controller/http/v0` (на `labstack/echo`) в рантайме не задействован. + +## Минимальный набор для локального запуска + +Приложение поднимается через `make run` (или `make run-dev` с `air`). Минимально необходимо задать (обязательные поля `config/config.go`): + +- `APP_NAME`, `APP_VERSION`, `LOG_LEVEL`; +- `POSTGRES_ADDRESS`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_PORT`, `POSTGRES_POOL_SIZE`; +- `HTTP_PORT` (любой — фактически используется `:8080`), а также **валидный** `PUBLIC_KEY` (иначе `panic`); +- `DJANGO_BASIC_AUTH`, `DJANGO_HOST`; +- хосты внешних сервисов: `NOTES_URL`, `FLOWS_URL`, `RESOURCES_URL`, `REMARKS_URL`, `ATTACHMENTS_URL`, `STATES_URL`, `SUBSCRIPTIONS_URL`, `EAV_URL`, `INSPECTIONS_URL`, `SYSTEM_LOG_URL`, `TARGET_URL`, `DOCUMENTATION_URL`, `BIM_V2_HOST`, `DRAWINGS_INTERNAL_URL`; +- `RELEASES_URL`, `RELEASES_TOKEN`; +- `S3_SERVICE_ACCOUNT` (путь к `.s3config.json`) и заполненный сам JSON-файл; +- `TRANSMITTALS_BASE_URL` (обязателен даже при выключенных трансмитталах). + +Необязательные (есть дефолты): `ENABLE_OBSERVABILITY`, `HTTP_BODY_LIMIT`, `HTTP_READ_BUFFER_SIZE`, `ENABLE_PERMISSIONS_FILTER`, `PERMISSIONS_FILTER_COMPANIES`, `USE_SUBSCRIPTIONS`, `WIDTH_THUMB_*`/`HEIGHT_THUMB_*`, `API_HOST_PREFIX`, `TRANSMITTALS_ENABLE`, `TRACER_*`, `SERVICE_NAME`, `VALKEY_*`, `OBSERVABILITY_COLLECTOR_ENDPOINT`. + +Готовые значения-примеры приведены в `pdm.env.example` (на основе `.example.env` репозитория). diff --git a/apps/documentations/pdm.env.example b/apps/documentations/pdm.env.example new file mode 100644 index 0000000..eeb10dc --- /dev/null +++ b/apps/documentations/pdm.env.example @@ -0,0 +1,116 @@ +# Пример переменных окружения сервиса pdm (документируемый образ pdmv2). +# Основан на .example.env репозитория; переменные читаются config/config.go +# библиотекой cleanenv (cleanenv.ReadEnv). Значения-заглушки, замените своими. +# Файл автоматически НЕ загружается приложением — переменные нужно экспортировать +# в окружение процесса (напр. через direnv/.envrc или `set -a && . ./.env`). + +# App +APP_NAME=pdm +APP_VERSION=0.1.0 + +# Logger +LOG_LEVEL=DEBUG + +# Postgres +POSTGRES_ADDRESS=127.0.0.1 +POSTGRES_DB=documentations +POSTGRES_USER=postgres +POSTGRES_PASSWORD=password +POSTGRES_PORT=5432 +POSTGRES_POOL_SIZE=10 +ENABLE_OBSERVABILITY=false + +# Http +# ВНИМАНИЕ: HTTP_PORT читается как обязательный, но сервер всё равно слушает :8080 (хардкод). +HTTP_PORT=8001 +# PUBLIC_KEY — публичный ключ в формате PEM (PKIX) для проверки JWT. +# Формально не обязателен по тегам, но при пустом/некорректном значении приложение падает (panic). +PUBLIC_KEY= +HTTP_BODY_LIMIT=268435456 +HTTP_READ_BUFFER_SIZE=98304 + +# Auth (Basic-токен для походов в Django/бэкенд Sarex) +DJANGO_BASIC_AUTH= + +# Users / service accounts (один и тот же хост используется как DJANGO_HOST) +DJANGO_HOST=https://stage.sarex.io + +# Notes +NOTES_URL=https://stage-api.sarex.io/notes + +# Flows +FLOWS_URL=https://stage-api.sarex.io/flows + +# Resources +RESOURCES_URL=http://localhost:9000 +ENABLE_PERMISSIONS_FILTER=false +PERMISSIONS_FILTER_COMPANIES= + +# Remarks +REMARKS_URL=https://stage-api.sarex.io/remarks + +# Attachments +ATTACHMENTS_URL=http://localhost:8000 +WIDTH_THUMB_ATTACHMENTS=100 +HEIGHT_THUMB_ATTACHMENTS=100 + +# States (workspaces) +STATES_URL=https://stage-api.sarex.io/workspaces +WIDTH_THUMB_STATES=100 +HEIGHT_THUMB_STATES=100 + +# Subscriptions +USE_SUBSCRIPTIONS=true +SUBSCRIPTIONS_URL=https://stage-api.sarex.io/subscriptions + +# Eav +EAV_URL=http://stage-api.sarex.io/eav + +# Inspections +INSPECTIONS_URL=https://stage-api.sarex.io/inspections + +# System log +SYSTEM_LOG_URL=http://localhost:8888 +API_HOST_PREFIX=/gateway + +# Target +TARGET_URL=https://stage.sarex.io + +# Documentation api +DOCUMENTATION_URL=http://localhost:6666/ + +# Bim v2 +BIM_V2_HOST=http://localhost:8888/ + +# Observability (OTLP-коллектор) +OBSERVABILITY_COLLECTOR_ENDPOINT= + +# Releases (GitLab) +RELEASES_URL=https://gitlab.com +RELEASES_TOKEN= + +# Drawings +DRAWINGS_INTERNAL_URL=http://localhost:6666 + +# S3 (путь к JSON-файлу с сервисным аккаунтом, см. .example.s3config.json) +S3_SERVICE_ACCOUNT=.s3config.json + +# Transmittals +TRANSMITTALS_ENABLE=true +TRANSMITTALS_BASE_URL=http://transmittal-service.transmittal-api-stage + +# Tracer (OpenTelemetry) +TRACER_USE=false +TRACER_HOST=localhost:4317 +TRACER_USE_INSECURE=true +SERVICE_NAME=Pdm +TRACER_LOGGER_NAME=tracer_logger + +# Valkey (кэш пользователей, опционально; при пустом VALKEY_ADDR не подключается) +VALKEY_ADDR= +VALKEY_LOGIN= +VALKEY_HOST= +VALKEY_PASSWORD= +VALKEY_DB=0 +VALKEY_SSL=false +VALKEY_SSL_CA_CERTS= diff --git a/apps/documentations/pdm.openapi.yaml b/apps/documentations/pdm.openapi.yaml new file mode 100644 index 0000000..690919c --- /dev/null +++ b/apps/documentations/pdm.openapi.yaml @@ -0,0 +1,918 @@ +openapi: 3.0.3 + +info: + title: PDM API + version: "0.1.0" + description: | + REST API сервиса **pdm** (`pdm/pdm`, образ `pdmv2`) — шлюз/агрегатор над + Postgres и множеством внутренних сервисов Sarex: документации, ресурсы и + разрешения, замечания, вложения, состояния/рабочие области, подписки, + атрибуты (EAV), инспекции, релизы, заметки, чертежи, BIM, трансмитталы, + системный лог. + + Сервис написан на Go (**Fiber v2**). Приложение собирается в + `internal/controller/http/v1.Setup` (`internal/controller/http/v1/router.go`), + точка входа — `cmd/httpserver/main.go`. Роутинг делится на группы: + + - публичный API — префикс `/api` с версиями `v1`/`v2`/`v3`/`v4` + (`group.Group("v1")` и т.д.); + - внутренний API — префикс `/internal` (healthcheck, pprof, служебные + ручки), без аутентификации на уровне приложения. + + Готовой спецификации (swagger) в репозитории нет — данный документ + восстановлен из роутеров `internal/controller/http/**/router.go`. + Сервер слушает порт `8080` (хардкод в `internal/app/http/httpserver.go`). + + ### Аутентификация + Аутентификация применяется middleware `pkg/httpserver/middleware/auth.go` ко + всем путям с префиксом `/api`. Токен передаётся заголовком + `Authorization: Bearer `. Поддерживаются два режима: + + 1. **sarex-backend** (по умолчанию) — если заголовка `Identity` нет, подпись + основного JWT проверяется публичным ключом из `PUBLIC_KEY` (PKIX). Из + claims извлекаются `user_id`, `is_superuser`, `company_ids`, + `service_accounts`, `permissions` и т.д. + 2. **Zitadel** — если передан дополнительный заголовок + `Identity: Bearer `, полезная нагрузка берётся из этого токена + (`urn:zitadel:iam:user:metadata`); основной токен кладётся как + `access_token`. Подпись identity-токена приложением не проверяется + (`ParseUnverified`) — доверие обеспечивается сетевым слоем. + + При отсутствии заголовка `Authorization`, неверной схеме (не `Bearer`) или + ошибке разбора токена возвращается **401 Unauthorized** (пустое тело). + Пути `/internal/*` аутентификации на уровне приложения не требуют. + + ### Пагинация + Списочные эндпоинты используют пагинацию через query-параметры `limit` и + `offset` (напр. `internal/controller/http/v1/document/downloaded.go`, + `flat.go`). Единого конверта ответа нет — формат зависит от эндпоинта. + + ### Обработка ошибок + Ошибки бизнес-слоя оборачиваются в `AppError` + (`internal/app_errors/errors.go`) и сериализуются как JSON + `{ "message": "...", "error_code": "GW-XXXX" }`. HTTP-статус выбирается по + `error_code` в `internal/app_errors/middleware.go`: + + | error_code | Статус | Значение | + | --- | --- | --- | + | `GW-0001` | 404 | Ресурс не найден | + | `GW-0002` | 401 | Не аутентифицирован | + | `GW-0003` | 403 | Нет доступа | + | `GW-0004` | 400 | Некорректный запрос | + | `GW-0014` | 400 | Ошибка валидации состояния | + | `GW-0000` | 500 | Системная ошибка | + + Не все роутеры используют обёртку `middleware.ErrorHandler` — часть + обработчиков возвращает ошибки/статусы напрямую, поэтому формат ответа об + ошибке может отличаться от `AppError`. + + ### Замечания (расхождения кода) + - Пути с сегментом `*` (напр. `/api/v1/disks/*/documents`, + `/api/v1/documents/*/attributes`, `/api/v1/targets/*/remarks/*`) — это + «жадные» wildcard-сегменты Fiber, захватывающие путь документа/цели + целиком (включая `/`). В спецификации они представлены параметром пути. + - Часть эндпоинтов завершается слэшем (`/`), часть — нет; поведение + определяется определениями групп во Fiber. + - v0-роутер (`internal/controller/http/v0`, на `labstack/echo`) в рантайме + не подключён. + + contact: + name: pdm + url: https://gitlab.com/sarex-team/pdm + +servers: + - url: http://pdm-api.documentations:8080 + description: Stage (внутренний адрес в кластере, namespace documentations) + - url: http://pdm-api.documentations-preprod:8080 + description: Preprod (внутренний адрес в кластере) + - url: http://pdm-api.documentations-prod:8080 + description: Production (внутренний адрес в кластере) + +security: + - bearerAuth: [] + +tags: + - name: documents + description: Документы, диски, проекты, атрибуты (v1/v2/v3/v4) + - name: resources + description: Ресурсы и разрешения (v1/v2) + - name: remarks + description: Замечания по таргетам + - name: attachments + description: Вложения + - name: states + description: Состояния рабочих областей + - name: subscriptions + description: Подписки + - name: system_log + description: Системный лог + - name: targets + description: Таргеты + - name: inspections + description: Инспекции + - name: users + description: Пользователи + - name: eav + description: Атрибуты (EAV) + - name: releases + description: Релизы + - name: notes + description: Заметки + - name: drawings + description: Чертежи (сечения и экспорты) + - name: internal + description: Служебные эндпоинты (без аутентификации) + +paths: + # ---------------- v1: documents ---------------- + /api/v1/disks/{diskPath}/documents: + get: + tags: [documents] + summary: Список документов по диску (v1) + parameters: + - $ref: '#/components/parameters/DiskPath' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/disks/{diskPath}/downloaded_documents: + get: + tags: [documents] + summary: Список скачанных документов диска + parameters: + - $ref: '#/components/parameters/DiskPath' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/disks/{diskPath}/flat_documents: + get: + tags: [documents] + summary: Плоский список документов диска + parameters: + - $ref: '#/components/parameters/DiskPath' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/disks/{diskId}/thumbnails: + post: + tags: [documents] + summary: Превью документов диска + parameters: + - name: diskId + in: path + required: true + schema: { type: string } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/projects/: + get: + tags: [documents] + summary: Получить проект по ID документа + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents: + post: + tags: [documents] + summary: Создать документ + responses: + '200': { $ref: '#/components/responses/Ok' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/{documentPath}/attributes: + get: + tags: [documents] + summary: Атрибуты документа + parameters: + - $ref: '#/components/parameters/DocumentPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + put: + tags: [documents] + summary: Создать/обновить атрибуты документа + parameters: + - $ref: '#/components/parameters/DocumentPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/{documentPath}/subscription/: + delete: + tags: [documents] + summary: Удалить подписку по ID документа + parameters: + - $ref: '#/components/parameters/DocumentPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/bin: + get: + tags: [documents] + summary: Список удалённых документов (корзина) + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/approving_users: + post: + tags: [documents] + summary: Согласующие пользователи + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/bundle_versions: + post: + tags: [documents] + summary: Версии бандлов документов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/ancestors: + post: + tags: [documents] + summary: Предки документов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/size: + get: + tags: [documents] + summary: Размер папки + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/related_documents: + get: + tags: [documents] + summary: Связанные документы + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + post: + tags: [documents] + summary: Создать связи документов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/documents/related_documents/bulk_delete: + post: + tags: [documents] + summary: Массовое удаление связей документов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: remarks ---------------- + /api/v1/targets/{targetPath}/remarks: + post: + tags: [remarks] + summary: Создать замечание для таргета + parameters: + - $ref: '#/components/parameters/TargetPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/targets/{targetPath}/remarks/{remarkPath}: + get: + tags: [remarks] + summary: Получить замечание + parameters: + - $ref: '#/components/parameters/TargetPath' + - $ref: '#/components/parameters/RemarkPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + patch: + tags: [remarks] + summary: Обновить замечание + parameters: + - $ref: '#/components/parameters/TargetPath' + - $ref: '#/components/parameters/RemarkPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: resources ---------------- + /api/v1/resources/: + get: + tags: [resources] + summary: Список ресурсов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resources/users-with-resources/: + post: + tags: [resources] + summary: Пользователи с ресурсами + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resources/permissions-bulk/: + patch: + tags: [resources] + summary: Массовое обновление разрешений + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resources/{uuid}/: + get: + tags: [resources] + summary: Ресурс по UUID + parameters: + - name: uuid + in: path + required: true + schema: { type: string, format: uuid } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resource-permissions/: + get: + tags: [resources] + summary: Разрешения на ресурсы + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + post: + tags: [resources] + summary: Массовое создание разрешений на ресурс + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resource-permissions/{id}/: + delete: + tags: [resources] + summary: Удалить разрешение на ресурс + parameters: + - $ref: '#/components/parameters/IdPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/bulk_delete/resource-permissions/: + post: + tags: [resources] + summary: Массовое удаление разрешений на ресурс + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/company-resource-permissions/: + get: + tags: [resources] + summary: Разрешения компаний на ресурсы + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + post: + tags: [resources] + summary: Создать разрешение компании на ресурс + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/company-resource-permissions/{id}/: + delete: + tags: [resources] + summary: Удалить разрешение компании на ресурс + parameters: + - $ref: '#/components/parameters/IdPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resources-rpc/resource-by-document-id/{id}/: + get: + tags: [resources] + summary: Ресурс по ID документа + parameters: + - $ref: '#/components/parameters/IdPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/resources-rpc/parent-document-by-resource-id/{uuid}/: + get: + tags: [resources] + summary: Родительская папка проекта по UUID ресурса + parameters: + - name: uuid + in: path + required: true + schema: { type: string, format: uuid } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: attachments ---------------- + /api/v1/attachments: + post: + tags: [attachments] + summary: Создать вложение + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [attachments] + summary: Список вложений + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/attachments/{id}: + get: + tags: [attachments] + summary: Вложение по ID + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + delete: + tags: [attachments] + summary: Удалить вложение + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: states (workspace) ---------------- + /api/v1/workspace/{ws_id}: + post: + tags: [states] + summary: Создать состояние с вложением + parameters: + - $ref: '#/components/parameters/WsId' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [states] + summary: Состояния с вложениями + parameters: + - $ref: '#/components/parameters/WsId' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/workspace/{ws_id}/dynamic_states: + post: + tags: [states] + summary: Создать динамические состояния + parameters: + - $ref: '#/components/parameters/WsId' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [states] + summary: Динамические состояния + parameters: + - $ref: '#/components/parameters/WsId' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/workspace/states/{id}: + get: + tags: [states] + summary: Состояние по ID + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/workspace/dynamic_states/{id}: + get: + tags: [states] + summary: Динамическое состояние по ID + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: subscriptions ---------------- + /api/v1/subscription/: + post: + tags: [subscriptions] + summary: Создать подписку + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: system_log ---------------- + /api/v1/system_log/: + get: + tags: [system_log] + summary: Отфильтрованный системный лог + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + post: + tags: [system_log] + summary: Записать в системный лог + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: targets ---------------- + /api/v1/targets/: + get: + tags: [targets] + summary: Список таргетов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/targets/{targetPath}/: + get: + tags: [targets] + summary: Таргет по ID + parameters: + - $ref: '#/components/parameters/TargetPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: inspections ---------------- + /api/v1/inspections/: + post: + tags: [inspections] + summary: Создать инспекцию + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [inspections] + summary: Список инспекций + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/{id}: + get: + tags: [inspections] + summary: Инспекция по ID + parameters: + - $ref: '#/components/parameters/IdInt' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + patch: + tags: [inspections] + summary: Обновить инспекцию + parameters: + - $ref: '#/components/parameters/IdInt' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + delete: + tags: [inspections] + summary: Удалить инспекцию + parameters: + - $ref: '#/components/parameters/IdInt' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/created_at/daterange: + get: + tags: [inspections] + summary: Диапазон дат создания + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/inspection_date/daterange: + get: + tags: [inspections] + summary: Диапазон дат инспекций + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/available_responsible_users: + get: + tags: [inspections] + summary: Доступные ответственные пользователи + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/available_inspection_dates: + get: + tags: [inspections] + summary: Доступные даты инспекций + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/inspections/export: + get: + tags: [inspections] + summary: Экспорт инспекций + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: users / eav / releases / notes ---------------- + /api/v1/users/: + get: + tags: [users] + summary: Пользователи по разрешению на ресурс + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/attribute/: + get: + tags: [eav] + summary: Получить атрибуты (EAV) + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/releases/{id}: + get: + tags: [releases] + summary: Список релизов по ID проекта + parameters: + - $ref: '#/components/parameters/IdInt' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/notes/{service_id}/{entity_id}/{instance_id}/: + get: + tags: [notes] + summary: Заметки по сервису/сущности/инстансу + parameters: + - { name: service_id, in: path, required: true, schema: { type: string } } + - { name: entity_id, in: path, required: true, schema: { type: string } } + - { name: instance_id, in: path, required: true, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v1: drawings ---------------- + /api/v1/drawings/cross-sections: + post: + tags: [drawings] + summary: Создать сечение + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [drawings] + summary: Сечения по ID инстанса + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/drawings/cross-sections/{cross_section_id}/data: + get: + tags: [drawings] + summary: Данные сечения + parameters: + - { name: cross_section_id, in: path, required: true, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/drawings/cross-sections/{cross_section_id}: + delete: + tags: [drawings] + summary: Удалить сечение + parameters: + - { name: cross_section_id, in: path, required: true, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/drawings/exports: + post: + tags: [drawings] + summary: Создать экспорт + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + get: + tags: [drawings] + summary: Список экспортов + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v1/drawings/exports/{export_id}: + delete: + tags: [drawings] + summary: Удалить экспорт + parameters: + - { name: export_id, in: path, required: true, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v2 ---------------- + /api/v2/resources/: + get: + tags: [resources] + summary: Расширенный список ресурсов (v2) + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v2/resources: + post: + tags: [resources] + summary: Создать расширенный ресурс (v2) + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v2/resources/{id}: + get: + tags: [resources] + summary: Расширенный ресурс по ID (v2) + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + patch: + tags: [resources] + summary: Обновить расширенный ресурс (v2) + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + delete: + tags: [resources] + summary: Удалить расширенный ресурс (v2) + parameters: + - $ref: '#/components/parameters/IdPathPlain' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v2/users/: + get: + tags: [users] + summary: Пользователи по ресурсу (v2) + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v2/disks/{diskPath}/documents: + get: + tags: [documents] + summary: Список документов по диску (v2) + parameters: + - $ref: '#/components/parameters/DiskPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- v3 / v4 ---------------- + /api/v3/disks/{diskPath}/documents: + get: + tags: [documents] + summary: Упрощённый список документов по диску (v3) + parameters: + - $ref: '#/components/parameters/DiskPath' + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + /api/v4/disks/{disk_id}/documents: + get: + tags: [documents] + summary: Поиск документов по диску (v4) + parameters: + - { name: disk_id, in: path, required: true, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/Ok' } + '401': { $ref: '#/components/responses/Unauthorized' } + + # ---------------- internal ---------------- + /internal/v1/documents/approving_users: + post: + tags: [internal] + summary: Согласующие пользователи (внутренний, без аутентификации) + security: [] + responses: + '200': { $ref: '#/components/responses/Ok' } + /internal/healthz/startup: + get: + tags: [internal] + summary: Startup-проба (БД, опционально Valkey) + security: [] + responses: + '200': { description: OK } + '503': { description: Service Unavailable } + /internal/healthz/live: + get: + tags: [internal] + summary: Liveness-проба + security: [] + responses: + '200': { description: OK } + /internal/healthz/ready: + get: + tags: [internal] + summary: Readiness-проба + security: [] + responses: + '200': { description: OK } + '503': { description: Service Unavailable } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. В режиме Zitadel + дополнительно передаётся заголовок `Identity: Bearer `. + + parameters: + Limit: + name: limit + in: query + required: false + schema: { type: integer, format: int64, minimum: 0 } + description: Размер страницы + Offset: + name: offset + in: query + required: false + schema: { type: integer, format: int64, minimum: 0 } + description: Смещение + DiskPath: + name: diskPath + in: path + required: true + schema: { type: string } + description: Жадный wildcard-сегмент Fiber (может содержать `/`) — путь/ID диска + DocumentPath: + name: documentPath + in: path + required: true + schema: { type: string } + description: Жадный wildcard-сегмент Fiber — путь/ID документа + TargetPath: + name: targetPath + in: path + required: true + schema: { type: string } + description: Жадный wildcard-сегмент Fiber — путь/ID таргета + RemarkPath: + name: remarkPath + in: path + required: true + schema: { type: string } + description: Жадный wildcard-сегмент Fiber — путь/ID замечания + WsId: + name: ws_id + in: path + required: true + schema: { type: string } + description: UUID рабочей области + IdPath: + name: id + in: path + required: true + schema: { type: string } + IdPathPlain: + name: id + in: path + required: true + schema: { type: string } + IdInt: + name: id + in: path + required: true + schema: { type: integer } + description: Числовой идентификатор (Fiber-ограничение `:id`) + + responses: + Ok: + description: Успешный ответ (структура зависит от эндпоинта) + content: + application/json: + schema: {} + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + Unauthorized: + description: Не аутентифицирован (пустое тело) + Forbidden: + description: Нет доступа + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: { $ref: '#/components/schemas/AppError' } + + schemas: + AppError: + type: object + description: Формат ошибки бизнес-слоя (`internal/app_errors/errors.go`) + properties: + message: + type: string + example: not found + error_code: + type: string + description: Внутренний код ошибки (`GW-XXXX`) + enum: [GW-0000, GW-0001, GW-0002, GW-0003, GW-0004, GW-0014] + example: GW-0001 + required: [message, error_code] diff --git a/apps/drawings/.env.example b/apps/drawings/.env.example new file mode 100644 index 0000000..3d8d2d7 --- /dev/null +++ b/apps/drawings/.env.example @@ -0,0 +1,30 @@ +# drawings-api — пример переменных окружения. +# Конфигурация читается через github.com/kelseyhightower/envconfig +# (config/config.go). Приложение НЕ загружает .env автоматически — +# переменные нужно экспортировать в окружение процесса самому, +# напр.: set -a && . ./.env && set +a + +# API +API_ADDRESS=localhost:6666 + +# Postgres +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_DB=drawings +POSTGRES_ADDRESS=localhost:6432 +POSTGRES_POOL_SIZE=10 +# TLS-подключение к БД. При ENABLE_SSL=true используется сертификат +# из YC-PG-CERTIFICATE (PEM-содержимое, не путь к файлу) +ENABLE_SSL=false +YC-PG-CERTIFICATE= + +# Workflow (интеграция с workflows-api через sdk-go) +WORKFLOW_HOST=http://workflows-api-service.proc/ +CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q +IMAGE_NAME_EXPORT_TO_DWG=cross-sections-to-dwg +IMAGE_TAG=develop +TASK_VERSION=1 +# Внутренний URL самого drawings-api — на него workflow вызывает webhook +DRAWING_INTERNAL_URL=http://drawings-api-service.aero/ +# URL сервиса attachments (передаётся в задачу экспорта) +ATTACHMENT_URL=http://attachments-service.documentations.svc.cluster.local:80 diff --git a/apps/drawings/CONFIGURATION.md b/apps/drawings/CONFIGURATION.md new file mode 100644 index 0000000..a8d0c34 --- /dev/null +++ b/apps/drawings/CONFIGURATION.md @@ -0,0 +1,120 @@ +# Конфигурация проекта drawings-api + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис написан на **Go** (`gitlab.com/sarex-team/rnd/drawings-api`) и настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (`envconfig.Process("", &Config)`). + +Особенности разбора: + +- **префикса нет** — переменные читаются по именам из тега `envconfig:"..."` (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`); +- вложенных секций через разделитель нет: `Config` — плоская композиция трёх структур (`Postgres`, `API`, `Workflow`), у каждого поля своё явное имя переменной; +- часть полей имеет дефолт через тег `default:"..."` (напр. `CONTAINER_REGISTRY`, `TASK_VERSION`); поля без дефолта при отсутствии переменной получают нулевое значение типа (пустая строка / `0` / `false`), ошибки старта из-за «обязательности» нет; +- при ошибке разбора (`envconfig.Process`) приложение завершается с `logger.Fatalf` (`config.MustParse`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Файл `.env` в репозитории — только шаблон; приложение его **не загружает автоматически** (в коде нет чтения `.env`/dotenv), переменные нужно экспортировать в окружение самому. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `.env` — шаблон, экспортируется вручную, напр. `set -a && . ./.env && set +a`. Сборка — `make drawings-api` / `make migrations` | +| Локально (контейнер) | `Dockerfile` (multi-stage, `golang:1.22`) + `entrypoint.sh`. Переменные пробрасываются через `--env`/`--env-file` при запуске контейнера | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`) и выбор окружения по ветке/тегу через `workflow.rules` | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала применяются миграции (`migrations migrate`), затем стартует основной бинарник (`drawings-api`). + +Точки входа (`cmd/`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `drawings-api` | `cmd/drawings-api` | HTTP API-сервер (gorilla/mux) | +| `migrations migrate` | `cmd/migrations` | Применение миграций БД (`robinjoseph08/go-pg-migrations`) | + +## Переменные приложения + +Все переменные ниже читаются кодом приложения (`config/config.go`). В столбце «Значение по умолчанию» указан дефолт из тега `default:"..."`; `—` означает, что дефолта нет (при отсутствии переменной поле получает нулевое значение типа). + +### API (`API`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера, напр. `0.0.0.0:8080` | + +### Postgres (`Postgres`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_ADDRESS` | string | — | Адрес PostgreSQL в формате `host:port` | +| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (`pg.Options.PoolSize`) | +| `ENABLE_SSL` | bool | — | Подключение к БД по TLS. При `true` строится `tls.Config` из `YC-PG-CERTIFICATE` | +| `YC-PG-CERTIFICATE` | string | — | PEM-содержимое CA-сертификата PostgreSQL (не путь к файлу). Используется только при `ENABLE_SSL=true` | + +> При `ENABLE_SSL=true` из содержимого `YC-PG-CERTIFICATE` собирается пул корневых сертификатов; `ServerName` берётся из хостовой части `POSTGRES_ADDRESS`, при этом в коде выставлен `InsecureSkipVerify: true`. Имя переменной `YC-PG-CERTIFICATE` содержит дефисы (нестандартно для env), но именно так указано в теге `envconfig`. + +### Workflow (`Workflow`) + +Интеграция с сервисом workflows через `gitlab.com/sarex-team/sdk-go/pkg/workflows`: создание workflow экспорта cross-section в DWG (задача парсинга + задача webhook-уведомления). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOW_HOST` | string | — | Базовый URL сервиса workflows | +| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов для задач workflow | +| `IMAGE_NAME_EXPORT_TO_DWG` | string | — | Имя образа задачи экспорта cross-section в DWG | +| `IMAGE_TAG` | string | — | Тег образов задач workflow | +| `TASK_VERSION` | string | `1` | Версия задачи экспорта (параметр `version`) | +| `DRAWING_INTERNAL_URL` | string | — | Внутренний URL самого drawings-api; на него workflow вызывает webhook `POST {DRAWING_INTERNAL_URL}internal/v1/exports/{export_id}/webhook` | +| `ATTACHMENT_URL` | string | — | URL сервиса attachments (передаётся в задачу экспорта как `attachment_url`) | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Деплой выполняется через `universal-chart` (`Chart.yaml`, зависимость `universal-chart`). Сервис `drawings-api` слушает порт `8080`; probes настроены на `/ping` (в чарте выключены). Обычные значения задаются в блоке `envs`, значения из секретов — в `secretEnvs`. Значения различаются по окружениям через ключи `_default` / `stage` / `preprod` / `production`. + +Обычные значения (`envs`) — те же переменные приложения, что описаны выше (`API_ADDRESS`, `ENABLE_SSL`, `WORKFLOW_HOST`, `CONTAINER_REGISTRY`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `TASK_VERSION`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL`), различаются адресами сервисов, тегами образов и флагом `ENABLE_SSL` по окружениям. + +Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default`) | Секрет (`stage`) | Ключ | +| --- | --- | --- | --- | +| `POSTGRES_USER` | `ya-pg-secret` | `drawings-postgresql-secret` | `username` | +| `POSTGRES_PASSWORD` | `ya-pg-secret` | `drawings-postgresql-secret` | `password` | +| `POSTGRES_DB` | `ya-pg-secret` | `drawings-postgresql-secret` | `database` | +| `POSTGRES_POOL_SIZE` | `ya-pg-secret` | `drawings-postgresql-secret` | `pool-size` | +| `POSTGRES_ADDRESS` | `ya-pg-secret` | `drawings-postgresql-secret` | `address` | +| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `drawings-postgresql-secret` | `ca.crt` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | +| ветка `stage` | `stage` | `aero` | `0.0.1-stage` | `universal-chart-stage` | +| ветка `master` | `preprod` | `drawings-preprod` | `0.0.1-preprod` | `universal-chart-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `drawings-prod` | `0.0.1-prod` | `universal-chart-production` | +| merge request | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные пайплайна: `SERVICE_NAME=drawings-api`, `DOCKERFILE_PATH=./Dockerfile`, `RELEASE_NAME=drawings-api`, `CHART_NAME=${SERVICE_NAME}`, `BUILD_ARGS` (`--build-arg CI_COMMIT_SHORT_SHA=…`), `HELM_SET_ARGS` (`--set universal-chart.services.drawings-api.image.name.=…`, `--set universal-chart.global.env=`, а также `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner`). + +## Замечания и потенциальные проблемы + +- **Нет обязательности полей.** В отличие от pydantic-конфигов других сервисов, `envconfig` не помечает поля обязательными — при отсутствии переменной поле молча получает нулевое значение. Например, пустой `POSTGRES_ADDRESS` не вызовет ошибку старта конфига, но приведёт к ошибке при подключении к БД. +- **Имя `YC-PG-CERTIFICATE` с дефисами** нестандартно для переменных окружения, но именно так задано в теге `envconfig` и в Helm-секрете. В отличие от других сервисов, здесь это **содержимое** сертификата (PEM), а не путь к файлу. +- **TLS к БД с `InsecureSkipVerify: true`.** При `ENABLE_SSL=true` корневой сертификат подхватывается, но проверка имени/цепочки фактически ослаблена флагом `InsecureSkipVerify`. +- **Webhook-петля.** `DRAWING_INTERNAL_URL` должен указывать на сам drawings-api внутри кластера — по нему workflow дергает `POST internal/v1/exports/{export_id}/webhook` для перевода экспорта в статус `done`. Неверный URL оставит экспорты в статусе `running`. +- **`.env` не загружается автоматически** — переменные нужно экспортировать вручную либо задавать через окружение контейнера. + +## Минимальный набор для локального запуска + +Минимально необходимо задать: + +- `API_ADDRESS` (напр. `localhost:6666`) +- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_ADDRESS`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL` (`false` локально; тогда `YC-PG-CERTIFICATE` не нужен) +- `WORKFLOW_HOST`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL` (для сценариев экспорта; `CONTAINER_REGISTRY` и `TASK_VERSION` имеют дефолты) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/drawings/openapi.yaml b/apps/drawings/openapi.yaml new file mode 100644 index 0000000..9d504c7 --- /dev/null +++ b/apps/drawings/openapi.yaml @@ -0,0 +1,426 @@ +openapi: 3.0.3 + +info: + title: Drawings API + version: "0.0.1" + description: | + REST API сервиса **drawings-api** (`gitlab.com/sarex-team/rnd/drawings-api`) — + управление разрезами (cross-sections) чертежей, их данными и экспортом + в DWG через сервис workflows. + + Сервис написан на **Go** (gorilla/mux, go-pg). Роутер собирается в + `cmd/drawings-api/bootstrap.go`. Помимо служебных эндпоинтов + (`/ping`, `/metrics`) есть две группы бизнес-маршрутов с одинаковым + набором операций: + + - `/api/v1/*` — публичный роутинг; + - `/internal/v1/*` — внутренний роутинг (набор тот же плюс webhook + экспорта, вызываемый воркером workflow). + + На все бизнес-маршруты навешены middleware: JSON-ответ (`rest.JSONResponse`), + request-id (`reqid.Middleware`) и логирование. Явной аутентификации в коде + сервиса нет — доступ ограничивается на уровне ingress/сети кластера. + + ### Экспорт в DWG + `POST /exports` создаёт запись экспорта и запускает workflow из двух задач + (парсинг cross-section в DWG + webhook-уведомление). По завершении workflow + вызывает `POST /internal/v1/exports/{export_id}/webhook`, который переводит + экспорт в статус `done`. + +servers: + - url: /api/v1 + description: Публичный префикс + - url: /internal/v1 + description: Внутренний префикс + +tags: + - name: service + description: Служебные эндпоинты + - name: cross-sections + description: Разрезы чертежей + - name: exports + description: Экспорт разрезов в DWG + +paths: + /ping: + get: + tags: [service] + summary: Healthcheck + description: Возвращает статус готовности. Доступен в корне (без префикса). + responses: + "200": + description: Сервис готов + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ready + + /metrics: + get: + tags: [service] + summary: Prometheus-метрики + description: Метрики в формате Prometheus. Доступен в корне (без префикса). + responses: + "200": + description: Метрики + content: + text/plain: + schema: + type: string + + # ----- Публичные маршруты (/api/v1) и внутренние (/internal/v1) идентичны, + # кроме webhook, который есть только на /internal/v1. Пути ниже указаны + # относительно префикса из блока servers. ----- + + /cross-sections: + post: + tags: [cross-sections] + summary: Создать разрез + description: Создаёт cross-section вместе с его данными (`data.raw_data`). + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCrossSectionRequest" + responses: + "200": + description: Созданный разрез + content: + application/json: + schema: + $ref: "#/components/schemas/CrossSection" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + get: + tags: [cross-sections] + summary: Список разрезов по instance_id + parameters: + - name: instance_id + in: query + required: true + description: UUID инстанса (чертежа) + schema: + type: string + format: uuid + responses: + "200": + description: Массив разрезов (пустой, если ничего не найдено) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CrossSection" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /cross-sections/{cs_id}: + delete: + tags: [cross-sections] + summary: Удалить разрез (soft-delete) + parameters: + - $ref: "#/components/parameters/CrossSectionId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "404": + description: Разрез не найден (возможно, уже удалён) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/StorageError" + + /cross-sections/{cs_id}/data: + get: + tags: [cross-sections] + summary: Данные разреза + parameters: + - $ref: "#/components/parameters/CrossSectionId" + responses: + "200": + description: Данные разреза + content: + application/json: + schema: + $ref: "#/components/schemas/Data" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports: + post: + tags: [exports] + summary: Создать экспорт разреза в DWG + description: | + Создаёт запись экспорта и запускает workflow экспорта в DWG. + В ответе `workflow_status` = `running`. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateExportRequest" + responses: + "200": + description: Созданный экспорт + content: + application/json: + schema: + $ref: "#/components/schemas/Export" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + get: + tags: [exports] + summary: Список экспортов по фильтрам + description: | + Хотя бы один из фильтров должен быть задан, иначе `400`. + Каждый параметр — список UUID/чисел через запятую. + parameters: + - name: cross_section_ids + in: query + required: false + description: UUID разрезов через запятую + schema: + type: string + - name: export_ids + in: query + required: false + description: UUID экспортов через запятую + schema: + type: string + - name: workflow_ids + in: query + required: false + description: UUID workflow через запятую + schema: + type: string + - name: attachment_ids + in: query + required: false + description: ID вложений (целые) через запятую + schema: + type: string + responses: + "200": + description: Массив экспортов (пустой, если ничего не найдено) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Export" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports/{export_id}: + delete: + tags: [exports] + summary: Удалить экспорт + parameters: + - $ref: "#/components/parameters/ExportId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports/{export_id}/webhook: + post: + tags: [exports] + summary: Webhook завершения экспорта (только /internal/v1) + description: | + Вызывается воркером workflow по завершении экспорта. Переводит + экспорт в статус `done`. Доступен только по внутреннему префиксу + `/internal/v1`. + parameters: + - $ref: "#/components/parameters/ExportId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + +components: + parameters: + CrossSectionId: + name: cs_id + in: path + required: true + description: UUID разреза + schema: + type: string + format: uuid + ExportId: + name: export_id + in: path + required: true + description: UUID экспорта + schema: + type: string + format: uuid + + responses: + OK: + description: Успешно (тело — строка `"OK"`) + content: + application/json: + schema: + type: string + example: OK + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + StorageError: + description: Внутренняя ошибка (ошибка хранилища) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + schemas: + CrossSection: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + created_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + instance_id: + type: string + format: uuid + documents: + type: object + additionalProperties: + $ref: "#/components/schemas/ConnectedDocument" + exports: + type: array + items: + $ref: "#/components/schemas/Export" + + ConnectedDocument: + type: object + properties: + color: + type: string + name: + type: string + + Data: + type: object + properties: + cross_section_id: + type: string + format: uuid + raw_data: + type: string + + CreateCrossSectionRequest: + type: object + description: Разрез плюс его данные. Наследует поля CrossSection. + allOf: + - $ref: "#/components/schemas/CrossSection" + - type: object + properties: + data: + $ref: "#/components/schemas/Data" + + Author: + type: object + properties: + id: + type: integer + format: int64 + first_name: + type: string + last_name: + type: string + + Export: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + author: + $ref: "#/components/schemas/Author" + created_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + cross_section_id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + nullable: true + workflow_status: + type: string + nullable: true + enum: [done, running, error] + file_type: + type: string + enum: [dwg] + attachment_id: + type: integer + nullable: true + + CreateExportRequest: + type: object + required: [cross_section_id, author, file_type, company_id] + properties: + cross_section_id: + type: string + format: uuid + author: + $ref: "#/components/schemas/Author" + file_type: + type: string + enum: [dwg] + company_id: + type: integer + format: int64 + + Error: + type: object + description: Ответ об ошибке (gotools/httperror). + properties: + error: + type: string diff --git a/apps/eav/.env.example b/apps/eav/.env.example new file mode 100644 index 0000000..7244416 --- /dev/null +++ b/apps/eav/.env.example @@ -0,0 +1,54 @@ +# Django +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_DEBUG=False +DJANGO_SECRET_KEY='v628rpgi^!!57jq9y7y3^by04c1bc@#6%0_a(ekxfmyat8gxew' + +# App +SERVICE_NAME=eav +VERSION=1.0.0 + +# Database (PostgreSQL) — читается только в config.settings.production +DJANGO_POSTGRES_HOST=127.0.0.1 +DJANGO_POSTGRES_PORT=6432 +DJANGO_POSTGRES_DATABASE=eav_db +DJANGO_POSTGRES_USER=sarex +DJANGO_POSTGRES_PASSWORD=password + +# Auth / JWT (RS512) — обязательны в config.settings.production +SIMPLE_JWT_ISSUER=django +# Replace newlines with \n +JWT_PRIVATE_KEY='' +JWT_PUBLIC_KEY='' + +# S3 (Yandex Object Storage, бото3) +YC_S3_ACCESS_KEY_ID= +YC_S3_SECRET_ACCESS_KEY= +YC_S3_BUCKET_NAME=eav +YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net + +# Kafka +KAFKA_ENABLED=True +KAFKA_HOST= +KAFKA_USERNAME=platform +KAFKA_PASSWORD= +KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt +SASL_MECHANISM=SCRAM-SHA-512 +SECURITY_PROTOCOL=SASL_SSL +ASSETS_TOPIC=assets_broadcast_test + +# Kafka topics (события EAV) +KAFKA_TOPIC_ATTRIBUTE_CREATED=eav.attribute.created.v1 +KAFKA_TOPIC_ATTRIBUTE_UPDATED=eav.attribute.updated.v1 +KAFKA_TOPIC_ATTRIBUTE_DELETED=eav.attribute.deleted.v1 +KAFKA_TOPIC_VALUE_OPTION_CREATED=eav.value_option.created.v1 +KAFKA_TOPIC_VALUE_OPTION_DELETED=eav.value_option.deleted.v1 + +# OpenTelemetry (трейсинг включается только если USE_OTEL задана) +USE_OTEL=False +SERVICE_NAME=eav.eav-backend +TRACER_ENDPOINT=localhost:4375 +USE_INSECURE=False +ENVIRONMENT=prod +MODULE=eav +TEAM=platform_team +COMPONENT=backend diff --git a/apps/eav/CONFIGURATION.md b/apps/eav/CONFIGURATION.md new file mode 100644 index 0000000..804ee9b --- /dev/null +++ b/apps/eav/CONFIGURATION.md @@ -0,0 +1,186 @@ +# Конфигурация проекта eav-python + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис — это Django-приложение (**Django 4.1 + Django REST Framework**), запускаемое как WSGI (`config.wsgi`) через **uWSGI** (порт `8000`, см. `compose/eav-backend/uwsgi.ini`). Настройки читаются из переменных окружения в `src/config/settings/base.py` и `src/config/settings/production.py`. Разбор выполняется частично через библиотеку [`django-environ`](https://django-environ.readthedocs.io/) (объект `env = environ.Env()`), частично напрямую через `os.getenv`. + +Особенности разбора: + +- **префикса/делимитера у секций нет** — каждая настройка задаётся плоской переменной окружения (напр. `DJANGO_POSTGRES_HOST`, `KAFKA_HOST`, `YC_S3_BUCKET_NAME`); +- **`.env` не загружается автоматически** — в коде нет вызова `environ.Env.read_env()` / `load_dotenv`, хотя `python-dotenv` присутствует в зависимостях. Переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s. Файл `.env` при этом в `.gitignore`; +- **выбор набора настроек** задаётся `DJANGO_SETTINGS_MODULE`: `config.settings.production` (боевой набор с БД, CORS, JWT), `config.settings.test` (только `base`), либо `config.settings.local` (по умолчанию в `manage.py`, в репозитории отсутствует, `.gitignore`); +- **часть переменных читается только в `production.py`** — БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`); в `base.py`/`test.py` их нет. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (manage.py / uWSGI) | Переменные окружения процесса (`.env` нужно экспортировать вручную) | +| Локально (docker-compose) | `docker-compose.yml`: блок `environment` сервиса `backend` + образ `postgres` (timescaledb-postgis) | +| Kubernetes (Helm, репозиторий приложения) | `.helm/values-.yaml`: блоки `backend.deployment.envs` (обычные значения) и `backend.deployment.secrets` (из k8s-секретов через `secretKeyRef`); шаблон `templates/server.yaml`, роутинг — `templates/mesh-config.yaml` (Istio VirtualService) | +| Kubernetes (infra, Flux/Kustomize) | `infra/iac/apps/eav/base/backend-deployment.yaml`: секреты инъектируются Vault-агентом (`vault.hashicorp.com/agent-inject-*`) и экспортируются в окружение в `args`; настройки `production.py` монтируются из `django-configmap` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci`, переменные пайплайна в `workflow.rules` | + +Способы запуска процессов: + +| Процесс | Точка входа | Назначение | +| --- | --- | --- | +| HTTP API | `compose/eav-backend/entrypoint.sh` → `uwsgi --ini uwsgi.ini` (`config.wsgi`, порт 8000) | REST API | +| Миграции | `entrypoint.sh` → `python3 manage.py migrate` (выполняется перед стартом uWSGI) | Миграции БД | +| Kafka-продюсер | `config/kafka.py` (инициализируется при импорте, если `KAFKA_ENABLED`) | Публикация событий EAV в топики | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала `manage.py migrate`, затем `uwsgi` (оба под `opentelemetry-instrument`). + +## Переменные приложения + +Дефолт `—` означает, что значения по умолчанию в коде нет. + +### Django / приложение + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.local` (в `manage.py`); в контейнере — `config.settings.production` | Какой набор настроек Django загружать | +| `DJANGO_DEBUG` | bool | `False` | Режим отладки Django (в `production.py` жёстко `False`) | +| `DJANGO_SECRET_KEY` | string | (захардкоженный дефолт) | Секретный ключ Django. В проде обязателен свой | +| `SERVICE_NAME` | string | `eav` | Имя сервиса (в `base.py`); в OTEL-секции дефолт `eav.eav-backend` | +| `VERSION` | string | `1.0.0` | Версия приложения | + +### Database — PostgreSQL (только `config.settings.production`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_POSTGRES_HOST` | string | — | Хост PostgreSQL | +| `DJANGO_POSTGRES_PORT` | int | `6432` | Порт PostgreSQL (в infra-манифесте — `5432`) | +| `DJANGO_POSTGRES_DATABASE` | string | — | Имя базы данных | +| `DJANGO_POSTGRES_USER` | string | — | Пользователь БД | +| `DJANGO_POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | + +> Engine — `django.db.backends.postgresql`. В `docker-compose.yml` поднимается `timescale/timescaledb-postgis` (проекту нужны расширения PostGIS/ltree). + +### Auth / JWT (только `config.settings.production`) + +Используются два механизма аутентификации (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): `ZitadelJWTAuthentication` (заголовок `Identity`) и `rest_framework_simplejwt` (RS512). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SIMPLE_JWT_ISSUER` | string | `django` | Значение claim `iss` (проверяется при верификации токена) | +| `JWT_PRIVATE_KEY` | string (PEM) | — | Приватный RSA-ключ (подпись). Экранированные `\n` заменяются на переводы строк. Обязателен | +| `JWT_PUBLIC_KEY` | string (PEM) | — | Публичный RSA-ключ (проверка). Экранированные `\n` заменяются на переводы строк. Обязателен | + +> `SIMPLE_JWT`: алгоритм `RS512`, `ACCESS_TOKEN_LIFETIME` 5 мин, `REFRESH_TOKEN_LIFETIME` 1 день, тип заголовка `Bearer`, claim пользователя — `user_id`. + +### S3 — Yandex Object Storage (`base.py`, boto3/django-storages) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | `None` | Access key | +| `YC_S3_SECRET_ACCESS_KEY` | string | `None` | Secret key | +| `YC_S3_BUCKET_NAME` | string | `None` | Бакет по умолчанию | +| `YC_S3_ENDPOINT_URL` | string | `None` | Эндпоинт S3 | + +> `DEFAULT_FILE_STORAGE`/`STATICFILES_STORAGE` — `storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL=public-read`. + +### Kafka (`base.py`, `config/kafka.py`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_ENABLED` | bool | `True` | Включить реального продюсера (иначе `MockProducer` — события не отправляются) | +| `KAFKA_HOST` | string | `""` | Адрес брокера (`bootstrap_servers`) | +| `KAFKA_USERNAME` | string | `platform` | Пользователь SASL | +| `KAFKA_PASSWORD` | string | `""` | Пароль SASL | +| `KAFKA_SSL_CAFILE` | string | `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt` | CA-сертификат для TLS | +| `SASL_MECHANISM` | string | `SCRAM-SHA-512` | Механизм SASL | +| `SECURITY_PROTOCOL` | string | `SASL_SSL` | Протокол безопасности Kafka | +| `ASSETS_TOPIC` | string | `assets_broadcast_test` | Топик рассылки по ассетам | + +Топики событий EAV: + +| Переменная | Значение по умолчанию | +| --- | --- | +| `KAFKA_TOPIC_ATTRIBUTE_CREATED` | `eav.attribute.created.v1` | +| `KAFKA_TOPIC_ATTRIBUTE_UPDATED` | `eav.attribute.updated.v1` | +| `KAFKA_TOPIC_ATTRIBUTE_DELETED` | `eav.attribute.deleted.v1` | +| `KAFKA_TOPIC_VALUE_OPTION_CREATED` | `eav.value_option.created.v1` | +| `KAFKA_TOPIC_VALUE_OPTION_DELETED` | `eav.value_option.deleted.v1` | + +### OpenTelemetry (`base.py`) + +Блок трейсинга активируется, только если задана переменная `USE_OTEL` (проверяется через `os.getenv('USE_OTEL', False)` — истинно при любом непустом значении). Используется `django-otel-tools`; при включении в начало `MIDDLEWARE` добавляется `OtelMiddleware`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool/string | `False` | Включить трейсинг и OTEL-логгер | +| `SERVICE_NAME` | string | `eav.eav-backend` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool/string | `False` | Небезопасное (без TLS) подключение к коллектору | +| `ENVIRONMENT` | string | `prod` | Атрибут ресурса `environment` | +| `MODULE` | string | `eav` | Атрибут ресурса `module` | +| `TEAM` | string | `platform_team` | Атрибут ресурса `team` | +| `COMPONENT` | string | `backend` | Атрибут ресурса `component` | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `backend.deployment.envs` для каждого окружения (`stage`/`preprod`/`production`): `DJANGO_SETTINGS_MODULE`, `USE_OTEL`, `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE`, `ENVIRONMENT`, `KAFKA_HOST`, `ASSETS_TOPIC` (различаются адресами коллектора/брокера и именами топиков). + +Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `DJANGO_POSTGRES_HOST` | `yc-pg-secret` | `host` | +| `DJANGO_POSTGRES_DATABASE` | `yc-pg-secret` | `database` | +| `DJANGO_POSTGRES_PORT` | `yc-pg-secret` | `port` (только preprod) | +| `DJANGO_POSTGRES_USER` | `yc-pg-secret` | `user` | +| `DJANGO_POSTGRES_PASSWORD` | `yc-pg-secret` | `password` | +| `DJANGO_CLICKHOUSE_HOST` | `yc-ch-secret` | `host` | +| `DJANGO_CLICKHOUSE_DATABASE` | `yc-ch-secret` | `database` | +| `DJANGO_CLICKHOUSE_USER` | `yc-ch-secret` | `user` | +| `DJANGO_CLICKHOUSE_PASSWORD` | `yc-ch-secret` | `password` | +| `YC_S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` | +| `YC_S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` | +| `YC_S3_BUCKET_NAME` | `yc-s3-secret` | `storage_bucket_name` | +| `YC_S3_ENDPOINT_URL` | `yc-s3-secret` | `endpoint_url` | +| `JWT_PRIVATE_KEY` | `jwt-secret` | `private_key` | +| `JWT_PUBLIC_KEY` | `jwt-secret` | `public_key` | +| `KAFKA_USERNAME` | `kafka-secret` / `yc-kafka-secret` | `username` | +| `KAFKA_PASSWORD` | `kafka-secret` / `yc-kafka-secret` | `password` | +| `KAFKA_HOST` | `yc-kafka-secret` | `host` (prod/stage) | + +Помимо env, чарт монтирует CA-сертификаты: PostgreSQL (`yc-pg-certificate` → `~/.postgresql/root.crt`) и Yandex Internal Root CA (`yc-ch-certificate` → `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt`, тот же путь, что в `KAFKA_SSL_CAFILE`), а также конфиг clickhouse-client. + +## Переменные в infra-манифесте (Flux/Kustomize, `infra/iac/apps/eav`) + +В отличие от Helm-чарта приложения, боевой деплой Sarex использует Vault-инъекцию (`base/backend-deployment.yaml`). Секреты рендерятся Vault-агентом в файлы `/vault/secrets/*` и экспортируются в окружение в `args` контейнера перед запуском `entrypoint.sh`: + +| Переменная(ые) | Источник (Vault path) | +| --- | --- | +| `DJANGO_POSTGRES_HOST/PORT/DATABASE/USER/PASSWORD` | `secrets/data/postgresql/apps/eav` | +| `YC_S3_ENDPOINT_URL/BUCKET_NAME/ACCESS_KEY_ID/SECRET_ACCESS_KEY` | `secrets/data/minio/apps/eav` | +| `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` | `secrets/data/vault/common/rsa_keys` | + +Прямо в `env` деплоймента задаются `KAFKA_ENABLED=False`, `ASSETS_TOPIC=sarex`, `DJANGO_SETTINGS_MODULE=config.settings.production`. Файл `production.py` монтируется из `django-configmap` (переопределяет `production.py` из образа; в нём `DEBUG=True`, `ALLOWED_HOSTS=['*']`, свои CORS/CSRF-домены и имена cookie `eav-sessionid`/`eav-csrftoken`). + +## Замечания и потенциальные проблемы + +- Приложение **не загружает `.env` автоматически** (нет `read_env`/`load_dotenv`). `python-dotenv` установлен, но не используется в настройках — переменные нужно экспортировать в окружение самому. +- Переменные БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`) читаются **только** в `config.settings.production`. При `test`/`base` их отсутствие не мешает старту, но БД по умолчанию не сконфигурирована. +- `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY` в `production.py` читаются через `env.str(...)` **без дефолта** — их отсутствие приводит к ошибке старта. В infra-варианте (`django-configmap`) используется `get_env_variable` с тем же требованием. +- `DJANGO_CLICKHOUSE_*` присутствуют в Helm-секретах, но **кодом приложения не читаются** (в текущих настройках ClickHouse не используется) — это подготовка/наследие инфраструктуры. +- `KAFKA_ENABLED`: при ложном значении используется `MockProducer` — события EAV в Kafka не публикуются (так сделано в infra-деплое: `KAFKA_ENABLED=False`). Значение разбирается `django-environ` как bool. +- Флаги OTEL (`USE_OTEL`, `USE_INSECURE`) читаются через `os.getenv(..., False)` и трактуются как истинные при **любой непустой строке**, включая `"False"`. Чтобы отключить — переменную нужно не задавать вовсе. +- `SERVICE_NAME` определяется дважды: как имя приложения (`base.py`, дефолт `eav`) и как имя сервиса в OTEL (дефолт `eav.eav-backend`) — фактически одна и та же переменная окружения. +- В `docker-compose.yml` захардкожен пароль БД (`zealot096`) — только для локального окружения. + +## Минимальный набор для локального запуска (`config.settings.production`) + +- `DJANGO_SETTINGS_MODULE=config.settings.production` +- `DJANGO_POSTGRES_HOST`, `DJANGO_POSTGRES_PORT`, `DJANGO_POSTGRES_DATABASE`, `DJANGO_POSTGRES_USER`, `DJANGO_POSTGRES_PASSWORD` +- `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` (обязательны; можно тестовую RSA-пару) +- `KAFKA_ENABLED=False` (чтобы не поднимать брокер) либо `KAFKA_HOST`/`KAFKA_USERNAME`/`KAFKA_PASSWORD` +- при работе с файлами: `YC_S3_ACCESS_KEY_ID`, `YC_S3_SECRET_ACCESS_KEY`, `YC_S3_BUCKET_NAME`, `YC_S3_ENDPOINT_URL` +- `USE_OTEL` — не задавать (иначе включится трейсинг) + +Готовые значения-примеры для всех переменных приведены в `.env.example`. diff --git a/apps/eav/openapi.yaml b/apps/eav/openapi.yaml new file mode 100644 index 0000000..236f17d --- /dev/null +++ b/apps/eav/openapi.yaml @@ -0,0 +1,1303 @@ +openapi: 3.0.3 + +info: + title: EAV Service API + version: "1.0.0" + description: | + REST API сервиса **eav-python** (`platform/eav-python`) — реализация паттерна + **EAV (Entity-Attribute-Value)** для платформы Sarex: управление атрибутами, + группами атрибутов, единицами измерения, опциями значений, схемами (доменами) + и ассетами. Компонент используется всеми модулями платформы (инспекции, + документы, задачи КСГ, замечания, BIM и т.д.) для гибкой атрибуции моделей. + + Сервис написан на Python (**Django 4.1 + Django REST Framework**) и запускается + как WSGI-приложение (`config.wsgi`) через uWSGI (порт `8000`). Роутинг задан в + `config/urls.py`. Внутри приложения существует несколько версий API, которые + снаружи публикуются под собственными префиксами через Istio VirtualService + (`.helm/templates/mesh-config.yaml`): + + | Внешний префикс (ingress) | Внутренний путь (приложение) | Назначение | + | --- | --- | --- | + | `/eav/api/v0` | `/api/v4` | Публичный API (защищённый дубликат v0) | + | `/eav/api/v1` | `/api/v6` | Публичный API (защищённый дубликат v1) | + | `/eav/api/v2` | `/api/v5` | Публичный API (защищённый дубликат v2) | + | `/eav/api/v3` | `/api/v3` | Публичный API v3 | + | `/eav/api/v4` | `/api/v4` | Публичный API v4 | + | `/eav/admin/` | `/eav/admin/` | Django-admin | + + Внутренние (не опубликованные через ingress) версии `/api/v0`, `/api/v1`, + `/api/v2` — незащищённые (исторические) варианты тех же ресурсов; + `/api/v4`–`/api/v6` — их защищённые дубликаты. Ниже документированы ресурсы + на примере пути `/api/v0/*` (форма запросов/ответов у соответствующих + защищённых версий совпадает). + + ### Аутентификация + Проверка выполняется по цепочке DRF + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): + + 1. **Zitadel** (`ZitadelJWTAuthentication`) — требуются одновременно заголовки + `Authorization: Bearer ` и `Identity: Identity `. Полезная нагрузка + (в т.ч. `tenant_identifier`) берётся из токена `Identity`. При отсутствии + заголовка `Identity` — переход к следующему механизму. + 2. **sarex-backend** (`rest_framework_simplejwt.JWTAuthentication`) — подпись + токена `Authorization: Bearer ` проверяется публичным RSA-ключом + (`JWT_PUBLIC_KEY`, алгоритм `RS512`). + 3. Дополнительно поддерживаются `SessionAuthentication` и `BasicAuthentication` + (для Django-admin / служебного доступа). + + Глобальные права — `AllowAny` (`DEFAULT_PERMISSION_CLASSES`); ограничение + доступа к отдельным ресурсам обеспечивается на уровне view/Istio. + + ### Пагинация + Списочные ответы используют DRF `LimitOffsetPagination` + (`PAGE_SIZE = 10000`). Управление — query-параметрами `limit` и `offset`. + + ### Мультиарендность + Многие эндпоинты принимают `company_id` и/или `tenant_identifier` (query) для + выборки атрибутов/схем в контексте конкретной компании. Специфичные для + компании атрибуты «замещают» общие (системные). + + ### Типы атрибутов (`TypeEnum`) + `0` — целочисленный, `1` — с плавающей запятой, `2` — строка, + `3` — одно из списка, `4` — многие из списка, `5` — да/нет, + `6` — дата со временем, `7` — дата. + +servers: + - url: https://api.sarex.io/eav/api + description: Production (external, через ingress) + - url: https://stage-api.sarex.io/eav/api + description: Stage (external, через ingress) + - url: http://eav-service.eav-prod/api + description: Внутренний адрес в кластере + +security: + - bearerAuth: [] + - bearerAuth: [] + identityAuth: [] + +paths: + /api/v0/attribute/: + get: + operationId: attribute_list + parameters: + - in: query + name: company_id + description: ID компании + schema: + type: string + nullable: true + title: ID компании + - in: query + name: tenant_identifier + description: Идентификатор компании + schema: + type: string + nullable: true + title: Идентификатор компании + tags: + - Attribute + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeRetrieve' + description: '' + post: + operationId: attribute_create + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + /api/v0/attribute/{id}/: + get: + operationId: attribute_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeRetrieve' + description: '' + put: + operationId: attribute_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + patch: + operationId: attribute_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + delete: + operationId: attribute_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + responses: + '204': + description: '' + /api/v0/attribute-group/: + get: + operationId: attribute_group_list + tags: + - Attribute Group + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeGroupRetrieve' + description: '' + post: + operationId: attribute_group_create + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + /api/v0/attribute-group/{id}/: + get: + operationId: attribute_group_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeGroupRetrieve' + description: '' + put: + operationId: attribute_group_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + patch: + operationId: attribute_group_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + delete: + operationId: attribute_group_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + responses: + '204': + description: '' + /api/v0/schema/: + get: + operationId: attribute_schema_list + parameters: + - in: query + name: model_name + description: Наименование модели + schema: + type: string + - in: query + name: service_name + description: Наименование сервиса + schema: + type: string + - in: query + name: type_identifier + description: ID типа модели + schema: + type: string + tags: + - Attribute Schema + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaRetrieve' + description: '' + post: + operationId: attribute_schema_create + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + /api/v0/schema/{id}/: + get: + operationId: attribute_schema_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaRetrieve' + description: '' + put: + operationId: attribute_schema_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + patch: + operationId: attribute_schema_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + delete: + operationId: attribute_schema_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + responses: + '204': + description: '' + /api/v0/unit-option/: + get: + operationId: unit_option_list + tags: + - Unit Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionRetrieve' + description: '' + post: + operationId: unit_option_create + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + /api/v0/unit-option/{id}/: + get: + operationId: unit_option_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionRetrieve' + description: '' + put: + operationId: unit_option_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + patch: + operationId: unit_option_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + delete: + operationId: unit_option_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + responses: + '204': + description: '' + /api/v0/value-option/: + get: + operationId: value_option_list + tags: + - Value Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionRetrieve' + description: '' + post: + operationId: value_option_create + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + /api/v0/value-option/{id}/: + get: + operationId: value_option_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionRetrieve' + description: '' + put: + operationId: value_option_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + patch: + operationId: value_option_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + delete: + operationId: value_option_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + responses: + '204': + description: '' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + JWT в заголовке `Authorization: Bearer `. Подпись проверяется + публичным RSA-ключом (RS512) для механизма sarex-backend. + identityAuth: + type: apiKey + in: header + name: Identity + description: >- + Токен Zitadel в заголовке `Identity: Identity ` (используется вместе + с `Authorization` для механизма ZitadelJWTAuthentication). + schemas: + AttributeRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + readOnly: true + title: Наименование атрибута + type: + type: string + readOnly: true + title: Тип атрибута + group: + type: integer + readOnly: true + nullable: true + title: Группа атрибута + author: + type: string + readOnly: true + title: Автор атрибута + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания атрибута + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления атрибута + options: + type: array + items: + $ref: '#/components/schemas/ValueOptionRetrieve' + title: Опции значения атрибута + required: + - author + - created_at + - group + - id + - name + - options + - tenant_identifier + - type + - updated_at + AttributeCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + title: Наименование атрибута + maxLength: 512 + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Тип атрибута + minimum: 0 + maximum: 32767 + group: + type: integer + nullable: true + title: Группа атрибута + author: + type: string + title: Автор атрибута + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + required: + - author + - id + - name + AttributeUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + title: Наименование атрибута + maxLength: 512 + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Тип атрибута + minimum: 0 + maximum: 32767 + group: + type: integer + nullable: true + title: Группа атрибута + author: + type: string + title: Автор атрибута + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + AttributeGroupRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID группы атрибутов + name: + type: string + readOnly: true + title: Наименование группы атрибутов + parent: + type: integer + readOnly: true + title: Родительская группа атрибутов + author: + type: string + readOnly: true + title: Автор группы атрибутов + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания группы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления группы атрибутов + required: + - author + - created_at + - id + - name + - parent + - tenant_identifier + - updated_at + AttributeSchemaRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + readOnly: true + title: Наименование схемы атрибутов + group: + type: integer + readOnly: true + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + readOnly: true + title: ID модели + type_identifier: + type: string + readOnly: true + title: ID типа модели + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeRetrieve' + title: Атрибуты + readOnly: true + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + is_common: + type: boolean + readOnly: true + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + required: + - attributes + - created_at + - group + - id + - is_common + - model_name + - name + - tenant_identifier + - type_identifier + - updated_at + AttributeSchemaCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + title: Наименование схемы атрибутов + maxLength: 512 + group: + type: integer + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + title: ID модели + maxLength: 512 + type_identifier: + type: string + title: ID типа модели + maxLength: 512 + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeCreate' + readOnly: true + title: Атрибуты + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + required: + - attributes + - created_at + - id + - model_name + - name + - tenant_identifier + - type_identifier + - updated_at + AttributeSchemaUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + title: Наименование схемы атрибутов + maxLength: 512 + group: + type: integer + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + title: ID модели + maxLength: 512 + type_identifier: + type: string + title: ID типа модели + maxLength: 512 + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeCreate' + readOnly: true + title: Атрибуты + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + UnitOptionRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + readOnly: true + title: Наименование единицы измерения + author: + type: string + readOnly: true + title: Автор единицы измерения + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания единицы измерения + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления единицы измерения + required: + - author + - created_at + - id + - name + - tenant_identifier + - updated_at + UnitOptionCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + title: Наименование единицы измерения + maxLength: 512 + author: + type: string + title: Автор единицы измерения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + required: + - author + - id + - name + UnitOptionUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + title: Наименование единицы измерения + maxLength: 512 + author: + type: string + title: Автор единицы измерения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + ValueOptionRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + attribute: + type: integer + readOnly: true + title: ID атрибута + value: + type: string + title: Значение + author: + type: string + readOnly: true + title: Автор опции значения + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания опции значения + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления опции значения + name: + type: string + readOnly: true + title: Наименование опции значения + required: + - attribute + - author + - created_at + - id + - name + - tenant_identifier + - updated_at + - value + ValueOptionCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + value: + type: string + title: Значение + author: + type: string + title: Автор опции значения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + name: + type: string + title: Наименование опции значения + maxLength: 1024 + required: + - author + - id + - value + ValueOptionUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + value: + type: string + title: Значение + author: + type: string + title: Автор опции значения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + name: + type: string + title: Наименование опции значения + maxLength: 1024 + TypeEnum: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + type: integer + description: |- + * `0` - Целочисленное + * `1` - С плавающей запятой + * `2` - Строковое + * `3` - Одно из списка + * `4` - Многие из списка + * `5` - Да/Нет + * `6` - Дата со временем + * `7` - Дата diff --git a/apps/flows/.env.example b/apps/flows/.env.example new file mode 100644 index 0000000..5f09e3e --- /dev/null +++ b/apps/flows/.env.example @@ -0,0 +1,187 @@ +# ============================================================================= +# flows-backend (.env.example) +# ============================================================================= +# Приложение читает переменные окружения напрямую через pydantic BaseSettings +# (src/flow/config.py). У верхнеуровневого класса Settings префикса нет — его +# поля задаются переменными с именем поля в ВЕРХНЕМ регистре (напр. BASE_HOST). +# Вложенные секции конфигурируются отдельными классами со своим env_prefix +# (PG_, DJANGO_, DOCUMENTATION_, RABBITMQ_, TRACING_ и т.д.). +# +# Приложение НЕ загружает .env автоматически (нет python-dotenv/env_file) — +# экспортируйте переменные в окружение самостоятельно, напр.: +# set -a && . ./.env && set +a +# ============================================================================= + +# App / общие настройки (класс Settings, без префикса) +SERVICE_NAME=review-service +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8000 +# Префикс за реверс-прокси (в кластере: /flows) +PROXY_PATH_PREFIX= +API_PREFIX=/api/v1 +API_INTERNAL_PREFIX=/internal/v1 +BASE_HOST=https://lk.sarex.io +DEBUG=False +# Проверка подписи JWT (True в кластере; False удобно для локальной разработки) +JWT_AUTH_ENABLE=True +# Таймаут gunicorn (используется в entrypoint.sh, не кодом) +TIMEOUT=120 + +# Feature-флаги +ENABLE_MAILINGS=True +ENABLE_MAILGUN=True +ENABLE_CELERY=True +ENABLE_EVENTS=True +ENABLE_ANALYTICS=False +SYNC_RESOURCE_ID=False + +# Logger (класс LoggerSettings, префикс LOG_) +LOG_LEVEL=INFO + +# Database — основной PostgreSQL (класс PostgresSettings, префикс PG_) +PG_HOST=127.0.0.1 +PG_PORT=6432 +PG_LOGIN=flow +PG_PASSWORD=password +PG_DB=flows_db + +# Documentation PG — БД сервиса документаций (класс DocumentationDBSettings, префикс DOCUMENTATION_PG_) +DOCUMENTATION_PG_HOST=127.0.0.1 +DOCUMENTATION_PG_PORT=6432 +DOCUMENTATION_PG_USERNAME=flow +DOCUMENTATION_PG_PASSWORD=password +DOCUMENTATION_PG_DATABASE=flows_db + +# RabbitMQ (класс RabbitSettings, префикс RABBITMQ_) +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=flow +RABBITMQ_PASSWORD=flow +RABBITMQ_VHOST=flows + +# Celery (класс CelerySettings, префикс CELERY_; брокер берётся из RABBITMQ_*) +CELERY_QUEUE=flow + +# Sarex backend (Django) (класс DjangoSettings, префикс DJANGO_) +DJANGO_USE=True +DJANGO_HOST=http://localhost:8000/api +DJANGO_TIMEOUT=60 +# base64(login:password) для Basic-auth +DJANGO_TOKEN= + +# Documentation service (класс DocumentationSettings, префикс DOCUMENTATION_) +DOCUMENTATION_USE=True +DOCUMENTATION_HOST=https://api.sarex.io/documentations/api/v1 +DOCUMENTATION_EXTERNAL_HOST=https://api.sarex.io/documentations/api/v1 +DOCUMENTATION_TIMEOUT=60 + +# EAV service (класс EAVSettings, префикс EAV_) +EAV_HOST=http://eav-service.eav-prod +EAV_TIMEOUT=60 + +# Planning management / MSP (класс PlanningManagementSettings, префикс PLANNING_) +PLANNING_USE=True +PLANNING_HOST=https://api.sarex.io/api/pm/msp +PLANNING_TIMEOUT=60 + +# Checklists service (класс CheckListsSettings, префикс CHECKLIST_) +CHECKLIST_USE=True +CHECKLIST_HOST=https://stage-api.sarex.io/checklists +CHECKLIST_TIMEOUT=60 + +# Workflows service (класс WorkflowsSettings, префикс WORKFLOWS_) +WORKFLOWS_USE=True +WORKFLOWS_HOST=https://lk.sarex.io/workflows/api/v1 +WORKFLOWS_TIMEOUT=60 + +# Gateway / Resources (поля класса Settings, используются при SYNC_RESOURCE_ID=1) +GATEWAY_URL=https://stage-api.sarex.io/gateway +RESOURCE_URL=https://stage-api.sarex.io/resources + +# Event bus (класс EventBusSettings, префикс EVENTS_) +EVENTS_HOST=ws://localhost:8000/ws +EVENTS_CONNECTION_TIMEOUT=5 +EVENTS_COUNT_RETRIES=100 + +# Admin panel (класс AdminPanelSettings, префикс ADMIN_PANEL_) +ADMIN_PANEL_SECRET_KEY=hex +ADMIN_PANEL_TOKEN_MAX_AGE=86400 + +# Auth (RSA public key для проверки JWT; в кластере монтируется как JWT_PUBLIC_KEY) +JWT_PUBLIC_KEY= + +# Sentry (класс SentrySettings, префикс SENTRY_) +SENTRY_DSN= +SENTRY_ENVIRONMENT=production +SENTRY_TRACES_SAMPLE_RATE=1.0 +SENTRY_SEND_DEFAULT_PII=True + +# Почта: SMTP (поля класса Settings; альтернатива Mailgun) +SMTP_HOST= +SMTP_PORT= +FROM_EMAIL= + +# OpenTelemetry / трейсинг (класс TraceSettings, префикс TRACING_) +TRACING_USE=False +TRACING_HOST=localhost:4317 +TRACING_INSECURE=False +TRACING_SERVICE_NAME=flows +TRACING_ENVIRONMENT=prod +TRACING_MODULE=flows +TRACING_TEAM=team_proc +TRACING_COMPONENT=backend + +# Прочее (не читается приложением, задаётся в инфраструктуре) +# ENABLE_METRICS=0 + +# ============================================================================= +# Переменные ТОЛЬКО для celery-воркера и scheduler +# (src/worker/notifications_config.py, src/worker/sync_config.py) +# ============================================================================= + +# Flows DB (класс FlowsDatabase, префикс FLOWS_DB_) +FLOWS_DB_HOST=127.0.0.1 +FLOWS_DB_PORT=6432 +FLOWS_DB_DB=flows_db +FLOWS_DB_USERNAME=flow +FLOWS_DB_PASSWORD=password + +# Issues DB (класс IssuesDatabase, префикс ISSUES_DB_) +ISSUES_DB_HOST=127.0.0.1 +ISSUES_DB_PORT=6432 +ISSUES_DB_DB=issues_db +ISSUES_DB_USERNAME=issues +ISSUES_DB_PASSWORD=password + +# RFI DB (класс RFIDatabase, префикс RFI_DB_) +RFI_DB_HOST=127.0.0.1 +RFI_DB_PORT=6432 +RFI_DB_DB=rfi_db +RFI_DB_USERNAME=rfi +RFI_DB_PASSWORD=password + +# Django-клиент воркера (класс DjangoClient, префикс DJANGO_) +DJANGO_BASE_HOST=https://lk.sarex.io +# DJANGO_HOST — см. выше +# DJANGO_AUTH — base64(login:password); в кластере берётся из секрета django +DJANGO_AUTH= + +# Resources-клиент воркера (класс ResourcesClient, префикс RESOURCES_) +RESOURCES_HOST=http://iams.iam.svc.cluster.local:8080 + +# Flows-клиент воркера (класс FlowsClient, префикс FLOWS_) +FLOWS_HOST=https://api.sarex.io/flows + +# Настройки рассылок воркера (класс NotificationGlobalSettings, префикс NOTIFICATION_SETTINGS_) +NOTIFICATION_SETTINGS_ENABLE_MAILINGS=True +NOTIFICATION_SETTINGS_USE_MAILGUN=True + +# Mailgun (класс MailgunClient, префикс MAILGUN_) +MAILGUN_HOST=https://api.mailgun.net/v3/mg.sarex.io +MAILGUN_API_KEY= + +# Отправка уведомлений через Workflows (класс WorkflowsNotificationsSettings, префикс WORKFLOWS_NOTIFICATIONS_) +WORKFLOWS_NOTIFICATIONS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q +WORKFLOWS_NOTIFICATIONS_SMTP_HOST=127.0.0.1 +WORKFLOWS_NOTIFICATIONS_SMTP_PORT=42069 +WORKFLOWS_NOTIFICATIONS_FROM_EMAIL=hello@sarex.io diff --git a/apps/flows/CONFIGURATION.md b/apps/flows/CONFIGURATION.md new file mode 100644 index 0000000..8240d61 --- /dev/null +++ b/apps/flows/CONFIGURATION.md @@ -0,0 +1,293 @@ +# Конфигурация проекта flows-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/flow/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `Settings` и набор вложенных классов `*Settings`). + +Особенности разбора: + +- у верхнеуровневого класса `Settings` **префикса нет** и не задан `env_nested_delimiter` — его собственные поля задаются переменными с именем поля в верхнем регистре (напр. `BASE_HOST`, `SERVICE_PORT`, `PROXY_PATH_PREFIX`); +- каждая вложенная секция — это **отдельный класс** `BaseSettings` со своим `env_prefix` (`class Config: env_prefix = "..."`), который читает переменные окружения независимо. Поэтому переменные «плоские» с префиксами: `PG_HOST`, `DJANGO_HOST`, `RABBITMQ_PORT`, `TRACING_USE` и т.д. — двойного подчёркивания для вложенности здесь нет; +- отсутствие обязательного поля без дефолта приводит к ошибке старта; большинство полей приложения имеют дефолты (см. таблицы ниже). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически** (в `config.py` не задан `env_file`, зависимости `python-dotenv` нет) — переменные нужно экспортировать в окружение самому, напр. `set -a && . ./.env && set +a`. + +Воркер (`src/worker`) использует **собственные** классы настроек (`src/worker/notifications_config.py`, `src/worker/sync_config.py`, `src/worker/celery.py`) с частично другими префиксами (`FLOWS_DB_`, `ISSUES_DB_`, `RFI_DB_`, `RESOURCES_`, `MAILGUN_`, `WORKFLOWS_NOTIFICATIONS_` и т.д.). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально | Переменные окружения процесса. `.env.example` — шаблон; приложение его **не** загружает автоматически, экспортируйте вручную | +| Контейнер | `Dockerfile` / `Dockerfile.worker`; запуск через `entrypoint.sh` (сначала `alembic upgrade head`, затем gunicorn) | +| Kubernetes (Helm, репозиторий) | `.helm/values.yaml` (`universal-chart`): блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `backend`, `worker`, `scheduler` | +| Kubernetes (kustomize, infra) | `iac/apps/flows/base/*.yaml`: env в `backend-deployment.yaml` / `celery-deployment.yaml`; секреты инжектируются агентом **HashiCorp Vault** (`vault.hashicorp.com/agent-inject-*`) и подгружаются в окружение перед стартом | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`), общий шаблон `generic/common-ci` (`universal-pipeline.yaml`) | + +Способы запуска процессов: + +| Процесс | Команда | Назначение | +| --- | --- | --- | +| HTTP API | `gunicorn ... flow.main:app` (`entrypoint.sh`) | Публичный и внутренний REST API (FastAPI) | +| Celery worker | `celery -A src.worker worker` | Обработчик фоновых задач (рассылки, синхронизация) | +| Celery beat (scheduler) | `celery -A src.worker beat -l INFO` | Периодические задачи (см. `beat_schedule` ниже) | +| Alembic | `alembic upgrade head` (в `entrypoint.sh`) | Миграции БД при старте контейнера | + +Порядок запуска в контейнере (`entrypoint.sh`): миграции (`alembic upgrade head`), затем `gunicorn -w 3 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --timeout $TIMEOUT ... flow.main:app`. + +Периодические задачи воркера (`src/worker/celery.py`, `beat_schedule`): + +| Задача | Расписание (UTC) | Назначение | +| --- | --- | --- | +| `sync_reviews` | `*/7` минут | Синхронизация review | +| `notify_users` | пн–пт, 06:00 | Рассылка уведомлений пользователям | +| `notify_admins_about_empty_steps` | пн–пт, 05:30 | Уведомление админов о пустых шагах | + +## Переменные приложения (API) + +Дефолт `—` означает, что значение обязательно (иначе ошибка старта). + +### Общие (`Settings`, без префикса) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SERVICE_NAME` | string | `review-service` | Имя сервиса | +| `SERVICE_HOST` | string | `0.0.0.0` | Адрес прослушивания (в кластере переопределяется внешним URL API) | +| `SERVICE_PORT` | int | `8000` | Порт | +| `PROXY_PATH_PREFIX` | string | `""` | Root path за реверс-прокси (в кластере `/flows`). Влияет на `root_path` FastAPI и на префикс админки | +| `API_PREFIX` | string | `/api/v1` | Префикс публичного API | +| `API_INTERNAL_PREFIX` | string | `/internal/v1` | Префикс внутреннего API | +| `BASE_HOST` | string | `https://lk.sarex.io` | Базовый внешний URL (для ссылок/писем) | +| `GATEWAY_URL` | string | `https://stage-api.sarex.io/gateway` | URL gateway (используется при `SYNC_RESOURCE_ID=1`) | +| `RESOURCE_URL` | string | `https://stage-api.sarex.io/resources` | URL сервиса ресурсов/IAM (используется при `SYNC_RESOURCE_ID=1`) | +| `REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов | +| `JWT_AUTH_ENABLE` | bool | `True` | Включить аутентификацию по JWT. `False` — все запросы идут от дефолтного пользователя (удобно локально) | +| `DEBUG` | bool | `False` | Режим отладки | +| `ENABLE_MAILINGS` | bool | `True` | Включить рассылки | +| `ENABLE_MAILGUN` | bool | `True` | Использовать Mailgun (иначе — SMTP) | +| `ENABLE_CELERY` | bool | `True` | Включить постановку задач в Celery | +| `ENABLE_EVENTS` | bool | `True` | Включить событийную шину | +| `ENABLE_ANALYTICS` | bool | `False` | Отправлять данные в аналитику | +| `SYNC_RESOURCE_ID` | bool | `False` | Определять `resource_id` через gateway/resources при создании review/документов | +| `SMTP_HOST` | string \| null | `None` | SMTP-хост (альтернатива Mailgun) | +| `SMTP_PORT` | int \| null | `None` | SMTP-порт | +| `FROM_EMAIL` | string \| null | `None` | Адрес отправителя писем | + +### Logger (`LoggerSettings`, префикс `LOG_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `INFO` | Уровень логирования (`INFO`/`DEBUG`/…); JSON-формат вывода | +| `LOG_FORMAT` | string | JSON-шаблон | Формат строки лога | + +### Auth + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_PUBLIC_KEY` | string | — | Публичный RSA-ключ для проверки подписи JWT. В кластере монтируется из секрета и экспортируется как `JWT_PUBLIC_KEY` перед стартом (см. `entrypoint`/Vault) | + +> Аутентификация выполняется в `src/flow/middleware.py` (`TokenUserMiddleware`). При наличии заголовка `identity` полезная нагрузка берётся из Zitadel-токена (`urn:zitadel:iam:user:metadata`), иначе — из основного `Authorization: Bearer `. Подпись проверяется публичным ключом. Пути `/docs/`, `/openapi.json/`, `/internal/` из проверки исключены. + +### Database — основной PostgreSQL (`PostgresSettings`, префикс `PG_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PG_HOST` | string | `""` | Хост PostgreSQL | +| `PG_PORT` | string | `6432` | Порт PostgreSQL (обычно pgbouncer) | +| `PG_LOGIN` | string | `""` | Пользователь БД | +| `PG_PASSWORD` | string | `""` | Пароль БД | +| `PG_DB` | string | `""` | Имя базы данных | + +> Итоговый DSN собирается свойством `PostgresSettings.url`: `postgresql://{login}:{password}@{host}:{port}/{db}`. + +### Documentation PG (`DocumentationDBSettings`, префикс `DOCUMENTATION_PG_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATION_PG_HOST` | string | `""` | Хост БД документаций | +| `DOCUMENTATION_PG_PORT` | string | `""` | Порт | +| `DOCUMENTATION_PG_USERNAME` | string | `""` | Пользователь | +| `DOCUMENTATION_PG_PASSWORD` | string | `""` | Пароль | +| `DOCUMENTATION_PG_DATABASE` | string | `""` | Имя базы | + +### RabbitMQ (`RabbitSettings`, префикс `RABBITMQ_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `RABBITMQ_HOST` | string | `localhost` | Хост | +| `RABBITMQ_PORT` | string | `5672` | Порт | +| `RABBITMQ_USERNAME` | string | `flow` | Пользователь | +| `RABBITMQ_PASSWORD` | string | `flow` | Пароль | +| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост (в кластере `flows`/`flow_preprod`/`flow_prod`) | + +### Celery (`CelerySettings`, префикс `CELERY_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CELERY_QUEUE` | string | `flow` | Очередь задач. Брокер — из `RABBITMQ_*` | + +### HTTP-клиенты внешних сервисов + +Каждый клиент — отдельный класс с полями `use`/`host`/`timeout` (и своим префиксом). Соединение создаётся httpx-клиентом. + +| Секция / префикс | Переменные | Назначение | +| --- | --- | --- | +| Sarex backend (Django) — `DJANGO_` | `DJANGO_USE` (`True`), `DJANGO_HOST` (`http://localhost:8000/api`), `DJANGO_TIMEOUT` (`60`), `DJANGO_TOKEN` (base64 `login:password` для Basic-auth) | Основной backend Sarex | +| Documentations — `DOCUMENTATION_` | `DOCUMENTATION_USE` (`True`), `DOCUMENTATION_HOST`, `DOCUMENTATION_EXTERNAL_HOST`, `DOCUMENTATION_TIMEOUT` (`60`) | Сервис документаций (внутренний и внешний хост) | +| EAV — `EAV_` | `EAV_HOST` (`http://eav-service.eav-prod`), `EAV_TIMEOUT` (`60`) | Сервис EAV (атрибуты) | +| Planning / MSP — `PLANNING_` | `PLANNING_USE` (`True`), `PLANNING_HOST` (`https://api.sarex.io/api/pm/msp`), `PLANNING_TIMEOUT` (`60`) | Планирование | +| Checklists — `CHECKLIST_` | `CHECKLIST_USE` (`True`), `CHECKLIST_HOST`, `CHECKLIST_TIMEOUT` (`60`) | Сервис чек-листов | +| Workflows — `WORKFLOWS_` | `WORKFLOWS_USE` (`True`), `WORKFLOWS_HOST` (`https://lk.sarex.io/workflows/api/v1`), `WORKFLOWS_TIMEOUT` (`60`) | Сервис workflows | + +### Event bus (`EventBusSettings`, префикс `EVENTS_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `EVENTS_HOST` | string | `ws://localhost:8000/ws` | Адрес WebSocket событийной шины | +| `EVENTS_CONNECTION_TIMEOUT` | int | `5` | Таймаут подключения (сек) | +| `EVENTS_COUNT_RETRIES` | int | `100` | Число попыток переподключения | + +### Admin panel (`AdminPanelSettings`, префикс `ADMIN_PANEL_`) + +Админка (`sqladmin`) монтируется по пути `/api/admin/` (с учётом `PROXY_PATH_PREFIX`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ADMIN_PANEL_SECRET_KEY` | string | `hex` | Секретный ключ сессии админки | +| `ADMIN_PANEL_TOKEN_MAX_AGE` | int | `86400` | Время жизни токена (сек) | + +### Sentry (`SentrySettings`, префикс `SENTRY_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SENTRY_DSN` | string | `""` | DSN Sentry | +| `SENTRY_ENVIRONMENT` | string | `production` | Окружение | +| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов | +| `SENTRY_SEND_DEFAULT_PII` | bool | `True` | Отправлять PII | + +### OpenTelemetry / трейсинг (`TraceSettings`, префикс `TRACING_`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACING_USE` | bool | `False` | Включить трейсинг (при `True` инициализируется OTLP + middleware) | +| `TRACING_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACING_INSECURE` | bool | `False` | Подключение без TLS | +| `TRACING_SERVICE_NAME` | string | `flows` | Имя сервиса в трейсах | +| `TRACING_ENVIRONMENT` | string | `prod` | Окружение (`stage`/`preprod`/`prod`) | +| `TRACING_MODULE` | string | `flows` | Атрибут `module` | +| `TRACING_TEAM` | string | `team_proc` | Атрибут `team` | +| `TRACING_COMPONENT` | string | `backend` | Атрибут `component` | + +## Переменные только для воркера и scheduler + +Читаются классами из `src/worker/*`, а не основным приложением. + +### Базы данных воркера + +| Секция / префикс | Переменные | Назначение | +| --- | --- | --- | +| Flows DB — `FLOWS_DB_` | `FLOWS_DB_HOST`, `FLOWS_DB_PORT`, `FLOWS_DB_DB`, `FLOWS_DB_USERNAME`, `FLOWS_DB_PASSWORD` | БД flows (для задач синхронизации/рассылок) | +| Issues DB — `ISSUES_DB_` | `ISSUES_DB_HOST`, `ISSUES_DB_PORT`, `ISSUES_DB_DB`, `ISSUES_DB_USERNAME`, `ISSUES_DB_PASSWORD` | БД issues | +| RFI DB — `RFI_DB_` | `RFI_DB_HOST`, `RFI_DB_PORT`, `RFI_DB_DB`, `RFI_DB_USERNAME`, `RFI_DB_PASSWORD` | БД RFI | + +Все пять полей каждой БД обязательны (без дефолтов). + +### Клиенты и рассылки воркера + +| Секция / префикс | Переменные | Назначение | +| --- | --- | --- | +| Django-клиент — `DJANGO_` | `DJANGO_BASE_HOST`, `DJANGO_HOST`, `DJANGO_AUTH` (Basic-auth) | Получение пользователей/токенов | +| Resources-клиент — `RESOURCES_` | `RESOURCES_HOST` | Пользователи, сгруппированные по ресурсам | +| Flows-клиент — `FLOWS_` | `FLOWS_HOST` | Внутренние вызовы flows API (`switch_to_next_step`) | +| Глобальные настройки рассылок — `NOTIFICATION_SETTINGS_` | `NOTIFICATION_SETTINGS_ENABLE_MAILINGS` (`True`), `NOTIFICATION_SETTINGS_USE_MAILGUN` (`True`) | Флаги рассылок | +| Mailgun — `MAILGUN_` | `MAILGUN_HOST`, `MAILGUN_API_KEY`, `MAILGUN_SENT_FROM` (`hello@sarex.io`) | Отправка писем через Mailgun | +| Workflows — `WORKFLOWS_` | `WORKFLOWS_HOST`, `WORKFLOWS_TIMEOUT` (`60`) | Постановка job в workflows | +| Уведомления через Workflows — `WORKFLOWS_NOTIFICATIONS_` | `WORKFLOWS_NOTIFICATIONS_TAG` (`email`), `WORKFLOWS_NOTIFICATIONS_REGISTRY`, `WORKFLOWS_NOTIFICATIONS_SMTP_HOST`, `WORKFLOWS_NOTIFICATIONS_SMTP_PORT`, `WORKFLOWS_NOTIFICATIONS_FROM_EMAIL` | Параметры job-рассылки | + +## Переменные инфраструктуры, сборки и деплоя + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `TIMEOUT` | `entrypoint.sh` | Таймаут gunicorn-воркеров (сек), напр. `120` (stage) / `900` (prod) | +| `ENABLE_METRICS` | `.helm/values.yaml` | Флаг метрик (`0`/`1`), кодом не читается | +| `PIP_INDEX_URL` / `--extra-index-url` | `requirements.txt` | Приватный индекс пакетов Nexus (`fastapi-otel-tools`) | +| `SERVICE_HOST` (в кластере) | `.helm/values.yaml`, kustomize | В кластере в `SERVICE_HOST` кладётся внешний URL API (`https://api.sarex.io/flows/api/v1`), переопределяя дефолт `0.0.0.0` | +| `SAREX_MAILER_HOST` | `.helm/values.yaml` | Хост mailer-сервиса | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт `universal-chart` описывает три сервиса — `backend`, `worker`, `scheduler`. Обычные значения задаются в блоке `envs` (с ключами по окружениям `_default`/`stage`/`preprod`/`production`), значения из секретов — в блоке `secretEnvs` (монтируются как env через `secretKeyRef`). + +Значения из секретов (backend): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `PG_DB` | `postgres-secret` / `flows-postgresql-secret` | `database` | +| `PG_LOGIN` | `postgres-secret` / `flows-postgresql-secret` | `username` | +| `PG_PASSWORD` | `postgres-secret` / `flows-postgresql-secret` | `password` | +| `PG_HOST` | `postgres-secret` / `flows-postgresql-secret` | `host` | +| `SENTRY_DSN` | `sentry-secret` | `dsn` | +| `SENTRY_ENVIRONMENT` | `sentry-secret` | `env` | +| `DJANGO_TOKEN` | `django-secret` | `token` | +| `RABBITMQ_USERNAME` | `rabbitmq-secret` / `flows-rabbitmq-secret` | `username` | +| `RABBITMQ_PASSWORD` | `rabbitmq-secret` / `flows-rabbitmq-secret` | `password` | +| `ADMIN_PANEL_SECRET_KEY` | `admin-secret` | `key` | +| `JWT_PUBLIC_KEY` | `jwt-secret` | `public_key` | +| `DOCUMENTATION_PG_*` | `documentations-postgresql-secret` / `documentations-postgres-secret` | `database`/`host`/`port`/`username`/`password` | + +Воркер дополнительно получает секреты `FLOWS_DB_*`, `ISSUES_DB_*`, `RFI_DB_*` (из соответствующих postgres-секретов), `DJANGO_AUTH` (`django-secret.token`), `MAILGUN_API_KEY` (`mailgun-secret.api-key`). + +Чарт также монтирует CA-сертификат PostgreSQL (`pg-cert` → `/root/.postgresql/root.crt`). + +## Переменные из kustomize-манифестов (`iac/apps/flows`) + +Инфраструктурный репозиторий разворачивает те же образы через kustomize (`base` + оверлеи `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`). Секреты инжектируются агентом **HashiCorp Vault** (аннотации `vault.hashicorp.com/agent-inject-*`) и подгружаются в окружение из файлов `/vault/secrets/*` перед запуском `entrypoint.sh`: + +| Секрет Vault | Переменные | +| --- | --- | +| `secrets/data/postgresql/apps/flows` | `PG_DB`, `PG_LOGIN`, `PG_HOST`, `PG_PORT`, `PG_PASSWORD`, `DOCUMENTATION_PG_*` | +| `secrets/data/rabbitmq/apps/flows` | `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_VHOST`, `RABBITMQ_HOST`, `RABBITMQ_PORT` | +| `secrets/data/vault/common/django_auth` | `DJANGO_TOKEN` | +| `secrets/data/vault/common/rsa_keys` | `JWT_PUBLIC_KEY` (public_key) | + +Остальные значения (`LOG_LEVEL`, `BASE_HOST`, `DJANGO_HOST`, `DOCUMENTATION_HOST`, `EAV_HOST`, `GATEWAY_URL`, `RESOURCE_URL`, `SERVICE_HOST`, `WORKFLOWS_HOST`, `CHECKLIST_HOST`, `SMTP_HOST`/`SMTP_PORT`, `FROM_EMAIL`, `ENABLE_*`, `SYNC_RESOURCE_ID`, `TIMEOUT` и т.д.) задаются напрямую в блоке `env` deployment-манифеста оверлея. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `proc` | `0.0.1-stage` | +| ветка `master` | `preprod` | `flows-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `flows-prod` | `0.0.1-prod` | + +Ключевые переменные пайплайна: `SERVICE_NAME=flows-backend`, `DOCKERFILE_PATH=Dockerfile`, `IMAGE_NAME_WORKER` (образ воркера, собирается job-ом `build_worker` из `Dockerfile.worker`), `HELM_SET_ARGS` (проброс образов backend/worker/scheduler и метаданных коммита в чарт). + +## Замечания и потенциальные проблемы + +- Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение (см. `.env.example`). +- У `Settings` нет `env_nested_delimiter`, поэтому вложенные секции конфигурируются **плоскими** переменными со своими префиксами (`PG_`, `DJANGO_`, `RABBITMQ_`, …), а не через `__`. +- Поле `service_host` (дефолт `0.0.0.0`) и переменная `SERVICE_HOST` совпадают по имени: в кластере в `SERVICE_HOST` кладётся внешний URL API, что переопределяет адрес прослушивания в объекте настроек. Реальный адрес/порт прослушивания при запуске в контейнере задаёт gunicorn (`-b 0.0.0.0:8000` в `entrypoint.sh`), а не поле настроек. +- Почта: при `ENABLE_MAILGUN=1` используется Mailgun (`MAILGUN_*` — в основном на стороне воркера), иначе — SMTP (`SMTP_HOST`/`SMTP_PORT`/`FROM_EMAIL`). +- Healthcheck-эндпоинта у сервиса нет; в чарте probes (`liveness`/`readiness`) отключены. +- Воркер использует отдельные классы настроек (pydantic v1 стиль `class Config`), у которых поля БД **обязательны** — при запуске воркера без `FLOWS_DB_*`/`ISSUES_DB_*`/`RFI_DB_*` будет ошибка. + +## Минимальный набор для локального запуска + +Минимально необходимо задать: + +- `SERVICE_PORT` (по умолчанию `8000`), `PROXY_PATH_PREFIX` (пусто локально) +- `JWT_AUTH_ENABLE=False` (чтобы не требовался `JWT_PUBLIC_KEY`) — иначе задайте `JWT_PUBLIC_KEY` +- `PG_HOST`, `PG_PORT`, `PG_LOGIN`, `PG_PASSWORD`, `PG_DB` +- `RABBITMQ_HOST`, `RABBITMQ_PORT`, `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_VHOST` (если `ENABLE_CELERY=1`/`ENABLE_EVENTS=1`) +- хосты внешних сервисов, которые реально используются: `DJANGO_HOST`, `DOCUMENTATION_HOST`, `EAV_HOST`, `CHECKLIST_HOST`, `WORKFLOWS_HOST`, `PLANNING_HOST` +- при `SYNC_RESOURCE_ID=1` — `GATEWAY_URL`, `RESOURCE_URL` +- `TRACING_USE=False` (иначе — `TRACING_HOST`, `TRACING_SERVICE_NAME`) +- для воркера — `FLOWS_DB_*`, `ISSUES_DB_*`, `RFI_DB_*`, `MAILGUN_*` или SMTP + +Готовые значения-примеры для всех переменных приведены в `.env.example`. diff --git a/apps/flows/ENDPOINTS.md b/apps/flows/ENDPOINTS.md new file mode 100644 index 0000000..a396dc4 --- /dev/null +++ b/apps/flows/ENDPOINTS.md @@ -0,0 +1,142 @@ +# Эндпоинты, с которыми взаимодействует flows-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `flows-frontend`). + +## Как устроено взаимодействие + +В отличие от единого реестра эндпоинтов, запросы во `flows-frontend` выполняются **точечно** из MobX-сторов (`module/store/stores/*.ts`) через общий HTTP-клиент `httpService`. + +`httpService` создаётся в `module/api/http-service.ts` фабрикой `createHttpService` из `@sarex-team/sdk-js`. Клиент предоставляет методы `getRequest`, `postRequest`, `putRequest`, `patchRequest`, `deleteRequest`, каждый из которых принимает объект вида: + +```ts +httpService.getRequest({ + service: "flows", // логическое имя сервиса (ключ из hosts.ts) + url: `/flows/${id}/?full=true`, // путь запроса относительно базового хоста сервиса + data: { ... }, // тело запроса (для post/put/patch) + // ...прочие опции axios +}); +``` + +Базовый хост сервиса подставляется по ключу `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV`. Итоговый URL = `<базовый хост сервиса>` + `url`. + +Окружение выбирается переменной `BUILD_ENV` (`module/env.js`): одно из `local`, `stage`, `prod`, `preprod`, `contour`, `severstal`, `uralchem`. В `webpack.config.js` значение прокидывается в бандл через `DefinePlugin`. Значение по умолчанию при резолве хоста — `prod`. + +Подключаемый удалённый модуль (`documentations`) описан отдельно в `module/api/modules-hosts.ts` и резолвится функцией `getModuleHost` (Module Federation, `remoteEntry.js`). + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Показаны `stage` и `prod`; дополнительно определены `local`, `preprod` и `contour` (в `contour` — относительные пути для изолированного контура; в `local` сервис `sarex` проксируется на `/sarex-backend`). + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `flows` | Сервис процессов согласования (flows, reviews, steps, statuses) | `https://stage-api.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | +| `sarex` | Локальный backend (Django `core`/`client`) | `""` (относительные пути) | `""` | +| `gateway_api_v1` | Gateway API v1 (ресурсы) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` | +| `gateway_api_v2` | Gateway API v2 (пользователи) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` | +| `documentations` | Сервис документации (диски, документы) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | +| `eav_api_v0` | Сервис EAV (атрибуты/схемы) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` | +| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` | +| `transmittals` | Сервис передачи документации (шаблоны) | `https://stage-api.sarex.io/transmittals/api/v1` | `https://api.sarex.io/transmittals/api/v1` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +Удалённые модули (`module/api/modules-hosts.ts`): + +| Модуль | `stage` | `prod` | +| --- | --- | --- | +| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | + +## Эндпоинты по сервисам + +### `flows` — Сервис процессов согласования + +Источник: `module/store/stores/processes.ts`, `module/store/stores/resources.ts`. + +| Метод (`*Request`) | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/flows/{id}/?full=true` | `processes.loadFlow` | Маршрут по id (с шагами/статусами) | +| POST | `/flows/filter/` | `processes` (фильтр) | Список маршрутов по фильтру | +| POST | `/flows/count_flows_by_resource/` | `processes` | Количество маршрутов по ресурсам | +| POST | `/flows/count_flows_by_resource/` | `processes.getCountByResourceId` | Количество маршрутов для одного ресурса | +| POST | `/flows/` | `processes.createFlow` | Создать маршрут | +| POST | `/flows/{id}/copy/?full=true` | `processes` (копирование) | Копировать маршрут | +| PUT | `/flows/{id}/?full=true` | `processes` (обновление) | Обновить маршрут | +| PATCH | `/flows/bulk-update/` | `processes.bulkUpdateFlow` | Массовое обновление маршрутов | +| POST | `/steps/` | `processes.createStep` | Создать шаг | +| PUT | `/steps/{id}/?full=true` | `processes.updateStep` | Обновить шаг | +| PATCH | `/steps/{stepId}/update_reviewers/` | `processes` | Обновить согласующих шага | +| GET | `/steps/{id}/active_reviews/` | `processes.getActiveReviewsForReviewer` | Активные review на шаге | +| POST | `/statuses/` | `processes.createStatus` | Создать статус | +| DELETE | `/statuses/{id}/` | `processes.deleteStatus` | Удалить статус | +| POST | `/reviews/count_by_resource_id/` | `resources` | Количество review по ресурсу | + +### `sarex` — Локальный backend (Django) + +Источник: `module/store/stores/users.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/api/client/settings/` | `users.getCurrentUser` | Настройки/данные текущего пользователя | +| GET | `/api/core/users/?company={id}&{query}` | `users.getUsers` / `getUsersByCompanyId` / `getAllUsersByCompanyId` | Пользователи компании | +| GET | `/api/core/admin/departments/?{query}` | `users.fetchDepartmentsSA` | Департаменты (service account) | +| GET | `/api/core/admin/departments/?company={id}&{query}` | `users.fetchDepartmentsByCompanyId` | Департаменты компании | +| GET | `/api/core/admin/positions/?{query}` | `users.fetchPositionsSA` | Должности (service account) | +| GET | `/api/core/admin/positions/?company={id}&{query}` | `users.fetchPositionsByCompanyId` | Должности компании | + +### `gateway_api_v1` — Gateway API v1 (ресурсы) + +Источник: `module/store/stores/resources.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/resources/?{query}` | `resources` | Список ресурсов (по фильтру) | +| GET | `/resources/?company_id={id}` | `resources` | Ресурсы компании | + +### `gateway_api_v2` — Gateway API v2 (пользователи) + +Источник: `module/store/stores/users.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/users/?{query}` | `users.getUsersByResourceId` | Пользователи по ресурсу | + +### `documentations` — Сервис документации + +Источник: `module/store/stores/documents.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/disks/{id}/documents` | `documents.fetchDocumentsByDiskId` | Документы диска | + +### `eav_api_v0` — Сервис EAV (атрибуты) + +Источник: `module/store/stores/attributes.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| GET | `/schema/?model_name=flow&company_id={id}` | `attributes.fetchFlowsAttributes` | Схема атрибутов для модели `flow` | + +### `checklists` — Сервис чек-листов + +Источник: `module/store/stores/checklists.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| POST | `/checklists/filter/` | `checklists.fetchChecklists` | Список чек-листов по фильтру | + +### `transmittals` — Сервис передачи документации + +Источник: `module/store/stores/transmittals.ts`. + +| Метод | Путь | Стор / метод | Назначение | +| --- | --- | --- | --- | +| POST | `/transmittal_templates` | `transmittals` | Шаблоны трансмитталов (пагинация по `next`, фильтр по `resources`) | + +### `zitadel` — IdP + +Хост определён в `hosts.ts` для аутентификации через SDK; прямых вызовов из сторов в текущей версии модуля нет (используется инфраструктурой `@sarex-team/sdk-js`). + +## Замечания + +- Единого файла-реестра эндпоинтов (`endpoints.ts`) во `flows-frontend` нет — вызовы разбросаны по сторам `module/store/stores/*`. При добавлении нового запроса указывайте `service` строго из ключей `hosts.ts`. +- Часть путей содержит завершающий слэш и query-параметры прямо в строке `url` (напр. `/flows/{id}/?full=true`) — это соответствует поведению backend (`flows-backend`), где роуты объявлены со слэшем на конце. +- Сервис `sarex` в `stage`/`prod` имеет пустой базовый хост (`""`), то есть запросы идут по относительным путям того же origin; в `local` он проксируется на `/sarex-backend`, в `contour` — на относительные пути контура. diff --git a/apps/flows/openapi.yaml b/apps/flows/openapi.yaml new file mode 100644 index 0000000..131e726 --- /dev/null +++ b/apps/flows/openapi.yaml @@ -0,0 +1,1736 @@ +openapi: 3.0.3 + +info: + title: Flows Service API + version: "1.0.0" + description: | + REST API сервиса **flows-backend** (`proc/flows-backend`) — управление + процессами согласования документации: маршрутами (`flows`), их шагами + (`steps`) и статусами (`statuses`), запусками согласования (`reviews`), + документами в согласовании (`documents`), действиями пользователей + (`user-actions`) и очередью задач согласующих (`tasks`). + + Сервис написан на Python (**FastAPI**). Приложение собирается фабрикой + `get_app` в `src/flow/main.py`. Роутинг состоит из двух зеркальных групп + (`src/flow/routers/__init__.py`): + + - публичный API — префикс `/api/v1` (`API_PREFIX`); + - внутренний API — префикс `/internal/v1` (`API_INTERNAL_PREFIX`), + предназначен для вызовов внутри кластера. Набор роутеров идентичен + публичному, но внутренний префикс исключён из проверки аутентификации. + + Оба префикса могут дополнительно предваряться `PROXY_PATH_PREFIX` + (в кластере — `/flows`). Админ-панель (`sqladmin`) смонтирована по + `/api/admin/`. Интерактивная документация Swagger доступна по `/docs`, + схема — по `/openapi.json` (с учётом `root_path`). + + Ниже описан публичный API (`/api/v1`). Внутренний API (`/internal/v1/*`) + имеет те же пути и тела, но не требует аутентификации на уровне приложения. + + ### Аутентификация + Публичные эндпоинты требуют заголовок `Authorization: Bearer ` + (`src/flow/middleware.py`, `TokenUserMiddleware`). Поддерживаются два режима: + + 1. **Zitadel** — если передан заголовок `identity` (`Bearer `), + полезная нагрузка берётся из этого токена + (`urn:zitadel:iam:user:metadata`). + 2. **sarex-backend** — если заголовка `identity` нет, данные берутся из + основного токена (подпись проверяется публичным RSA-ключом + `JWT_PUBLIC_KEY`). + + Проверка отключается флагом `JWT_AUTH_ENABLE=False` (тогда все запросы идут + от дефолтного администратора). Пути `/docs/`, `/openapi.json/` и весь + `/internal/*` из проверки исключены. + + ### Авторизация (права) + Доступ к группам проверяется в `PermissionManager` (`src/flow/dependencies.py`) + по правам пользователя (`src/flow/utils/permissions.py`): напр. `flows`/`steps`/ + `statuses` требуют `base.can_view_flow`/`base.can_add_flow`/… , `reviews`/ + `documents` — `base.can_view_review`/`base.can_add_review`/… Пользователь с + признаком администратора проверки прав пропускает. + + ### Пагинация + Списочные эндпоинты используют limit/offset (`LimitOffsetParams`, + по умолчанию `limit=1000`, `offset=0`). Часть «тяжёлых» списков (`reviews`, + подсчёты) возвращается как готовый JSON (`Response(media_type=application/json)`), + поэтому их тело в схеме описано обобщённо. + + ### Обработка ошибок + Ошибки бизнес-логики возвращаются как `{"detail": "..."}` с + соответствующим статусом (`400`/`403`/`404`). Ошибки валидации тела/query + (Pydantic) отдаются FastAPI в стандартном формате `422`. + +servers: + - url: https://api.sarex.io/flows/api/v1 + description: production + - url: https://api.preprod.sarex.io/flows/api/v1 + description: preprod + - url: https://stage-api.sarex.io/flows/api/v1 + description: stage + +security: + - bearerAuth: [] + +tags: + - name: flows + description: Маршруты согласования + - name: reviews + description: Запуски согласования + - name: steps + description: Шаги маршрута + - name: statuses + description: Статусы согласования + - name: documents + description: Документы в согласовании + - name: user_actions + description: Действия пользователей + - name: tasks + description: Очередь задач согласующих + +paths: + /flows/: + get: + tags: [flows] + summary: Список маршрутов + description: Фильтры передаются query-параметрами (`MainFilters`). При `full=true` возвращаются вложенные шаги/статусы. + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: full, schema: { type: boolean, default: false } } + - { in: query, name: resource_id, schema: { type: string }, description: "CSV UUID ресурсов" } + - { in: query, name: company_id, schema: { type: string } } + - { in: query, name: is_active, schema: { type: boolean } } + - { in: query, name: flow_type, schema: { $ref: '#/components/schemas/FlowType' } } + - { in: query, name: name, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: [ { $ref: '#/components/schemas/Flow' }, { $ref: '#/components/schemas/FullFlow' } ] + post: + tags: [flows] + summary: Создать маршрут + parameters: + - { in: query, name: full, schema: { type: boolean, default: false } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/FlowCreate' } + responses: + '201': + description: Создан + content: + application/json: + schema: + oneOf: [ { $ref: '#/components/schemas/Flow' }, { $ref: '#/components/schemas/FullFlow' } ] + '400': { $ref: '#/components/responses/BadRequest' } + + /flows/filter/: + post: + tags: [flows] + summary: Список маршрутов по фильтру (тело) + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MainFilterPostRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Flow' } } + + /flows/light/: + get: + tags: [flows] + summary: Облегчённый список маршрутов (id, name) + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/FlowLight' } } + + /flows/count_flows_by_resource/: + get: + tags: [flows] + summary: Количество маршрутов, сгруппированное по resource_id + responses: + '200': { $ref: '#/components/responses/JsonObject' } + post: + tags: [flows] + summary: То же по фильтру (тело) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MainFilterPostRequest' } + responses: + '200': { $ref: '#/components/responses/JsonObject' } + + /flows/bulk-update/: + patch: + tags: [flows] + summary: Массовое обновление маршрутов (watchers/approvers) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/FlowBulkUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Flow' } } + '404': { $ref: '#/components/responses/NotFound' } + + /flows/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [flows] + summary: Маршрут по id + parameters: + - { in: query, name: full, schema: { type: boolean, default: false } } + responses: + '200': + description: OK + content: + application/json: + schema: + oneOf: [ { $ref: '#/components/schemas/Flow' }, { $ref: '#/components/schemas/FullFlow' } ] + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [flows] + summary: Обновить маршрут + parameters: + - { in: query, name: full, schema: { type: boolean, default: false } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/FlowCreate' } + responses: + '200': + description: OK + content: + application/json: + schema: + oneOf: [ { $ref: '#/components/schemas/Flow' }, { $ref: '#/components/schemas/FullFlow' } ] + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [flows] + summary: Удалить маршрут + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Flow' } + '404': { $ref: '#/components/responses/NotFound' } + + /flows/{instance_id}/copy/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + post: + tags: [flows] + summary: Копировать маршрут + parameters: + - { in: query, name: full, schema: { type: boolean, default: false } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CopyFlow' } + responses: + '201': + description: Создан + content: + application/json: + schema: + oneOf: [ { $ref: '#/components/schemas/Flow' }, { $ref: '#/components/schemas/FullFlow' } ] + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/: + get: + tags: [reviews] + summary: Список review (готовый JSON) + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: join_user_actions, schema: { type: boolean, default: false } } + - { in: query, name: resource_id, schema: { type: string } } + - { in: query, name: flow_id, schema: { type: string } } + - { in: query, name: company_id, schema: { type: string } } + - { in: query, name: status, schema: { type: string }, description: "CSV значений StateReview" } + responses: + '200': { $ref: '#/components/responses/JsonArray' } + post: + tags: [reviews] + summary: Создать review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseReview' } + responses: + '201': + description: Создан + content: + application/json: + schema: { $ref: '#/components/schemas/Review' } + '400': { $ref: '#/components/responses/BadRequest' } + + /reviews/filter/: + post: + tags: [reviews] + summary: Список review по фильтру (тело) + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: join_user_actions, schema: { type: boolean, default: false } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MainFilterPostRequest' } + responses: + '200': { $ref: '#/components/responses/JsonArray' } + + /reviews/light/: + get: + tags: [reviews] + summary: Облегчённый список review + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: resource_id, schema: { type: string } } + - { in: query, name: flow_id, schema: { type: string } } + - { in: query, name: company_id, schema: { type: string } } + responses: + '200': { $ref: '#/components/responses/JsonArray' } + + /reviews/tasks-count/: + get: + tags: [reviews] + summary: Количество задач текущего пользователя + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/TasksCount' } + + /reviews/count_by_resource_id/: + get: + tags: [reviews] + summary: Количество review по resource_id + responses: + '200': { $ref: '#/components/responses/JsonObject' } + post: + tags: [reviews] + summary: То же по фильтру (тело) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MainFilterPostRequest' } + responses: + '200': { $ref: '#/components/responses/JsonObject' } + + /reviews/count_by_reviewer_id/: + get: + tags: [reviews] + summary: Количество review по reviewer_id + responses: + '200': { $ref: '#/components/responses/JsonObject' } + post: + tags: [reviews] + summary: То же по фильтру (тело) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/MainFilterPostRequest' } + responses: + '200': { $ref: '#/components/responses/JsonObject' } + + /reviews/bulk-reviewers-update/: + patch: + tags: [reviews] + summary: Массовое обновление согласующих в review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BulkReviewersUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Review' } } + + /reviews/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [reviews] + summary: Review по id + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [reviews] + summary: Обновить review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/UpdateBaseReview' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [reviews] + summary: Частичное обновление review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PatchBaseReview' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [reviews] + summary: Удалить review + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/restart/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + post: + tags: [reviews] + summary: Пересчитать динамические поля review + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/documents/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [reviews] + summary: Документы review + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: document_type, schema: { type: string }, description: "CSV" } + - { in: query, name: bundle_id, schema: { type: string }, description: "CSV" } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [reviews] + summary: Обновить статус документов review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseUpdateDocument' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/start/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Начать проверку (таймтрекинг) + responses: + '200': { $ref: '#/components/responses/JsonObject' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/approve/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Пройти review согласующим (принять/отклонить/подписать/аннулировать) + requestBody: + required: false + content: + application/json: + schema: { $ref: '#/components/schemas/StatusForReview' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/update-bundles/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Обновить bundle_id подписанных документов + requestBody: + required: true + content: + application/json: + schema: { type: object, additionalProperties: true } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/update-documents/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Обновить copied-id документов + requestBody: + required: true + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/UpdateDocumentCopiedIds' } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/change_reviewers/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Заменить согласующих на текущем шаге + requestBody: + required: true + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/PatchCurrentReviewers' } } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/change-min-reviewers/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Изменить минимальное число согласующих на шаге + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PatchMinReviewersOnStep' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/set-step/{step_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + - { in: path, name: step_id, required: true, schema: { type: integer } } + patch: + tags: [reviews] + summary: Принудительно установить шаг review (только админ) + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '400': { $ref: '#/components/responses/BadRequest' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/switch_to_next_step/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [reviews] + summary: Перевести review на следующий шаг (только superuser) + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{instance_id}/time-tracking/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [reviews] + summary: Таймтрекинг review + responses: + '200': { $ref: '#/components/responses/JsonObject' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{review_id}/checklist-results/: + parameters: + - { in: path, name: review_id, required: true, schema: { type: integer } } + patch: + tags: [reviews] + summary: Обновить результаты чек-листов review + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ChecklistResultsUpdateRequest' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /reviews/{review_id}/transmittal-created/: + parameters: + - { in: path, name: review_id, required: true, schema: { type: integer } } + post: + tags: [reviews] + summary: Зафиксировать созданную по review передачу (transmittal) + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TransmittalForReviewCreated' } + responses: + '200': { $ref: '#/components/responses/ReviewOk' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + + /steps/: + get: + tags: [steps] + summary: Список шагов + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: full, schema: { type: boolean, default: false } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Step' } } + post: + tags: [steps] + summary: Создать шаг + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseStep' } + responses: + '201': + description: Создан + content: + application/json: + schema: { $ref: '#/components/schemas/Step' } + '400': { $ref: '#/components/responses/BadRequest' } + + /steps/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [steps] + summary: Шаг по id + parameters: + - { in: query, name: full, schema: { type: boolean, default: false } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Step' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [steps] + summary: Обновить шаг + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseStep' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Step' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [steps] + summary: Удалить шаг + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Step' } + '404': { $ref: '#/components/responses/NotFound' } + + /steps/{instance_id}/active_reviews/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [steps] + summary: Активные review на шаге + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/ActiveReview' } } + '404': { $ref: '#/components/responses/NotFound' } + + /steps/{instance_id}/update_reviewers/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [steps] + summary: Обновить согласующих шага + requestBody: + required: true + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/UpdatedReviewer' } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Step' } + '404': { $ref: '#/components/responses/NotFound' } + + /steps/{instance_id}/get_reviewers/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [steps] + summary: Допустимые согласующие для шага + parameters: + - { in: query, name: review_id, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { type: object, additionalProperties: true } } + '404': { $ref: '#/components/responses/NotFound' } + + /statuses/: + get: + tags: [statuses] + summary: Список статусов + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Status' } } + post: + tags: [statuses] + summary: Создать статус + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseStatus' } + responses: + '201': + description: Создан + content: + application/json: + schema: { $ref: '#/components/schemas/Status' } + '400': { $ref: '#/components/responses/BadRequest' } + + /statuses/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [statuses] + summary: Статус по id + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Status' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [statuses] + summary: Обновить статус + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseStatus' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Status' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [statuses] + summary: Удалить статус + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Status' } + '404': { $ref: '#/components/responses/NotFound' } + + /documents/: + get: + tags: [documents] + summary: Список документов + description: При `full=true` возвращаются расширенные записи (`ExtendDocument`) с данными review. + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: full, schema: { type: boolean, default: false } } + - { in: query, name: flow_ids, schema: { type: string }, description: "CSV" } + - { in: query, name: review_ids, schema: { type: string }, description: "CSV" } + - { in: query, name: document_ids, schema: { type: string }, description: "CSV" } + - { in: query, name: bundle_ids, schema: { type: string }, description: "CSV" } + - { in: query, name: review_status, schema: { type: string }, description: "CSV" } + - { in: query, name: document_types, schema: { type: string }, description: "CSV" } + - { in: query, name: document_copied_ids, schema: { type: string }, description: "CSV" } + - { in: query, name: bundle_copied_ids, schema: { type: string }, description: "CSV" } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: [ { $ref: '#/components/schemas/Document' }, { $ref: '#/components/schemas/ExtendDocument' } ] + post: + tags: [documents] + summary: Создать документы (пакетно) + requestBody: + required: true + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/BaseDocument' } } + responses: + '201': + description: Создано + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + '400': { $ref: '#/components/responses/BadRequest' } + + /documents/filter/: + post: + tags: [documents] + summary: Список документов по фильтру (тело) + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/DocumentFilterRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: [ { $ref: '#/components/schemas/Document' }, { $ref: '#/components/schemas/ExtendDocument' } ] + + /documents/change-copy-paths/: + patch: + tags: [documents] + summary: Изменить пути копирования документов + requestBody: + required: true + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/ChangeDocumentCopyPath' } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Document' } } + + /documents/set-status/: + patch: + tags: [documents] + summary: Установить статус документам + parameters: + - { in: query, name: document_ids, required: true, schema: { type: string }, description: "CSV id документов" } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SetDocumentStatusUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/SetDocumentStatusRead' } } + '400': { $ref: '#/components/responses/BadRequest' } + + /documents/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + get: + tags: [documents] + summary: Документ по id + responses: + '200': { $ref: '#/components/responses/DocumentOk' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [documents] + summary: Обновить статус документа + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseUpdateDocument' } + responses: + '200': { $ref: '#/components/responses/DocumentOk' } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [documents] + summary: Установить bundle_id и статус документа + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SetDocumentBundleIdAndStatus' } + responses: + '200': { $ref: '#/components/responses/DocumentOk' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [documents] + summary: Удалить документ + responses: + '200': { $ref: '#/components/responses/DocumentOk' } + '404': { $ref: '#/components/responses/NotFound' } + + /user-actions/: + post: + tags: [user_actions] + summary: Создать действие пользователя + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseUserAction' } + responses: + '201': + description: Создано + content: + application/json: + schema: { type: object, properties: { detail: { type: string } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /user-actions/bulk-delete-user-action/: + delete: + tags: [user_actions] + summary: Массовое удаление действий + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ListUserActionsForDelete' } + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + deleted_ids: { type: array, items: { type: integer } } + + /user-actions/{instance_id}/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [user_actions] + summary: Обновить key/value действия + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/UpdateUserAction' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/UserActions' } + '404': { $ref: '#/components/responses/NotFound' } + + /tasks/: + get: + tags: [tasks] + summary: Список задач очереди + parameters: + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { in: query, name: review_id, schema: { type: integer } } + - { in: query, name: reviewer_id, schema: { type: integer } } + - { in: query, name: is_active, schema: { type: boolean } } + - { in: query, name: resource_id, schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Task' } } + + /tasks/reviewers-max-end-dates/: + get: + tags: [tasks] + summary: Максимальные даты окончания задач по согласующим + parameters: + - { in: query, name: reviewers_ids, required: true, schema: { type: string }, description: "CSV id" } + - { in: query, name: duration, required: true, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/ReviewerMaxTaskEndDate' } } + + /tasks/{instance_id}/change-priority/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [tasks] + summary: Изменить приоритет задачи + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TaskUpdatePriority' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Task' } + '404': { $ref: '#/components/responses/NotFound' } + + /tasks/{instance_id}/change-duration/: + parameters: + - { $ref: '#/components/parameters/InstanceId' } + patch: + tags: [tasks] + summary: Изменить длительность задачи + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TaskUpdateDuration' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Task' } + '404': { $ref: '#/components/responses/NotFound' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: "Основной токен. Дополнительно может передаваться заголовок `identity: Bearer ` для режима Zitadel." + + parameters: + Limit: + in: query + name: limit + schema: { type: integer, minimum: 0, default: 1000 } + Offset: + in: query + name: offset + schema: { type: integer, minimum: 0, default: 0 } + InstanceId: + in: path + name: instance_id + required: true + schema: { type: integer } + + responses: + BadRequest: + description: Ошибка запроса + content: + application/json: + schema: { $ref: '#/components/schemas/ApiError' } + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: { $ref: '#/components/schemas/ApiError' } + NotFound: + description: Не найдено + content: + application/json: + schema: { $ref: '#/components/schemas/ApiError' } + JsonObject: + description: Готовый JSON-объект (структура зависит от группировки) + content: + application/json: + schema: { type: object, additionalProperties: true } + JsonArray: + description: Готовый JSON-массив review (сериализуется на стороне сервиса) + content: + application/json: + schema: { type: array, items: { type: object, additionalProperties: true } } + ReviewOk: + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Review' } + DocumentOk: + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + + schemas: + ApiError: + type: object + properties: + detail: + oneOf: [ { type: string }, { type: array, items: { type: object } } ] + + FlowType: + type: string + enum: [document, schedule] + Action: + type: string + enum: ['Отсутствует', 'Скопировать в папку'] + StatusKey: + type: string + enum: ['Согласовано', 'Не согласовано'] + StateReview: + type: string + nullable: true + enum: ['Начато', 'Открыто', 'Копирование документов', 'На подписании', 'Закрыто', 'Завершено', 'Аннулировано'] + StepType: + type: string + enum: ['Инициализирующий', 'Обычный', 'Финальный'] + ChangeCopyPathRole: + type: string + enum: ['Инициатор при запуске', 'Утверждающий при завершении', 'Инициатор и утверждающий', 'Возможность отсутствует'] + AcceptanceByReviewer: + type: string + enum: [accepted, rejected, partly_rejected] + TimeTrackingMode: + type: string + enum: [auto, manual] + CompletionNotificationsMode: + type: string + enum: [disabled, positive_documents_only, all_documents] + TransmittalDocumentsStatus: + type: string + enum: [accepted_only, all_documents] + TransmittalDocumentsType: + type: string + enum: [copy_only, original_only] + BulkListUpdateType: + type: string + enum: [replace, add] + + FlowActonAdditions: + type: object + properties: + enable_stamp: { type: boolean } + enable_signature: { type: boolean } + enable_qr_code: { type: boolean } + enable_base_plan: { type: boolean } + required: [enable_stamp, enable_signature, enable_qr_code, enable_base_plan] + + StepChecklistAssignment: + type: object + properties: + required: { type: boolean } + reviewers: { type: array, items: { type: string, format: uuid } } + required: [required, reviewers] + + BaseFlow: + type: object + properties: + name: { type: string } + description: { type: string, nullable: true } + action: { $ref: '#/components/schemas/Action' } + additions: { $ref: '#/components/schemas/FlowActonAdditions' } + meta_data: { type: object, additionalProperties: true } + folder_dst: { type: string, nullable: true } + folder_dst_relative: { type: string, nullable: true } + folder_label: { type: string, nullable: true } + is_active: { type: boolean, nullable: true } + count_of_step: { type: integer, minimum: 0 } + launchers: { type: array, items: { type: string, format: uuid } } + approvers: { type: array, nullable: true, items: { type: string, format: uuid } } + company_id: { type: integer } + creator_id: { type: integer, nullable: true } + changer_id: { type: integer, nullable: true } + signatories: { type: array, nullable: true, items: { type: integer } } + resource_id: { type: string, format: uuid, nullable: true } + steps: { type: array, items: { type: integer } } + statuses: { type: array, items: { type: integer } } + change_copy_path_role: { $ref: '#/components/schemas/ChangeCopyPathRole' } + final_step_duration: { type: integer, nullable: true } + step_watchers: { type: object, additionalProperties: { type: array, items: { type: string } } } + flow_type: { $ref: '#/components/schemas/FlowType' } + finish_step_after_review: { type: boolean } + skip_empty_steps: { type: boolean } + time_tracking_mode: { $ref: '#/components/schemas/TimeTrackingMode' } + attributes: { type: object, additionalProperties: true } + unset_attributes: { type: array, items: { type: integer } } + completion_notifications_mode: { $ref: '#/components/schemas/CompletionNotificationsMode' } + completion_notifications_receivers: { type: array, items: { type: string, format: uuid } } + checklists: { type: object, additionalProperties: { $ref: '#/components/schemas/StepChecklistAssignment' } } + transmittal_after_review: { type: boolean } + transmittal_templates: { type: array, items: { type: string, format: uuid } } + transmittal_doc_statuses: { $ref: '#/components/schemas/TransmittalDocumentsStatus' } + transmittal_doc_types: { $ref: '#/components/schemas/TransmittalDocumentsType' } + folder_dst_ai_assist_enabled: { type: boolean } + required: [name, additions, meta_data, count_of_step, launchers, company_id] + + FlowCreate: + allOf: + - { $ref: '#/components/schemas/BaseFlow' } + + Flow: + allOf: + - { $ref: '#/components/schemas/BaseFlow' } + - type: object + properties: + id: { type: integer } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + required: [id, created_at, updated_at] + + FullFlow: + allOf: + - { $ref: '#/components/schemas/BaseFlow' } + - type: object + properties: + id: { type: integer } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + steps: { type: array, items: { $ref: '#/components/schemas/FullStep' } } + statuses: { type: array, items: { $ref: '#/components/schemas/Status' } } + required: [id, created_at, updated_at] + + FlowLight: + type: object + properties: + id: { type: integer } + name: { type: string } + required: [id, name] + + CopyFlow: + type: object + properties: + name: { type: string } + required: [name] + + BulkUpdateListUUID4FieldAction: + type: object + properties: + action: { $ref: '#/components/schemas/BulkListUpdateType' } + value: { type: array, items: { type: string, format: uuid } } + required: [action, value] + + FlowBulkUpdate: + type: object + properties: + ids: { type: array, items: { type: integer } } + watchers: { $ref: '#/components/schemas/BulkUpdateListUUID4FieldAction' } + approvers: { $ref: '#/components/schemas/BulkUpdateListUUID4FieldAction' } + required: [ids] + + BaseStep: + type: object + properties: + name: { type: string } + duration: { type: integer, minimum: 0 } + min_reviewers: { type: integer, nullable: true } + all_verify: { type: boolean, default: true } + force_close: { type: boolean, default: false } + force_close_block_status: { type: boolean, default: false } + setup_next_step: { type: boolean, default: false } + task_queue: { type: boolean, default: false } + set_status: { type: boolean, default: false } + blocking_step: { type: boolean, default: false } + flow_id: { type: integer } + reviewers: { type: array, items: { type: string, format: uuid } } + blocking_reviewers: { type: array, items: { type: string, format: uuid } } + only_final_step_blocking: { type: boolean, default: false } + min_reviewers_by_sa: { type: object, additionalProperties: { type: integer } } + close_on_rejection: { type: boolean, default: false } + complete_on_rejection: { type: boolean, default: false } + complete_on_rejection_reviewers: { type: array, items: { type: string, format: uuid } } + checklists: { type: object, additionalProperties: { $ref: '#/components/schemas/StepChecklistAssignment' } } + force_next_step_reviewers: { type: array, items: { type: string, format: uuid } } + pinned_reviewers: { type: array, items: { type: string, format: uuid } } + required: [name, duration, flow_id] + + Step: + allOf: + - { $ref: '#/components/schemas/BaseStep' } + - type: object + properties: + id: { type: integer } + type: { $ref: '#/components/schemas/StepType' } + required: [id, type] + + FullStep: + allOf: + - { $ref: '#/components/schemas/Step' } + + ActiveReview: + type: object + properties: + id: { type: integer } + name: { type: string } + required: [id, name] + + UpdatedReviewer: + type: object + properties: + new_account: { type: string, format: uuid } + old_account: { type: string, format: uuid, nullable: true } + skip: { type: boolean } + required: [new_account, skip] + + BaseStatus: + type: object + properties: + key: { $ref: '#/components/schemas/StatusKey' } + value: { type: string } + flow_id: { type: integer } + required: [value, flow_id] + + Status: + allOf: + - { $ref: '#/components/schemas/BaseStatus' } + - type: object + properties: + id: { type: integer } + required: [id] + + StatusForDocument: + type: object + properties: + key: { type: string } + value: { type: string } + id: { type: integer, nullable: true } + is_copied: { type: boolean, nullable: true } + bundles_history: { type: array, nullable: true, items: {} } + required: [key, value] + + BaseDocument: + type: object + properties: + document_id: { type: integer, minimum: 1 } + bundle_id: { type: string, format: uuid } + review_id: { type: integer, minimum: 1 } + instance_id: { type: integer, nullable: true } + document_type: { type: string, nullable: true } + document_copied_id: { type: integer, nullable: true } + bundle_copied_id: { type: string, format: uuid, nullable: true } + required: [document_id, bundle_id, review_id] + + Document: + allOf: + - { $ref: '#/components/schemas/BaseDocument' } + - type: object + properties: + id: { type: integer } + status: { $ref: '#/components/schemas/StatusForDocument' } + copy_to_folder_dst: { type: string, nullable: true } + copy_to_folder_label: { type: string, nullable: true } + is_accepted: { type: boolean, nullable: true } + statuses: { type: array, nullable: true, items: { $ref: '#/components/schemas/SetDocumentStatusRead' } } + acceptance_by_reviewer: { $ref: '#/components/schemas/AcceptanceByReviewer' } + required: [id] + + ExtendDocument: + allOf: + - { $ref: '#/components/schemas/Document' } + - type: object + properties: + review: { type: string } + review_status: { type: string } + review_completed_at: { type: string, format: date-time, nullable: true } + flow_id: { type: integer } + review_comments: { type: array, items: { $ref: '#/components/schemas/ReviewComment' } } + required: [review, review_status, flow_id] + + BaseUpdateDocument: + type: object + properties: + status: { $ref: '#/components/schemas/StatusForDocument' } + required: [status] + + ChangeDocumentCopyPath: + type: object + properties: + id: { type: integer } + copy_to_folder_dst: { type: string } + copy_to_folder_label: { type: string } + required: [id, copy_to_folder_dst, copy_to_folder_label] + + SetDocumentStatusUpdate: + type: object + properties: + status_id: { type: integer } + required: [status_id] + + SetDocumentStatusRead: + type: object + properties: + id: { type: integer } + document_id: { type: integer } + user_id: { type: integer } + step: { $ref: '#/components/schemas/Step' } + status: { $ref: '#/components/schemas/Status' } + required: [id, document_id, user_id, step, status] + + SetDocumentBundleIdAndStatus: + type: object + properties: + bundle_id: { type: string, format: uuid, nullable: true } + status: { $ref: '#/components/schemas/StatusForDocument' } + + UpdateDocumentCopiedIds: + type: object + properties: + document_id: { type: integer } + document_copied_id: { type: integer } + bundle_copied_id: { type: string, format: uuid } + required: [document_id, document_copied_id, bundle_copied_id] + + DocumentFilterRequest: + type: object + properties: + flow_ids: { type: string, nullable: true } + review_ids: { type: string, nullable: true } + document_ids: { type: string, nullable: true } + bundle_ids: { type: string, nullable: true } + review_status: { type: string, nullable: true } + document_types: { type: string, nullable: true } + document_copied_ids: { type: string, nullable: true } + bundle_copied_ids: { type: string, nullable: true } + full: { type: boolean, default: false } + + ReviewComment: + type: object + properties: + comment: { type: string } + creator_id: { type: integer } + created_at: { type: string, format: date-time } + step_name: { type: string } + required: [comment, creator_id, created_at, step_name] + + BaseCreateDocument: + type: object + properties: + document_id: { type: integer, minimum: 1 } + bundle_id: { type: string, format: uuid, nullable: true } + is_folder: { type: boolean, default: false } + instance_id: { type: integer, nullable: true } + required: [document_id] + + BaseReview: + type: object + properties: + name: { type: string } + flow_id: { type: integer, minimum: 1 } + folder_dst: { type: string, nullable: true } + folder_label: { type: string, nullable: true } + resource_id: { type: string, format: uuid, nullable: true } + meta_data: { type: object, additionalProperties: true } + documents: { type: array, items: { $ref: '#/components/schemas/BaseCreateDocument' } } + comment: { type: string, nullable: true } + attributes: { type: object, additionalProperties: true } + checklist_results: { type: object, additionalProperties: { type: array, items: { type: integer } } } + required: [name, flow_id] + + UpdateBaseReview: + type: object + properties: + name: { type: string } + flow_id: { type: integer, minimum: 1 } + folder_dst: { type: string, nullable: true } + folder_label: { type: string, nullable: true } + resource_id: { type: string, format: uuid, nullable: true } + meta_data: { type: object, additionalProperties: true } + attributes: { type: object, additionalProperties: true } + required: [name, flow_id] + + PatchBaseReview: + type: object + properties: + name: { type: string, nullable: true } + resource_id: { type: string, format: uuid, nullable: true } + status: { $ref: '#/components/schemas/StateReview' } + attributes: { type: object, nullable: true, additionalProperties: true } + + PatchCurrentReviewers: + type: object + properties: + was: { type: string, format: uuid, nullable: true } + became: { type: string, format: uuid, nullable: true } + required: { type: boolean, default: false } + force_next_step_reviewer: { type: boolean, default: false } + + PatchMinReviewersOnStep: + type: object + properties: + new_value: { type: integer, minimum: 1 } + required: [new_value] + + StatusForReview: + type: object + properties: + status: { $ref: '#/components/schemas/StateReview' } + comment: { type: string, nullable: true } + duration: { type: integer, minimum: 0, nullable: true } + reviewers: { type: array, nullable: true, items: { $ref: '#/components/schemas/PatchCurrentReviewers' } } + + ReviewersUpdate: + type: object + properties: + id: { type: integer } + current_reviewers: { type: array, nullable: true, items: { type: string, format: uuid } } + completed_reviewers: { type: array, nullable: true, items: { type: string, format: uuid } } + current_signatories: { type: array, nullable: true, items: { type: integer } } + completed_signatories: { type: array, nullable: true, items: { type: integer } } + required: [id] + + BulkReviewersUpdate: + type: object + properties: + reviews: { type: array, items: { $ref: '#/components/schemas/ReviewersUpdate' } } + required: [reviews] + + Review: + type: object + description: | + Итоговая структура review. Сериализатор дополнительно раскладывает + user_actions по шагам flow и добавляет агрегаты `count_of_document`, + `Согласовано`, `Не согласовано`. + properties: + id: { type: integer } + name: { type: string } + status: { $ref: '#/components/schemas/StateReview' } + folder_dst: { type: string, nullable: true } + folder_label: { type: string, nullable: true } + current_step: { type: integer, nullable: true } + current_step_id: { type: integer } + current_step_expired_at: { type: string, format: date-time, nullable: true } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + expired_at: { type: string, format: date-time, nullable: true } + completed_at: { type: string, format: date-time, nullable: true } + min_reviewers: { type: integer, nullable: true } + creator_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + current_reviewers: { type: array, items: { type: string, format: uuid } } + completed_reviewers: { type: array, items: { type: string, format: uuid } } + current_signatories: { type: array, nullable: true, items: { type: integer } } + completed_signatories: { type: array, nullable: true, items: { type: integer } } + flow: { $ref: '#/components/schemas/FullFlow' } + meta_data: { type: object, additionalProperties: true } + current_reviewers_by_sa: { type: object, additionalProperties: { type: integer } } + current_reviewers_changes: { type: array, items: { type: string, format: uuid } } + time_tracking_mode: { $ref: '#/components/schemas/TimeTrackingMode' } + attributes: { type: object, additionalProperties: true } + checklist_results: { type: object, additionalProperties: { type: array, items: { type: integer } } } + force_next_step_reviewers: { type: array, items: { type: string, format: uuid } } + transmittal_ids: { type: array, items: { type: string, format: uuid } } + count_of_document: { type: integer } + required: [id, name, status, current_step_id, created_at, updated_at, creator_id] + + ChecklistResultsUpdateRequest: + type: object + properties: + step_id: { type: integer } + add_results: { type: array, items: { type: integer } } + remove_results: { type: array, items: { type: integer } } + required: [step_id] + + TransmittalForReviewCreated: + type: object + properties: + review_id: { type: integer } + transmittal_id: { type: string, format: uuid } + required: [review_id, transmittal_id] + + BaseUserAction: + type: object + properties: + action: { type: string } + key: { type: string, nullable: true } + value: { type: string } + creator_id: { type: integer } + step_id: { type: integer } + document_id: { type: integer, nullable: true } + review_id: { type: integer } + required: [action, value, creator_id, step_id, review_id] + + UserActions: + type: object + properties: + id: { type: integer } + action: { type: string } + key: { type: string, nullable: true } + value: { type: string } + creator_id: { type: integer } + created_at: { type: string, format: date-time } + step_id: { type: integer } + document_id: { type: integer, nullable: true } + review_id: { type: integer } + required: [id, action, value, creator_id, created_at, step_id, review_id] + + ListUserActionsForDelete: + type: object + properties: + ids: { type: array, items: { type: integer } } + required: [ids] + + UpdateUserAction: + type: object + properties: + key: { type: string, nullable: true } + value: { type: string, nullable: true } + + Task: + type: object + properties: + id: { type: integer } + priority: { type: integer, minimum: 1, nullable: true } + start_date: { type: string, format: date-time } + duration: { type: integer, minimum: 0 } + end_date: { type: string, format: date-time } + review: { type: object, additionalProperties: true } + reviewer_id: { type: integer } + is_active: { type: boolean } + step_id: { type: integer, nullable: true } + review_expired_at: { type: string, format: date-time, nullable: true } + current_reviewers: { type: array, items: { type: integer } } + completed_reviewers: { type: array, items: { type: integer } } + required: [id, start_date, duration, end_date, reviewer_id, is_active] + + TaskUpdatePriority: + type: object + properties: + priority: { type: integer, minimum: 1 } + required: [priority] + + TaskUpdateDuration: + type: object + properties: + duration: { type: integer, minimum: 1 } + required: [duration] + + TasksCount: + type: object + properties: + tasks_count: { type: integer } + required: [tasks_count] + + ReviewerMaxTaskEndDate: + type: object + properties: + reviewer_id: { type: integer } + max_task_end_date: { type: string, format: date-time } + required: [reviewer_id, max_task_end_date] + + MainFilterPostRequest: + type: object + description: Схема фильтрации для POST /flows/filter/, /reviews/filter/ и подсчётов. + properties: + created_at_after: { type: string, nullable: true } + created_at_before: { type: string, nullable: true } + updated_at_after: { type: string, nullable: true } + updated_at_before: { type: string, nullable: true } + expired_before: { type: string, nullable: true } + current_step_expired_before: { type: string, nullable: true } + resource_id: { type: array, nullable: true, items: { type: string } } + flow_id: { type: array, nullable: true, items: { type: string } } + launcher_id: { type: array, nullable: true, items: { type: string } } + reviewers_ids: { type: array, nullable: true, items: { type: string } } + reviewers_sa: { type: array, nullable: true, items: { type: string } } + launcher_sa: { type: array, nullable: true, items: { type: string } } + action: { type: array, nullable: true, items: { type: string } } + approvers: { type: array, nullable: true, items: { type: string } } + step_watchers: { type: array, nullable: true, items: { type: string } } + reviewers: { type: array, nullable: true, items: { type: string } } + current_reviewers: { type: array, nullable: true, items: { type: string } } + completed_reviewers: { type: array, nullable: true, items: { type: string } } + signatories: { type: array, nullable: true, items: { type: string } } + current_signatories: { type: array, nullable: true, items: { type: string } } + completed_signatories: { type: array, nullable: true, items: { type: string } } + company_id: { type: string, nullable: true } + creator_id: { type: string, nullable: true } + changer_id: { type: string, nullable: true } + name: { type: string, nullable: true } + status: { type: array, nullable: true, items: { $ref: '#/components/schemas/StateReview' } } + flow_type: { $ref: '#/components/schemas/FlowType' } + full: { type: boolean, default: false } + is_active: { type: boolean, nullable: true } + enable_stamp: { type: boolean, nullable: true } + enable_signature: { type: boolean, nullable: true } + enable_qr_code: { type: boolean, nullable: true } + has_reviewers: { type: boolean, nullable: true } + attributes: { type: object, nullable: true, additionalProperties: true } diff --git a/apps/iam/.env.example b/apps/iam/.env.example new file mode 100644 index 0000000..0d05897 --- /dev/null +++ b/apps/iam/.env.example @@ -0,0 +1,75 @@ +# iams-v2 — пример переменных окружения. +# Конфиг читается из окружения (github.com/sethvargo/go-envconfig). +# Перед разбором подхватывается env-файл через godotenv: путь из ENV_FILE, иначе ".env". +# Значения ниже — дефолты из кода и примеры для локального запуска. + +# App +ENVIRONMENT=local +LOG_LEVEL=info +SERVICE_NAME=iams +SERVICE_VERSION=0.0.0 + +# HTTP (prefix HTTP_) +HTTP_PORT=8080 +# ReadBufferSize (Fiber/fasthttp), байты +HTTP_READ_BUFFER_SIZE=131072 + +# Database (prefix DB_) +DB_DSN=postgres://postgres:secret@localhost:5432/postgres?sslmode=disable +DB_MIGRATIONS_PATH=migrations + +# Auth — внешний контур /external/api/* (prefix AUTH_) +# false ⇒ группа /external/api не регистрируется, JWT не разбирается +AUTH_ENABLED=false + +# Sonyflake — machine id для генерации resource.id (prefix SONYFLAKE_) +SONYFLAKE_MACHINE_ID=1 + +# Zitadel Management API (prefix ZITADEL_) +# при ENABLED=true обязательны HOST, ACCESS_TOKEN, ORG_RULES_FILE +ZITADEL_ENABLED=false +ZITADEL_HOST= +ZITADEL_ACCESS_TOKEN= +ZITADEL_ORG_RULES_FILE=config/zitadel/org-rules-stage.json + +# S3 (presigned GET для вложений виджета) (prefix S3_) +# при ENABLED=true обязательны ENDPOINT_URL, BUCKET_NAME, ACCESS_KEY_ID, SECRET_ACCESS_KEY +S3_ENABLED=false +S3_ENDPOINT_URL=https://storage.yandexcloud.net +S3_BUCKET_NAME= +S3_REGION=ru-central1 +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_PRESIGN_EXPIRES=1h + +# Kafka (prefix KAFKA_) +KAFKA_ENABLED=false +KAFKA_BROKERS=localhost:9092 +KAFKA_SECURITY_PROTOCOL=PLAINTEXT +KAFKA_SASL_MECHANISM= +KAFKA_SASL_PLAIN_USERNAME= +KAFKA_SASL_PLAIN_PASSWORD= +KAFKA_SSL_CAFILE= +# Политика имён топиков (prefix KAFKA_TOPIC_) +# По умолчанию topic = event_type. PREFIX добавляется ко всем именам. +KAFKA_TOPIC_PREFIX= +# Путь к JSON-файлу {event_type: topic} с переопределениями +KAFKA_TOPIC_OVERRIDES_FILE= +# Legacy-топик (BrokerMessage пользователя) +KAFKA_TOPIC_LEGACY_AMS_SYNC=ams-sync + +# OpenTelemetry (prefix OTEL_) +OTEL_ENABLED=false +OTEL_HOST=localhost +OTEL_PORT=4317 +OTEL_INSECURE=true +OTEL_SERVICE_NAME=iams + +# Переменная выбора env-файла (читается до разбора конфига) +# ENV_FILE=.env + +# --- Вспомогательное (не читается конфигом приложения) --- +# DSN для интеграционных тестов (общий контейнер Postgres): см. Makefile/README +# TEST_DB_DSN=postgres://postgres:secret@localhost:5432/postgres?sslmode=disable +# Включение Kafka в docker compose для сервиса http +# COMPOSE_KAFKA_ENABLED=false diff --git a/apps/iam/CONFIGURATION.md b/apps/iam/CONFIGURATION.md new file mode 100644 index 0000000..57edd88 --- /dev/null +++ b/apps/iam/CONFIGURATION.md @@ -0,0 +1,174 @@ +# Конфигурация проекта iams-v2 + +Документ описывает все переменные окружения и способы конфигурирования сервиса **iams** (`platform/iams-v2`) — сервиса управления пользователями, ресурсами и правами доступа (IAM). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `internal/app/config.go` (функция `app.Load`) через библиотеку [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig) (структура `Config`). + +Особенности разбора: + +- вложенные секции задаются полями-структурами с тегом `env:", prefix=_"`, напр. `DB_` → `Config.DB`, `S3_` → `Config.S3`; +- у большинства полей задан дефолт через `env:"NAME, default=..."`; поля без дефолта при отсутствии остаются нулевыми, а обязательность проверяется отдельными валидаторами (см. ниже); +- перед разбором окружения подхватывается env-файл через [`godotenv`](https://github.com/joho/godotenv): путь берётся из переменной `ENV_FILE`, иначе `.env`. Отсутствие файла **не** является ошибкой (ошибка `godotenv.Load` игнорируется), реальные значения читаются из окружения процесса. + +Отдельного конфиг-файла (yaml/toml) у приложения нет, за исключением двух внешних файлов, путь к которым задаётся переменными: `ZITADEL_ORG_RULES_FILE` (JSON-правила организаций Zitadel) и `KAFKA_TOPIC_OVERRIDES_FILE` (JSON-переопределения имён топиков). + +Валидация на старте (`app.Load`): + +- при `S3_ENABLED=true` обязательны `S3_ENDPOINT_URL`, `S3_BUCKET_NAME`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` — иначе ошибка старта; +- при `ZITADEL_ENABLED=true` обязательны `ZITADEL_HOST`, `ZITADEL_ACCESS_TOKEN`, `ZITADEL_ORG_RULES_FILE` — иначе ошибка старта; +- при заданном `KAFKA_TOPIC_OVERRIDES_FILE` файл читается и парсится как JSON `{event_type: topic}` — при ошибке чтения/парсинга сервис не стартует. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса + env-файл (`.env` или `ENV_FILE`), который автоматически загружается `godotenv` | +| Локально (docker compose) | `docker-compose.yml`: сервис `http` получает `DB_DSN`, `KAFKA_ENABLED`, `S3_ENABLED` из окружения хоста; Postgres — из блока `environment` | +| Kubernetes (Helm) | `.helm/values.yaml` (universal-chart): блоки `envs` (обычные значения по окружениям `_default`/`stage`/`production`), `secretEnvs` (значения из k8s-секретов) и `volumes` (монтирование CA-сертификата Kafka) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переключение стенда по ветке/тегу (`workflow.rules`), общие шаблоны из `generic/common-ci`, `HELM_SET_ARGS` | + +Способы запуска процессов (`cmd/*`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `server` (docker `ENTRYPOINT`, `make build`) | `cmd/server/main.go` | HTTP API (Fiber). Флаг `-env-file=PATH` переопределяет `ENV_FILE` | +| `cli migrate` | `cmd/cli/main.go` | Прогон миграций БД (`golang-migrate`). Требует `DB_DSN`; путь миграций — `DB_MIGRATIONS_PATH` | +| `seed` | `cmd/seed/main.go` | Наполнение БД тестовыми данными (флаги `--seed`, `--resources`, `--users`, `--tenant-id`, `--type-id`) | + +Docker-образ (`docker/httpserver/Dockerfile`) собирает статический бинарник `server` (scratch-образ) и копирует каталог `config/` (файлы правил Zitadel). Миграции применяются отдельной командой `cli migrate` (в локальном сценарии — целью `make postgres-up`). + +## Переменные приложения + +В столбце «Переменная» указано полное имя (с учётом префикса секции). Дефолт `—` означает, что в коде значения по умолчанию нет (поле остаётся нулевым, если не задано). + +### App (верхний уровень, без префикса) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENVIRONMENT` | string | `local` | Окружение развёртывания (`local`/`stage`/`prod`) | +| `LOG_LEVEL` | string | `info` | Уровень логирования (`debug`/`info`/`warn`/`error`) | +| `SERVICE_NAME` | string | `iams` | Имя сервиса | +| `SERVICE_VERSION` | string | `0.0.0` | Версия сервиса | + +### HTTP (`HTTP_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_PORT` | string | `8080` | Порт HTTP-сервера | +| `HTTP_READ_BUFFER_SIZE` | int | `131072` | Размер буфера чтения запроса (Fiber/fasthttp), байты | + +### Database (`DB_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_DSN` | string | — | DSN PostgreSQL (`postgres://user:pass@host:port/db?sslmode=...`). Обязателен для работы БД и команды `cli migrate` | +| `DB_MIGRATIONS_PATH` | string | `migrations` | Путь к каталогу SQL-миграций | + +### Auth (`AUTH_*`) + +Конфигурация внешнего контура `/external/api/*`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_ENABLED` | bool | `false` | При `false` внешний контур не регистрируется и пользовательская авторизация не выполняется; внутренние эндпоинты работают без изменений | + +### Sonyflake (`SONYFLAKE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SONYFLAKE_MACHINE_ID` | uint16 | `1` | Machine ID генератора идентификаторов `resource.id` для новых записей | + +### Zitadel (`ZITADEL_*`) + +Management API (Service User с правами администратора). При `ENABLED=true` `HOST`, `ACCESS_TOKEN`, `ORG_RULES_FILE` обязательны. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ZITADEL_ENABLED` | bool | `false` | Включить интеграцию с Zitadel | +| `ZITADEL_HOST` | string | — | Хост Zitadel (напр. `https://login.sarex.io`) | +| `ZITADEL_ACCESS_TOKEN` | string | — | Access token сервисного пользователя | +| `ZITADEL_ORG_RULES_FILE` | string | — | Путь к JSON-файлу правил организаций (`config/zitadel/org-rules-.json`) | + +### S3 (`S3_*`) + +Presigned GET для вложений виджета в приватном S3-совместимом бакете. При `ENABLED=true` `ENDPOINT_URL`, `BUCKET_NAME`, `ACCESS_KEY_ID`, `SECRET_ACCESS_KEY` обязательны. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_ENABLED` | bool | `false` | Включить S3-интеграцию | +| `S3_ENDPOINT_URL` | string | — | Эндпоинт S3 (напр. `https://storage.yandexcloud.net`) | +| `S3_BUCKET_NAME` | string | — | Имя бакета | +| `S3_REGION` | string | `ru-central1` | Регион | +| `S3_ACCESS_KEY_ID` | string | — | Access key | +| `S3_SECRET_ACCESS_KEY` | string | — | Secret key | +| `S3_PRESIGN_EXPIRES` | duration | `1h` | Срок жизни presigned-ссылки (Go duration, напр. `1h`, `15m`) | + +### Kafka (`KAFKA_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_ENABLED` | bool | `false` | Включить Kafka-продюсер | +| `KAFKA_BROKERS` | string | `localhost:9092` | Список брокеров | +| `KAFKA_SECURITY_PROTOCOL` | string | `PLAINTEXT` | Протокол (`PLAINTEXT`/`SASL_SSL`/...) | +| `KAFKA_SASL_MECHANISM` | string | — | SASL-механизм (напр. `SCRAM-SHA-512`) | +| `KAFKA_SASL_PLAIN_USERNAME` | string | — | SASL-логин | +| `KAFKA_SASL_PLAIN_PASSWORD` | string | — | SASL-пароль | +| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату для SSL | + +Политика имён топиков (`KAFKA_TOPIC_*`): + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_TOPIC_PREFIX` | string | — | Префикс, добавляемый ко всем именам топиков. По умолчанию `topic = event_type` | +| `KAFKA_TOPIC_OVERRIDES_FILE` | string | — | Путь к JSON-файлу `{event_type: topic}` с переопределениями. Читается на старте | +| `KAFKA_TOPIC_LEGACY_AMS_SYNC` | string | `ams-sync` | Legacy-топик для `BrokerMessage` пользователя | + +### OpenTelemetry (`OTEL_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `OTEL_ENABLED` | bool | `false` | Включить трейсинг | +| `OTEL_HOST` | string | `localhost` | Хост OTLP/gRPC-коллектора | +| `OTEL_PORT` | string | `4317` | Порт коллектора | +| `OTEL_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `OTEL_SERVICE_NAME` | string | `iams` | Имя сервиса в трейсах | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — `universal-chart` (сервис `iams`). Обычные значения задаются в блоке `envs` с профилями `_default`/`stage`/`production` и содержат те же переменные `ENVIRONMENT`, `LOG_LEVEL`, `AUTH_ENABLED`, `HTTP_*`, `DB_MIGRATIONS_PATH`, `ZITADEL_*`, `S3_*`, `KAFKA_*`, `OTEL_*` (различаются адресами БД/брокеров/коллектора, бакетом, доменом Zitadel и путём к файлу правил). + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `DB_DSN` | `iams-secret` | `db-dsn` | +| `ZITADEL_ACCESS_TOKEN` | `iams-secret` | `zitadel-access-token` | +| `KAFKA_SASL_PLAIN_USERNAME` | `iams-secret` | `kafka-sasl-plain-username` | +| `KAFKA_SASL_PLAIN_PASSWORD` | `iams-secret` | `kafka-sasl-plain-password` | +| `S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` | +| `S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` | + +Помимо env, чарт монтирует CA-сертификат Kafka из секрета `ya-ca-secret` как файл `/etc/ca-certificates/Yandex/ca-cert` — именно на него указывает `KAFKA_SSL_CAFILE` в конфигурации стенда/прода. + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, порт `8080`, `replicaCount`: `_default=1`, `production=4`), `image.name` (`cr.yandex/.../iams:latest`), `service.*` (порт/targetPort `8080`). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | Chart version | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `platform` | `0.0.1-stage` | +| тег (`CI_COMMIT_TAG`) | `production` | `iam` | `0.0.1-prod` | +| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные пайплайна: `SERVICE_NAME=iams`, `DOCKERFILE_PATH=./docker/httpserver/Dockerfile`, `RELEASE_NAME`, `CHART_NAME`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, ссылки на GitLab). Джобы `lint` (`go vet` + `golangci-lint`) и `test` (`go test ./...`) выполняются на merge request. + +## Замечания и минимальный набор для локального запуска + +- Приложение **загружает env-файл автоматически** (`godotenv`): достаточно положить `.env` рядом с бинарником или указать путь через `ENV_FILE` / флаг `-env-file`. Отсутствие файла не является ошибкой. +- Три интеграции выключены по умолчанию (`ENABLED=false`): `AUTH`, `ZITADEL`, `S3`, `KAFKA`, `OTEL`. Включение любой из `S3`/`ZITADEL` требует заполнения обязательных полей (см. валидаторы выше), иначе сервис не стартует. +- Для локального запуска минимально необходимо задать `DB_DSN` (Postgres поднимается через `docker compose up -d postgres`, миграции — `make postgres-up` → `cli migrate`). Остальные секции можно оставить с дефолтами (`ENABLED=false`). +- Готовые значения-примеры для всех переменных приведены в `.env.example`. diff --git a/apps/iam/ENDPOINTS.md b/apps/iam/ENDPOINTS.md new file mode 100644 index 0000000..798b31f --- /dev/null +++ b/apps/iam/ENDPOINTS.md @@ -0,0 +1,207 @@ +# Эндпоинты сервиса iams-v2 + +Документ описывает все HTTP-эндпоинты, которые **предоставляет** сервис `iams` (IAM: пользователи, ресурсы/проекты, права доступа). В отличие от фронтенд-модулей, здесь описан контракт самого сервиса. + +## Как устроено взаимодействие + +Сервер — Fiber (`internal/controller/http/server.go`). Маршруты регистрируются двумя наборами: + +- **Внутренний контур** (`RegisterAPIRoutes`) — без аутентификации на уровне приложения, доступ ограничивается сетевым слоем (не публикуется через ingress). Базовые группы: `/api/v0`, `/api/admin/v0`, `/api/v1`, `/api/v2`. +- **Внешний контур** (`RegisterExternalAPIRoutes`) — регистрируется только при `AUTH_ENABLED=true`. Перед маршрутами выполняется разбор JWT (`AuthMiddleware`) и пометка контекста (`ExternalAPIMiddleware`, для админ-группы дополнительно `ExternalUserAdminAPIMiddleware`). Базовые группы: `/external/api/v0`, `/external/api/admin/v0`, `/external/api/v1`, `/external/api/v2`. Внешний контур публикует **подмножество** внутренних маршрутов, часть — только на чтение. + +Глобальные middleware: `requestid`, `recover`, OTel (при `OTEL_ENABLED` и заданном `OTEL_SERVICE_NAME`), логирование. + +### Аутентификация (внешний контур) + +Токен передаётся заголовком `Authorization: Bearer ` (`internal/controller/http/jwt/parser.go`). **Подпись токена не проверяется** — доверие делегируется вышестоящему gateway/virtual service. Схема разбора выбирается по заголовку `Identity`: + +- заголовок `Identity` отсутствует/пуст → legacy-формат (Django SimpleJWT); +- заголовок `Identity` задан → Zitadel (клейм `urn:zitadel:iam:user:metadata`, значения полей — base64). + +### Формат ответов и ошибок + +Успешные ответы — JSON (списки: `{ count, results }`; часть эндпоинтов возвращает объект напрямую). Ошибки — JSON `{ "error": "...", "id": "..." }` (`httpx.Response`). Доменный `Kind` маппится на HTTP-статус (`httpx/error_resolve.go`): `InvalidArgument/OutOfRange`→`400`, `Unauthenticated`→`401`, `PermissionDenied`→`403`, `NotFound`→`404`, `Aborted/AlreadyExists`→`409`, `PreconditionFailed`→`412`, `ResourceExhausted`→`429`, `Unavailable`→`503`, `Internal/Unknown/DataLoss`→`500`. Идентификатор запроса — заголовок из `requestid`. + +### Пагинация и фильтры + +Списки — через query `limit`/`offset` (по умолчанию `limit=1000`). Дополнительные фильтры задаются query-параметрами (напр. для `/resource`: `parent_id`, `type`, `tenant_id`, `name`, `code`, `public_id`, `target_id`, `service_accounts`; для `/users`: `username`, `search`, `is_active`, `is_staff`, `is_superuser`, `service_account_id`, `id`, `company_id`, `departments`, `positions`, `groups`). + +## Служебные эндпоинты + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/health` | Health-check: проверка БД и (при включении) Kafka. `200` — ok, `503` — недоступность зависимости | + +## Внутренний контур + +### Users (`/api/v0/users`, `/api/admin/v0`, `/api/v1`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/users` | Список пользователей (фильтры, пагинация) | +| POST | `/api/v0/users` | Создать пользователя (если включён use case) | +| PATCH | `/api/v0/users` | Массовое обновление пользователей (legacy bulk_update) | +| GET | `/api/v0/users/:id` | Пользователь по id | +| PUT | `/api/v0/users/:id` | Полное обновление пользователя | +| PATCH | `/api/v0/users/:id` | Частичное обновление пользователя | +| DELETE | `/api/v0/users/:id` | Удалить пользователя | +| POST | `/api/admin/v0/users/activation` | Активация/деактивация пользователей компании | +| POST | `/api/v1/users-with-resources` | Пользователи с их ресурсами (тело: `tenant_id`, `id[]`) | +| GET | `/api/v1/users-grouped-by-resource/` | Пользователи, сгруппированные по ресурсу (если включён use case) | + +### Permission check (`/api/v0`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| POST | `/api/v0/permissions-check` | Проверка набора прав пользователя (`user_id`, `checks[]`) | + +### Groups (`/api/v0/groups`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/groups` | Список групп | +| POST | `/api/v0/groups` | Создать группу | +| POST | `/api/v0/groups/search` | Поиск групп (пагинация, сортировка, фильтры) | +| GET | `/api/v0/groups/:id` | Группа по id | +| PUT | `/api/v0/groups/:id` | Обновить группу | +| PATCH | `/api/v0/groups/:id` | Частично обновить группу | +| DELETE | `/api/v0/groups/:id` | Удалить группу | + +### Positions (`/api/v0/positions`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/positions` | Список должностей | +| POST | `/api/v0/positions` | Создать должность | +| GET | `/api/v0/positions/:id` | Должность по id | +| PUT | `/api/v0/positions/:id` | Обновить должность | +| PATCH | `/api/v0/positions/:id` | Частично обновить должность | +| DELETE | `/api/v0/positions/:id` | Удалить должность | + +### Departments (`/api/v0/departments`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/departments` | Список подразделений | +| POST | `/api/v0/departments` | Создать подразделение | +| GET | `/api/v0/departments/:id` | Подразделение по id | +| PUT | `/api/v0/departments/:id` | Обновить подразделение | +| PATCH | `/api/v0/departments/:id` | Частично обновить подразделение | +| DELETE | `/api/v0/departments/:id` | Удалить подразделение | + +### Django permissions (`/api/v0/permissions`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/permissions` | Список permission'ов | +| POST | `/api/v0/permissions` | Создать permission | +| POST | `/api/v0/permissions/search` | Поиск permission'ов | +| GET | `/api/v0/permissions/:id` | Permission по id | +| DELETE | `/api/v0/permissions/:id` | Удалить permission | + +### Resource types v0 (`/api/v0/resource-types`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/resource-types` | Список типов ресурсов | +| POST | `/api/v0/resource-types` | Создать тип | +| GET | `/api/v0/resource-types/:id` | Тип по id | +| PUT | `/api/v0/resource-types/:id` | Обновить тип | +| PATCH | `/api/v0/resource-types/:id` | Частично обновить тип | +| DELETE | `/api/v0/resource-types/:id` | Удалить тип | + +### Resources v0 (`/api/v0/resources`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/resources` | Список ресурсов (legacy v0) | +| POST | `/api/v0/resources` | Создать ресурс | +| GET | `/api/v0/resources/:id` | Ресурс по id | +| PUT | `/api/v0/resources/:id` | Обновить ресурс | +| PATCH | `/api/v0/resources/:id` | Частично обновить ресурс | +| DELETE | `/api/v0/resources/:id` | Удалить ресурс | + +### Projects v0 (`/api/v0/projects`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/projects` | Список проектов | +| POST | `/api/v0/projects` | Создать проект | +| GET | `/api/v0/projects/:publicID` | Проект по public id | +| PUT | `/api/v0/projects/:publicID` | Обновить проект | +| PATCH | `/api/v0/projects/:publicID` | Частично обновить проект | +| DELETE | `/api/v0/projects/:publicID` | Удалить проект | + +### Widgets v0 (`/api/v0/widgets`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v0/widgets` | Список виджетов | +| POST | `/api/v0/widgets` | Создать виджет | +| GET | `/api/v0/widgets/:publicID` | Виджет по public id | +| PUT | `/api/v0/widgets/:publicID` | Обновить виджет | +| PATCH | `/api/v0/widgets/:publicID` | Частично обновить виджет | +| DELETE | `/api/v0/widgets/:publicID` | Удалить виджет | + +### Resources v1 (`/api/v1/resource`, `/api/v1/targets`, `/api/v1/service-accounts`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v1/resource` | Список ресурсов (v1) | +| POST | `/api/v1/resource` | Создать ресурс | +| GET | `/api/v1/resource/:publicID` | Ресурс по public id | +| PUT | `/api/v1/resource/:publicID` | Обновить ресурс | +| PATCH | `/api/v1/resource/:publicID` | Частично обновить ресурс | +| DELETE | `/api/v1/resource/:publicID` | Удалить ресурс | +| GET | `/api/v1/targets/:target_id/resource` | Ресурс по target id (только внутренний) | +| GET | `/api/v1/resources-grouped-by-sa` | Ресурсы, сгруппированные по сервисному аккаунту (если включено) | +| GET | `/api/v1/service-accounts` | Список сервисных аккаунтов | + +### Widgets v2 (`/api/v2/resource`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v2/resource` | Список виджетов (v2) | +| POST | `/api/v2/resource` | Создать виджет | +| GET | `/api/v2/resource/:publicID` | Виджет по public id | +| PUT | `/api/v2/resource/:publicID` | Обновить виджет | +| PATCH | `/api/v2/resource/:publicID` | Частично обновить виджет | +| DELETE | `/api/v2/resource/:publicID` | Удалить виджет | + +### Permissions v1 (`/api/v1`) + +| Метод | Путь | Назначение | +| --- | --- | --- | +| GET | `/api/v1/resource_permission` | Список прав на ресурсы | +| POST | `/api/v1/resource_permission` | Создать права (пакет `items[]`) | +| GET | `/api/v1/resource_permission/:id` | Право по id | +| DELETE | `/api/v1/resource_permission/:id` | Удалить право | +| PATCH | `/api/v1/permissions-bulk` | Массовое изменение прав (`tenant_id`, `permissions`) | +| GET | `/api/v1/company_resource_permission` | Список прав компании на ресурсы | +| POST | `/api/v1/company_resource_permission` | Создать права компании (пакет `items[]`) | +| GET | `/api/v1/company_resource_permission/:id` | Право компании по id | +| DELETE | `/api/v1/company_resource_permission/:id` | Удалить право компании | + +## Внешний контур (`/external/api/*`, только при `AUTH_ENABLED=true`) + +Публикуется подмножество внутренних маршрутов; часть — только на чтение. Пути идентичны внутренним, но с префиксом `/external/api`. + +| Группа | Маршруты | Отличия от внутреннего контура | +| --- | --- | --- | +| `/external/api/v0/users` | `GET /`, `GET /:id`, `PATCH /` | Только чтение + массовый PATCH (bulk) | +| `/external/api/admin/v0/users` | `POST /activation`, полный CRUD `/users` | Полный доступ (админ-группа) | +| `/external/api/admin/v0/positions` | `/positions` (CRUD) | Как внутренний | +| `/external/api/admin/v0/departments` | `/departments` (CRUD) | Как внутренний | +| `/external/api/admin/v0/groups` | `/groups` (CRUD + search) | Как внутренний | +| `/external/api/admin/v0/permissions` | `/permissions` (django) | Как внутренний | +| `/external/api/v0/resource-types` | `/resource-types` (CRUD) | Как внутренний | +| `/external/api/v0/resources` | `/resources` (CRUD, v0) | Как внутренний | +| `/external/api/v0/projects` | `GET /`, `GET /:publicID` | Только чтение | +| `/external/api/v0/widgets` | `/widgets` (CRUD) | Как внутренний | +| `/external/api/v1/resource` | `GET /`, `GET /:publicID` | Только чтение | +| `/external/api/v2/resource` | `/resource` (виджеты v2, CRUD) | Как внутренний | +| `/external/api/v1/resource_permission` | CRUD-подмножество | Как внутренний | +| `/external/api/v1/permissions-bulk` | `PATCH /` | Как внутренний | +| `/external/api/v1/company_resource_permission` | CRUD-подмножество | Как внутренний | + +> Часть маршрутов регистрируется условно — только если соответствующий хендлер/use case подключён при инициализации сервера (проверки `Has*` в хендлерах). При отсутствии сервиса группа не регистрируется. diff --git a/apps/iam/openapi.yaml b/apps/iam/openapi.yaml new file mode 100644 index 0000000..258f28f --- /dev/null +++ b/apps/iam/openapi.yaml @@ -0,0 +1,1170 @@ +openapi: 3.0.3 + +info: + title: iams-v2 API + version: "1.0.0" + description: | + REST API сервиса **iams** (`platform/iams-v2`) — управление пользователями, + ресурсами/проектами, оргструктурой (должности, подразделения, группы) и + правами доступа (IAM). + + Сервис написан на Go (**Fiber**). Приложение собирается в + `internal/controller/http/server.go` (`NewServer`). Роутинг состоит из двух + контуров: + + - **внутренний** — группы `/api/v0`, `/api/admin/v0`, `/api/v1`, `/api/v2`; + аутентификации на уровне приложения нет, доступ ограничивается сетевым + слоем (через ingress не публикуется); + - **внешний** — группы `/external/api/v0`, `/external/api/admin/v0`, + `/external/api/v1`, `/external/api/v2`; регистрируется только при + `AUTH_ENABLED=true`, публикует подмножество внутренних маршрутов (часть — + только на чтение). + + Health-check доступен по `GET /api/health`. + + ### Аутентификация + Внешние эндпоинты требуют заголовок `Authorization: Bearer ` + (`internal/controller/http/jwt/parser.go`). **Подпись токена не проверяется** — + доверие делегируется вышестоящему gateway/virtual service. Схема разбора + выбирается по заголовку `Identity`: + + 1. заголовок `Identity` отсутствует/пуст → legacy-формат (Django SimpleJWT); + 2. заголовок `Identity` задан → Zitadel (клейм + `urn:zitadel:iam:user:metadata`, значения полей — base64). + + Внутренние эндпоинты (`/api/*`) аутентификации на уровне приложения не требуют. + + ### Пагинация + Списочные ответы используют `limit`/`offset` (по умолчанию `limit=1000`) и + оборачиваются в `{ count, results }`. + + ### Обработка ошибок + Ошибки возвращаются как JSON `{ error, id }` (`httpx.Response`). Доменный + `Kind` маппится на HTTP-статус (`httpx/error_resolve.go`): + `InvalidArgument`/`OutOfRange` → `400`, `Unauthenticated` → `401`, + `PermissionDenied` → `403`, `NotFound` → `404`, + `Aborted`/`AlreadyExists` → `409`, `PreconditionFailed` → `412`, + `ResourceExhausted` → `429`, `Unavailable` → `503`, + `Internal`/`Unknown`/`DataLoss` → `500`. + +servers: + - url: /api + description: Внутренний контур (без auth) + - url: /external/api + description: Внешний контур (JWT, только при AUTH_ENABLED=true) + +tags: + - name: health + - name: users + - name: permission-check + - name: groups + - name: positions + - name: departments + - name: django-permissions + - name: resource-types + - name: resources-v0 + - name: projects-v0 + - name: widgets-v0 + - name: resources + - name: widgets + - name: resource-permissions + - name: company-resource-permissions + - name: service-accounts + +paths: + /health: + get: + tags: [health] + summary: Health-check + description: Проверка БД и (при включении) Kafka. Обслуживается как `/api/health`. + security: [] + responses: + "200": + description: Сервис доступен + content: + application/json: + schema: + type: object + properties: + status: { type: string, example: ok } + "503": + description: Недоступна зависимость (БД/Kafka) + content: + application/json: + schema: + type: object + properties: + status: { type: string, example: unhealthy } + error: { type: string, example: database_unavailable } + + /v0/users: + get: + tags: [users] + summary: Список пользователей + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: username, in: query, schema: { type: string } } + - { name: search, in: query, schema: { type: string } } + - { name: is_active, in: query, schema: { type: boolean } } + - { name: is_staff, in: query, schema: { type: boolean } } + - { name: is_superuser, in: query, schema: { type: boolean } } + - { name: service_account_id, in: query, schema: { type: string, format: uuid } } + - { name: id, in: query, description: "CSV из id", schema: { type: string } } + - { name: company_id, in: query, schema: { type: integer } } + - { name: departments, in: query, description: "CSV", schema: { type: string } } + - { name: positions, in: query, description: "CSV", schema: { type: string } } + - { name: groups, in: query, description: "CSV", schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: { type: integer } + results: + type: array + items: { $ref: "#/components/schemas/User" } + default: { $ref: "#/components/responses/Error" } + post: + tags: [users] + summary: Создать пользователя + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UserCreateInput" } + responses: + "201": + description: Создан + content: + application/json: + schema: { $ref: "#/components/schemas/User" } + default: { $ref: "#/components/responses/Error" } + patch: + tags: [users] + summary: Массовое обновление пользователей (legacy bulk_update) + requestBody: + required: true + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/UserBulkPatchItem" } + responses: + "200": + description: OK + default: { $ref: "#/components/responses/Error" } + + /v0/users/{id}: + parameters: + - { name: id, in: path, required: true, schema: { type: integer, format: int64 } } + get: + tags: [users] + summary: Пользователь по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/User" } + default: { $ref: "#/components/responses/Error" } + put: + tags: [users] + summary: Полное обновление пользователя + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UserUpdateInput" } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + patch: + tags: [users] + summary: Частичное обновление пользователя + requestBody: + required: true + content: + application/json: + schema: { type: object } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + delete: + tags: [users] + summary: Удалить пользователя + responses: + "204": { description: Удалён } + default: { $ref: "#/components/responses/Error" } + + /admin/v0/users/activation: + post: + tags: [users] + summary: Активация/деактивация пользователей компании + requestBody: + required: true + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/CompanyUserActivationItem" } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + + /v1/users-with-resources: + post: + tags: [users] + summary: Пользователи с их ресурсами + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [tenant_id, id] + properties: + tenant_id: { type: integer, format: int64 } + id: + type: array + items: { type: integer, format: int64 } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + + /v1/users-grouped-by-resource/: + get: + tags: [users] + summary: Пользователи, сгруппированные по ресурсу + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + + /v0/permissions-check: + post: + tags: [permission-check] + summary: Проверка прав пользователя + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UserPermissionCheckInput" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/UserPermissionCheckOutput" } + default: { $ref: "#/components/responses/Error" } + + /v0/groups: + get: + tags: [groups] + summary: Список групп + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + post: + tags: [groups] + summary: Создать группу + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/GroupCreateInput" } + responses: + "201": { description: Создана } + default: { $ref: "#/components/responses/Error" } + + /v0/groups/search: + post: + tags: [groups] + summary: Поиск групп + requestBody: + required: true + content: + application/json: + schema: { type: object } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + + /v0/groups/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [groups] + summary: Группа по id + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + put: + tags: [groups] + summary: Обновить группу + requestBody: + required: true + content: { application/json: { schema: { $ref: "#/components/schemas/GroupUpdateInput" } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + patch: + tags: [groups] + summary: Частично обновить группу + requestBody: + required: true + content: { application/json: { schema: { type: object } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + delete: + tags: [groups] + summary: Удалить группу + responses: + "204": { description: Удалена } + default: { $ref: "#/components/responses/Error" } + + /v0/positions: + get: + tags: [positions] + summary: Список должностей + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + post: + tags: [positions] + summary: Создать должность + requestBody: + required: true + content: { application/json: { schema: { $ref: "#/components/schemas/PositionCreateInput" } } } + responses: + "201": { description: Создана } + default: { $ref: "#/components/responses/Error" } + + /v0/positions/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [positions] + summary: Должность по id + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + put: + tags: [positions] + summary: Обновить должность + requestBody: + required: true + content: { application/json: { schema: { $ref: "#/components/schemas/PositionUpdateInput" } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + patch: + tags: [positions] + summary: Частично обновить должность + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + delete: + tags: [positions] + summary: Удалить должность + responses: + "204": { description: Удалена } + default: { $ref: "#/components/responses/Error" } + + /v0/departments: + get: + tags: [departments] + summary: Список подразделений + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + post: + tags: [departments] + summary: Создать подразделение + requestBody: + required: true + content: { application/json: { schema: { $ref: "#/components/schemas/DepartmentCreateInput" } } } + responses: + "201": { description: Создано } + default: { $ref: "#/components/responses/Error" } + + /v0/departments/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [departments] + summary: Подразделение по id + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + put: + tags: [departments] + summary: Обновить подразделение + requestBody: + required: true + content: { application/json: { schema: { $ref: "#/components/schemas/DepartmentUpdateInput" } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + patch: + tags: [departments] + summary: Частично обновить подразделение + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + delete: + tags: [departments] + summary: Удалить подразделение + responses: + "204": { description: Удалено } + default: { $ref: "#/components/responses/Error" } + + /v0/permissions: + get: + tags: [django-permissions] + summary: Список permission'ов + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + post: + tags: [django-permissions] + summary: Создать permission + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, codename] + properties: + name: { type: string } + codename: { type: string } + responses: + "201": { description: Создан } + default: { $ref: "#/components/responses/Error" } + + /v0/permissions/search: + post: + tags: [django-permissions] + summary: Поиск permission'ов + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + + /v0/permissions/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [django-permissions] + summary: Permission по id + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + delete: + tags: [django-permissions] + summary: Удалить permission + responses: + "204": { description: Удалён } + default: { $ref: "#/components/responses/Error" } + + /v0/resource-types: + get: + tags: [resource-types] + summary: Список типов ресурсов + responses: + "200": { description: OK } + default: { $ref: "#/components/responses/Error" } + post: + tags: [resource-types] + summary: Создать тип + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name] + properties: + name: { type: string } + parent_id: { type: integer, format: int64, nullable: true } + responses: + "201": { description: Создан } + default: { $ref: "#/components/responses/Error" } + + /v0/resource-types/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [resource-types] + summary: Тип по id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + put: + tags: [resource-types] + summary: Обновить тип + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [resource-types] + summary: Частично обновить тип + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [resource-types] + summary: Удалить тип + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v0/resources: + get: + tags: [resources-v0] + summary: Список ресурсов (v0) + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: { type: integer } + results: + type: array + items: { $ref: "#/components/schemas/ResourceV0Output" } + default: { $ref: "#/components/responses/Error" } + post: + tags: [resources-v0] + summary: Создать ресурс + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, tenant_id] + properties: + name: { type: string } + parent_id: { type: string, nullable: true } + type_id: { type: integer, format: int64 } + tenant_id: { type: integer, format: int64, minimum: 1 } + created_by: { type: integer, format: int64 } + responses: { "201": { description: Создан }, default: { $ref: "#/components/responses/Error" } } + + /v0/resources/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: string } } ] + get: + tags: [resources-v0] + summary: Ресурс по id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + put: + tags: [resources-v0] + summary: Обновить ресурс + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [resources-v0] + summary: Частично обновить ресурс + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [resources-v0] + summary: Удалить ресурс + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v0/projects: + get: + tags: [projects-v0] + summary: Список проектов + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + post: + tags: [projects-v0] + summary: Создать проект + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "201": { description: Создан }, default: { $ref: "#/components/responses/Error" } } + + /v0/projects/{publicID}: + parameters: [ { name: publicID, in: path, required: true, schema: { type: string, format: uuid } } ] + get: + tags: [projects-v0] + summary: Проект по public id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + put: + tags: [projects-v0] + summary: Обновить проект + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [projects-v0] + summary: Частично обновить проект + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [projects-v0] + summary: Удалить проект + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v0/widgets: + get: + tags: [widgets-v0] + summary: Список виджетов + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + post: + tags: [widgets-v0] + summary: Создать виджет + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "201": { description: Создан }, default: { $ref: "#/components/responses/Error" } } + + /v0/widgets/{publicID}: + parameters: [ { name: publicID, in: path, required: true, schema: { type: string, format: uuid } } ] + get: + tags: [widgets-v0] + summary: Виджет по public id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + put: + tags: [widgets-v0] + summary: Обновить виджет + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [widgets-v0] + summary: Частично обновить виджет + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [widgets-v0] + summary: Удалить виджет + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v1/resource: + get: + tags: [resources] + summary: Список ресурсов (v1) + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: parent_id, in: query, schema: { type: string, format: uuid } } + - { name: type, in: query, schema: { type: integer } } + - { name: tenant_id, in: query, description: "CSV", schema: { type: string } } + - { name: name, in: query, schema: { type: string } } + - { name: code, in: query, schema: { type: string } } + - { name: public_id, in: query, schema: { type: string, format: uuid } } + - { name: target_id, in: query, schema: { type: integer } } + - { name: service_accounts, in: query, description: "CSV из uuid", schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: { type: integer } + results: + type: array + items: { $ref: "#/components/schemas/Resource" } + default: { $ref: "#/components/responses/Error" } + post: + tags: [resources] + summary: Создать ресурс + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceCreateInput" } + responses: { "201": { description: Создан }, default: { $ref: "#/components/responses/Error" } } + + /v1/resource/{publicID}: + parameters: [ { name: publicID, in: path, required: true, schema: { type: string, format: uuid } } ] + get: + tags: [resources] + summary: Ресурс по public id + responses: + "200": + description: OK + content: { application/json: { schema: { $ref: "#/components/schemas/Resource" } } } + default: { $ref: "#/components/responses/Error" } + put: + tags: [resources] + summary: Обновить ресурс + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ResourceUpdateInput" } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [resources] + summary: Частично обновить ресурс + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [resources] + summary: Удалить ресурс + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v1/targets/{target_id}/resource: + parameters: [ { name: target_id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [resources] + summary: Ресурс по target id (только внутренний контур) + responses: + "200": + description: OK + content: { application/json: { schema: { $ref: "#/components/schemas/Resource" } } } + default: { $ref: "#/components/responses/Error" } + + /v1/resources-grouped-by-sa: + get: + tags: [resources] + summary: Ресурсы, сгруппированные по сервисному аккаунту + description: Регистрируется, только если включён соответствующий use case. + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + + /v1/service-accounts: + get: + tags: [service-accounts] + summary: Список сервисных аккаунтов + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: { type: integer } + results: + type: array + items: { type: string, format: uuid } + default: { $ref: "#/components/responses/Error" } + + /v2/resource: + get: + tags: [widgets] + summary: Список виджетов (v2) + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + post: + tags: [widgets] + summary: Создать виджет + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/WidgetCreateInput" } } } } + responses: { "201": { description: Создан }, default: { $ref: "#/components/responses/Error" } } + + /v2/resource/{publicID}: + parameters: [ { name: publicID, in: path, required: true, schema: { type: string, format: uuid } } ] + get: + tags: [widgets] + summary: Виджет по public id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + put: + tags: [widgets] + summary: Обновить виджет + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + patch: + tags: [widgets] + summary: Частично обновить виджет + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [widgets] + summary: Удалить виджет + responses: { "204": { description: Удалён }, default: { $ref: "#/components/responses/Error" } } + + /v1/resource_permission: + get: + tags: [resource-permissions] + summary: Список прав на ресурсы + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + post: + tags: [resource-permissions] + summary: Создать права (пакет) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [items] + properties: + items: + type: array + items: { $ref: "#/components/schemas/ResourcePermissionCreateInput" } + responses: { "201": { description: Создано }, default: { $ref: "#/components/responses/Error" } } + + /v1/resource_permission/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [resource-permissions] + summary: Право по id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [resource-permissions] + summary: Удалить право + responses: { "204": { description: Удалено }, default: { $ref: "#/components/responses/Error" } } + + /v1/permissions-bulk: + patch: + tags: [resource-permissions] + summary: Массовое изменение прав + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [tenant_id, permissions] + properties: + tenant_id: { type: integer, format: int64, minimum: 1 } + permissions: + type: object + description: "map[resource_uuid][]permission_uuid" + additionalProperties: + type: array + items: { type: string, format: uuid } + unrestricted_permissions: + type: object + additionalProperties: true + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + + /v1/company_resource_permission: + get: + tags: [company-resource-permissions] + summary: Список прав компании + parameters: [ { $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/Offset" } ] + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + post: + tags: [company-resource-permissions] + summary: Создать права компании (пакет) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [items] + properties: + items: + type: array + items: { $ref: "#/components/schemas/CompanyResourcePermissionCreateInput" } + responses: { "201": { description: Создано }, default: { $ref: "#/components/responses/Error" } } + + /v1/company_resource_permission/{id}: + parameters: [ { name: id, in: path, required: true, schema: { type: integer, format: int64 } } ] + get: + tags: [company-resource-permissions] + summary: Право компании по id + responses: { "200": { description: OK }, default: { $ref: "#/components/responses/Error" } } + delete: + tags: [company-resource-permissions] + summary: Удалить право компании + responses: { "204": { description: Удалено }, default: { $ref: "#/components/responses/Error" } } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + `Authorization: Bearer `. Требуется во внешнем контуре + (`/external/api/*`). Дополнительный заголовок `Identity` переключает + разбор в режим Zitadel. Подпись не проверяется приложением. + + parameters: + Limit: + name: limit + in: query + description: Размер страницы (по умолчанию 1000) + schema: { type: integer, default: 1000, minimum: 1 } + Offset: + name: offset + in: query + schema: { type: integer, default: 0, minimum: 0 } + + responses: + Error: + description: Ошибка + content: + application/json: + schema: { $ref: "#/components/schemas/ApiError" } + + schemas: + ApiError: + type: object + properties: + error: { type: string, description: Человекочитаемое сообщение } + id: { type: string, description: Стабильный slug доменной ошибки } + + User: + type: object + properties: + id: { type: integer, format: int64 } + username: { type: string } + email: { type: string, format: email } + first_name: { type: string } + last_name: { type: string } + is_staff: { type: boolean } + is_active: { type: boolean } + is_superuser: { type: boolean } + job_title: { type: string } + companies: + type: array + items: { type: integer, format: int64 } + + UserCreateInput: + type: object + required: [username, email, first_name, last_name] + properties: + username: { type: string } + email: { type: string, format: email } + first_name: { type: string } + last_name: { type: string } + is_staff: { type: boolean } + is_active: { type: boolean } + is_superuser: { type: boolean } + job_title: { type: string } + companies: + type: array + items: { type: integer, format: int64 } + departments: + type: array + items: { type: integer, format: int64 } + groups: + type: array + items: { type: integer, format: int64 } + enable_notifications: { type: boolean } + + UserUpdateInput: + type: object + required: [username, email] + properties: + username: { type: string } + email: { type: string, format: email } + first_name: { type: string } + last_name: { type: string } + is_staff: { type: boolean } + is_active: { type: boolean } + is_superuser: { type: boolean } + job_title: { type: string } + companies: + type: array + items: { type: integer, format: int64 } + + UserBulkPatchItem: + type: object + required: [id] + properties: + id: { type: integer, format: int64 } + username: { type: string } + email: { type: string, format: email } + first_name: { type: string } + last_name: { type: string } + is_staff: { type: boolean } + is_active: { type: boolean } + is_superuser: { type: boolean } + job_title: { type: string } + companies: { type: array, items: { type: integer, format: int64 } } + departments: { type: array, items: { type: integer, format: int64 } } + groups: { type: array, items: { type: integer, format: int64 } } + enable_notifications: { type: boolean } + + CompanyUserActivationItem: + type: object + required: [user_id, company_id] + properties: + user_id: { type: integer, format: int64, minimum: 1 } + company_id: { type: integer, format: int64, minimum: 1 } + is_active: { type: boolean } + + UserPermissionCheckInput: + type: object + required: [user_id, checks] + properties: + user_id: { type: integer, format: int64, minimum: 1 } + tenant_id: { type: integer, format: int64 } + checks: + type: array + minItems: 1 + maxItems: 100 + items: + type: object + required: [permission] + properties: + permission: { type: string } + public_resource_id: { type: string, format: uuid, nullable: true } + + UserPermissionCheckOutput: + type: object + properties: + results: + type: array + items: + type: object + properties: + permission: { type: string } + public_resource_id: { type: string, format: uuid, nullable: true } + allowed: { type: boolean } + reason: { type: string } + + GroupCreateInput: + type: object + required: [name] + properties: + name: { type: string } + description: { type: string } + is_public: { type: boolean } + company_id: { type: integer, format: int64, nullable: true } + permission_ids: { type: array, items: { type: integer, format: int64 } } + + GroupUpdateInput: + allOf: + - $ref: "#/components/schemas/GroupCreateInput" + - type: object + required: [id] + properties: + id: { type: integer, format: int64 } + + PositionCreateInput: + type: object + required: [name, company_id] + properties: + name: { type: string } + description: { type: string } + company_id: { type: integer, format: int64, minimum: 1 } + users: { type: array, items: { type: integer, format: int64 } } + groups: { type: array, items: { type: integer, format: int64 } } + + PositionUpdateInput: + allOf: + - $ref: "#/components/schemas/PositionCreateInput" + - type: object + required: [id] + properties: + id: { type: integer, format: int64 } + + DepartmentCreateInput: + type: object + required: [name, company_id] + properties: + name: { type: string } + description: { type: string } + company_id: { type: integer, format: int64, minimum: 1 } + legal_entity: { type: string } + contractor: { type: object, nullable: true } + users: { type: array, items: { type: integer, format: int64 } } + groups: { type: array, items: { type: integer, format: int64 } } + + DepartmentUpdateInput: + allOf: + - $ref: "#/components/schemas/DepartmentCreateInput" + - type: object + required: [id] + properties: + id: { type: integer, format: int64 } + + Resource: + type: object + properties: + public_id: { type: string, format: uuid } + parent_id: { type: string, format: uuid, nullable: true } + type_id: { type: integer, format: int64 } + tenant_id: { type: integer, format: int64 } + name: { type: string } + description: { type: string } + code: { type: string } + target_id: { type: integer, format: int64 } + show_in_overview: { type: boolean } + location_verbose: { type: string } + latitude: { type: number, format: double } + longitude: { type: number, format: double } + widgets: { type: object, additionalProperties: true } + attributes: { type: object, additionalProperties: true } + meta: { type: object, additionalProperties: true } + + ResourceCreateInput: + type: object + required: [name, tenant_id] + properties: + parent_id: { type: string, format: uuid, nullable: true, description: "public_id родителя" } + name: { type: string } + type_id: { type: integer, format: int64, description: "0/опущено — тип «Проект»" } + tenant_id: { type: integer, format: int64, minimum: 1 } + created_by: { type: integer, format: int64 } + target_id: { type: integer, format: int64 } + code: { type: string } + + ResourceUpdateInput: + type: object + required: [name, tenant_id] + properties: + parent_id: { type: string, format: uuid, nullable: true } + type_id: { type: integer, format: int64 } + tenant_id: { type: integer, format: int64, minimum: 1 } + name: { type: string } + description: { type: string } + code: { type: string } + target_id: { type: integer, format: int64 } + planning_widget_id: { type: integer, format: int64, nullable: true } + work_schedule_project_id: { type: integer, format: int64, nullable: true } + show_in_overview: { type: boolean } + location_verbose: { type: string } + latitude: { type: number, format: double, minimum: -90, maximum: 90 } + longitude: { type: number, format: double, minimum: -180, maximum: 180 } + widgets: { type: object, additionalProperties: true } + attributes: { type: object, additionalProperties: true } + meta: { type: object, additionalProperties: true } + + ResourceV0Output: + type: object + properties: + id: { type: string } + public_id: { type: string } + parent_id: { type: string, nullable: true } + path: { type: string } + type_id: { type: integer, format: int64 } + tenant_id: { type: integer, format: int64 } + name: { type: string } + created_by: { type: integer, format: int64 } + created_at: { type: string } + updated_at: { type: string } + + WidgetCreateInput: + type: object + required: [name, tenant_id] + properties: + parent_id: { type: string, format: uuid, nullable: true } + type_id: { type: integer, format: int64 } + tenant_id: { type: integer, format: int64, minimum: 1 } + name: { type: string } + description: { type: string } + code: { type: string } + target_id: { type: integer, format: int64 } + show_in_overview: { type: boolean } + latitude: { type: number, format: double, minimum: -90, maximum: 90 } + longitude: { type: number, format: double, minimum: -180, maximum: 180 } + coordinate_system: { type: integer, format: int64 } + widgets: { type: object, additionalProperties: true } + attributes: { type: object, additionalProperties: true } + meta: { type: object, additionalProperties: true } + + ResourcePermissionCreateInput: + type: object + required: [public_resource_id, service_account] + properties: + public_resource_id: { type: string, format: uuid } + type: { type: integer, format: int64, minimum: 0 } + service_account: { type: string, format: uuid } + created_by: { type: integer, format: int64 } + + CompanyResourcePermissionCreateInput: + type: object + required: [tenant_id, service_account] + properties: + tenant_id: { type: integer, format: int64, minimum: 1 } + service_account: { type: string, format: uuid } + created_by: { type: integer, format: int64 } + +security: + - bearerAuth: [] diff --git a/apps/inspections/.env.example b/apps/inspections/.env.example new file mode 100644 index 0000000..0c2abd3 --- /dev/null +++ b/apps/inspections/.env.example @@ -0,0 +1,65 @@ +# Общие настройки процесса +PYTHONPATH=src +PICCOLO_CONF=db.config + +# App +DEBUG=true +SERVICE_URL=https://stage.sarex.io + +# HTTP App +HTTP_APP_HOST=0.0.0.0 +HTTP_APP_PORT=8000 +HTTP_APP_ROOT_PATH="" +HTTP_APP_WORKERS=1 +HTTP_APP_ADMIN_ENABLE=true + +# Database +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres + +# Kafka +KAFKA_HOST=host +KAFKA_USERNAME=username +KAFKA_PASSWORD=password +# Если задан KAFKA_SSL_CERT (для http-приложения) или KAFKA_SSL_CAFILE (для kafka-consumer) — +# подключение идёт по SASL-SCRAM-SHA-512 поверх TLS, иначе без SSL. +KAFKA_SSL_CERT="" +KAFKA_SSL_CAFILE=ssl_cafile +KAFKA_EAV_ASSETS_TOPIC=eav_assets_topic + +# OpenTelemetry +OTEL_ENABLE=false +OTEL_URL=http://signoz-otel-collector-external.signoz.svc.cluster.local:4317 +OTEL_SERVICE_NAME=inspections-backend.inspections-stage +OTEL_INSECURE=true + +# Auth (JWT) +# Если false — middleware аутентификации не подключается, используется дефолтный пользователь. +JWT_AUTH_ENABLE=false + +# Notifications +NOTIFICATIONS_ENABLE=false +NOTIFICATIONS_EMAIL_FROM=hello@sarex.io + +# Sarex backend +SAREX_BACKEND_URL=https://stage.sarex.io +SAREX_BACKEND_TIMEOUT=30 +# base64(login:password) +SAREX_BACKEND_AUTH=base64(login:password) + +# EAV (сервис атрибутов) +EAV_URL=https://stage-api.sarex.io/eav +EAV_TIMEOUT=30 + +# Workflows (процессы / отправка email) +WORKFLOWS_URL=http://workflows-api-service.processing-stage +WORKFLOWS_TIMEOUT=30 +WORKFLOWS_EMAIL_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/notification:email + +# Мобильное приложение (версии) +MOBILE_APP_CURRENT_VERSION=1.0.0 +MOBILE_APP_RECOMMENDED_VERSION=1.0.0 +MOBILE_APP_REQUIRED_VERSION=1.0.0 diff --git a/apps/inspections/CONFIGURATION.md b/apps/inspections/CONFIGURATION.md new file mode 100644 index 0000000..c9049eb --- /dev/null +++ b/apps/inspections/CONFIGURATION.md @@ -0,0 +1,216 @@ +# Конфигурация проекта inspections-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `Config` и вложенные `*Config`). + +Особенности разбора: + +- у приложения **нет единого общего префикса** — каждая секция задаётся собственным `env_prefix` в своём `SettingsConfigDict` (напр. `HTTP_APP_`, `DATABASE_`, `KAFKA_`, `OTEL_`, `JWT_AUTH_`, `NOTIFICATIONS_`, `SAREX_BACKEND_`, `EAV_`, `WORKFLOWS_`, `MOBILE_APP_`); +- две переменные верхнего уровня (`DEBUG`, `SERVICE_URL`) читаются без префикса; +- вложенности через разделитель нет — плоские имена вида ``, напр. `DATABASE_HOST` → `database.host`; +- почти у всех полей есть значения по умолчанию, поэтому отсутствие переменной обычно не приводит к ошибке старта — берётся дефолт из кода. В таблицах ниже приведены дефолты из `src/config.py`. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Часовой пояс приложения зафиксирован в коде: `app_timezone = ZoneInfo("Europe/Moscow")`. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (uv) | `Makefile` через `SET_ENV` делает `set -a; source .env; set +a` перед запуском команд. Шаблон переменных — `.env.template` (в репозитории; `.env*` в `.gitignore`, кроме `.env.template`) | +| Docker | `docker/http/Dockerfile` и `docker/kafka/Dockerfile` фиксируют `PYTHONPATH=src` и `PICCOLO_CONF=db.config`; прикладные переменные пробрасываются рантаймом (k8s) | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `api` и `kafka-app` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`), сборка образа и деплой универсального чарта | + +Способы запуска процессов: + +| Команда (`Makefile`) | Точка входа | Назначение | +| --- | --- | --- | +| `make run` | `src/cmd/http/main.py` | HTTP API (uvicorn, `app.http:create_app`, `factory=True`) | +| — (kafka-consumer) | `src/cmd/kafka/main.py` | Обработчик Kafka-событий (FastStream) | +| `make migrate` | `piccolo migrations forwards all` | Применение миграций Piccolo | +| `make migrations` | `piccolo migrations new inspections --auto` | Генерация новой миграции | +| `make sync_eav` | `src/cmd/scripts/sync_eav.py` | Разовая синхронизация EAV | +| `make playground` | `piccolo playground run` | Локальный playground Piccolo (sqlite) | +| `make format` / `make format-check` | `ruff` | Форматирование/проверка кода | + +Порядок запуска в контейнере HTTP (`docker/http/entrypoint.sh`): сначала выполняются миграции (`piccolo migrations forwards all`), затем стартует `src/cmd/http/main.py`. Контейнер kafka-consumer (`docker/kafka/entrypoint.sh`) запускает только `src/cmd/kafka/main.py` (миграции не применяет). + +## Переменные приложения + +В столбце «Переменная» указано полное имя (префикс + поле). Дефолт `—` означает, что значения по умолчанию у поля нет. + +### App (верхний уровень) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DEBUG` | bool | `True` | Режим отладки: логирование SQL-запросов Piccolo, `reload=True` у uvicorn, непродакшн-режим админки | +| `SERVICE_URL` | string | `https://stage.sarex.io` | Внешний базовый URL Sarex, используется при формировании ссылок (уведомления, экспорт) | + +### HTTP App (`HTTP_APP_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_APP_HOST` | string | `0.0.0.0` | Адрес прослушивания | +| `HTTP_APP_PORT` | int | `8000` | Порт | +| `HTTP_APP_ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси, напр. `/inspections`) | +| `HTTP_APP_WORKERS` | int | `1` | Число воркеров uvicorn | +| `HTTP_APP_ADMIN_ENABLE` | bool | `True` | Монтировать ли Piccolo-admin по пути `/admin/` | + +### Database (`DATABASE_*`) + +PostgreSQL через Piccolo (`PostgresEngine`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_HOST` | string | `postgres` | Хост PostgreSQL | +| `DATABASE_PORT` | int | `5432` | Порт PostgreSQL | +| `DATABASE_NAME` | string | `postgres` | Имя базы данных | +| `DATABASE_USER` | string | `postgres` | Пользователь БД | +| `DATABASE_PASSWORD` | string | `postgres` | Пароль пользователя БД | + +### Kafka (`KAFKA_*`) + +Используется FastStream (`KafkaBroker`). Продюсер — в HTTP-приложении (публикация событий инспекций), консьюмер — отдельный процесс (`kafka-app`), подписан на топик EAV-ассетов. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_HOST` | string | `""` | Bootstrap-сервер (`bootstrap_servers=[host]`) | +| `KAFKA_USERNAME` | string | `""` | Пользователь (SASL) | +| `KAFKA_PASSWORD` | string | `""` | Пароль (SASL) | +| `KAFKA_SSL_CERT` | string | `""` | CA-сертификат как строка (`cadata`). Если задан — HTTP-приложение подключается по `SASLScram512` поверх TLS, иначе без SSL | +| `KAFKA_SSL_CAFILE` | string | `""` | Путь к CA-файлу (`cafile`). Если задан — kafka-consumer подключается по `SASLScram512` поверх TLS, иначе без SSL | +| `KAFKA_EAV_ASSETS_TOPIC` | string | `""` | Топик событий EAV-ассетов, на который подписан consumer | + +> Механизм безопасности отличается между процессами: HTTP-приложение (`app/http.py`) смотрит на `KAFKA_SSL_CERT` (строка сертификата), а kafka-consumer (`app/kafka.py`) — на `KAFKA_SSL_CAFILE` (путь к файлу). + +### OpenTelemetry (`OTEL_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `OTEL_ENABLE` | bool | `False` | Включить трейсинг/структурированное логирование. При `True` отключается `access_log` uvicorn | +| `OTEL_URL` | string | `http://signoz-otel-collector-external.signoz.svc.cluster.local:4317` | Адрес OTLP-коллектора | +| `OTEL_SERVICE_NAME` | string | `inspections-backend.inspections-stage` | Имя сервиса в трейсах | +| `OTEL_INSECURE` | bool | `True` | Небезопасное (без TLS) подключение к коллектору | + +### Auth (`JWT_AUTH_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_AUTH_ENABLE` | bool | `False` | Подключать ли middleware `TokenUserMiddleware`. При `False` используется дефолтный пользователь из `entity/context.py` (для локальной разработки) | + +> Middleware декодирует JWT **без проверки подписи** (`verify_signature: False`). При наличии заголовка `identity` полезная нагрузка берётся из него (метаданные Zitadel), иначе — из `authorization`. Роуты `/docs/`, `/openapi.json/`, `/admin/`, `/internal/` исключены из проверки. + +### Notifications (`NOTIFICATIONS_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `NOTIFICATIONS_ENABLE` | bool | `False` | Включить отправку уведомлений об инспекциях | +| `NOTIFICATIONS_EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя писем | + +### Sarex backend (`SAREX_BACKEND_*`) + +HTTP-клиент основного бэкенда Sarex (данные пользователей и т.п.). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SAREX_BACKEND_URL` | string | `https://stage.sarex.io` | Базовый URL | +| `SAREX_BACKEND_TIMEOUT` | int | `30` | Таймаут запроса (сек) | +| `SAREX_BACKEND_AUTH` | string (base64) | `base64(login:password)` | Basic-auth в виде base64(`login:password`) | + +### EAV (`EAV_*`) + +HTTP-клиент сервиса атрибутов (EAV). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `EAV_URL` | string | `https://stage-api.sarex.io/eav` | Базовый URL | +| `EAV_TIMEOUT` | int | `30` | Таймаут запроса (сек) | + +### Workflows (`WORKFLOWS_*`) + +HTTP-клиент сервиса процессов; используется, в т.ч. для запуска отправки email. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOWS_URL` | string | `http://workflows-api-service.processing-stage` | Базовый URL | +| `WORKFLOWS_TIMEOUT` | int | `30` | Таймаут запроса (сек) | +| `WORKFLOWS_EMAIL_DOCKER_IMAGE` | string | `cr.yandex/crp3ccidau046kdj8g9q/notification:email` | Docker-образ шага отправки email | + +### Mobile App (`MOBILE_APP_*`) + +Значения отдаются эндпоинтом `GET /api/v1/mobile-app/version/`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MOBILE_APP_CURRENT_VERSION` | string | `""` | Текущая версия | +| `MOBILE_APP_RECOMMENDED_VERSION` | string | `""` | Рекомендуемая версия | +| `MOBILE_APP_REQUIRED_VERSION` | string | `""` | Минимально требуемая версия | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения через `pydantic-settings`, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `PYTHONPATH=src` | `.env.template`, `Dockerfile` | Корень пакета приложения | +| `PICCOLO_CONF=db.config` | `.env.template`, `Dockerfile` | Модуль конфигурации Piccolo (`APP_REGISTRY`, `DB`, `ADMIN_ASGI_APP`) | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — обёртка над зависимостью `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`). Описаны два сервиса: `api` (HTTP) и `kafka-app` (consumer), у каждого свои блоки `envs` и `secretEnvs`. Значения различаются по окружениям через ключи `_default`/`stage`/`preprod`/`production`. + +Обычные значения (`envs`) содержат те же переменные приложения, что и выше (различаются `SERVICE_URL`, `EAV_URL`, `WORKFLOWS_URL`, `OTEL_*`, `KAFKA_EAV_ASSETS_TOPIC`, `HTTP_APP_ROOT_PATH=/inspections`, `HTTP_APP_WORKERS=3`, `JWT_AUTH_ENABLE=true`, `NOTIFICATIONS_ENABLE` и т.п.). Отличия от дефолтов кода: в проде `DEBUG=false`, `OTEL_ENABLE=true`, `JWT_AUTH_ENABLE=true`. + +Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default` / `stage`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `DATABASE_USER` | `ya-pg-secret` / `inspections-postgresql-secret` | `username` | +| `DATABASE_PORT` | `ya-pg-secret` / `inspections-postgresql-secret` | `port` | +| `DATABASE_NAME` | `ya-pg-secret` / `inspections-postgresql-secret` | `database` | +| `DATABASE_HOST` | `ya-pg-secret` / `inspections-postgresql-secret` | `host` | +| `DATABASE_PASSWORD` | `ya-pg-secret` / `inspections-postgresql-secret` | `password` | +| `KAFKA_HOST` | `yc-kafka-secret` / `inspections-kafka-secret` | `host` | +| `KAFKA_USERNAME` | `yc-kafka-secret` / `inspections-kafka-secret` | `username` | +| `KAFKA_PASSWORD` | `yc-kafka-secret` / `inspections-kafka-secret` | `password` | +| `KAFKA_SSL_CERT` | `inspections-kafka-secret` | `cert` | +| `SAREX_BACKEND_AUTH` | `sarex-backend-auth-secret` | `key` | + +Помимо env, у сервиса `kafka-app` смонтирован CA-сертификат Yandex как файл `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt` (секрет `yc-ch-certificate`, ключ `certificate`) — на него указывает `KAFKA_SSL_CAFILE`. + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, probes), `image.*`, `service.*`, `imagePullSecrets`, у `kafka-app` — `command` (`python src/cmd/kafka/main.py`) и `volumes`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | Release / Chart | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `proc` | `inspections-backend` | +| ветка `master` | `preprod` | `inspections-preprod` | `inspections-backend` | +| тег (`CI_COMMIT_TAG`) | `production` | `inspections-prod` | `sarex-inspections` | +| `merge_request_event` | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные пайплайна: `SERVICE_NAME=inspections-backend`, `DOCKERFILE_PATH=./docker/http/Dockerfile`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (проброс `image.name`, `global.env`, метаданных коммита для сервисов `api` и `kafka-app`). Job `lint` прогоняет `ruff check` и `ruff format --check` на образе `uv 0.7.13 / python3.13`. + +## Замечания + +- Приложение **не загружает `.env` автоматически** — переменные экспортируются в окружение (в `Makefile` — через `set -a; source .env; set +a`, в k8s — через env-блоки чарта). +- Аутентификация в middleware декодирует JWT **без проверки подписи**; безопасность обеспечивается сетевым слоем/ingress. При `JWT_AUTH_ENABLE=false` активен захардкоженный дефолтный пользователь (`entity/context.py`), пригодный только для локальной разработки. +- Большинство полей конфигурации имеют дефолты — при отсутствии переменной сервис стартует со значением из кода. Для реального окружения значения задаются в `.helm/values.yaml`. +- Для экспорта поддерживается единственный формат — `xlsx` (`InspectionExportType`). + +## Минимальный набор для локального запуска + +Скопировать `.env.template` в `.env`, поднять PostgreSQL и (при необходимости обработки событий) Kafka, применить миграции (`make migrate`) и запустить API (`make run`). Минимально стоит задать: + +- `DEBUG`, `SERVICE_URL` +- `HTTP_APP_HOST`, `HTTP_APP_PORT` +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `JWT_AUTH_ENABLE=false` (для дебага без токена) +- `KAFKA_*` (если нужен consumer/публикация событий) +- `SAREX_BACKEND_*`, `EAV_*`, `WORKFLOWS_*` (для интеграций) +- `OTEL_ENABLE=false` локально diff --git a/apps/inspections/ENDPOINTS.md b/apps/inspections/ENDPOINTS.md new file mode 100644 index 0000000..56d0fc4 --- /dev/null +++ b/apps/inspections/ENDPOINTS.md @@ -0,0 +1,115 @@ +# Эндпоинты, с которыми взаимодействует inspections-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `inspections-frontend`). + +## Как устроено взаимодействие + +Запросы выполняются через общий `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). У сервиса есть методы `getRequest`, `postRequest`, `patchRequest`, `deleteRequest`, каждый из которых принимает объект с полями: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (дописывается к базовому хосту сервиса); +- `data` — тело запроса (для POST/PATCH); +- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer` и т.п.); +- `showErrorNotification` — показывать ли уведомление об ошибке. + +Базовый хост подставляется по паре «`service` + окружение». Окружение определяется глобальной переменной сборки `BUILD_ENV` (`local`/`stage`/`preprod`/`prod`/`contour`); при её отсутствии используется `prod` (`const buildEnv = BUILD_ENV ?? "prod"`). В режиме `local` для http-сервиса выставляется `type: "original"`. `BUILD_ENV` задаётся при сборке (напр. `BUILD_ENV=stage npm start`) и прокидывается через webpack DefinePlugin. + +Определения запросов сгруппированы по файлам в `module/api/` (`inspections.ts`, `assets.ts`, `Issues.ts`, `premises-api.ts`) и по стор-файлам в `module/Inspections/store/` (`inspections.ts`, `filters.ts`, `resource.ts`, `calendar.ts`, `issuesStore.ts`). + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url` запроса. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `inspections` | Сервис событий/инспекций (этот бэкенд) | `https://stage-api.sarex.io/inspections/api/v1` | `https://api.sarex.io/inspections/api/v1` | +| `sarexApi` | Gateway/API Sarex (`/gateway`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` | +| `eavV0` | Сервис атрибутов EAV, API v0 | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` | +| `eavV4` | Сервис атрибутов EAV, API v4 (ассеты) | `https://stage-api.sarex.io/eav/api/v4` | `https://api.sarex.io/eav/api/v4` | +| `issues` | Сервис замечаний | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` | +| `premises` | Сервис помещений | `https://stage-api.sarex.io/premises/api/v1` | `https://api.sarex.io/premises/api/v1` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local`, `preprod` и `contour`. В `local` сервис `sarex` проксируется на `/sarex-backend`, а `inspections`/`eav`/`issues`/`premises` указывают на стейдж. В `contour` все хосты пустые (относительные пути для изолированного контура). Сервис `zitadel` в hosts объявлен, но прямых запросов из модуля к нему нет — аутентификация обрабатывается на уровне SDK/платформы. + +## Эндпоинты по сервисам + +### `inspections` — Сервис событий/инспекций + +Базовый хост уже включает `/api/v1`, поэтому в путях ниже он не повторяется. + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| GET | `/inspections/` | `store/resource.ts` | Создание/список событий | +| POST | `/inspections/` | `store/resource.ts` | Создать событие | +| POST | `/inspections/filter/` | `store/resource.ts`, `store/calendar.ts` | Список событий с фильтрами и пагинацией в теле | +| GET | `/inspections/{id}/` | `api/inspections.ts`, `store/resource.ts` | Событие по id | +| PATCH | `/inspections/{id}/` | `api/inspections.ts` | Частичное обновление события | +| GET | `/inspections/types/` | `api/inspections.ts` | Типы событий компании (query `company_id`) | +| POST | `/inspections/status-count/` | `store/inspections.ts` | Счётчики по статусам (фильтры в теле) | +| POST | `/inspections/filter-options/` | `store/filters.ts` | Доступные значения фильтров | +| GET | `/inspections/aggregate/created_at/minmax/` | `store/filters.ts` | Мин/макс по дате создания | +| GET | `/inspections/aggregate/inspection_dt/minmax/` | `store/filters.ts` | Мин/макс по дате проведения | +| GET | `/inspections/change-history/` | `api/inspections.ts` | История изменений (пагинация, фильтры в query) | +| GET | `/inspections/export/` | `store/inspections.ts` | Экспорт событий (xlsx) | +| POST | `/inspections/unavailable-dates/` | `api/inspections.ts` | Недоступные даты для исполнителей | +| POST | `/inspections/unavailable-users/` | `api/inspections.ts` | Недоступные исполнители на интервал | + +### `sarexApi` — Gateway/API Sarex + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| GET | `/gateway/api/v1/resources/?company_id={companyId}` | `store/inspections.ts` | Ресурсы (проекты) компании | +| GET | `/gateway/api/v2/users/` | `store/issuesStore.ts`, `store/resource.ts` | Пользователи (с пагинацией/фильтрами) | +| GET | `/gateway/api/v1/attachments/?company_id={companyId}&instance_id={id}&model_name=inspection` | `store/resource.ts` | Вложения события | +| POST | `/gateway/api/v1/attachments/` | `store/resource.ts` | Создать вложение | +| DELETE | `/gateway/api/v1/attachments/{id}` | `store/resource.ts` | Удалить вложение | + +### `sarex` — Локальный сервис данных + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| GET | `/api/client/settings/` | `store/inspections.ts` | Клиентские настройки | +| GET | `/api/core/users/` | `store/inspections.ts` | Пользователи | +| GET | `/api/core/admin/departments/?company={companyId}` | `store/inspections.ts` | Отделы компании | +| GET | `/api/core/admin/positions/?company={companyId}` | `store/inspections.ts` | Должности компании | + +### `eavV4` — Сервис атрибутов EAV (ассеты) + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| POST | `/assets/search/` | `api/assets.ts` | Поиск ассетов по списку id | +| GET | `/assets/` | `api/assets.ts` | Список ассетов (фильтры/пагинация/теги в query) | + +> Теги ассетов (`EnumAssetTags`): `location`, `project_structure`, `events.can_be_selected`, `remarks.can_be_selected`. + +### `eavV0` — Сервис атрибутов EAV (атрибуты) + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| GET | `/attribute/?company_id={companyId}` | `store/inspections.ts` | Атрибуты компании | + +### `issues` — Сервис замечаний + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| POST | `/issues/` | `api/Issues.ts` | Создать замечание | +| GET | `/issues/` | `api/Issues.ts` | Список замечаний (фильтры по компании/ресурсу/событию/типам) | +| POST | `/attachments/` | `api/Issues.ts`, `store/issuesStore.ts` | Прикрепить медиа к замечанию | +| GET | `/companies/{companyId}/status-model/` | `api/Issues.ts` | Статусная модель замечаний компании | +| GET | `/issue-types/` | `api/Issues.ts` | Типы замечаний компании | + +### `premises` — Сервис помещений + +Базовый хост уже включает `/api/v1`. + +| Метод | Путь | Где вызывается | Назначение | +| --- | --- | --- | --- | +| GET | `/premises/{id}/` | `api/premises-api.ts` | Помещение по id | +| POST | `/premises/filter/` | `api/premises-api.ts` | Помещения по фильтру (пагинация в query) | +| POST | `/premise_types/filter/` | `api/premises-api.ts` | Типы помещений по фильтру (пагинация в query) | + +## Обработка ошибок + +Показ уведомлений об ошибках управляется флагом `showErrorNotification` в параметрах запроса (включается точечно для части запросов). Для сериализации query-параметров-массивов местами используется `query-string` с `arrayFormat: "comma"` (напр. в `api/Issues.ts`). Часть запросов в `api/assets.ts` оборачивает ошибку в `throw new Error(...)`. diff --git a/apps/inspections/openapi.yaml b/apps/inspections/openapi.yaml new file mode 100644 index 0000000..613ab51 --- /dev/null +++ b/apps/inspections/openapi.yaml @@ -0,0 +1,1243 @@ +openapi: 3.0.3 + +info: + title: Inspections + version: "0.1.0" + description: | + REST API сервиса **inspections-backend** (`proc/inspections-backend`) — управление + событиями/инспекциями (создание, редактирование, поиск, экспорт), их типами, + статусами, атрибутами, историей изменений и расчётом доступности исполнителей. + + Сервис написан на Python (**FastAPI** + **Piccolo ORM**, PostgreSQL). Приложение + собирается фабрикой `create_app` в `src/app/http.py`. Помимо HTTP-приложения есть + отдельный процесс-консьюмер Kafka (`src/app/kafka.py`), который слушает события + EAV-ассетов; в OpenAPI он не отражён. + + Роутинг: корневой роутер имеет префикс `/api` (`controller/http/api/router.py`), + вложенный — `/v1` (`controller/http/api/v1/router.py`), ресурс инспекций — + `/inspections` (`controller/http/api/v1/inspection.py`). За реверс-прокси + добавляется `root_path` (в k8s — `/inspections`, задаётся `HTTP_APP_ROOT_PATH`). + + Схема OpenAPI отдаётся по `/openapi.json/`, документация ReDoc — по `/docs/` + (Swagger UI отключён). Админ-панель Piccolo монтируется по `/admin/` + (если `HTTP_APP_ADMIN_ENABLE=true`). + + ### Аутентификация + Если включён middleware (`JWT_AUTH_ENABLE=true`), все запросы, кроме `/docs/`, + `/openapi.json/`, `/admin/`, `/internal/`, требуют заголовок + `Authorization: Bearer `. Поддерживается дополнительный заголовок + `identity` (метаданные Zitadel): при его наличии полезная нагрузка берётся из + него, иначе — из `Authorization`. Токен декодируется **без проверки подписи** + (`verify_signature=False`) — доверие обеспечивается сетевым слоем/ingress. + При отсутствии заголовка `Authorization` middleware возвращает `401`. + + При `JWT_AUTH_ENABLE=false` middleware не подключается и используется + захардкоженный дефолтный пользователь (`entity/context.py`) — режим локальной + разработки. + + ### Права доступа + Операции защищены правами (`InspectionPermission`): `core.can_view_inspections`, + `core.can_create_inspection`, `core.can_edit_inspection`, + `core.can_delete_inspection`, `core.can_view_all_inspections`. Права берутся из + токена; при их отсутствии проверяются права по сервисным аккаунтам на конкретный + тип события. Недостаток прав — ответ `403`. + + ### Пагинация + Списочные ответы используют limit/offset-пагинацию и оборачиваются в `Page` + (`{ count, result }`). Сортировка задаётся параметрами `order_by` и `ascending`. + + ### Обработка ошибок + Доменные ошибки маппятся на HTTP-статусы (`controller/http/errors.py`) и + возвращаются как `{ "detail": "<текст>" }`. Ошибки валидации тела/параметров + (Pydantic) отдаются FastAPI в стандартном формате `422`. + +servers: + - url: https://api.sarex.io/inspections + description: production + - url: https://api.preprod.sarex.io/inspections + description: preprod + - url: https://stage-api.sarex.io/inspections + description: stage + - url: http://localhost:8000 + description: local (без root_path) + +tags: + - name: Inspections + description: События / инспекции + - name: Mobile app + description: Версии мобильного приложения + +security: + - bearerAuth: [] + +paths: + /api/v1/mobile-app/version/: + get: + tags: [Mobile app] + summary: Версии мобильного приложения + operationId: get_mobile_app_version + responses: + "200": + description: Текущая, рекомендуемая и минимально требуемая версии + content: + application/json: + schema: + $ref: "#/components/schemas/MobileAppVersion" + "401": + $ref: "#/components/responses/Unauthorized" + + /api/v1/inspections/: + get: + tags: [Inspections] + summary: Список событий + description: Требует право `core.can_view_inspections`. + operationId: get_all + parameters: + - $ref: "#/components/parameters/OrderByInspection" + - $ref: "#/components/parameters/Ascending" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - $ref: "#/components/parameters/Search" + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + - $ref: "#/components/parameters/TypeIdList" + - $ref: "#/components/parameters/StatusIdList" + responses: + "200": + description: Страница событий + content: + application/json: + schema: + $ref: "#/components/schemas/PageInspectionRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + post: + tags: [Inspections] + summary: Создать событие + description: Требует право `core.can_create_inspection`. + operationId: create + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionCreate" + responses: + "201": + description: Созданное событие + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionRead" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/filter/: + post: + tags: [Inspections] + summary: Список событий (фильтры в теле) + description: | + Аналог `GET /api/v1/inspections/` с передачей фильтров и пагинации в теле + запроса. Требует право `core.can_view_inspections`. + operationId: post_filter_all + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionFiltersWithPagination" + responses: + "200": + description: Страница событий + content: + application/json: + schema: + $ref: "#/components/schemas/PageInspectionRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/light/: + get: + tags: [Inspections] + summary: Список событий (облегчённое представление) + description: Требует право `core.can_view_inspections`. + operationId: get_all_light + parameters: + - $ref: "#/components/parameters/OrderByInspection" + - $ref: "#/components/parameters/Ascending" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - $ref: "#/components/parameters/Search" + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + responses: + "200": + description: Страница облегчённых событий + content: + application/json: + schema: + $ref: "#/components/schemas/PageInspectionLightRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/inspections/export/: + get: + tags: [Inspections] + summary: Экспорт событий (xlsx) + description: | + Возвращает файл экспорта. Поддерживается единственный тип — `xlsx`. + Требует право `core.can_view_inspections`. + operationId: export + parameters: + - name: type + in: query + required: false + schema: + $ref: "#/components/schemas/InspectionExportType" + - $ref: "#/components/parameters/Search" + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + - $ref: "#/components/parameters/TypeIdList" + - $ref: "#/components/parameters/StatusIdList" + responses: + "200": + description: Файл экспорта + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename=inspections_export.xlsx + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: string + format: binary + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/inspections/types/: + get: + tags: [Inspections] + summary: Типы событий компании + description: Требует право `core.can_view_inspections`. + operationId: get_types + parameters: + - name: company_id + in: query + required: true + description: ID компании + schema: + type: integer + example: 1 + responses: + "200": + description: Список типов событий (полное представление) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectionTypeFullRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/inspections/status-count/: + get: + tags: [Inspections] + summary: Счётчики по статусам (фильтры в query) + description: Требует право `core.can_view_inspections`. + operationId: get_status_count + parameters: + - $ref: "#/components/parameters/Search" + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + - $ref: "#/components/parameters/TypeIdList" + - $ref: "#/components/parameters/StatusIdList" + responses: + "200": + description: Счётчики по статусам, сгруппированные по ресурсам + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectionStatusCountRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + post: + tags: [Inspections] + summary: Счётчики по статусам (фильтры в теле) + description: Требует право `core.can_view_inspections`. + operationId: post_filter_status_count + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionFilters" + responses: + "200": + description: Счётчики по статусам, сгруппированные по ресурсам + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectionStatusCountRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/unavailable-dates/: + post: + tags: [Inspections] + summary: Недоступные даты для исполнителей + description: Требует право `core.can_view_inspections`. + operationId: get_unavailable_dates + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionUnavailableDatesRequest" + responses: + "200": + description: Список отрезков недоступных дат + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectionUnavailableDatesRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/unavailable-users/: + post: + tags: [Inspections] + summary: Недоступные исполнители на интервал + description: Требует право `core.can_view_inspections`. + operationId: get_unavailable_users + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionUnavailableUsersRequest" + responses: + "200": + description: Список недоступных пользователей + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionUnavailableUsersRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/change-history/: + get: + tags: [Inspections] + summary: История изменений событий + description: Требует право `core.can_view_inspections`. + operationId: get_change_history + parameters: + - $ref: "#/components/parameters/OrderByChangeHistory" + - $ref: "#/components/parameters/Ascending" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - name: created_at_after + in: query + schema: { type: string, format: date-time, nullable: true } + - name: created_at_before + in: query + schema: { type: string, format: date-time, nullable: true } + - name: updated_at_after + in: query + schema: { type: string, format: date-time, nullable: true } + - name: updated_at_before + in: query + schema: { type: string, format: date-time, nullable: true } + - name: inspection_public_id + in: query + schema: + type: array + nullable: true + items: { type: string, format: uuid } + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + responses: + "200": + description: Страница записей истории изменений + content: + application/json: + schema: + $ref: "#/components/schemas/PageInspectionChangeRecordRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/inspections/filter-options/: + post: + tags: [Inspections] + summary: Доступные значения фильтров + description: | + Возвращает возможные значения для перечисленных полей фильтра. + Требует право `core.can_view_inspections`. + operationId: get_filter_options + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionFilterOptionsRequest" + responses: + "200": + description: Маппинг «поле фильтра → список значений» + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionFilterOptionsRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/inspections/aggregate/{field_name}/{aggregate_func}/: + get: + tags: [Inspections] + summary: Агрегация по полю события + description: Требует право `core.can_view_inspections`. + operationId: aggregate + parameters: + - name: field_name + in: path + required: true + schema: + $ref: "#/components/schemas/InspectionAggregateField" + - name: aggregate_func + in: path + required: true + schema: + $ref: "#/components/schemas/AggregateFunc" + - $ref: "#/components/parameters/Search" + - $ref: "#/components/parameters/CompanyIdList" + - $ref: "#/components/parameters/ResourceIdList" + responses: + "200": + description: Результат агрегации + content: + application/json: + schema: + $ref: "#/components/schemas/AggregateResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /api/v1/inspections/{instance_id}/: + get: + tags: [Inspections] + summary: Событие по id + description: Требует право `core.can_view_inspections`. + operationId: retrieve + parameters: + - $ref: "#/components/parameters/InstanceId" + responses: + "200": + description: Событие + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionRead" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + patch: + tags: [Inspections] + summary: Частичное обновление события + description: Требует право `core.can_edit_inspection`. + operationId: partial_update + parameters: + - $ref: "#/components/parameters/InstanceId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionPartialUpdate" + responses: + "200": + description: Обновлённое событие + content: + application/json: + schema: + $ref: "#/components/schemas/InspectionRead" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + delete: + tags: [Inspections] + summary: Удалить событие + description: Требует право `core.can_delete_inspection`. + operationId: delete + parameters: + - $ref: "#/components/parameters/InstanceId" + responses: + "204": + description: Удалено + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. Опционально — + дополнительный заголовок `identity: Bearer ` (метаданные Zitadel). + Подпись токена не проверяется. + + parameters: + InstanceId: + name: instance_id + in: path + required: true + description: Публичный ID (UUID) события + schema: + type: string + format: uuid + Limit: + name: limit + in: query + description: Максимальное количество объектов + schema: { type: integer, default: 100 } + Offset: + name: offset + in: query + description: Количество пропущенных объектов + schema: { type: integer, default: 0 } + Ascending: + name: ascending + in: query + description: Сортировка по возрастанию + schema: { type: boolean, default: true } + OrderByInspection: + name: order_by + in: query + description: Поле сортировки + schema: + type: string + enum: [id, inspection_dt] + default: id + OrderByChangeHistory: + name: order_by + in: query + description: Поле сортировки + schema: + type: string + enum: [id, created_at] + default: id + Search: + name: search + in: query + description: Поиск по названию, локации и описанию + schema: { type: string, nullable: true } + CompanyIdList: + name: company_id + in: query + description: ID компании + schema: + type: array + nullable: true + items: { type: integer } + ResourceIdList: + name: resource_id + in: query + description: ID ресурса (проекта) + schema: + type: array + nullable: true + items: { type: string, format: uuid } + TypeIdList: + name: type_id + in: query + description: ID типа + schema: + type: array + nullable: true + items: { type: integer } + StatusIdList: + name: status_id + in: query + description: ID статуса + schema: + type: array + nullable: true + items: { type: integer } + + responses: + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPError" + example: { detail: "Bad Request" } + Unauthorized: + description: Не аутентифицирован + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPError" + example: { detail: "Authorization header is required" } + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPError" + example: { detail: "Forbidden" } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPError" + example: { detail: "Not Found" } + ValidationError: + description: Ошибка валидации (Pydantic / FastAPI) + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + + schemas: + HTTPError: + type: object + properties: + detail: + type: string + description: Описание ошибки + required: [detail] + + HTTPValidationError: + type: object + properties: + detail: + type: array + items: + type: object + properties: + loc: + type: array + items: + anyOf: + - { type: string } + - { type: integer } + msg: { type: string } + type: { type: string } + + MobileAppVersion: + type: object + properties: + current_version: { type: string, example: "1.0.0" } + recommended_version: { type: string, example: "1.0.0" } + required_version: { type: string, example: "1.0.0" } + required: [current_version, recommended_version, required_version] + + AttributeValueType: + description: Значение атрибута (одиночное) + nullable: true + anyOf: + - { type: boolean } + - { type: integer } + - { type: number } + - { type: string } + - { type: array, items: { type: integer } } + + AttributeMultivalueType: + description: Мультизначение атрибута + type: array + items: + nullable: true + anyOf: + - { type: boolean } + - { type: integer } + - { type: number } + - { type: string } + + InspectionExportType: + type: string + enum: [xlsx] + default: xlsx + + InspectionAggregateField: + type: string + enum: [created_at, inspection_dt] + + AggregateFunc: + type: string + enum: [minmax] + + AggregateResponse: + type: object + properties: + min: + nullable: true + description: Минимальное значение + example: 0 + max: + nullable: true + description: Максимальное значение + example: 1 + + AllowedToSetRole: + type: string + enum: [author, responsible_user, author_or_responsible_user] + + SetRole: + type: string + enum: [author, responsible_user] + + InspectionFilters: + type: object + description: Фильтры выборки событий + properties: + search: { type: string, nullable: true, description: Поиск по названию, локации и описанию } + created_at_after: { type: string, format: date-time, nullable: true } + created_at_before: { type: string, format: date-time, nullable: true } + updated_at_after: { type: string, format: date-time, nullable: true } + updated_at_before: { type: string, format: date-time, nullable: true } + public_id: + type: array + nullable: true + items: { type: string, format: uuid } + company_id: + type: array + nullable: true + items: { type: integer, minimum: 1 } + resource_id: + type: array + nullable: true + items: { type: string, format: uuid } + premise_id: + type: array + nullable: true + items: { type: string, format: uuid } + author_id: + type: array + nullable: true + items: { type: integer, minimum: 1 } + inspection_dt_after: { type: string, format: date-time, nullable: true } + inspection_dt_before: { type: string, format: date-time, nullable: true } + inspection_dt_end_after: { type: string, format: date-time, nullable: true } + inspection_dt_end_before: { type: string, format: date-time, nullable: true } + responsible_user_id: + type: array + nullable: true + items: { type: integer, minimum: 1 } + type_id: + type: array + nullable: true + items: { type: integer } + status_id: + type: array + nullable: true + items: { type: integer } + attributes: + type: object + nullable: true + description: >- + Маппинг id атрибута → значение. Может передаваться JSON-строкой. + additionalProperties: + oneOf: + - $ref: "#/components/schemas/AttributeValueType" + - $ref: "#/components/schemas/AttributeMultivalueType" + + InspectionFiltersWithPagination: + allOf: + - $ref: "#/components/schemas/InspectionFilters" + - type: object + properties: + order_by: + type: string + enum: [id, inspection_dt] + default: id + ascending: { type: boolean, default: true } + limit: { type: integer, default: 100 } + offset: { type: integer, default: 0 } + + InspectionExportFilters: + allOf: + - $ref: "#/components/schemas/InspectionFilters" + - type: object + properties: + type: + $ref: "#/components/schemas/InspectionExportType" + + InspectionFilterOptionsRequest: + type: object + properties: + filters: + $ref: "#/components/schemas/InspectionFilters" + fields: + type: array + description: Список полей, для которых нужно получить значения + items: { type: string } + example: [author_id, company_id] + required: [fields] + + InspectionFilterOptionsRead: + type: object + properties: + options: + type: object + description: Маппинг «поле фильтра» → «список значений» + additionalProperties: true + example: { company_id: [1, 2, 3, null] } + required: [options] + + AssetAttributeCreate: + type: object + properties: + attribute_id: { type: integer, example: 1 } + asset_id: { type: string, example: "1" } + order: { type: integer, example: 1 } + required: [attribute_id, asset_id, order] + + AssetAttributeRead: + allOf: + - $ref: "#/components/schemas/AssetAttributeCreate" + - type: object + properties: + value: + $ref: "#/components/schemas/AttributeValueType" + required: [value] + + AttributeRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + attribute_id: { type: integer, example: 1 } + attribute_name: { type: string, example: "Название атрибута" } + asset_id: { type: string, nullable: true, example: "1" } + asset_name: { type: string, nullable: true, example: "Название ассета" } + root_asset_id: { type: string, nullable: true, example: "1" } + required: { type: boolean, example: false } + resource_id: { type: string, format: uuid, nullable: true } + required: [id, created_at, updated_at, attribute_id, attribute_name, required] + + AttachmentsChange: + type: object + description: Состояние вложений до и после изменения + properties: + before: + type: array + items: { type: string } + after: + type: array + items: { type: string } + example: { before: ["report.pdf"], after: ["report.pdf", "photo.jpg"] } + + StatusSettingRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + inspection_status_id: { type: integer, example: 1 } + set_service_account_id: { type: string, format: uuid, nullable: true } + set_role: + nullable: true + allOf: [{ $ref: "#/components/schemas/SetRole" }] + comment_required: { type: boolean, example: false } + required: [id, created_at, updated_at, inspection_status_id, comment_required] + + StatusRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + inspection_status_model_id: { type: integer, example: 1 } + name: { type: string, example: "Название статуса" } + color: { type: string, example: "#000000" } + is_final: { type: boolean, example: false } + allowed_to_set: + nullable: true + deprecated: true + allOf: [{ $ref: "#/components/schemas/AllowedToSetRole" }] + comment_required: { type: boolean, deprecated: true, example: false } + set_status_ids: + type: array + items: { type: integer } + example: [1, 2, 3] + settings: + type: array + items: { $ref: "#/components/schemas/StatusSettingRead" } + required: [id, created_at, updated_at, inspection_status_model_id, name, color, is_final, comment_required] + + StatusWithCountRead: + allOf: + - $ref: "#/components/schemas/StatusRead" + - type: object + properties: + count: { type: integer, example: 1 } + required: [count] + + StatusModelRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + inspection_type_id: { type: integer, example: 1 } + resource_id: { type: string, format: uuid, nullable: true } + statuses: + type: array + items: { $ref: "#/components/schemas/StatusRead" } + required: [id, created_at, updated_at, inspection_type_id] + + InspectionTypePermissionsRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + inspection_type_id: { type: integer, example: 1 } + service_account_id: { type: string, format: uuid } + permissions: + type: array + items: { $ref: "#/components/schemas/InspectionPermission" } + required: [id, created_at, updated_at, inspection_type_id, service_account_id] + + InspectionPermission: + type: string + enum: + - core.can_create_inspection + - core.can_view_inspections + - core.can_edit_inspection + - core.can_delete_inspection + - core.can_view_all_inspections + + InspectionTypeLightRead: + type: object + properties: + id: { type: integer, example: 1 } + issue_types: + type: array + items: { type: integer } + example: [1, 2] + required: [id] + + InspectionTypeRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + company_id: { type: integer, example: 1 } + name: { type: string, example: "Название типа события" } + event_duration: + type: integer + description: Длительность события в минутах + example: 30 + columns_order: + type: array + items: { type: string } + example: [name, description, "attribute[1]"] + issue_types: + type: array + items: { type: integer } + example: [1, 2] + required: [id, created_at, updated_at, company_id, name, event_duration] + + InspectionTypeFullRead: + allOf: + - $ref: "#/components/schemas/InspectionTypeRead" + - type: object + properties: + permissions: + type: array + items: { $ref: "#/components/schemas/InspectionTypePermissionsRead" } + status_models: + type: array + items: { $ref: "#/components/schemas/StatusModelRead" } + attributes: + type: array + items: { $ref: "#/components/schemas/AttributeRead" } + inspection_dt_overlap_allowed: { type: boolean, example: false } + inspection_dt_current_day_allowed: { type: boolean, example: false } + required: [inspection_dt_overlap_allowed, inspection_dt_current_day_allowed] + + InspectionCreate: + type: object + properties: + company_id: { type: integer, minimum: 1, example: 1 } + resource_id: { type: string, format: uuid } + name: { type: string, minLength: 1, maxLength: 255, example: "Название события" } + inspection_dt: + type: string + format: date-time + description: Дата/время проведения (обнуляются секунды; не в прошлом) + inspection_dt_end: { type: string, format: date-time, nullable: true } + responsible_users: + type: array + minItems: 1 + items: { type: integer, minimum: 1 } + example: [1, 2] + location: { type: string, nullable: true } + description: { type: string, nullable: true } + type_id: { type: integer, minimum: 1, example: 1 } + status_id: { type: integer, nullable: true, example: 1 } + attributes: + type: object + additionalProperties: + $ref: "#/components/schemas/AttributeMultivalueType" + example: { "1": [null], "2": [true, false], "3": [1, 2] } + asset_attributes: + type: array + items: { $ref: "#/components/schemas/AssetAttributeCreate" } + premise_id: { type: string, format: uuid, nullable: true } + required: [company_id, resource_id, name, inspection_dt, responsible_users, type_id] + + InspectionPartialUpdate: + type: object + description: Все поля опциональны + properties: + name: { type: string, minLength: 1, maxLength: 255, nullable: true } + inspection_dt: { type: string, format: date-time, nullable: true } + inspection_dt_end: { type: string, format: date-time, nullable: true } + responsible_users: + type: array + minItems: 1 + nullable: true + items: { type: integer, minimum: 1 } + location: { type: string, nullable: true } + description: { type: string, nullable: true } + type_id: { type: integer, nullable: true } + status_id: { type: integer, nullable: true } + attributes: + type: object + nullable: true + additionalProperties: + $ref: "#/components/schemas/AttributeMultivalueType" + asset_attributes: + type: array + nullable: true + items: { $ref: "#/components/schemas/AssetAttributeCreate" } + status_comment: { type: string, nullable: true } + attachments: + nullable: true + allOf: [{ $ref: "#/components/schemas/AttachmentsChange" }] + premise_id: { type: string, format: uuid, nullable: true } + + InspectionLightRead: + type: object + properties: + public_id: { type: string, format: uuid } + name: { type: string, example: "Название события" } + type: + $ref: "#/components/schemas/InspectionTypeLightRead" + required: [public_id, name, type] + + InspectionRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + public_id: { type: string, format: uuid } + company_id: { type: integer, example: 1 } + resource_id: { type: string, format: uuid } + author_id: { type: integer, example: 1 } + name: { type: string, example: "Название события" } + inspection_dt: { type: string, format: date-time } + inspection_dt_end: { type: string, format: date-time } + responsible_users: + type: array + items: { type: integer } + example: [1, 2] + location: { type: string, nullable: true } + description: { type: string, nullable: true } + type: + $ref: "#/components/schemas/InspectionTypeRead" + status: + $ref: "#/components/schemas/StatusRead" + attributes: + type: object + additionalProperties: + $ref: "#/components/schemas/AttributeMultivalueType" + asset_attributes: + type: array + items: { $ref: "#/components/schemas/AssetAttributeRead" } + premise_id: { type: string, format: uuid, nullable: true } + required: + - id + - created_at + - updated_at + - public_id + - company_id + - resource_id + - author_id + - name + - inspection_dt + - inspection_dt_end + - responsible_users + - type + - status + - attributes + - premise_id + + InspectionStatusCountRead: + type: object + properties: + resource_id: { type: string, format: uuid } + statuses: + type: array + items: { $ref: "#/components/schemas/StatusWithCountRead" } + required: [resource_id, statuses] + + InspectionUnavailableDatesRequest: + type: object + properties: + company_id: { type: integer, minimum: 1, nullable: true, example: 1 } + responsible_users: + type: array + minItems: 1 + items: { type: integer, minimum: 1 } + example: [1, 2] + excluded_inspection_ids: + type: array + items: { type: integer } + example: [1, 2] + required: [responsible_users] + + InspectionUnavailableDatesRead: + type: object + properties: + unavailable_from: { type: string, format: date-time } + unavailable_to: { type: string, format: date-time } + required: [unavailable_from, unavailable_to] + + InspectionUnavailableUsersRequest: + type: object + properties: + company_id: { type: integer, minimum: 1, nullable: true, example: 1 } + inspection_dt: { type: string, format: date-time } + inspection_dt_end: { type: string, format: date-time, nullable: true } + users: + type: array + minItems: 1 + items: { type: integer, minimum: 1 } + example: [1, 2] + required: [inspection_dt, users] + + InspectionUnavailableUsersRead: + type: object + properties: + unavailable_users: + type: array + items: { type: integer } + example: [1, 2] + required: [unavailable_users] + + InspectionChangeRecordRead: + type: object + properties: + id: { type: integer, example: 1 } + created_at: { type: string, format: date-time } + inspection_id: { type: integer, example: 1 } + created_by: { type: integer, example: 1023 } + field_name: { type: string, example: "name" } + attribute_name: { type: string, nullable: true, example: "Локация" } + was: + oneOf: + - $ref: "#/components/schemas/AttributeValueType" + - $ref: "#/components/schemas/AttributeMultivalueType" + became: + oneOf: + - $ref: "#/components/schemas/AttributeValueType" + - $ref: "#/components/schemas/AttributeMultivalueType" + was_text: { type: string, nullable: true } + became_text: { type: string, nullable: true } + required: [id, created_at, inspection_id, created_by, field_name, attribute_name, was, became, was_text, became_text] + + PageInspectionRead: + type: object + properties: + count: { type: integer, example: 1 } + result: + type: array + items: { $ref: "#/components/schemas/InspectionRead" } + required: [count, result] + + PageInspectionLightRead: + type: object + properties: + count: { type: integer, example: 1 } + result: + type: array + items: { $ref: "#/components/schemas/InspectionLightRead" } + required: [count, result] + + PageInspectionChangeRecordRead: + type: object + properties: + count: { type: integer, example: 1 } + result: + type: array + items: { $ref: "#/components/schemas/InspectionChangeRecordRead" } + required: [count, result] diff --git a/apps/issues/.env.example b/apps/issues/.env.example new file mode 100644 index 0000000..c8ac5ed --- /dev/null +++ b/apps/issues/.env.example @@ -0,0 +1,101 @@ +# Django +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_ADMIN_SECRET_KEY='' +DJANGO_TOKEN=django-token + +# Environment +ENVIRONMENT=production +ENVIRONMENT_CLIENT=stage + +# Database (PostgreSQL) +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD=password +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 + +# Sarex auth (basic) +SAREX_USERNAME= +SAREX_PASSWORD= + +# External services +AERO_HOST=https://stage.sarex.io +AERO_PUBLIC_HOST=https://stage.sarex.io +BASE_AERO_URL=https://stage.sarex.io +BASE_AUTH_URL=https://stage.sarex.io +SAREX_API=https://stage.sarex.io +SAREX_HOST=https://stage.sarex.io +SERVICE_URL=https://stage.sarex.io +GATEWAY_URL=https://stage-api.sarex.io/gateway +DOCUMENTATIONS_URL=http://documentations-api-svc.documentations.svc.cluster.local:8000 +WORKFLOWS_URL=http://workflows-api-service.platform.svc.cluster.local:8000 +WORKFLOWS_HOST=http://workflows-api-service.platform.svc.cluster.local:8000 +RESOURCES_API_HOST=http://iams.platform.svc.cluster.local:8080 +REVIEW_HOST=https://stage-api.sarex.io/flows +INSPECTION_HOST=https://stage-api.sarex.io/inspections +EAV_HOST=http://eav-service.eav-stage + +# RabbitMQ / Celery broker +RABBITMQ_USERNAME=mcc +RABBITMQ_PASSWORD=mcc +RABBITMQ_HOSTNAME=rabbitmq-service +RABBITMQ_VHOST=api + +# Redis (Celery result backend) +REDIS_HOST=redis +REDIS_DB=0 + +# Kafka +KAFKA_HOST= +KAFKA_USERNAME= +KAFKA_PASSWORD= +KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt +KAFKA_EAV_ASSETS_TOPIC=assets-broadcast-test +KAFKA_ISSUES_TOPIC=issues-broadcast + +# S3 (Yandex Cloud) — общий бакет +YC_S3_ACCESS_KEY_ID= +YC_S3_SECRET_ACCESS_KEY= +YC_S3_BUCKET_NAME= +YC_S3_ENDPOINT_URL= +YC_S3_VERIFY=true + +# S3 — бакет предписаний +PRESCRIPTION_S3_ACCESS_KEY_ID= +PRESCRIPTION_S3_SECRET_ACCESS_KEY= +PRESCRIPTION_S3_BUCKET= +PRESCRIPTION_S3_ENDPOINT_URL= + +# Email +ENABLE_MAILGUN=True +EMAIL_FROM=hello@sarex.io +EMAIL_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/notification:email +MAILGUN_BASE_URL=https://api.mailgun.net/v3/mg.sarex.io +MAILGUN_API_KEY= +USE_NOTIFICATIONS=True +# SMTP (альтернатива Mailgun) +SMTP_HOST= +SMTP_PORT= + +# Prescriptions workflow +PRESCRIPTION_WF_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/rendering-template:develop +PRESCRIPTION_WF_RESULT_PATH=prescriptions-storage-stage +PRESCRIPTION_WF_CALLBACK=cr.yandex/crp3ccidau046kdj8g9q/webhook-caller:develop +PRESCRIPTION_INTERNAL_HOST=http://issues-backend-service.proc.svc.cluster.local:80/internal +DOCX_TO_PDF_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/docx-to-pdf:latest + +# Export +EXPORT_WF_CROPPING_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/crop-issue-pin-area:stage + +# OpenTelemetry +USE_OTEL=False +SERVICE_NAME=issues-backend.sarex-issues +TRACER_ENDPOINT=localhost:4375 +USE_INSECURE=True +MODULE=issues +TEAM=proc_team +COMPONENT=backend + +# uWSGI / infra +API_ADDRESS=8000 +SENTRY_KEY= diff --git a/apps/issues/CONFIGURATION.md b/apps/issues/CONFIGURATION.md new file mode 100644 index 0000000..0a188bd --- /dev/null +++ b/apps/issues/CONFIGURATION.md @@ -0,0 +1,245 @@ +# Конфигурация проекта issues-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса замечаний (Issues). + +## Способы конфигурирования + +Сервис настраивается **через переменные окружения**. Это Django-приложение; настройки читаются в `src/config/settings/base.py` и `src/config/settings/production.py` напрямую через `os.getenv(...)`. В начале `base.py` вызывается `load_dotenv()` ([`python-dotenv`](https://pypi.org/project/python-dotenv/)), поэтому при локальном запуске файл `.env` из рабочего каталога **подхватывается автоматически**. + +Активный модуль настроек задаётся переменной `DJANGO_SETTINGS_MODULE` (в контейнере/Helm — `config.settings.production`) либо флагом `--settings=config.settings.production` у `manage.py`. Модуль `production.py` импортирует всё из `base.py` и переопределяет `DEBUG=False`, `ALLOWED_HOSTS`, `SIMPLE_JWT`, `LOGGING` и часть внешних хостов. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Дополнительно на инфраструктурном уровне используются `config/settings/base.py` для Celery/Kafka/OTel и Helm-чарт для задания переменных в Kubernetes. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально | Переменные окружения процесса и файл `.env` (грузится `load_dotenv()` в `base.py`) | +| Локально (Kafka) | `docker compose --file local-kafka-docker-compose.yml up -d` поднимает брокер; консьюмер — `manage.py consume_kafka` | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для каждого сервиса (`api`, `celery`, `celery-beat`, `kafka-app`) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общий шаблон `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); окружение выбирается по ветке/тегу | + +Способы запуска процессов: + +| Процесс | Команда | Назначение | +| --- | --- | --- | +| HTTP API | `uwsgi` (entrypoint) / `manage.py runserver` | REST API (DRF), OpenAPI-схема через drf-spectacular | +| Celery worker | `celery -A config worker -l info -E --concurrency=2` | Обработчик фоновых задач (`issues.tasks`, `issues.notifications`, `prescriptions.tasks`) | +| Celery beat | `celery -A config beat -l info` | Периодические задачи (ежедневный инкремент счётчиков, отчёт о просрочках) | +| Kafka consumer | `python3 run_kafka_app.py` / `manage.py consume_kafka` | Консьюмер Kafka (топики ассетов и замечаний) | + +Порядок старта в контейнере задаётся `compose/server/entrypoint.sh` (миграции + запуск uWSGI по `compose/server/uwsgi.ini`). Базовый образ — `python:3.10-slim-bookworm` (`compose/server/Dockerfile`). + +## Переменные приложения + +Дефолт `—` означает, что явного значения по умолчанию в коде нет (`os.getenv` вернёт `None`); для корректной работы переменную нужно задать. + +### Django и окружение + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.production` (Helm) | Модуль настроек Django | +| `DJANGO_ADMIN_SECRET_KEY` | string | `''` | `SECRET_KEY` Django | +| `DJANGO_TOKEN` | string | `django-token` | Служебный токен | +| `ENVIRONMENT` | string | `production` | Окружение развёртывания (в т.ч. атрибут OTel) | +| `ENVIRONMENT_CLIENT` | string | `production` | Клиентское окружение (`stage`/`preprod`/`production`) | + +### База данных (PostgreSQL) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_NAME` | string | — | Имя базы данных | +| `DATABASE_USER` | string | — | Пользователь БД | +| `DATABASE_PASSWORD` | string | — | Пароль пользователя БД | +| `DATABASE_HOST` | string | — | Хост PostgreSQL | +| `DATABASE_PORT` | int | — | Порт PostgreSQL | + +> Движок — `django.db.backends.postgresql`. В Kubernetes значения приходят из секрета (`issues-postgresql-secret` для stage, `ya-pg-secret` для preprod/production), CA-сертификат монтируется как `/root/.postgresql/ca.crt`. + +### Внешние сервисы (URL) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SAREX_API` | string | — | Базовый API Sarex (`SAREX_HOST` по умолчанию равен ему) | +| `SAREX_HOST` | string | `= SAREX_API` | Хост Sarex | +| `AERO_HOST` | string | `https://stage.sarex.io` | Хост Aero | +| `AERO_PUBLIC_HOST` | string | `https://stage.sarex.io` (в `production.py` — из env) | Публичный хост Aero | +| `BASE_AERO_URL` | string | `https://lk.sarex.io` | Базовый URL Aero | +| `BASE_AUTH_URL` | string | `https://lk.sarex.io` | Базовый URL аутентификации | +| `SERVICE_URL` | string | `https://lk.sarex.io` | URL сервиса | +| `GATEWAY_URL` | string | `https://lk.sarex.io` | URL gateway | +| `DOCUMENTATIONS_URL` | string | `https://lk.sarex.io` | URL сервиса документаций | +| `WORKFLOWS_URL` | string | `https://lk.sarex.io` | URL сервиса workflows | +| `WORKFLOWS_HOST` | string | `https://lk.sarex.io` | Хост workflows | +| `RESOURCES_API_HOST` | string | `https://lk.sarex.io` (в `production.py` — `http://sarex-resources-service.resources-prod`) | Хост сервиса ресурсов (IAM) | +| `REVIEW_HOST` | string | `https://lk.sarex.io` | Хост сервиса review/flows | +| `INSPECTION_HOST` | string | `https://lk.sarex.io` | Хост сервиса инспекций | +| `EAV_HOST` | string | `http://eav-service.eav-stage` | Хост сервиса атрибутов (EAV) | +| `SAREX_USERNAME` | string | — | Логин для basic-auth Sarex | +| `SAREX_PASSWORD` | string | — | Пароль для basic-auth Sarex | + +### RabbitMQ и Celery + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `RABBITMQ_USERNAME` | string | `mcc` | Пользователь брокера | +| `RABBITMQ_PASSWORD` | string | `mcc` | Пароль брокера | +| `RABBITMQ_HOSTNAME` | string | `rabbitmq-service` | Хост брокера | +| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост | +| `REDIS_HOST` | string | `redis` | Хост Redis (result backend) | +| `REDIS_DB` | int | `0` | Номер БД Redis | + +> `CELERY_BROKER_URL` собирается как `amqp://{user}:{password}@{hostname}/{vhost}` + `?heartbeat=30`. `CELERY_RESULT_BACKEND` — `redis://{REDIS_HOST}:6379/{REDIS_DB}`. Расписание beat: инкремент счётчиков `1:00`, отчёт о просрочках `6:00` (`Europe/Moscow`). + +### Kafka + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_HOST` | string | — | Адрес брокера Kafka | +| `KAFKA_USERNAME` | string | — | Пользователь | +| `KAFKA_PASSWORD` | string | — | Пароль | +| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату (в Helm — YandexInternalRootCA) | +| `KAFKA_EAV_ASSETS_TOPIC` | string | — | Топик трансляции ассетов (EAV) | +| `KAFKA_ISSUES_TOPIC` | string | — | Топик трансляции замечаний | + +### S3 (Yandex Cloud) + +Основное хранилище (`django-storages`, `S3Boto3Storage`): + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | — | Access key | +| `YC_S3_SECRET_ACCESS_KEY` | string | — | Secret key | +| `YC_S3_BUCKET_NAME` | string | — | Имя бакета | +| `YC_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 | +| `YC_S3_VERIFY` | bool | `None` | Проверять TLS-сертификат (`"true"` → `True`) | + +Хранилище предписаний (отдельный бакет): + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PRESCRIPTION_S3_ACCESS_KEY_ID` | string | — | Access key | +| `PRESCRIPTION_S3_SECRET_ACCESS_KEY` | string | — | Secret key | +| `PRESCRIPTION_S3_BUCKET` | string | — | Имя бакета | +| `PRESCRIPTION_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 | + +### Почта (Mailgun / SMTP) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_MAILGUN` | bool | `True` | Использовать Mailgun | +| `MAILGUN_BASE_URL` | string | `https://api.mailgun.net/v3/mg.sarex.io` | URL API Mailgun | +| `MAILGUN_API_KEY` | string | (задан дефолт в коде) | API-ключ Mailgun (в проде — из секрета) | +| `EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя | +| `EMAIL_DOCKER_IMAGE` | string | `cr.yandex/.../notification:email` | Образ сервиса нотификаций | +| `USE_NOTIFICATIONS` | bool | `True` | Включить отправку уведомлений (`False`/`false`/`0` → выкл.) | +| `SMTP_HOST` | string | `None` (в `production.py` — `""`) | SMTP-хост (альтернатива Mailgun) | +| `SMTP_PORT` | int | `None` | SMTP-порт | + +### Предписания (workflow) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PRESCRIPTION_WF_IMAGE` | string | — | Образ workflow генерации предписаний | +| `DOCX_TO_PDF_IMAGE` | string | — | Образ конвертера DOCX→PDF | +| `PRESCRIPTION_WF_RESULT_PATH` | string | — | Путь/бакет результата | +| `PRESCRIPTION_WF_CALLBACK` | string | — | Образ webhook-caller | +| `PRESCRIPTION_INTERNAL_HOST` | string | — | Внутренний хост колбэков предписаний | +| `EXPORT_WF_CROPPING_DOCKER_IMAGE` | string | `cr.yandex/.../crop-issue-pin-area:prod` | Образ кропа области пина для экспорта | + +### OpenTelemetry + +Трейсинг подключается только если `USE_OTEL` истинно (`django_otel_tools`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool | `False` | Включить трейсинг/логирование через OTel | +| `SERVICE_NAME` | string | `issues-backend.sarex-issues` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение к коллектору | +| `MODULE` | string | `issues` | Атрибут трейсов | +| `TEAM` | string | `proc_team` | Атрибут трейсов | +| `COMPONENT` | string | `backend` | Атрибут трейсов | + +## Переменные инфраструктуры + +Не читаются кодом приложения напрямую (или используются вспомогательными компонентами), но участвуют в запуске/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_ADDRESS` | Helm (`envs`) | Порт uWSGI (`8000`) | +| `SENTRY_KEY` | Helm (`envs`) | DSN Sentry (задан для stage) | +| `SAREX_MAILER_URL` | Helm (`envs`) | URL сервиса рассылок (`http://mailer-service.mailer:8000`) | +| `MAILGUN_HOST` | Helm (`envs`) | Хост Mailgun на уровне чарта | +| `NPM_TOKEN`, `BUILD_ENV` | CI/Dockerfile | Сборка (актуально для фронтенда) | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — обёртка над `universal-chart`. Определены четыре сервиса: `api`, `celery`, `celery-beat`, `kafka-app`. Обычные значения (`envs`) задаются для окружений `_default`/`stage`/`preprod`/`production` (различаются адресами БД/сервисов, топиками Kafka, образами, `SERVICE_NAME`, `TRACER_ENDPOINT`). + +Значения из секретов (блок `secretEnvs`, монтируются через `secretKeyRef`): + +| Переменная | Секрет (stage / preprod-prod) | Ключ | +| --- | --- | --- | +| `KAFKA_USERNAME` | `issues-kafka-secret` / `yc-kafka-secret` | `username` | +| `KAFKA_PASSWORD` | `issues-kafka-secret` / `yc-kafka-secret` | `password` | +| `KAFKA_HOST` | `issues-kafka-secret` / `yc-kafka-secret` | `host` | +| `SAREX_USERNAME` | `sarex-auth` | `username` | +| `SAREX_PASSWORD` | `sarex-auth` | `password` | +| `DATABASE_HOST` | `issues-postgresql-secret` / `ya-pg-secret` | `host` | +| `DATABASE_NAME` | `issues-postgresql-secret` / `ya-pg-secret` | `database` | +| `DATABASE_PORT` | `issues-postgresql-secret` / `ya-pg-secret` | `port` | +| `DATABASE_USER` | `issues-postgresql-secret` / `ya-pg-secret` | `username` | +| `DATABASE_PASSWORD` | `issues-postgresql-secret` / `ya-pg-secret` | `password` | +| `YC_S3_ACCESS_KEY_ID` | `issues-s3-secret` / `yc-s3-secret` | `key_id` | +| `YC_S3_SECRET_ACCESS_KEY` | `issues-s3-secret` / `yc-s3-secret` | `access_key` | +| `YC_S3_BUCKET_NAME` | `issues-s3-secret` / `yc-s3-secret` | `storage_bucket_name` | +| `YC_S3_ENDPOINT_URL` | `issues-s3-secret` / `yc-s3-secret` | `endpoint_url` | +| `RABBITMQ_VHOST` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `vhost` | +| `RABBITMQ_USERNAME` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `user` | +| `RABBITMQ_HOSTNAME` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `host` | +| `RABBITMQ_PASSWORD` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `password` | +| `MAILGUN_API_KEY` | `mailgun-secret` | `api-key` | +| `DJANGO_TOKEN` | `django-secret` | `token` | +| `DJANGO_ADMIN_SECRET_KEY` | `django-admin-secret` | `secret_key` | +| `PRESCRIPTION_S3_ACCESS_KEY_ID` | `prescription-s3-secret` | `key_id` | +| `PRESCRIPTION_S3_SECRET_ACCESS_KEY` | `prescription-s3-secret` | `access_key` | +| `PRESCRIPTION_S3_BUCKET` | `prescription-s3-secret` | `storage_bucket_name` | +| `PRESCRIPTION_S3_ENDPOINT_URL` | `prescription-s3-secret` | `endpoint_url` | + +Дополнительно чарт монтирует конфиг uWSGI (`uwsgi-configmap` → `/opt/server/uwsgi.ini`, только сервис `api`), CA-сертификат PostgreSQL (`yc-ch-certificate` → `/root/.postgresql/ca.crt`) и внутренний CA Яндекса (`YandexInternalRootCA.crt`). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Основные переменные: `SERVICE_NAME=issues`, `DOCKERFILE_PATH=./compose/server/Dockerfile`. Окружение выбирается по ветке/тегу: + +| Условие | STAND | Namespace | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `proc` | `0.0.1-stage` | +| ветка `master` | `preprod` | `issues-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `issues-prod` | `0.0.1-prod` | + +`HELM_SET_ARGS` для каждого окружения проставляет образы четырёх сервисов (`api`, `celery`, `celery-beat`, `kafka-app`), `universal-chart.global.env` и метаданные коммита (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`). + +## Замечания и потенциальные проблемы + +- В отличие от FastAPI-сервисов, переменные не имеют единого префикса и читаются напрямую через `os.getenv`. Файл `.env` подхватывается автоматически (`load_dotenv()` в `base.py`). +- `DEBUG` в `base.py` установлен в `True`; в `production.py` переопределяется на `False`. Для боевого окружения обязателен модуль `config.settings.production`. +- `MAILGUN_API_KEY` имеет захардкоженный дефолт в коде — в реальных окружениях его нужно переопределять секретом. +- Ряд переменных без дефолта (`DATABASE_*`, `KAFKA_*`, `YC_S3_*`, `SAREX_USERNAME`/`SAREX_PASSWORD`, `PRESCRIPTION_WF_*`) обязательны для полноценной работы соответствующих подсистем. +- Переменные `SAREX_MAILER_URL`, `MAILGUN_HOST`, `SENTRY_KEY`, `API_ADDRESS` задаются в Helm, но не читаются кодом приложения напрямую. + +## Минимальный набор для локального запуска + +Postgres, RabbitMQ, Redis и Kafka поднимаются локально; приложение — `python ./src/manage.py runserver --settings=config.settings.production`, консьюмер — `manage.py consume_kafka`. Минимально необходимо задать: + +- `DJANGO_ADMIN_SECRET_KEY` +- `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_HOST`, `DATABASE_PORT` +- `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_HOSTNAME`, `RABBITMQ_VHOST`, `REDIS_HOST` +- `KAFKA_HOST`, `KAFKA_USERNAME`, `KAFKA_PASSWORD`, `KAFKA_EAV_ASSETS_TOPIC`, `KAFKA_ISSUES_TOPIC` (для консьюмера) +- `YC_S3_*` (для работы с файлами) и при необходимости `PRESCRIPTION_S3_*` +- внешние URL: `SAREX_API`, `AERO_HOST`, `GATEWAY_URL`, `DOCUMENTATIONS_URL`, `WORKFLOWS_URL`, `RESOURCES_API_HOST`, `EAV_HOST`, `INSPECTION_HOST`, `REVIEW_HOST` +- почта: `ENABLE_MAILGUN` + `MAILGUN_*` **или** `SMTP_HOST`/`SMTP_PORT` +- `USE_OTEL=False` для локальной разработки + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/issues/ENDPOINTS.md b/apps/issues/ENDPOINTS.md new file mode 100644 index 0000000..c3d3bd1 --- /dev/null +++ b/apps/issues/ENDPOINTS.md @@ -0,0 +1,138 @@ +# Эндпоинты, с которыми взаимодействует issues-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `issues-frontend`). + +## Как устроено взаимодействие + +Запросы сгруппированы по доменным API-объектам в каталоге `module/api/` (`IssuesApi`, `CoreApi`, `PrescriptionsApi`, `InspectionsApi`, `AttributesApi`, `AssetsApi`, `ContractsApi`, `ResourcesApi`, `TemplatesApi`, `PremisesApi`, `DocumentationApi`). Каждый метод вызывает единый `httpService` (`module/api/http-service.ts`). + +`httpService` создаётся фабрикой `createHttpService` из `@sarex-team/sdk-js` и принимает карту хостов `hosts` (`module/api/hosts.ts`) и текущее окружение `BUILD_ENV`. Вызов задаётся объектом: + +- `service` — логическое имя сервиса (ключ из `hosts`, см. таблицу ниже); +- метод — `getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`; +- `url` — путь запроса (дописывается к базовому хосту сервиса); +- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer`, `responseType` и т.п.); +- `data` — тело запроса; +- `showErrorNotification`, `queryKey`, `cache` — опции показа ошибок, ключа кеша и кеширования. + +Итоговый URL = `<базовый хост сервиса для BUILD_ENV>` + `url`. Базовый хост выбирается по `BUILD_ENV` (`local`/`stage`/`prod`/`preprod`/`contour`). + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Приведены `stage` и `prod`; в `contour` используются относительные пути, в `preprod` — домен `api.preprod.sarex.io`, в `local` — как в `stage`, но `sarex` проксируется на `/sarex-backend`. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `issues` | Сервис замечаний (issues-backend, собственный API) | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` | +| `prescriptions` | Предписания (issues-backend) | `https://stage-api.sarex.io/issues/api/prescriptions` | `https://api.sarex.io/issues/api/prescriptions` | +| `sarexApi` | Gateway/API Sarex (`/gateway`, `/issues`, `/inspections`, `/contracts`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` | +| `gateway` | Gateway Sarex | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `documentations` | Сервис документации | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | +| `eav` | Сервис атрибутов/ассетов (EAV) | `https://stage-api.sarex.io/eav/api` | `https://api.sarex.io/eav/api` | +| `files` | Сервис файлов | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` | +| `premises` | Сервис помещений | `https://stage-api.sarex.io/premises/api/v1` | `https://api.sarex.io/premises/api/v1` | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `comparisons` | Сервис сравнений | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `remarks` | Сервис замечаний (remarks) | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `bim` / `bimv2` | BIM-API | `https://stage-api.sarex.io/bim` (`/bimv2`) | `https://api.sarex.io/bim` (`/bimv2`) | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Часть сервисов объявлена в карте хостов, но напрямую в `module/api/*` не вызывается (`workspaces`, `workflows`, `comparisons`, `remarks`, `bim`, `bimv2`, `google`, `zitadel`) — они используются инфраструктурой SDK / другими слоями. Сервис `prescriptions` объявлен в хостах, но методы предписаний фактически ходят через `sarexApi` по пути `/issues/api/prescriptions`. + +Подключаемый удалённый модуль `documentations` описан отдельно в `module/api/module-hosts.ts` (`remoteEntry.js` микрофронтенда documentations). + +## Эндпоинты по сервисам + +### `issues` — Сервис замечаний (собственный API) + +Файл `module/api/issuesApi.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `IssuesApi.getStatusModels` | GET | `/companies/{companyId}/status-model/` | Модель статусов компании (по `resource_id`, `issue_type_id`) | +| `IssuesApi.getTypes` | GET | `/issue-types/` | Типы замечаний компании (`company_id`) | +| `IssuesApi.getIssue` | GET | `/issues/{public_id}/` | Замечание по публичному id | +| `IssuesApi.editIssue` | PATCH | `/issues/{publicId}/` | Редактировать замечание | +| `IssuesApi.getChanges` | GET | `/issue-changes/` | История изменений замечания (`issue_id`) | + +### `sarexApi` — Gateway/API Sarex + +Файлы `issuesApi.ts`, `prescriptionsApi.ts`, `inspections.ts`, `contractsApi.ts`, `attributes.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `IssuesApi.postComment` | POST | `/issues/api/comments/` | Добавить комментарии | +| `IssuesApi.deleteComment` | DELETE | `/issues/api/comments/{id}/` | Удалить комментарий | +| `IssuesApi.postAttachment` | POST | `/issues/api/attachments/` | Загрузить вложение (multipart) | +| `IssuesApi.deleteAttachment` | DELETE | `/issues/api/attachments/{id}/` | Удалить вложение (`issue_public_id`) | +| `PrescriptionsApi.postPrescription` | POST | `/issues/api/prescriptions/` | Создать предписание | +| `PrescriptionsApi.getPrescriptions` | GET | `/issues/api/prescriptions/` | Список предписаний (сериализованные фильтры в query) | +| `InspectionsApi.getInspections` | GET | `/inspections/api/v1/inspections/light/` | Список инспекций (`company_id`, `limit`, `offset`) | +| `InspectionsApi.getInspectionTypes` | GET | `/inspections/api/v1/inspections/types/` | Типы инспекций (`company_id`) | +| `ContractsApi.getContracts` | GET | `/contracts/api/v0/contracts/` | Договоры (`tenant_id`, `contractor_id`, `resource_id`) | +| `AttributesApi.getDocumentAttributes` | GET | `/gateway/api/v1/documents/{documentId}/attributes/` | Атрибуты документа | + +### `sarex` — Локальный сервис данных + +Файл `module/api/coreApi.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `CoreApi.getUserSettings` | GET | `/api/client/settings/` | Клиентские настройки | +| `CoreApi.getUsers` | GET | `/api/core/users/` | Пользователи компании (`company`, `limit`, `offset`, `show_inactive`) | +| `CoreApi.getDepartments` | GET | `/api/core/admin/departments/` | Отделы компании | +| `CoreApi.getPositions` | GET | `/api/core/admin/positions/` | Должности компании | + +### `gateway` — Gateway Sarex + +Файлы `coreApi.ts`, `resources.ts`, `templatesApi.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `CoreApi.getGatewayUsers` | GET | `/api/v2/users/` | Пользователи по ресурсу/правам (`resource_id`, `permissions`, `limit`, `offset`) | +| `ResourcesApi.getResourceFullInfo` | GET | `/api/v2/resources/{resourceId}/` | Полная информация о ресурсе | +| `TemplatesApi.getTemplates` | GET | `/api/v1/disks/{diskId}/flat_documents/` | Плоский список документов диска (`type`) | + +### `documentations` — Сервис документации + +Файлы `documentation.ts`, `templatesApi.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `DocumentationApi.getDocumentById` | GET | `/documents/{docId}` | Документ по id (опц. `extend`) | +| `TemplatesApi.getDisks` | GET | `/disks` | Список дисков | + +### `eav` — Атрибуты и ассеты (EAV) + +Файлы `assets.ts`, `attributes.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `AssetsApi.getAssetsPost` | POST | `/v4/assets/search/` | Поиск ассетов | +| `AssetsApi.getAssetsGet` | GET | `/v4/assets/` | Список ассетов | +| `AttributesApi.getAttributes` | GET | `/v0/attribute/` | Атрибуты компании (`company_id`) | + +### `files` — Сервис файлов + +Файл `documentation.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `DocumentationApi.getPage` | GET | `/pages/{sourceId}/{pageId}` | Страница файла (ответ `blob`) | + +### `premises` — Сервис помещений + +Файл `premises-api.ts`. + +| Метод API | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `PremisesApi.getPremise` | GET | `/premises/{id}/` | Помещение по id | +| `PremisesApi.getPremisesFilter` | POST | `/premises/filter/` | Фильтрация помещений (`limit`, `offset` в query) | +| `PremisesApi.getPremiseTypesFilter` | POST | `/premise_types/filter/` | Фильтрация типов помещений (`limit`, `offset` в query) | + +## Обработка ошибок + +Глобальная обработка выполняется в `module/api/http-service.ts`: `httpService` обёрнут в `Proxy`, который для методов запросов (`getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`) перехватывает ошибку и при статусе `403` показывает toast-уведомление (`react-toastify`) с текстом `detail` из ответа либо сообщением «У вас недостаточно прав для выполнения данного действия», после чего пробрасывает ошибку дальше. Для отдельных запросов показ уведомлений включается флагом `showErrorNotification: true`. В окружении `local` SDK переключается в режим `original` (`setSharedHttpServiceConfig({ type: "original" })`). diff --git a/apps/issues/openapi.yaml b/apps/issues/openapi.yaml new file mode 100644 index 0000000..14a94bb --- /dev/null +++ b/apps/issues/openapi.yaml @@ -0,0 +1,3278 @@ +openapi: 3.0.3 +info: + title: Issues_v2 API + version: 1.0.0 + description: Sarex Issues_v2 +paths: + /api/attachments/: + get: + operationId: api_attachments_list + description: Получить список всех файлов вложений + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedAttachmentReadList' + description: '' + post: + operationId: api_attachments_create + description: Добавить новый файл + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentWrite' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttachmentWrite' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttachmentWrite' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentWrite' + description: '' + /api/attachments/{id}/: + get: + operationId: api_attachments_retrieve + description: Получить конкретный файл по ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Файла вложения. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentRead' + description: '' + put: + operationId: api_attachments_update + description: Изменить файл + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Файла вложения. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentWrite' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttachmentWrite' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttachmentWrite' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentWrite' + description: '' + patch: + operationId: api_attachments_partial_update + description: Изменить файл частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Файла вложения. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttachmentWrite' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedAttachmentWrite' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedAttachmentWrite' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttachmentWrite' + description: '' + delete: + operationId: api_attachments_destroy + description: Удалить файл + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Файла вложения. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '204': + description: '' + /api/comments/: + get: + operationId: api_comments_list + description: Получить список существующих комментариев + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCommentReadList' + description: '' + post: + operationId: api_comments_create + description: Создать комментарий + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CommentCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CommentCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/CommentCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/CommentCreate' + description: '' + /api/comments/{id}/: + get: + operationId: api_comments_retrieve + description: Получить комментарий по его ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Комментария. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CommentRead' + description: '' + put: + operationId: api_comments_update + description: Изменить комментарий + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Комментария. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CommentCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CommentCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/CommentCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CommentCreate' + description: '' + patch: + operationId: api_comments_partial_update + description: Изменить комментарий частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Комментария. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCommentCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCommentCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCommentCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CommentCreate' + description: '' + delete: + operationId: api_comments_destroy + description: Удалить комментарий + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) Комментария. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '204': + description: '' + /api/companies/{tenant_id}/status-model/: + get: + operationId: api_companies_status_model_retrieve + description: Получить список статусных моделей для данной компании + parameters: + - in: path + name: tenant_id + schema: + type: integer + required: true + description: ID компании + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: object + properties: + id: + type: integer + description: ID статусной модели + title: + type: string + description: Название статусной модели + attribute_id: + type: integer + description: ID атрибута + tenant_id: + type: integer + description: ID компании + statuses: + type: array + description: Статусы + items: + type: object + properties: + id: + type: integer + description: ID статуса + status_model: + type: integer + description: ID статусной модели + value_option_id: + type: integer + description: ID опции значения + color: + type: string + description: Цветовой HEX код + name: + type: string + description: Техническое имя статуса + label: + type: string + description: Отображаемое имя статуса + transitions: + type: object + description: Связи с другими статусами + properties: + inputs: + type: array + items: + type: object + properties: + status_id: + type: integer + description: ID входящего статуса + permissions: + type: array + items: + type: object + properties: + id: + type: integer + description: ID разрешения + service_account_id: + type: integer + description: ID служебного аккаунта + type: + type: string + description: Роль + outputs: + type: array + items: + type: object + properties: + status_id: + type: integer + description: ID целевого статуса + permissions: + type: array + items: + type: object + properties: + id: + type: integer + description: ID разрешения + service_account_id: + type: integer + description: ID служебного аккаунта + type: + type: string + description: Роль + example: + id: 63 + title: "Тестовая статусная модель" + attribute_id: 1 + tenant_id: 1 + statuses: + - id: 1 + status_model: 63 + value_option_id: 1 + color: "#43c079" + name: "created" + label: "Открыто" + transitions: + inputs: [ ] + outputs: + - status_id: 2 + permissions: + - id: 6 + service_account_id: null + type: "admin" + - id: 7 + service_account_id: null + type: "author" + - id: 8 + service_account_id: null + type: "responsible" + - id: 2 + status_model: 63 + value_option_id: 1 + color: "#f0a401" + name: "in_progress" + label: "В процессе" + transitions: + inputs: + - status_id: 1 + permissions: + - id: 6 + service_account_id: null + type: "admin" + - id: 7 + service_account_id: null + type: "author" + - id: 8 + service_account_id: null + type: "responsible" + outputs: + - status_id: 3 + permissions: + - id: 9 + service_account_id: null + type: "admin" + - id: 10 + service_account_id: null + type: "author" + - id: 3 + status_model: 63 + value_option_id: 1 + color: "#ff5c4a" + name: "done" + label: "Закрыто" + transitions: + inputs: + - status_id: 2 + permissions: + - id: 9 + service_account_id: null + type: "admin" + - id: 10 + service_account_id: null + type: "author" + outputs: [ ] + description: '' + /api/companies/{tenant_id}/status-model/v2/: + get: + operationId: api_companies_status_model_v2_retrieve + description: Получить список групп статусных моделей для данной компании + parameters: + - in: path + name: tenant_id + schema: + type: integer + required: true + description: ID компании + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + properties: + groups: + type: object + properties: + default_company_statuses: + type: array + description: Набор статусов по умолчанию + items: + type: object + properties: + id: + type: integer + description: ID статуса + status_model: + type: integer + description: Статусная модель + value_option_id: + type: integer + description: ID опции значения + color: + type: string + description: Цветовой HEX код + name: + type: string + description: Техническое имя статуса + label: + type: string + description: Отображаемое имя статуса + transitions: + type: object + description: Связи с другими статусами + properties: + inputs: + type: array + items: + type: object + properties: + status_id: + type: integer + description: ID входящего статуса + permissions: + type: array + items: + type: object + properties: + id: + type: integer + description: ID разрешения + service_account_id: + type: integer + description: ID служебного аккаунта + type: + type: string + description: Роль + outputs: + type: array + items: + type: object + properties: + status_id: + type: integer + description: ID целевого статуса + permissions: + type: array + items: + type: object + properties: + id: + type: integer + description: ID разрешения + service_account_id: + type: integer + description: ID служебного аккаунта + type: + type: string + description: Роль + per_project_statuses: + type: object + description: Статусы по проекту + per_target_statuses: + type: object + description: Статусы по объекту + example: + groups: + default_company_statuses: + - id: 3 + status_model: 63 + value_option_id: 1 + color: "#ff5c4a" + name: "done" + label: "Закрыто" + transitions: + inputs: + - status_id: 2 + permissions: + - id: 9 + service_account_id: null + type: "admin" + - id: 10 + service_account_id: null + type: "author" + outputs: [ ] + - id: 2 + status_model: 63 + value_option_id: 1 + color: "#f0a401" + name: "in_progress" + label: "В процессе" + transitions: + inputs: + - status_id: 1 + permissions: + - id: 6 + service_account_id: null + type: "admin" + - id: 7 + service_account_id: null + type: "author" + - id: 8 + service_account_id: null + type: "responsible" + outputs: + - status_id: 3 + permissions: + - id: 9 + service_account_id: null + type: "admin" + - id: 10 + service_account_id: null + type: "author" + - id: 1 + status_model: 63 + value_option_id: 1 + color: "#43c079" + name: "created" + label: "Открыто" + transitions: + inputs: [ ] + outputs: + - status_id: 2 + permissions: + - id: 6 + service_account_id: null + type: "admin" + - id: 7 + service_account_id: null + type: "author" + - id: 8 + service_account_id: null + type: "responsible" + /api/issue-changes/: + get: + operationId: api_issue_changes_list + description: Получить список изменений в замечаниях. + parameters: + - in: query + name: issue_id + schema: + type: string + format: uuid + description: UUID замечания + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + - in: query + name: resource_id + schema: + type: string + format: uuid + description: UUID ресурса (проекта) + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedIssueChangeList' + description: '' + post: + operationId: api_issue_changes_create + description: Создать запись об изменении замечания (происходит автоматически при изменении замечания) + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueChange' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueChange' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + description: '' + /api/issue-changes/{id}/: + get: + operationId: api_issue_changes_retrieve + description: Получить запись об изменении замечания по ID. + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) изменения в замечании. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + description: '' + put: + operationId: api_issue_changes_update + description: Отредактировать запись об изменении замечания + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) изменения в замечании. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueChange' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueChange' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + description: '' + patch: + operationId: api_issue_changes_partial_update + description: Отредактировать запись об изменении замечания частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) изменения в замечании. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedIssueChange' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIssueChange' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIssueChange' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueChange' + description: '' + delete: + operationId: api_issue_changes_destroy + description: Удалить запись об изменении замечания + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) изменения в замечании. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: '' + /api/issues/: + get: + operationId: api_issues_list + description: Получить список всех замечаний + parameters: + - in: query + name: author_id + schema: + type: string + description: ID автора + - in: query + name: bundle_id + schema: + type: string + format: uuid + description: ID бандла + - in: query + name: created_at_gte + schema: + type: string + format: date-time + description: Создано после указанных даты и времени + - in: query + name: created_at_lte + schema: + type: string + format: date-time + description: Создано до указанных даты и времени + - in: query + name: deadline_gte + schema: + type: string + format: date-time + description: Срок исполнения после указанных даты и времени + - in: query + name: deadline_lte + schema: + type: string + format: date-time + description: Срок исполнения до указанных даты и времени + - in: query + name: document_id + schema: + type: integer + description: ID документа + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + - in: query + name: resource_id + schema: + type: string + description: UUID ресурса (проекта) + - in: query + name: responsible_users + schema: + type: string + description: Список ответственных пользователей + - in: query + name: status + schema: + type: string + description: Статус + - in: query + name: status_id + schema: + type: string + description: Статус (аналогично предыдущему) + - in: query + name: status_ids + schema: + type: string + description: Список статусов + - in: query + name: target_id + schema: + type: string + description: ID проекта + - in: query + name: tenant_id (or company_id) + schema: + type: string + description: ID компании + - in: query + name: workspace_id + schema: + type: string + description: ID рабочего пространства Workspace + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedIssueReadList' + description: '' + post: + operationId: api_issues_create + description: Создать новое замечание + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCreate' + description: '' + /api/issues/{id}/: + get: + operationId: api_issues_retrieve + description: Получить замечание по его ID + parameters: + - in: path + name: id + schema: + type: string + description: UUID замечания. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueRead' + description: '' + put: + operationId: api_issues_update + description: Изменить (редактировать) замечание + parameters: + - in: path + name: id + schema: + type: string + description: UUID замечания. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCreate' + description: '' + patch: + operationId: api_issues_partial_update + description: Изменить (редактировать) замечание частично + parameters: + - in: path + name: id + schema: + type: string + description: UUID замечания. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedIssueCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIssueCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIssueCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCreate' + description: '' + delete: + operationId: api_issues_destroy + description: Удалить замечание (soft_delete) + parameters: + - in: path + name: id + schema: + type: string + description: UUID замечания. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '204': + description: '' + /api/issues/count/: + get: + operationId: api_issues_count_retrieve + description: Получить количество замечаний по версиям документа внутри этого документа + parameters: + - in: query + name: document_id + schema: + type: string + description: ID документа + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + type: object + properties: + results: + type: object + description: Результаты + additionalProperties: + type: object + description: Результаты для каждого document_id в запросе + properties: + id: + type: string + description: Идентификатор версии документа (UUID) + count: + type: integer + description: Количество замечаний + example: + results: + "80232": + "168ee056-ae4f-429c-ba9a-c2c620dc2d56": 3 + "16e488bf-773e-4106-9ef2-862bd7d37100": 14 + "b9b3399d-cab3-4c7b-9e6d-9812fb30974d": 64 + "495e5d8e-11f1-4424-b24c-f1868505ded2": 2 + "5a0b2b85-fb7d-43ca-ab49-b6e6ec9af362": 6 + description: '' + /api/issues/daterange/: + get: + operationId: api_issues_daterange_retrieve + description: Получить минимальное и максимальное значения для полей даты и времени + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + properties: + ranges: + type: object + properties: + created_at: + type: object + properties: + min: + type: string + format: date-time + max: + type: string + format: date-time + deadline: + type: object + properties: + min: + type: string + format: date-time + max: + type: string + format: date-time + example: + ranges: + created_at: + min: "2022-10-28T05:44:51.494000+00:00" + max: "2024-04-17T08:52:12.587010+00:00" + deadline: + min: "1970-01-01T00:00:00+00:00" + max: "2024-11-30T15:00:00+00:00" + /api/issues/export/: + get: + operationId: api_issues_export_retrieve + description: Экспортировать реестр замечаний в xlsx файл (с учетом query параметров) + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: string + format: binary + description: '' + /api/issues/status-count/: + get: + operationId: api_issues_status_count_retrieve + description: Получить подсчет статусов для данной компании + parameters: + - in: query + name: tenant_id (or company_id) + schema: + type: string + description: ID компании + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + '1': + type: integer + description: Количество статусов с ID 1 + '2': + type: integer + description: Количество статусов с ID 2 + '3': + type: integer + description: Количество статусов с ID 3 + example: + "64f1a204-42a0-4d18-b991-61df5439d218": + '1': 0 + '2': 0 + '3': 1 + "eef223fb-0fdc-4c7b-bec7-ce8a145df108": + '1': 44 + '2': 13 + '3': 9 + /api/issues/status-count-v2/: + get: + operationId: api_issues_status_count_v2_retrieve + description: Получить подсчет статусов, сгруппированный по id проекта + parameters: + - in: query + name: tenant_id (or company_id) + schema: + type: string + description: ID компании + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: object + properties: + id: + type: integer + description: ID + label: + type: string + description: Название + color: + type: string + description: Цветовой HEX код + count: + type: integer + description: Количество + example: + "64f1a204-42a0-4d18-b991-61df5439d218": + - id: 3 + label: "Закрыто" + color: "#ff5c4a" + count: 1 + - id: 2 + label: "В процессе" + color: "#f0a401" + count: 0 + - id: 1 + label: "Открыто" + color: "#43c079" + count: 0 + "eef223fb-0fdc-4c7b-bec7-ce8a145df108": + - id: 3 + label: "Закрыто" + color: "#ff5c4a" + count: 9 + - id: 2 + label: "В процессе" + color: "#f0a401" + count: 13 + - id: 1 + label: "Открыто" + color: "#43c079" + count: 44 + description: '' + /api/status/: + get: + operationId: api_status_list + description: Получить список существующих статусов + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedIssueStatusList' + description: '' + post: + operationId: api_status_create + description: Создать новый статус + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueStatus' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueStatus' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + description: '' + /api/status-models/: + get: + operationId: api_status_models_list + description: Получить список существующих статусных моделей + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCustomStatusModelReadList' + description: '' + post: + operationId: api_status_models_create + description: Создать новую статусную модель + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + description: '' + /api/status-models/{id}/: + get: + operationId: api_status_models_retrieve + description: Получить статусную модель по ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статусной модели. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelRead' + description: '' + put: + operationId: api_status_models_update + description: Изменить статусную модель + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статусной модели. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + description: '' + patch: + operationId: api_status_models_partial_update + description: Изменить статусную модель частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статусной модели. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomStatusModelCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCustomStatusModelCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCustomStatusModelCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CustomStatusModelCreate' + description: '' + delete: + operationId: api_status_models_destroy + description: Удалить статусную модель + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статусной модели. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: '' + /api/status-relations/: + get: + operationId: api_status_relations_list + description: Получить список связей между статусами + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedIssueStatusRelationList' + description: '' + post: + operationId: api_status_relations_create + description: Создать связь между статусами + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + description: '' + /api/status-relations/{id}/: + get: + operationId: api_status_relations_retrieve + description: Получить связь между статусами по ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) существующей связи. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/StatusRelationResponse' + description: '' + put: + operationId: api_status_relations_update + description: Изменить связь между статусами + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) существующей связи. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + description: '' + patch: + operationId: api_status_relations_partial_update + description: Изменить связь между статусами частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) существующей связи. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedIssueStatusRelation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIssueStatusRelation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIssueStatusRelation' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatusRelation' + description: '' + delete: + operationId: api_status_relations_destroy + description: Удалить связь между статусами + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) существующей связи. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: '' + /api/status/{id}/: + get: + operationId: api_status_retrieve + description: Получить статус по ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статуса. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + description: '' + put: + operationId: api_status_update + description: Изменить (редактировать) статус + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статуса. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueStatus' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueStatus' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + description: '' + patch: + operationId: api_status_partial_update + description: Изменить (редактировать) статус частично + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статуса. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedIssueStatus' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIssueStatus' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIssueStatus' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueStatus' + description: '' + delete: + operationId: api_status_destroy + description: Удалить статус + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) статуса. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: '' + /api/transition-permissions/: + get: + operationId: api_transition_permissions_list + description: Получить список существующих разрешений для изменения статусов + parameters: + - name: limit + required: false + in: query + description: Количество элементов на страницу. + schema: + type: integer + - name: offset + required: false + in: query + description: Индекс начального элемента. + schema: + type: integer + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedIssueTransitionPermissionReadList' + description: '' + post: + operationId: api_transition_permissions_create + description: Создать новое разрешение для изменения статусов + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + description: '' + /api/transition-permissions/{id}/: + get: + operationId: api_transition_permissions_retrieve + description: Получить разрешение по ID + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) доступа к изменению статуса. + замечания. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionRead' + description: '' + put: + operationId: api_transition_permissions_update + description: Изменить (редактировать) разрешение на изменение статуса + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) доступа к изменению статуса. + замечания. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + required: true + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + description: '' + patch: + operationId: api_transition_permissions_partial_update + description: Изменить (редактировать частично) разрешение на изменение статуса + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) доступа к изменению статуса. + замечания. + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedIssueTransitionPermissionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIssueTransitionPermissionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIssueTransitionPermissionCreate' + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/IssueTransitionPermissionCreate' + description: '' + delete: + operationId: api_transition_permissions_destroy + description: Удалить доступ к изменению статуса + parameters: + - in: path + name: id + schema: + type: integer + description: Уникальный идентификатор (ID) доступа к изменению статуса. + замечания. + required: true + tags: + - api + security: + - jwtAuth: [] + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: '' +components: + schemas: + AttachmentRead: + type: object + properties: + id: + type: integer + readOnly: true + title: ID файла + file_name: + type: string + readOnly: true + title: Имя файла + author_id: + type: integer + readOnly: true + title: ID автора + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время добавления файла + file: + type: string + format: uri + nullable: true + title: Файл + type: + type: string + readOnly: true + title: Тип файла + issues: + type: string + readOnly: true + nullable: true + title: Список UUID замечаний, в которых данный файл использован в качестве вложения + issue: + type: string + readOnly: true + title: UUID замечания, к которому приложен данный файл + required: + - author_id + - created_at + - file + - file_name + - id + - issue + - issues + - type + AttachmentWrite: + type: object + properties: + id: + type: integer + readOnly: true + title: ID файла + file_name: + type: string + title: Имя файла + maxLength: 512 + author_id: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + title: ID автора + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время добавления файла + file: + type: string + format: uri + nullable: true + title: Файл + type: + type: string + readOnly: true + title: Тип файла + issue: + type: string + format: uuid + nullable: true + title: UUID замечания, к которому приложен данный файл + required: + - author_id + - created_at + - file + - file_name + - id + - type + AttributesSnapshot: + type: object + properties: + snapshot_dt: + type: string + format: date-time + title: Дата и время актуальности атрибутов + data: + nullable: true + title: Значения атрибутов + CommentCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID комментария + author_id: + type: integer + title: ID автора + text: + type: string + title: Текст комментария + maxLength: 8192 + attachments: + type: array + items: + type: integer + title: Файлы, прикрепленные к комментарию + issue: + type: string + format: uuid + nullable: true + title: Замечание, к которому оставлен комментарий + reply_to_comment: + type: integer + nullable: true + title: Комментарий, в ответ на который оставлен комментарий + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания комментария + required: + - author_id + - created_at + - id + - issue + - text + CommentRead: + type: object + properties: + id: + type: integer + readOnly: true + title: ID комментария + author_id: + type: integer + readOnly: true + title: ID автора комментария + text: + type: string + readOnly: true + title: Текст комментария + attachments: + type: array + items: + $ref: '#/components/schemas/AttachmentRead' + readOnly: true + title: Файлы, прикрепленные к комментарию + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания комментария + updated_at: + type: string + format: date-time + readOnly: true + title: Дата и время обновления комментария + issue: + type: string + format: uuid + title: Замечание, к которому оставлен комментарий + reply_to_comment: + type: integer + title: Комментарий, в ответ на который оставлен комментарий + deleted_at: + type: string + format: date-time + readOnly: true + nullable: true + title: Дата и время удаления комментария + required: + - attachments + - author_id + - created_at + - deleted_at + - id + - issue + - text + - updated_at + CustomStatusModelCreate: + type: object + properties: + title: + type: string + maxLength: 512 + title: Название + attribute_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Атрибута + tenant_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Компании + statuses: + type: array + items: + $ref: '#/components/schemas/IssueStatus' + title: Список статусов, включенных в данную статусную модель (объектов IssueStatus) + required: + - attribute_id + - tenant_id + CustomStatusModelRead: + type: object + properties: + id: + type: integer + readOnly: true + title: ID статусной модели + title: + type: string + readOnly: true + title: Название + attribute_id: + type: integer + readOnly: true + title: ID Атрибута + tenant_id: + type: integer + readOnly: true + title: ID Компании + statuses: + type: array + items: + $ref: '#/components/schemas/IssueStatus' + readOnly: true + title: Список статусов, включенных в данную статусную модель (объектов IssueStatus) + required: + - attribute_id + - id + - statuses + - tenant_id + - title + IssueChange: + type: object + properties: + id: + type: string + format: uuid + title: ID записи об изменении + issue_id: + type: string + format: uuid + title: ID замечания, в которое вносятся изменения + cipher: + type: string + title: Шифр + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время внесения изменений в замечание + author_id: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + title: ID автора внесенных изменений + action: + type: string + title: Произведенное действие + maxLength: 512 + was: + type: string + title: Было + became: + type: string + title: Стало + required: + - action + - author_id + - became + - cipher + - created_at + - id + - issue_id + - was + IssueCreate: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + title: UUID замечания + cipher: + type: string + readOnly: true + title: Шифр + document_id: + type: integer + maximum: 2147483647 + minimum: 0 + nullable: true + title: ID документа + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания замечания + title: + type: string + title: Название замечания + author_id: + type: integer + title: ID автора + status: + type: integer + nullable: true + title: Статус + status_id: + type: integer + title: Статус (аналогично предыдущему, в запросе может передаваться один из вариантов или оба сразу) + tenant_id: + type: integer + maximum: 2147483647 + minimum: 0 + nullable: true + title: ID компании + completion_date: + type: string + format: date-time + nullable: true + title: Дата исполнения + attachments: + type: array + items: + type: integer + title: Прикрепленные файлы + target_id: + type: integer + nullable: true + title: ID проекта + workspace_id: + type: string + format: uuid + nullable: true + title: ID рабочего пространства + app_instance_id: + type: string + format: uuid + nullable: true + title: ID объекта приложения + state_id: + type: string + format: uuid + nullable: true + title: ID состояния + y_coordinate: + type: number + format: double + nullable: true + title: Координата по Y + x_coordinate: + type: number + format: double + nullable: true + title: Координата по X + z_coordinate: + type: number + format: double + nullable: true + title: Координата по Z + bundle_id: + type: string + format: uuid + nullable: true + title: ID бандла + meta: + nullable: true + title: Мета данные + resource_id: + type: string + format: uuid + title: UUID ресурса (проекта) + user: + readOnly: true + title: Пользователь + comments: + type: array + title: Комментарии + items: + $ref: '#/components/schemas/CommentCreate' + attributes: + title: Атрибуты + items: + $ref: '#/components/schemas/AttributesSnapshot' + note: + type: string + nullable: true + title: Описание замечания + required: + - cipher + - created_at + - id + - user + IssueRead: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + title: ID замечания + public_id: + type: string + format: uuid + readOnly: true + title: Публичный ID замечания + cipher: + type: string + readOnly: true + title: Шифр + document_id: + type: integer + readOnly: true + nullable: true + title: ID документа + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания замечания + updated_at: + type: string + format: date-time + readOnly: true + title: Дата и время обновления замечания + title: + type: string + readOnly: true + title: Заголовок + author_id: + type: integer + readOnly: true + title: ID автора замечания + status: + readOnly: true + title: Статус (объект) + items: + $ref: '#/components/schemas/IssueStatus' + status_id: + type: integer + title: Статус (id) + tenant_id: + type: integer + readOnly: true + nullable: true + title: ID компании + completion_date: + type: string + format: date-time + readOnly: true + nullable: true + title: Дата исполнения + responsible_users: + type: array + items: + type: integer + readOnly: true + title: Список ID ответственных пользователей + attachments: + type: array + items: + type: integer + title: Прикрепленные файлы + target_id: + type: integer + readOnly: true + nullable: true + title: ID проекта + workspace_id: + type: string + format: uuid + readOnly: true + nullable: true + title: ID рабочего пространства + app_instance_id: + type: string + format: uuid + readOnly: true + nullable: true + title: ID объекта приложения + deleted_at: + type: string + format: date-time + readOnly: true + nullable: true + title: Дата и время удаления замечания + state_id: + type: string + format: uuid + readOnly: true + nullable: true + title: ID состояния + y_coordinate: + type: number + format: double + readOnly: true + nullable: true + title: Координата по Y + x_coordinate: + type: number + format: double + readOnly: true + nullable: true + title: Координата по X + z_coordinate: + type: number + format: double + readOnly: true + nullable: true + title: Координата по Z + bundle_id: + type: string + format: uuid + readOnly: true + nullable: true + title: ID бандла + meta: + readOnly: true + nullable: true + title: Мета данные + resource_id: + type: string + format: uuid + readOnly: true + nullable: true + title: ID ресурса (проекта) + user: + type: object + readOnly: true + title: Пользователь + properties: + last_name: + type: string + description: Фамилия + first_name: + type: string + description: Имя + comments: + type: string + readOnly: true + title: Комментарии + comments_ids: + type: array + items: + type: integer + title: Список ID комментариев к данному замечанию + attributes: + type: array + title: Атрибуты + items: + type: object + properties: + id: + type: integer + title: ID атрибута + values: + type: array + items: + type: integer + title: Значения атрибута + note: + type: string + readOnly: true + title: Deprecated описание (старый комментарий) + required: + - app_instance_id + - attachments + - author_id + - bundle_id + - cipher + - comments + - comments_ids + - completion_date + - created_at + - deleted_at + - document_id + - id + - meta + - note + - public_id + - resource_id + - responsible_users + - state_id + - status + - status_id + - target_id + - tenant_id + - title + - updated_at + - user + - workspace_id + - x_coordinate + - y_coordinate + - z_coordinate + IssueStatus: + type: object + properties: + id: + type: integer + readOnly: true + title: ID статуса + status_model: + type: integer + nullable: true + title: Модель статусов + value_option_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Опций + color: + type: string + title: Цвет + pattern: ^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$ + maxLength: 25 + name: + type: string + title: Название + maxLength: 512 + label: + type: string + title: Отображаемое название + maxLength: 512 + transitions: + type: object + title: Связь с другими статусами + properties: + inputs: + type: array + items: + $ref: '#/components/schemas/IssueStatusRelation' + outputs: + type: array + items: + $ref: '#/components/schemas/IssueStatusRelation' + readOnly: true + required: + - id + - transitions + - value_option_id + IssueStatusRelation: + type: object + properties: + status_id: + type: integer + title: Статус ID + permissions: + type: array + items: + $ref: '#/components/schemas/IssueTransitionPermissionRead' + required: + - status_id + - permissions + StatusRelationResponse: + type: object + properties: + inputs: + type: integer + title: Приходящий статус (из какого статуса осуществляется переход в текущий) + outputs: + type: integer + title: Целевой статус (в какой статус осуществляется переход из текущего) + required: + - inputs + - outputs + IssueTransitionPermissionCreate: + type: object + properties: + id: + type: integer + readOnly: true + service_account_id: + type: string + format: uuid + nullable: true + title: Служебный аккаунт + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Роль + relation: + type: integer + title: Связь + required: + - id + - relation + - type + IssueTransitionPermissionRead: + type: object + properties: + id: + type: integer + readOnly: true + service_account_id: + type: string + format: uuid + readOnly: true + nullable: true + title: Служебный аккаунт + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + readOnly: true + title: Роль + required: + - id + - service_account_id + - type + PaginatedAttachmentReadList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/attachments/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/attachments/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/AttachmentRead' + PaginatedCommentReadList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/comments/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/comments/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/CommentRead' + PaginatedCustomStatusModelReadList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/status-models/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/status-models/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/CustomStatusModelRead' + PaginatedIssueChangeList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/issue-changes/?limit=100&offset=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/issue-changes/?limit=100&offset=100 + results: + type: array + items: + $ref: '#/components/schemas/IssueChange' + PaginatedIssueReadList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/issues/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/issues/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/IssueRead' + PaginatedIssueStatusList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/status/?limit=100&offset=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/status/?limit=100&offset=100 + results: + type: array + items: + $ref: '#/components/schemas/IssueStatus' + PaginatedIssueStatusRelationList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/status-relations/?limit=100&offset=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/status-relations/?limit=100&offset=100 + results: + type: array + items: + $ref: '#/components/schemas/IssueStatusRelation' + PaginatedIssueTransitionPermissionReadList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://example.org/api/transition-permissions/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://example.org/api/transition-permissions/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/IssueTransitionPermissionRead' + PatchedAttachmentWrite: + type: object + properties: + id: + type: integer + readOnly: true + file_name: + type: string + title: Имя файла + maxLength: 512 + author_id: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + title: ID автора + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время добавления файла + file: + type: string + format: uri + nullable: true + title: Файл + type: + type: string + readOnly: true + title: Тип файла + issue: + type: string + format: uuid + nullable: true + title: UUID замечания, к которому приложен данный файл + PatchedCommentCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID комментария + author_id: + type: integer + title: ID автора + text: + type: string + title: Текст комментария + maxLength: 8192 + attachments: + type: array + items: + type: integer + title: Файлы, прикрепленные к комментарию + issue: + type: string + format: uuid + nullable: true + title: Замечание, к которому оставлен комментарий + reply_to_comment: + type: integer + nullable: true + title: Комментарий, в ответ на который оставлен комментарий + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания комментария + PatchedCustomStatusModelCreate: + type: object + properties: + title: + type: string + maxLength: 512 + title: Название + attribute_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Атрибута + tenant_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Компании + statuses: + type: array + items: + $ref: '#/components/schemas/IssueStatus' + title: Список статусов, включенных в данную статусную модель (объектов IssueStatus) + PatchedIssueChange: + type: object + properties: + id: + type: string + format: uuid + title: ID записи об изменении замечания + issue_id: + type: string + format: uuid + title: ID замечания, в которое вносятся изменения + cipher: + type: string + title: Шифр + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время внесения изменений в замечание + author_id: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + title: ID автора внесенных изменений + action: + type: string + title: Произведенное действие + maxLength: 512 + was: + type: string + title: Было + became: + type: string + title: Стало + PatchedIssueCreate: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + title: UUID замечания + cipher: + type: string + readOnly: true + title: Шифр + document_id: + type: integer + maximum: 2147483647 + minimum: 0 + nullable: true + title: ID документа + created_at: + type: string + format: date-time + readOnly: true + title: Дата и время создания замечания + title: + type: string + title: Название замечания + author_id: + type: integer + title: ID автора + status: + type: integer + nullable: true + title: Статус + status_id: + type: integer + title: Статус + tenant_id: + type: integer + maximum: 2147483647 + minimum: 0 + nullable: true + title: ID компании + completion_date: + type: string + format: date-time + nullable: true + title: Дата исполнения + attachments: + type: array + items: + type: integer + title: Прикрепленные файлы + target_id: + type: integer + nullable: true + title: ID проекта + workspace_id: + type: string + format: uuid + nullable: true + title: ID рабочего пространства + app_instance_id: + type: string + format: uuid + nullable: true + title: ID объекта приложения + state_id: + type: string + format: uuid + nullable: true + title: ID состояния + y_coordinate: + type: number + format: double + nullable: true + title: Координата по Y + x_coordinate: + type: number + format: double + nullable: true + title: Координата по X + z_coordinate: + type: number + format: double + nullable: true + title: Координата по Z + bundle_id: + type: string + format: uuid + nullable: true + title: ID бандла + meta: + nullable: true + title: Мета данные + resource_id: + type: string + format: uuid + title: ID ресурса (проекта) + user: + readOnly: true + title: Пользователь + comments: + type: array + title: Комментарии + items: + $ref: '#/components/schemas/CommentCreate' + attributes: + title: Атрибуты + items: + $ref: '#/components/schemas/AttributesSnapshot' + note: + type: string + nullable: true + title: Описание + PatchedIssueStatus: + type: object + properties: + id: + type: integer + readOnly: true + title: ID статуса + status_model: + type: integer + nullable: true + title: Модель статусов + value_option_id: + type: integer + maximum: 2147483647 + minimum: 0 + title: ID Опций + color: + type: string + title: Цвет + pattern: ^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$ + maxLength: 25 + name: + type: string + title: Название + maxLength: 512 + label: + type: string + title: Отображаемое название + maxLength: 512 + transitions: + type: object + additionalProperties: + type: array + items: {} + readOnly: true + PatchedIssueStatusRelation: + type: object + properties: + inputs: + type: integer + title: Приходящий статус (из какого статуса осуществляется переход в текущий) + outputs: + type: integer + title: Целевой статус (в какой статус осуществляется переход из текущего) + PatchedIssueTransitionPermissionCreate: + type: object + properties: + id: + type: integer + readOnly: true + service_account_id: + type: string + format: uuid + nullable: true + title: ID Пользователя + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Роль + relation: + type: integer + title: Связь + TypeEnum: + enum: + - admin + - author + - responsible + type: string + description: |- + * `admin` - ADMIN + * `author` - AUTHOR + * `responsible` - RESPONSIBLE + securitySchemes: + basicAuth: + type: http + scheme: basic + cookieAuth: + type: apiKey + in: cookie + name: sessionid + jwtAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/apps/mapper/.env.example b/apps/mapper/.env.example new file mode 100644 index 0000000..68c57f2 --- /dev/null +++ b/apps/mapper/.env.example @@ -0,0 +1,43 @@ +# Mapper (flows mapper) — пример переменных окружения +# Все переменные читаются классами pydantic-settings в src/app/config.py. +# У каждого класса свой env_prefix; вложенного делимитера ("__") НЕТ. +# Значения ниже — дефолты из кода (stage-хосты). Приложение НЕ загружает .env +# автоматически (env_file не задан) — переменные нужно экспортировать в окружение. + +# App (класс Settings, без префикса) +API_PREFIX=/api/v1 + +# Logger (префикс LOG_) +LOG_LEVEL=INFO +LOG_FORMAT='[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s' + +# Redis (префикс REDIS_) — кеш ответов внешних сервисов +REDIS_USE=True +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_EXPIRE_DAYS=1 + +# Documentation service (префикс DOCUMENTATION_) +DOCUMENTATION_HOST=https://stage-api.sarex.io/documentations/api/v1 +DOCUMENTATION_TIMEOUT=30 +DOCUMENTATION_RETRIES=3 + +# Flow service (префикс FLOW_) +FLOW_HOST=https://stage-api.sarex.io/flows/api/v1 +FLOW_TIMEOUT=30 +FLOW_RETRIES=3 + +# Django / sarex-backend (префикс DJANGO_) +DJANGO_HOST=https://stage.sarex.io/api +DJANGO_TIMEOUT=30 +DJANGO_RETRIES=3 + +# Note service (префикс NOTE_) +NOTE_HOST=https://stage-api.sarex.io/notes/api/v1 +NOTE_TIMEOUT=30 +NOTE_RETRIES=3 + +# ВНИМАНИЕ: переменная TIMEOUT (без префикса), которая задаётся в Helm/Kustomize +# как "120", НИ ОДНИМ классом настроек не читается. Реальный таймаут HTTP-клиентов +# берётся из _TIMEOUT (по умолчанию 30). См. CONFIGURATION.md. diff --git a/apps/mapper/CONFIGURATION.md b/apps/mapper/CONFIGURATION.md new file mode 100644 index 0000000..0483a67 --- /dev/null +++ b/apps/mapper/CONFIGURATION.md @@ -0,0 +1,193 @@ +# Конфигурация проекта mapper (flows mapper) + +Документ описывает все переменные окружения и способы конфигурирования сервиса `mapper` — mini-HTTP-сервиса, который объединяет (мапит) данные сервисов документации (`documentations`) и процессов (`flows`), а также заметок (`notes`) и Django-бэкенда Sarex. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/app/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). + +В отличие от многих сервисов, здесь **нет единого корневого префикса и нет вложенного делимитера** (`env_nested_delimiter`). Вместо этого каждая секция описана отдельным классом `BaseSettings` со своим `env_prefix` (задаётся во вложенном классе `Config`): + +| Класс | `env_prefix` | Секция | +| --- | --- | --- | +| `Settings` | — (без префикса) | Корневые настройки (`API_PREFIX`) | +| `LoggerSettings` | `LOG_` | Логирование | +| `RedisSettings` | `REDIS_` | Кеш Redis | +| `DocumentationSettings` | `DOCUMENTATION_` | HTTP-клиент сервиса документации | +| `FlowSettings` | `FLOW_` | HTTP-клиент сервиса процессов | +| `DjangoSettings` | `DJANGO_` | HTTP-клиент Django-бэкенда | +| `NoteSettings` | `NOTE_` | HTTP-клиент сервиса заметок | + +`DocumentationSettings`, `FlowSettings`, `DjangoSettings` и `NoteSettings` наследуются от общего класса `AsyncSessionManager` (поля `host`, `timeout`, `retries`), поэтому у каждого из них одинаковый набор из трёх переменных: `_HOST`, `_TIMEOUT`, `_RETRIES`. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически** (в `config.py` не задан `env_file`, зависимости `python-dotenv` нет) — переменные нужно экспортировать в окружение самому, напр. `set -a && . ./.env && set +a`, либо пробрасывать через контейнер/оркестратор. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (uvicorn/gunicorn) | Переменные окружения процесса. Redis поднимается через `docker-compose.yaml` (только сервис `redis`) | +| Контейнер | `Dockerfile` → `entrypoint.sh`: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` | +| Kubernetes (Helm, из репозитория сервиса) | `.helm/values.yaml`, блок `universal-chart.services.backend.envs`; деплой из `.gitlab-ci.yml` | +| Kubernetes (GitOps, инфра-репозиторий) | `iac/apps/mapper/*`: Kustomize-база `base/deployment.yaml` (env + секреты Vault) и Flux `HelmRelease` в overlay'ах `brusnika-*` | + +Точки входа: + +| Команда | Назначение | +| --- | --- | +| `uvicorn main:app` / `python app/main.py` | Локальный запуск (в `main.py` порт `8002`, host `0.0.0.0`) | +| `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` | Прод-запуск (`entrypoint.sh`, порт `8000`) | + +Стек: Python 3.10 (`python:3.10-slim-buster`), FastAPI, httpx (асинхронные клиенты), Redis (кеш), PyJWT (разбор токенов). + +## Переменные приложения + +Дефолт `—` означает, что значение обязательно (иначе ошибка старта). Все дефолты ниже соответствуют коду `config.py`. + +### App (класс `Settings`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_PREFIX` | string | `/api/v1` | Префикс маршрутов API | + +### Logger (`LOG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `INFO` | Уровень логирования (имя уровня `logging`; при неизвестном значении используется `INFO`) | +| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат строки лога | + +### Redis (`REDIS_*`) + +Кеширует JSON-ответы внешних сервисов (по ключу `"{user_id}_{url}"`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `REDIS_USE` | bool | `True` | Включить кеш. При `True` на старте выполняется `PING` (падение при недоступном Redis) | +| `REDIS_HOST` | string | `localhost` | Хост Redis | +| `REDIS_PORT` | int | `6379` | Порт Redis | +| `REDIS_DB` | int | `0` | Номер базы Redis | +| `REDIS_EXPIRE_DAYS` | int | `1` | TTL записей кеша в днях (в секундах — `expire_days * 24 * 3600`) | + +### HTTP-клиенты внешних сервисов + +Все четыре клиента наследуют `AsyncSessionManager` (`host`, `timeout`, `retries`). Клиент httpx создаётся с `verify=False` (проверка TLS-сертификата отключена) и транспортом с числом ретраев `retries`. Токены пробрасываются заголовками `Authorization` (всегда) и `Identity` (в режиме Zitadel). + +| Секция / префикс | Назначение | Дефолт `HOST` | +| --- | --- | --- | +| `DOCUMENTATION_*` | Сервис документации (диски, документы, бандлы) | `https://stage-api.sarex.io/documentations/api/v1` | +| `FLOW_*` | Сервис процессов (flows, review-данные) | `https://stage-api.sarex.io/flows/api/v1` | +| `DJANGO_*` | Django-бэкенд Sarex (target-links) | `https://stage.sarex.io/api` | +| `NOTE_*` | Сервис заметок (notes) | `https://stage-api.sarex.io/notes/api/v1` | + +Для каждого — три переменные (пример для `FLOW`): + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `FLOW_HOST` | string | см. выше | Базовый URL сервиса | +| `FLOW_TIMEOUT` | int | `30` | Таймаут запроса (сек) | +| `FLOW_RETRIES` | int | `3` | Число повторов транспорта httpx | + +## Аутентификация + +Аутентификация выполняется в `src/app/dependensies.py` (`get_user_data`) на основе заголовков запроса и **без проверки подписи токена** (`jwt.decode(..., options={"verify_signature": False})`). Публичный ключ не используется, отдельных переменных для ключа нет. + +| Условие | Режим | Как извлекается `user_id` | +| --- | --- | --- | +| Есть заголовки `Authorization` и `Identity` | `zitadel` | Из payload `Identity`-токена, поле `urn:zitadel:iam:user:metadata.user_id` (base64) | +| Есть только `Authorization` | `sarex` | Из payload основного токена, поле `user_id` | +| Заголовков нет | — | `401 Unauthorized` | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в сборке/запуске. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`mapper`), используется как `CHART_NAME` | +| `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile | +| `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` | Аргументы сборки / источник триггера (`app`) | +| `IMAGE_NAME`, `CI_COMMIT_SHA`, `CI_PROJECT_URL`, `CI_JOB_URL`, `CI_PROJECT_NAMESPACE` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Прокидываются в universal-chart (образ, commit, ссылки, owner) | + +## Переменные из Helm-чарта репозитория сервиса (`.helm/values.yaml`) + +Чарт зависит от `universal-chart` (`oci://cr.yandex/.../charts`, версия `0.1.7`). Переменные приложения задаются в блоке `services.backend.envs` с разбивкой по окружениям (`_default`/`stage`/`preprod`/`production`). + +| Переменная | `stage` | `preprod` | `production` | +| --- | --- | --- | --- | +| `DOCUMENTATION_HOST` | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.preprod.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | +| `FLOW_HOST` | `https://stage-api.sarex.io/flows/api/v1` | `https://api.preprod.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | +| `DJANGO_HOST` | `https://stage.sarex.io/api` | `https://preprod.sarex.io/api` | `https://lk.sarex.io/api` | +| `NOTE_HOST` | `https://stage-api.sarex.io/notes/api/v1` | `https://api.preprod.sarex.io/notes/api/v1` | `https://api.sarex.io/notes/api/v1` | +| `REDIS_USE` | `0` | `0` | `0` | +| `TIMEOUT` | `120` | `120` | `120` | + +Прочие параметры чарта (не переменные приложения): `deployment.*` (имя, порт `8000`, `replicaCount` 1/1/3/3, ресурсы, `probes.liveness/readiness` — **отключены**), `image.name` (`cr.yandex/.../mapper`), `service.*` (ClusterIP, порт `8000`), `imagePullSecrets` (`dockerhub`), `labels.monitoring=prometheus`. + +## Переменные из инфра-репозитория (`iac/apps/mapper`) + +GitOps-деплой через Kustomize + Flux, namespace `mapper`. Здесь же лежит настоящий документ. + +Структура: + +| Путь | Назначение | +| --- | --- | +| `base/` | Базовый Kustomize (`namespace`, `serviceaccount` `mapper-vault`, `deployment`, `service`) | +| `yc-k8s-test/` | Overlay поверх `base` (патч `replicas: 1`) | +| `brusnika-stage/` | Flux `HelmRelease` (universal-chart), хосты `test.sarex.brusnika.tech`, `imagePullSecrets: dockerhub` | +| `brusnika-prod/` | Flux `HelmRelease` (universal-chart), хосты `cde.brusnika.ru`, `imagePullSecrets: regcred` | + +Обычные env в `base/deployment.yaml` (production-хосты Sarex): + +| Переменная | Значение | +| --- | --- | +| `DOCUMENTATION_HOST` | `https://api.sarex.io/documentations/api/v1` | +| `FLOW_HOST` | `https://api.sarex.io/flows/api/v1` | +| `DJANGO_HOST` | `https://lk.sarex.io/api` | +| `NOTE_HOST` | `https://api.sarex.io/notes/api/v1` | +| `REDIS_USE` | `0` | +| `TIMEOUT` | `120` | + +Секреты монтируются через **HashiCorp Vault Agent** (аннотации `vault.hashicorp.com/*`, роль `mapper`) как файлы в `/vault/secrets/*`, которые перед стартом экспортируются в окружение (`set -a && . /vault/secrets/... && set +a`): + +| Файл секрета | Источник (Vault path) | Экспортируемые переменные | +| --- | --- | --- | +| `mapper-django-auth` | `secrets/data/vault/common/django_auth` | `MAPPER_DJANGO_TOKEN` | +| `mapper-db` | `secrets/data/postgresql/apps/mapper` | `MAPPER_DB_USER`, `MAPPER_DB_PASSWORD`, `MAPPER_DB_HOST`, `MAPPER_DB_PORT`, `MAPPER_DB_NAME` | +| `mapper-rabbitmq` | `secrets/data/rabbitmq/apps/mapper` | `MAPPER_RABBITMQ_VHOST`, `MAPPER_RABBITMQ_USERNAME`, `MAPPER_RABBITMQ_PASSWORD`, `MAPPER_RABBITMQ_HOST`, `MAPPER_RABBITMQ_PORT` | +| `mapper-s3` | `secrets/data/minio/apps/mapper` | `MAPPER_S3_ENDPOINT`, `MAPPER_S3_REGION`, `MAPPER_S3_BUCKET`, `MAPPER_S3_ACCESS_KEY_ID`, `MAPPER_S3_SECRET_ACCESS_KEY` | +| `mapper-kafka` | `secrets/data/kafka/apps/mapper` | `MAPPER_KAFKA_BOOTSTRAP_SERVERS`, `MAPPER_KAFKA_SECURITY_PROTOCOL`, `MAPPER_KAFKA_SASL_MECHANISM`, `MAPPER_KAFKA_USERNAME`, `MAPPER_KAFKA_PASSWORD` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу: + +| Условие | STAND | Namespace | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `platform` | `0.0.1-stage` | +| ветка `master` | `preprod` | `mapper-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `mapper-prod` | `0.0.1-prod` | +| merge request | — (сборка образа отключена) | — | — | + +Стадия `test`: job `linter` (`flake8 src/app`, `max-line-length=120`) и `typechecker` (`mypy src/app` с `types-redis`; `disallow_untyped_defs=True`). + +## Замечания и потенциальные проблемы + +- **`TIMEOUT` не читается приложением.** В Helm/Kustomize задаётся `TIMEOUT=120`, но клиенты читают `DOCUMENTATION_TIMEOUT`/`FLOW_TIMEOUT`/`DJANGO_TIMEOUT`/`NOTE_TIMEOUT` (каждый со своим префиксом). Без префикса переменная игнорируется — реальный таймаут остаётся `30`. Чтобы поднять таймаут, задавайте `_TIMEOUT`. +- **Секреты Vault не используются кодом.** `MAPPER_DB_*`, `MAPPER_RABBITMQ_*`, `MAPPER_S3_*`, `MAPPER_KAFKA_*`, `MAPPER_DJANGO_TOKEN` монтируются и экспортируются в окружение (`base/deployment.yaml`), но текущая версия приложения ни PostgreSQL, ни RabbitMQ, ни S3, ни Kafka, ни `MAPPER_DJANGO_TOKEN` **не читает** (в `config.py` таких настроек нет). Похоже, инфраструктура заготовлена наперёд либо унаследована из шаблона. +- **Кеш отключён во всех окружениях (`REDIS_USE=0`).** При этом в `get_response` (`utils.py`) при не-200 ответе апстрима и выключенном кеше возвращается `None`, а роутер отдаёт `400 Bad Request`. То есть при выключенном Redis запасного кеша нет. +- **Подпись JWT не проверяется** (`verify_signature=False`) ни в режиме `zitadel`, ни в `sarex`. Доверие к токену — на сетевом слое (Istio/ingress). Публичный ключ не настраивается. +- **TLS-проверка апстримов отключена** (`httpx.AsyncClient(verify=False)`) для всех четырёх клиентов. +- **Нет healthcheck-эндпоинта.** Пробы `liveness`/`readiness` в чарте выключены — это согласовано. +- **Порты различаются:** локально `main.py` слушает `8002`, в контейнере gunicorn — `8000` (проброшен в k8s Service). +- **`docker-compose.yaml`** поднимает только Redis (redis-stack-server); само приложение в compose не описано. + +## Минимальный набор для локального запуска + +Поднять Redis (`docker compose up redis`) либо задать `REDIS_USE=False`, затем `uvicorn main:app` из `src`. Минимально стоит задать (у остальных есть рабочие дефолты для stage): + +- `REDIS_USE` (`False`, если Redis не поднят) и при необходимости `REDIS_HOST`/`REDIS_PORT` +- при работе против нестандартных стендов — `DOCUMENTATION_HOST`, `FLOW_HOST`, `DJANGO_HOST`, `NOTE_HOST` +- `LOG_LEVEL` (по желанию) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/mapper/ENDPOINTS.md b/apps/mapper/ENDPOINTS.md new file mode 100644 index 0000000..a1c9785 --- /dev/null +++ b/apps/mapper/ENDPOINTS.md @@ -0,0 +1,95 @@ +# Эндпоинты сервиса mapper + +Документ описывает HTTP-интерфейс сервиса `mapper` (flows mapper): собственные эндпоинты, которые сервис публикует, и внешние эндпоинты сервисов Sarex, к которым он обращается для сборки ответа. + +## Как устроено взаимодействие + +`mapper` — асинхронный FastAPI-прокси-агрегатор. Каждый входящий запрос: + +1. проходит аутентификацию (`app/dependensies.py:get_user_data`) — из заголовков `Authorization` и опционального `Identity` извлекается `user_id` (подпись JWT не проверяется); +2. создаёт httpx-клиенты к нужным внешним сервисам (`app/config.py`, базовые хосты — из `*_HOST`, `verify=False`, заголовки авторизации пробрасываются); +3. параллельно-последовательно запрашивает 2 внешних сервиса через `ServiceManager.get_response()` (`app/utils.py`); +4. при `REDIS_USE=True` кеширует успешные (200) ответы в Redis по ключу `"{user_id}_{url}"`, а при ошибке апстрима возвращает данные из кеша; +5. объединяет ответы (`modify_pdm_data` / `modify_notes_data`) и отдаёт результат. + +Если любой из двух апстримов вернул `None` (ошибка и нет кеша) — роутер отвечает `400 Bad Request`. + +## Собственные эндпоинты (что публикует mapper) + +Базовый префикс — `API_PREFIX` (по умолчанию `/api/v1`). Оба эндпоинта требуют заголовок `Authorization` (и `Identity` для режима Zitadel). + +| Метод | Путь | Назначение | Ответ | +| --- | --- | --- | --- | +| GET | `/api/v1/disks/{disk}/documents/` | Документы диска, обогащённые review-данными из сервиса процессов | `object` (`{"documents": [...]}`) | +| GET | `/api/v1/notes/{service}/{entity}/{instance_id}/` | Заметки сущности, обогащённые target-links из Django-бэкенда | `array` (список заметок) | + +### `GET /api/v1/disks/{disk}/documents/` + +Параметры пути: `disk` (string). + +Логика (`routers.py:get_documents`): + +- запрос к **documentations**: `GET /disks/{disk}/documents` → берётся поле `documents`; +- запрос к **flows**: `GET /documents/?full=true`; +- `modify_pdm_data` матчит по `document_id`/`bundle_id` и добавляет `review_data` в соответствующие бандлы документов. + +### `GET /api/v1/notes/{service}/{entity}/{instance_id}/` + +Параметры пути: `service`, `entity`, `instance_id` (string). Дополнительно **все query-параметры запроса пробрасываются** в сервис заметок (к ним добавляется `full=true`). + +Логика (`routers.py:get_notes`): + +- запрос к **notes**: `GET /notes/{service}/{entity}/{instance_id}/?full=true&<проброшенные query>`; +- запрос к **Django**: `GET /core/target-links/` (полный путь — `{DJANGO_HOST}/core/target-links/`); +- `modify_notes_data` заменяет id-ссылки в поле `links` каждой заметки на объекты target-links. + +Полное описание схем — в `openapi.yaml`. + +## Внешние сервисы и базовые хосты по окружениям + +Базовые хосты берутся из `*_HOST` (`app/config.py`). Итоговый URL = `` + путь ниже. Значения по окружениям — из `.helm/values.yaml` (деплой из репозитория сервиса) и overlay'ов инфра-репозитория. + +| Сервис | Переменная | Дефолт в коде (stage) | preprod | production | brusnika-stage | brusnika-prod | +| --- | --- | --- | --- | --- | --- | --- | +| documentations | `DOCUMENTATION_HOST` | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.preprod.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | `https://test.sarex.brusnika.tech/documentations/api/v1` | `https://cde.brusnika.ru/documentations/api/v1` | +| flows | `FLOW_HOST` | `https://stage-api.sarex.io/flows/api/v1` | `https://api.preprod.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | `https://test.sarex.brusnika.tech/flows/api/v1` | `https://cde.brusnika.ru/flows/api/v1` | +| django (sarex-backend) | `DJANGO_HOST` | `https://stage.sarex.io/api` | `https://preprod.sarex.io/api` | `https://lk.sarex.io/api` | `https://test.sarex.brusnika.tech/api` | `https://cde.brusnika.ru/api` | +| notes | `NOTE_HOST` | `https://stage-api.sarex.io/notes/api/v1` | `https://api.preprod.sarex.io/notes/api/v1` | `https://api.sarex.io/notes/api/v1` | `https://test.sarex.brusnika.tech/notes/api/v1` | `https://cde.brusnika.ru/notes/api/v1` | + +## Эндпоинты внешних сервисов (что вызывает mapper) + +### `documentations` + +| Метод | Путь | Параметры | Назначение | Где используется | +| --- | --- | --- | --- | --- | +| GET | `/disks/{disk}/documents` | `disk` (path) | Документы диска (поле `documents` в ответе) | `get_documents` | + +### `flows` + +| Метод | Путь | Параметры | Назначение | Где используется | +| --- | --- | --- | --- | --- | +| GET | `/documents/` | `full=true` (query) | Документы процессов с review-данными | `get_documents` | + +### `notes` + +| Метод | Путь | Параметры | Назначение | Где используется | +| --- | --- | --- | --- | --- | +| GET | `/notes/{service}/{entity}/{instance_id}/` | `service`, `entity`, `instance_id` (path); `full=true` + проброшенные query | Заметки сущности | `get_notes` | + +### `django` (sarex-backend) + +| Метод | Путь | Параметры | Назначение | Где используется | +| --- | --- | --- | --- | --- | +| GET | `/core/target-links/` | — | Связи (target-links); из ответа берётся `results`, если ответ — объект | `get_notes` | + +## Заголовки и аутентификация + +- `Authorization: ` — обязателен для обоих эндпоинтов; пробрасывается во все внешние запросы как есть. +- `Identity: ` — опционален; при наличии включается режим Zitadel, заголовок также пробрасывается во внешние запросы. +- Ответы при ошибках: `401 Unauthorized` (нет `Authorization`), `400 Bad Request` (ошибка апстрима без кеша), `422 Unprocessable Entity` (ошибка валидации параметров пути, стандартный ответ FastAPI). + +## Замечания + +- В коде клиент к сервису заметок называется `NoteSettings`/`note`, к Django — `DjangoSettings`/`django`. Пути `/documents/` (flows) и `/notes/.../` (notes) содержат завершающий слэш — важно для совпадения с маршрутами апстрима. +- Кеш ключуется по `user_id` + URL, поэтому проброшенные query-параметры в `get_notes` не входят в ключ кеша (URL берётся без query). При включённом Redis это стоит учитывать. +- Healthcheck-эндпоинта у сервиса нет. diff --git a/apps/mapper/openapi.yaml b/apps/mapper/openapi.yaml new file mode 100644 index 0000000..344e6c5 --- /dev/null +++ b/apps/mapper/openapi.yaml @@ -0,0 +1,230 @@ +openapi: 3.0.3 + +info: + title: Mapper Service API + version: "1.0.0" + description: | + REST API сервиса **mapper** (flows mapper) — mini-HTTP-сервис, который + объединяет (мапит) данные нескольких сервисов Sarex: документы дисков из + сервиса документации (`documentations`) обогащаются review-данными из + сервиса процессов (`flows`); заметки из сервиса `notes` обогащаются + связями (target-links) из Django-бэкенда. + + Сервис написан на Python (**FastAPI**), приложение создаётся в + `src/app/main.py` (`app = FastAPI()`), маршруты — в `src/app/routers.py` + с префиксом `API_PREFIX` (по умолчанию `/api/v1`). + + ### Аутентификация + Оба эндпоинта требуют заголовок `Authorization`. Опциональный заголовок + `Identity` включает режим Zitadel. Подпись JWT **не проверяется** + (`verify_signature=False`, `src/app/dependensies.py`) — доверие к токену + обеспечивается сетевым слоем (Istio/ingress). Из токена извлекается + `user_id`, который используется как часть ключа кеша Redis. + + ### Агрегация и кеш + Каждый запрос обращается к двум внешним сервисам через `ServiceManager` + (`src/app/utils.py`). При `REDIS_USE=True` успешные (200) ответы апстрима + кешируются в Redis (ключ `"{user_id}_{url}"`, TTL `REDIS_EXPIRE_DAYS` + суток), а при ошибке апстрима отдаётся кеш. Если хотя бы один апстрим + вернул ошибку и кеша нет — сервис отвечает `400 Bad Request`. + + Схемы ответов заданы как свободные JSON-структуры (`object`/`array`), + так как сервис проксирует и объединяет ответы внешних сервисов без + фиксированной модели. + +servers: + - url: https://api.sarex.io/mapper + description: production (Sarex) + - url: https://stage-api.sarex.io/mapper + description: stage (Sarex) + - url: https://cde.brusnika.ru/mapper + description: production (Brusnika) + - url: https://test.sarex.brusnika.tech/mapper + description: stage (Brusnika) + +tags: + - name: documents + description: Документы дисков, обогащённые review-данными процессов + - name: notes + description: Заметки, обогащённые связями (target-links) + +paths: + /api/v1/disks/{disk}/documents/: + get: + tags: + - documents + summary: Документы диска с review-данными + operationId: get_documents + description: | + Возвращает документы диска из сервиса документации, обогащённые + review-данными из сервиса процессов. Внутри выполняются запросы + `GET {DOCUMENTATION_HOST}/disks/{disk}/documents` и + `GET {FLOW_HOST}/documents/?full=true`, после чего review-данные + добавляются в поле `review_data` соответствующих бандлов документов + (`modify_pdm_data`). + security: + - bearerAuth: [] + parameters: + - name: disk + in: path + required: true + description: Идентификатор диска + schema: + type: string + - name: Authorization + in: header + required: true + description: Токен доступа (пробрасывается во внешние сервисы) + schema: + type: string + - name: Identity + in: header + required: false + description: Identity-токен (включает режим Zitadel) + schema: + type: string + responses: + "200": + description: Успешный ответ + content: + application/json: + schema: + $ref: "#/components/schemas/DocumentsResponse" + "400": + description: Один из внешних сервисов недоступен и данных в кеше нет + "401": + description: Отсутствует заголовок Authorization + "422": + $ref: "#/components/responses/ValidationError" + + /api/v1/notes/{service}/{entity}/{instance_id}/: + get: + tags: + - notes + summary: Заметки сущности со связями (target-links) + operationId: get_notes + description: | + Возвращает заметки сущности из сервиса `notes`, у которых поле `links` + заменено на объекты связей (target-links) из Django-бэкенда. Внутри + выполняются запросы + `GET {NOTE_HOST}/notes/{service}/{entity}/{instance_id}/?full=true` + (с проброской всех query-параметров запроса) и + `GET {DJANGO_HOST}/core/target-links/`, после чего выполняется + объединение (`modify_notes_data`). + security: + - bearerAuth: [] + parameters: + - name: service + in: path + required: true + description: Логическое имя сервиса-владельца сущности + schema: + type: string + - name: entity + in: path + required: true + description: Тип сущности + schema: + type: string + - name: instance_id + in: path + required: true + description: Идентификатор экземпляра сущности + schema: + type: string + - name: Authorization + in: header + required: true + description: Токен доступа (пробрасывается во внешние сервисы) + schema: + type: string + - name: Identity + in: header + required: false + description: Identity-токен (включает режим Zitadel) + schema: + type: string + responses: + "200": + description: | + Успешный ответ — список заметок. Все дополнительные query-параметры + запроса проксируются в сервис заметок. + content: + application/json: + schema: + $ref: "#/components/schemas/NotesResponse" + "400": + description: Один из внешних сервисов недоступен и данных в кеше нет + "401": + description: Отсутствует заголовок Authorization + "422": + $ref: "#/components/responses/ValidationError" + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Токен передаётся заголовком `Authorization`. Подпись не проверяется + приложением. Для режима Zitadel дополнительно передаётся заголовок + `Identity`. + + responses: + ValidationError: + description: Ошибка валидации параметров запроса + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + + schemas: + DocumentsResponse: + type: object + description: | + Ответ агрегатора документов. Структура повторяет ответ сервиса + документации, где в бандлы добавлено поле `review_data` с данными + из сервиса процессов. + properties: + documents: + type: array + items: + type: object + additionalProperties: true + additionalProperties: true + + NotesResponse: + type: array + description: | + Список заметок сервиса `notes`, где поле `links` каждой заметки + заменено на объекты связей (target-links). + items: + type: object + additionalProperties: true + + ValidationErrorItem: + type: object + properties: + loc: + type: array + items: + anyOf: + - type: string + - type: integer + msg: + type: string + type: + type: string + required: + - loc + - msg + - type + + HTTPValidationError: + type: object + properties: + detail: + type: array + items: + $ref: "#/components/schemas/ValidationErrorItem" diff --git a/apps/measurements/.env.example b/apps/measurements/.env.example new file mode 100644 index 0000000..16be797 --- /dev/null +++ b/apps/measurements/.env.example @@ -0,0 +1,50 @@ +# ============================================================================ +# measurements — пример переменных окружения (HTTP-сервис на FastAPI) +# +# Скопируйте нужные строки в config.env / .env сервиса. +# Переменные, помеченные (обяз.), обязательны — без них процесс не стартует. +# Конфигурация читается через pydantic-settings (src/measurements/config.py). +# bool принимает 1/0, true/false, yes/no. +# ============================================================================ + +# --- S3 / MinIO (обяз.) ----------------------------------------------------- +# Единственная обязательная переменная. JSON-строка с доступами к S3. +# Разбирается в S3CredentialsSettings.from_env(); если не задана — +# ValueError и процесс не стартует. +# Поля: host, login, password (обяз.), verify (bool, по умолч. false), +# buckets (список; если пуст — читается через list_buckets()). +S3_JSON_SETTINGS='{"host":"https://s3.example.com","login":"login","password":"password","verify":false,"buckets":["measurements"]}' + +# --- Логирование (префикс LOG_) --------------------------------------------- +LOG_LEVEL=INFO # уровень логирования (по умолчанию INFO) +# LOG_FORMAT='{"timestamp": "%(asctime)s", "level": "%(levelname)s", "message": "%(message)s"}' # формат JSON-лога + +# --- Приложение (ApplicationSettings, без префикса) ------------------------- +# AUTH=0 # включить CustomAuthenticationMiddleware (по умолч. false) +# SHOW_UI=0 # показывать Swagger/redoc (по умолч. false — docs отключены) +# USE_SENTRY=0 # инициализировать Sentry (по умолч. false) +# DEBUG=0 # флаг отладки (по умолч. false) +# CLASSIC_MODE=1 # классический режим расчётов (по умолч. true) +# BLOCK_SIZE=256 # размер блока обработки растра (по умолч. 256) +# BLOCK_SIZE_FACTOR=10 # множитель площади блока (по умолч. 10) +# CPU_NUMBER=10 # число используемых CPU (по умолч. 10) + +# --- Django / ЛК (префикс DJANGO_; читается при AUTH=1) ---------------------- +# DJANGO_USE=1 # использовать интеграцию с Django (по умолч. true) +DJANGO_HOST=https://lk.sarex.io # базовый URL Django/ЛК (по умолч. https://lk.sarex.io) +# DJANGO_TIMEOUT=10 # таймаут HTTP-запросов к Django, сек (по умолч. 10) + +# --- Sentry (префикс SENTRY_; читается при USE_SENTRY=1) --------------------- +# SENTRY_DSN= # DSN проекта Sentry (по умолч. пусто) +# SENTRY_ENVIRONMENT=production # окружение (по умолч. production) +# SENTRY_TRACES_SAMPLE_RATE=1.0 # доля трейсов (по умолч. 1.0) +# SENTRY_SEND_DEFAULT_PII=1 # отправлять PII (по умолч. true) + +# --- Трейсинг OpenTelemetry (префикс TRACING_; читается при TRACING_USE=1) --- +TRACING_USE=0 # включить OTEL-трейсинг и otel-логгер (по умолч. false) +# TRACING_HOST=localhost:4317 # адрес OTLP-коллектора (по умолч. localhost:4317) +# TRACING_SERVICE_NAME=measurements # имя сервиса в трейсах (по умолч. measurements) +# TRACING_INSECURE=0 # подключение без TLS (по умолч. false) + +# --- Задаётся в манифестах, кодом приложения НЕ читается --------------------- +# S3_JSON_FILE=/opt/cred_s3.json # присутствует в .helm/values.yaml, но код читает только S3_JSON_SETTINGS diff --git a/apps/measurements/CONFIGURATION.md b/apps/measurements/CONFIGURATION.md new file mode 100644 index 0000000..d0f993b --- /dev/null +++ b/apps/measurements/CONFIGURATION.md @@ -0,0 +1,150 @@ +# Конфигурация measurements + +Документ описывает все переменные окружения и способы конфигурирования сервиса репозитория `measurements`: + +- **measurements** — HTTP-сервис на FastAPI (`src/measurements`), запускается через gunicorn/uvicorn (`entrypoint.sh`, `measurements.main:app`). Считает измерения по растрам (GeoTIFF), читая их напрямую из S3/MinIO через GDAL (`vsis3`). Отдельного воркера у сервиса нет. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/measurements/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): классы `LoggerSettings`, `SentrySettings`, `DjangoSettings`, `ApplicationSettings`, `TraceSettings`, `S3CredentialsSettings`, `Store`. Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Каждый класс задаёт свой префикс через `class Config: env_prefix` (`LOG_`, `SENTRY_`, `DJANGO_`, `TRACING_`, `S3_`); у `ApplicationSettings` префикса нет — её поля читаются по имени напрямую (`AUTH`, `SHOW_UI`, `USE_SENTRY` и т.п.). Почти все переменные имеют значения по умолчанию, поэтому обязательна фактически одна — **`S3_JSON_SETTINGS`**: её отсутствие приводит к `ValueError` в `S3CredentialsSettings.from_env()` и процесс не стартует. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | `docker-compose.yaml` — образ `measurements`, проброс порта `8000:8000`, инлайн `environment: S3_JSON_SETTINGS`. Сервис запускается `entrypoint.sh` (gunicorn, 4 воркера, uvicorn worker, таймаут 240) | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml` (universal-chart, dependency `oci://…/charts`): блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов). Per-env значения через ключи `_default`/`stage`/`preprod`/`production` | +| Kubernetes — этот infra-репозиторий (`iac/apps/measurements`) | `base/` — kustomize-манифесты с инъекцией секрета S3 через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + патч реплик), `brusnika-stage`/`brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блок `secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); в `workflow.rules` задаются переменные пайплайна (`STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `SERVICE_NAME`, `DOCKERFILE_PATH`) | + +**Миграции БД.** Отсутствуют. Сервис не хранит собственное состояние в реляционной БД (`psycopg2` присутствует в зависимостях, но код измерений работает с растрами из S3). Шага миграций в `entrypoint.sh` нет. + +--- + +## measurements (`measurements`) + +Переменные читаются набором классов `*Settings` в `config.py`, инстанцируемых на уровне модуля: `settings = ApplicationSettings()`, `store = Store()`, `logger = LoggerSettings().logger`, `tracing_settings = TraceSettings()`. + +### S3 / MinIO (обязательно) + +Класс `S3CredentialsSettings`. Единственный обязательный источник конфигурации — переменная `S3_JSON_SETTINGS` (JSON-строка). Валидатор `from_env` (`model_validator(mode='before')`) читает её из окружения и при отсутствии выбрасывает `ValueError`. Доступы к S3 используются как boto3-клиентом (список бакетов), так и GDAL (`AWS_S3_ENDPOINT`/`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, драйвер `vsis3`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `S3_JSON_SETTINGS` | string (JSON) | да | — | JSON с доступами к S3. Поля: `host`, `login`, `password` (обяз.); `verify` (bool, по умолч. `false`); `buckets` (список; если пуст — бакеты запрашиваются через `list_buckets()`). Пример: `{"host":"https://s3…","login":"…","password":"…","verify":false,"buckets":["measurements"]}` | + +> В `host` поддерживаются схемы `http://`/`https://`: при `http://` GDAL переключается на `AWS_HTTPS=NO`, отключает `GDAL_DISABLE_READDIR_ON_OPEN` и `AWS_VIRTUAL_HOSTING`. + +### Логирование (префикс `LOG_`) + +Класс `LoggerSettings`. Настраивает JSON-логгер (`python-json-logger`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `LOG_LEVEL` | string | нет | `INFO` | Уровень логирования (`INFO`/`DEBUG`/…); неизвестное значение → `INFO` | +| `LOG_FORMAT` | string | нет | JSON-шаблон | Формат строки лога для `JsonFormatter` | + +### Приложение (`ApplicationSettings`, без префикса) + +Поля читаются по имени напрямую (регистронезависимо). Управляют поведением сервиса и подключением middleware в `main.py`. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `AUTH` | bool | нет | `false` | Подключить `CustomAuthenticationMiddleware` (проверка JWT `authorization`/`identity`) | +| `SHOW_UI` | bool | нет | `false` | Включить Swagger/redoc; при `false` `docs_url`/`redoc_url` отключены | +| `USE_SENTRY` | bool | нет | `false` | Инициализировать Sentry SDK и `SentryAsgiMiddleware` | +| `DEBUG` | bool | нет | `false` | Флаг отладки | +| `CLASSIC_MODE` | bool | нет | `true` | Классический режим расчётов | +| `BLOCK_SIZE` | int | нет | `256` | Размер блока обработки растра; участвует в `area_factor` | +| `BLOCK_SIZE_FACTOR` | int | нет | `10` | Множитель площади блока (`area_factor = BLOCK_SIZE_FACTOR × BLOCK_SIZE²`) | +| `CPU_NUMBER` | int | нет | `10` | Число используемых CPU | + +> `DEBUG`, `CLASSIC_MODE`, `BLOCK_SIZE`, `BLOCK_SIZE_FACTOR`, `CPU_NUMBER` задаются в конфиге, но в текущих обработчиках напрямую не считываются (в `main.py` используются только `SHOW_UI`, `USE_SENTRY`, `AUTH`). Оставлены как настраиваемые параметры. + +### Django / ЛК (префикс `DJANGO_`) + +Класс `DjangoSettings`. Используется `CustomAuthenticationMiddleware`/`DjangoUserMiddleware` при включённой авторизации (`AUTH=1`) для запросов к ЛК. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `DJANGO_USE` | bool | нет | `true` | Использовать интеграцию с Django | +| `DJANGO_HOST` | string | нет | `https://lk.sarex.io` | Базовый URL Django/ЛК | +| `DJANGO_TIMEOUT` | int | нет | `10` | Таймаут HTTP-запросов к Django, сек | + +### Sentry (префикс `SENTRY_`) + +Класс `SentrySettings`. Значения передаются в `sentry_sdk.init(**settings.sentry.kwargs)` только при `USE_SENTRY=1`. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SENTRY_DSN` | string | нет | `""` | DSN проекта Sentry | +| `SENTRY_ENVIRONMENT` | string | нет | `production` | Имя окружения в Sentry | +| `SENTRY_TRACES_SAMPLE_RATE` | float | нет | `1.0` | Доля трейсов | +| `SENTRY_SEND_DEFAULT_PII` | bool | нет | `true` | Отправлять PII | + +### Трейсинг (OpenTelemetry, префикс `TRACING_`) + +Класс `TraceSettings`. Активируется при `TRACING_USE=1` (`fastapi-otel-tools`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `TRACING_USE` | bool | нет | `false` | Включает OTEL-трейсинг, otel-логгер и `OtelMiddleware` | +| `TRACING_HOST` | string | нет | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACING_SERVICE_NAME` | string | нет | `measurements` | Имя сервиса в трейсах | +| `TRACING_INSECURE` | bool | нет | `false` | Небезопасное (без TLS) подключение к коллектору | + +> Тип `bool` в pydantic принимает `1`/`0`, `true`/`false`, `yes`/`no`. + +--- + +## Инфраструктурные и вспомогательные переменные + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `S3_JSON_FILE` | `.helm/values.yaml` (`envs`) | Путь к файлу с доступами S3 (`/opt/cred_s3.json`). **Кодом не читается** — приложение использует только `S3_JSON_SETTINGS` | +| `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` (`variables`) | Параметры сборки образа | +| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `universal-pipeline` (деплой чарта per-env: `stage`/`preprod`/`production`) | + +--- + +## Деплой из этого репозитория (`iac/apps/measurements`) + +В `base/` используется **kustomize** (не собственный Helm-чарт сервиса). Доступ к S3/MinIO инъектируется агентом **Vault** и подгружается в окружение процесса до старта. + +### `base/` + +- `deployment.yaml` — единственный Deployment `measurements` (namespace `measurements`). Аннотации Vault (`agent-inject`, `role: measurements`) формируют шаблон секрета `measurements-s3` из `secrets/data/minio/apps/measurements`, собирая `S3_JSON_SETTINGS='{"host":…,"login":…,"password":…,"verify":false,"buckets":["measurements"]}'`. Контейнер запускается командой `set -a; . /vault/secrets/measurements-s3; set +a; exec /opt/entrypoint.sh`. Инлайн задан только `TRACING_USE=false`. Порт `8000` (`http`), `serviceAccountName: measurements-vault`, `imagePullSecrets: regcred`, ресурсы `cpu 25m` / `memory 128Mi`. +- `service.yaml` — `Service` `measurements-svc` (ClusterIP, порт `8000` → `8000`). +- `namespace.yaml` — namespace `measurements` с `istio-injection: enabled`. +- `serviceaccount.yaml` — SA `measurements-vault`. +- `kustomization.yaml` собирает `namespace`, `serviceaccount`, `deployment`, `service`. + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + патч `replicas.yaml` (реплики Deployment `measurements` = 1). +- **`brusnika-stage`** / **`brusnika-prod`** — Flux `HelmRelease` `measurements` на `universal-chart` `0.1.7` (source `yc-oci-charts`). Секрет `S3_JSON_SETTINGS` берётся из k8s-секрета `s3-json-settings` (`secretEnvs`). `replicaCount`: `stage 1`, `preprod 3`, `production 3`; `imagePullSecrets: regcred`; `labels.monitoring: prometheus`; сервис `measurements-service` (ClusterIP, `8000`). + +--- + +## Замечания и потенциальные проблемы + +- **`S3_JSON_SETTINGS` — единственная жёстко обязательная переменная.** Локальный `docker-compose.yaml` задаёт её значением-заглушкой (`{"host":"host","login":"login","password":"password"}`) — для реальной работы значение нужно заменить. +- **`S3_JSON_FILE` в `.helm/values.yaml` кодом не читается** — приложение использует только `S3_JSON_SETTINGS` (в этом infra-репозитории она и инъектируется Vault). Расхождение способов передачи доступов между собственным чартом и infra-репо. +- **Опечатка в `middleware.py`:** в `DjangoUserMiddleware` используется `settings.django.self.timeout` вместо `settings.django.timeout` — лишний `.self` приведёт к `AttributeError`. Сам `DjangoUserMiddleware` в `main.py` не подключается (подключается `CustomAuthenticationMiddleware`). +- **Отсутствует поле `jwt_public_key`:** `CustomAuthenticationMiddleware` при отсутствии заголовка `identity` вызывает `jwt.decode(key=settings.jwt_public_key, …)`, но такого поля в `ApplicationSettings` нет — при `AUTH=1` и запросе без `identity` это приведёт к `AttributeError`. Если планируется проверка подписи, следует добавить переменную (напр. `JWT_PUBLIC_KEY`) в конфиг. +- **`brusnika-stage`/`brusnika-prod`: `image.name` указывает на `documentations` (`…/documentations:prod_5904312b`), а не на `measurements`** — вероятно скопировано из другого сервиса; для measurements образ должен указывать на `…/measurements`. +- **Проверки liveness/readiness отключены** во всех манифестах (`probes.*.enabled: false`); HTTP-эндпоинта healthcheck у сервиса нет. +- **Probes/порт в brusnika-оверлеях:** `deployment.port` задан `8080`, тогда как контейнер (`entrypoint.sh` → gunicorn) слушает `8000`, и `service.port`/`targetPort` = `8000`. + +--- + +## Минимальный набор для локального запуска + +- `S3_JSON_SETTINGS` (обязателен) — реальные доступы к S3/MinIO с бакетом(ами) растров. +- при необходимости: `LOG_LEVEL`, `TRACING_USE` (+ `TRACING_*`), `USE_SENTRY` (+ `SENTRY_*`), `AUTH` (+ `DJANGO_*`). + +Сервис слушает `0.0.0.0:8000` (gunicorn, 4 воркера uvicorn). См. пример значений в `.env.example`. diff --git a/apps/measurements/openapi.json b/apps/measurements/openapi.json new file mode 100644 index 0000000..1bf5cff --- /dev/null +++ b/apps/measurements/openapi.json @@ -0,0 +1,927 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "measurements", + "description": "HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO через GDAL.\n\nВсе эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `:<путь/к/файлу.tif>`; система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе координат растра.\n\nАутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.", + "version": "0.0.1" + }, + "paths": { + "/api/point": { + "post": { + "summary": "Height/temperature at a single point", + "description": "Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3.", + "operationId": "point_api_point_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffPoint" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object", + "title": "Response Point Api Point Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/points": { + "post": { + "summary": "Height/temperature at multiple points", + "description": "Батч-версия /point: массив высот/температур для списка точек.", + "operationId": "points_api_points_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffPoints" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "type": "array", + "title": "Response Points Api Points Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/profile": { + "post": { + "summary": "Elevation profile along a polyline", + "description": "Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки.", + "operationId": "profile_api_profile_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffProfile" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "type": "array", + "title": "Response Profile Api Profile Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/volume": { + "post": { + "summary": "Volume inside a polygon", + "description": "Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек).", + "operationId": "volume_api_volume_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffVolume" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "additionalProperties": { + "type": "number" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Response Volume Api Volume Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/multiple": { + "post": { + "summary": "Volume difference between two DEMs", + "description": "Разница объёмов между master- и slave-растром в пределах полигона.", + "operationId": "multiple_api_multiple_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffMultiple" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "number" + }, + "type": "object", + "title": "Response Multiple Api Multiple Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/transform": { + "post": { + "summary": "GeoTIFF affine transform", + "description": "Возвращает 6 коэффициентов GDAL GeoTransform растра.", + "operationId": "transform_api_transform_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 6, + "minItems": 6, + "title": "Response Transform Api Transform Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/info": { + "post": { + "summary": "GeoTIFF metadata", + "description": "Метаданные растра (размер, проекция, geotransform и т.п.).", + "operationId": "info_api_info_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Response Info Api Info Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/statistics": { + "post": { + "summary": "Raster min/max statistics", + "description": "Пара (min, max) значений растра.", + "operationId": "statistics_api_statistics_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2, + "title": "Response Statistics Api Statistics Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/bounds": { + "post": { + "summary": "Tile bounds by zoom range", + "description": "Границы тайлов (XYZ) для диапазона zoom_from..zoom_to.", + "operationId": "bounds_api_bounds_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "items": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "type": "array" + }, + "type": "object", + "title": "Response Bounds Api Bounds Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Tiff": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "zoom_to": { + "type": "integer", + "title": "Zoom To", + "default": 21 + }, + "zoom_from": { + "type": "integer", + "title": "Zoom From", + "default": 14 + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "Tiff" + }, + "TiffMultiple": { + "properties": { + "master_path": { + "type": "string", + "title": "Master Path" + }, + "slave_path": { + "type": "string", + "title": "Slave Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "master_path", + "slave_path", + "points" + ], + "title": "TiffMultiple", + "example": [ + { + "master_path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 37.34743572357015, + 55.68881158675471 + ], + [ + 37.347128864435845, + 55.6888704629146 + ], + [ + 37.34719182945381, + 55.68896771656465 + ], + [ + 37.34732057806979, + 55.688942063559054 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "slave_path": "geotiff_dems/NTG030521_DEM.tif" + } + ] + }, + "TiffPoint": { + "properties": { + "altitude_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Altitude Path" + }, + "temperature_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Temperature Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "point": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2, + "title": "Point" + } + }, + "type": "object", + "required": [ + "point" + ], + "title": "TiffPoint", + "example": [ + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "point": [ + 59.95821631734607, + 57.9660901731621 + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "point": [ + 1494214.348999979, + 516505.3900003205 + ], + "proj": "", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "point": [ + 1494214.348999979, + 516505.3900003205 + ], + "proj": "" + } + ] + }, + "TiffPoints": { + "properties": { + "altitude_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Altitude Path" + }, + "temperature_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Temperature Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "points" + ], + "title": "TiffPoints", + "example": [ + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ], + [ + 59.95563056285039, + 57.966613723012216 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "points": [ + [ + 1494221.1549999786, + 516495.86600015266 + ], + [ + 1494214.348999979, + 516505.3900003205 + ] + ], + "proj": "", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + } + ] + }, + "TiffProfile": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "path", + "points" + ], + "title": "TiffProfile", + "example": [ + { + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs" + }, + { + "path": "geotiff_dems/BLG_080122_DEM.tif", + "points": [ + [ + 3334617.713961677, + 590691.7743950449 + ], + [ + 3334977.6348458366, + 590688.296080064 + ] + ] + } + ] + }, + "TiffVolume": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "level": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Level" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + }, + "mode": { + "allOf": [ + { + "$ref": "#/components/schemas/VolumeMode" + } + ], + "default": "cv2" + } + }, + "type": "object", + "required": [ + "path", + "points" + ], + "title": "TiffVolume", + "example": [ + { + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ], + [ + 59.95563056285039, + 57.966613723012216 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs" + }, + { + "mode": "pillow", + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 1494221.1549999786, + 516495.86600015266 + ], + [ + 1494214.348999979, + 516505.3900003205 + ] + ], + "proj": "" + } + ] + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "VolumeMode": { + "type": "string", + "enum": [ + "cv2", + "pillow" + ], + "title": "VolumeMode" + } + } + } +} \ No newline at end of file diff --git a/apps/measurements/openapi.yaml b/apps/measurements/openapi.yaml new file mode 100644 index 0000000..627f629 --- /dev/null +++ b/apps/measurements/openapi.yaml @@ -0,0 +1,565 @@ +openapi: 3.1.0 +info: + title: measurements + description: 'HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO + через GDAL. + + + Все эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `:<путь/к/файлу.tif>`; + система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе + координат растра. + + + Аутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.' + version: 0.0.1 +paths: + /api/point: + post: + summary: Height/temperature at a single point + description: Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3. + operationId: point_api_point_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffPoint' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + title: Response Point Api Point Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/points: + post: + summary: Height/temperature at multiple points + description: 'Батч-версия /point: массив высот/температур для списка точек.' + operationId: points_api_points_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffPoints' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + type: array + title: Response Points Api Points Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/profile: + post: + summary: Elevation profile along a polyline + description: Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки. + operationId: profile_api_profile_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffProfile' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + type: array + title: Response Profile Api Profile Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/volume: + post: + summary: Volume inside a polygon + description: Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек). + operationId: volume_api_volume_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffVolume' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + anyOf: + - additionalProperties: + type: number + type: object + - type: 'null' + title: Response Volume Api Volume Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/multiple: + post: + summary: Volume difference between two DEMs + description: Разница объёмов между master- и slave-растром в пределах полигона. + operationId: multiple_api_multiple_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffMultiple' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: number + type: object + title: Response Multiple Api Multiple Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/transform: + post: + summary: GeoTIFF affine transform + description: Возвращает 6 коэффициентов GDAL GeoTransform растра. + operationId: transform_api_transform_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + prefixItems: + - type: number + - type: number + - type: number + - type: number + - type: number + - type: number + type: array + maxItems: 6 + minItems: 6 + title: Response Transform Api Transform Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/info: + post: + summary: GeoTIFF metadata + description: Метаданные растра (размер, проекция, geotransform и т.п.). + operationId: info_api_info_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Info Api Info Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/statistics: + post: + summary: Raster min/max statistics + description: Пара (min, max) значений растра. + operationId: statistics_api_statistics_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + title: Response Statistics Api Statistics Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/bounds: + post: + summary: Tile bounds by zoom range + description: Границы тайлов (XYZ) для диапазона zoom_from..zoom_to. + operationId: bounds_api_bounds_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + items: + items: + type: integer + type: array + type: array + type: object + title: Response Bounds Api Bounds Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + Tiff: + properties: + path: + type: string + title: Path + zoom_to: + type: integer + title: Zoom To + default: 21 + zoom_from: + type: integer + title: Zoom From + default: 14 + type: object + required: + - path + title: Tiff + TiffMultiple: + properties: + master_path: + type: string + title: Master Path + slave_path: + type: string + title: Slave Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - master_path + - slave_path + - points + title: TiffMultiple + example: + - master_path: geotiff_dems/NTG030521_DEM.tif + points: + - - 37.34743572357015 + - 55.68881158675471 + - - 37.347128864435845 + - 55.6888704629146 + - - 37.34719182945381 + - 55.68896771656465 + - - 37.34732057806979 + - 55.688942063559054 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + slave_path: geotiff_dems/NTG030521_DEM.tif + TiffPoint: + properties: + altitude_path: + anyOf: + - type: string + - type: 'null' + title: Altitude Path + temperature_path: + anyOf: + - type: string + - type: 'null' + title: Temperature Path + proj: + type: string + title: Proj + default: '' + point: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + title: Point + type: object + required: + - point + title: TiffPoint + example: + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + point: + - 59.95821631734607 + - 57.9660901731621 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + temperature_path: geotiff_dems/THERMAL_DEM.tif + - point: + - 1494214.348999979 + - 516505.3900003205 + proj: '' + temperature_path: geotiff_dems/THERMAL_DEM.tif + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + point: + - 1494214.348999979 + - 516505.3900003205 + proj: '' + TiffPoints: + properties: + altitude_path: + anyOf: + - type: string + - type: 'null' + title: Altitude Path + temperature_path: + anyOf: + - type: string + - type: 'null' + title: Temperature Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - points + title: TiffPoints + example: + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + - - 59.95563056285039 + - 57.966613723012216 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + temperature_path: geotiff_dems/THERMAL_DEM.tif + - points: + - - 1494221.1549999786 + - 516495.86600015266 + - - 1494214.348999979 + - 516505.3900003205 + proj: '' + temperature_path: geotiff_dems/THERMAL_DEM.tif + TiffProfile: + properties: + path: + type: string + title: Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - path + - points + title: TiffProfile + example: + - path: geotiff_dems/NTG030521_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + - path: geotiff_dems/BLG_080122_DEM.tif + points: + - - 3334617.713961677 + - 590691.7743950449 + - - 3334977.6348458366 + - 590688.296080064 + TiffVolume: + properties: + path: + type: string + title: Path + proj: + type: string + title: Proj + default: '' + level: + anyOf: + - type: number + - type: 'null' + title: Level + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + mode: + allOf: + - $ref: '#/components/schemas/VolumeMode' + default: cv2 + type: object + required: + - path + - points + title: TiffVolume + example: + - path: geotiff_dems/NTG030521_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + - - 59.95563056285039 + - 57.966613723012216 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + - mode: pillow + path: geotiff_dems/NTG030521_DEM.tif + points: + - - 1494221.1549999786 + - 516495.86600015266 + - - 1494214.348999979 + - 516505.3900003205 + proj: '' + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError + VolumeMode: + type: string + enum: + - cv2 + - pillow + title: VolumeMode diff --git a/apps/message-hub/.env.example b/apps/message-hub/.env.example new file mode 100644 index 0000000..8b128c0 --- /dev/null +++ b/apps/message-hub/.env.example @@ -0,0 +1,87 @@ +# ============================================================================= +# Message Hub — пример конфигурации (.env) +# Версия: 0.1.0 +# Скопируйте в .env и заполните значения. +# ============================================================================= + +# --- Приложение (префикс SETTINGS_) --- +SETTINGS_DEBUG=False +# Соответствие логических топиков реальным именам топиков Kafka. +# Допустимые ключи: planning, assets, issues +SETTINGS_TOPICS={"planning": "planning", "assets": "assets", "issues": "issues"} +# Проверять SSL-сертификаты у S3 и всех HTTP-клиентов внешних сервисов (1/0) +SETTINGS_VERIFY_SSL=1 +SETTINGS_RETRY_DELAY=3 +SETTINGS_MAX_RETRIES=3 +SETTINGS_REQUEST_RETRIES=2 +SETTINGS_REQUEST_DELAY=2 +SETTINGS_MESSAGE_SKIP_AGE=300 +SETTINGS_CACHE_EXPIRATION=120 +SETTINGS_SENDER=noreply@sarex.io +# SETTINGS_WORKER_TIMEOUT=30 + +# --- Логирование (префикс LOG_) --- +# LOG_LEVEL=INFO +# LOG_FORMAT=[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s + +# --- База данных PostgreSQL (префикс DB_) --- +DB_HOST=localhost +DB_PORT=5433 +DB_DATABASE=sarex_db +DB_USERNAME=sarex +DB_PASSWORD=sarex +# DB_DIALECT=postgresql+psycopg + +# --- Kafka (префикс KAFKA_) --- +KAFKA_HOST= +KAFKA_PORT= +KAFKA_USERNAME= +KAFKA_PASSWORD= +# PLAINTEXT | SSL | SASL_PLAINTEXT | SASL_SSL +KAFKA_SECURITY_PROTOCOL= +# PLAINTEXT | SCRAM-SHA-512 +KAFKA_SASL_MECHANISM= +KAFKA_SSL_CAFILE= + +# --- Redis / кеш (префикс CACHE_) --- +CACHE_HOST=localhost +CACHE_PORT=6378 +CACHE_PASSWORD= +CACHE_SSL=0 +# CACHE_SSL_CA_CERTS=/opt/ssl/ca.pem + +# --- S3 (Yandex Object Storage, префикс S3_) --- +S3_HOST=http://localhost:9000 +S3_LOGIN=minioadmin +S3_PASSWORD=minioadmin +S3_BUCKET=mybucket + +# --- Внешние HTTP-сервисы (HOST / TIMEOUT на каждый префикс) --- +# Sarex backend +SAREX_HOST=http://localhost:8001 +SAREX_TIMEOUT=60 +# PM backend +PM_HOST=http://localhost:8001 +PM_TIMEOUT=60 +# Issues backend +ISSUES_HOST=http://localhost:8001 +ISSUES_TIMEOUT=60 +# BI backend +BI_HOST=http://localhost:8001 +BI_TIMEOUT=60 +# EAV service +EAV_HOST=http://localhost:8001 +EAV_TIMEOUT=60 +# HTML -> PDF converter (export-project) +PDF_CONVERTER_HOST=http://localhost:8001 +PDF_CONVERTER_TIMEOUT=60 +# Mailer service +MAILER_HOST=http://localhost:8001 +MAILER_PREFIX=/api/v1 +MAILER_TIMEOUT=60 + +# --- Инфраструктурные переменные (gunicorn / контейнер) --- +# Не читаются классом Settings, используются entrypoint.sh и docker-compose +# PYTHONPATH=src +# WORKERS=2 +# WORKER_TIMEOUT=30 diff --git a/apps/message-hub/CONFIGURATION.md b/apps/message-hub/CONFIGURATION.md new file mode 100644 index 0000000..c10584b --- /dev/null +++ b/apps/message-hub/CONFIGURATION.md @@ -0,0 +1,208 @@ +# Конфигурация проекта message-hub +# Версия: 0.1.0 + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **через переменные окружения**. Разбор выполняется в `src/config/` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). Настройки разбиты на несколько классов, каждый со своим префиксом: + +- `Settings` (`src/config/__init__.py`) — общий класс приложения, префикс `SETTINGS_`; +- `DBSettings` (`src/config/db.py`) — префикс `DB_`; +- `KafkaSettings` (`src/config/kafka.py`) — префикс `KAFKA_`; +- `RedisSettings` (`src/config/redis.py`) — префикс `CACHE_`; +- `ServiceConfig` и наследники (`src/config/sarex.py`) — префиксы `SAREX_`, `PM_`, `ISSUES_`, `BI_`, `EAV_`, `PDF_CONVERTER_`; +- `S3Settings` (`src/config/s3.py`) — префикс `S3_`; +- `MailerSettings` (`src/config/mailer.py`) — префикс `MAILER_`; +- `LoggerSettings` (`src/config/logger.py`) — префикс `LOG_`. + +Особенности разбора: + +- у каждого класса задан `env_file='.env'` и `extra='ignore'` — при наличии файла `.env` в рабочей директории он загружается автоматически, лишние переменные игнорируются; +- вложенных секций через разделитель нет — каждая группа настроек читается отдельным классом по своему префиксу; +- поле `VERIFY_SSL` в `S3Settings` и во всех `ServiceConfig` объявлено с `alias='SETTINGS_VERIFY_SSL'` — то есть единая переменная `SETTINGS_VERIFY_SSL` управляет проверкой TLS-сертификатов сразу для S3 и всех HTTP-клиентов внешних сервисов; +- у большинства полей есть значения по умолчанию, поэтому формально сервис стартует и без `.env`, но с дефолтными (локальными) адресами БД, Kafka, Redis и сервисов. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Файл `.env` в рабочей директории (загружается `pydantic-settings`) и/или переменные окружения процесса | +| Локально (контейнеры) | `docker-compose.yaml`: блок `environment` для сервиса `message-hub` (`PYTHONPATH`, `KAFKA_HOST`, `KAFKA_PORT`) | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `universal-chart.services.message-hub.envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов); базовый чарт — `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и build-args | + +Запуск процесса (`docker/entrypoint.sh`): единый ASGI-процесс поднимается через `gunicorn` с воркерами `uvicorn.workers.UvicornWorker` на `0.0.0.0:8000`: + +``` +gunicorn -w $WORKERS -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --timeout $WORKER_TIMEOUT --access-logfile - main:app +``` + +Приложение `main:app` (`src/main.py`) объединяет в одном ASGI-приложении: FastStream-брокер Kafka (потребители сообщений), HTTP-роуты health-проверок и Socket.IO-сервер (`AsyncServer` поверх `AsyncRedisManager`). Отдельных точек входа для воркеров/крон-задач нет — `pyproject.toml` не содержит `[project.scripts]`. + +## Переменные приложения + +### App (`SETTINGS_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SETTINGS_TOPICS` | dict (JSON) | `{}` | Соответствие логических топиков (`planning`/`assets`/`issues`) реальным именам топиков Kafka. Валидатор запрещает ключи вне набора `assets`/`planning`/`issues` | +| `SETTINGS_DEBUG` | bool | `False` | Режим отладки | +| `SETTINGS_VERIFY_SSL` | bool | `True` | Проверка TLS-сертификатов для S3 и всех HTTP-клиентов внешних сервисов (общий флаг через alias) | +| `SETTINGS_WORKER_TIMEOUT` | int | `30` | Таймаут воркера (поле `WORKER_TIMEOUT` класса `Settings`) | +| `SETTINGS_RETRY_DELAY` | int | `3` | Стартовая задержка (сек) между повторами обработки сообщения Kafka; удваивается на каждой попытке | +| `SETTINGS_MAX_RETRIES` | int | `3` | Число попыток обработки сообщения Kafka перед `ack` | +| `SETTINGS_REQUEST_RETRIES` | int | `2` | Число повторов HTTP-запросов к внешним сервисам | +| `SETTINGS_REQUEST_DELAY` | int | `2` | Задержка (сек) между повторами HTTP-запросов | +| `SETTINGS_MESSAGE_SKIP_AGE` | int | `300` | Возраст сообщения (сек), старше которого оно пропускается | +| `SETTINGS_CACHE_EXPIRATION` | int | `120` | TTL (сек) ключей присутствия пользователей в Redis (WebSocket) | +| `SETTINGS_SENDER` | string | `noreply@sarex.io` | Адрес отправителя по умолчанию | + +### Логирование (`LOG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `INFO` | Уровень логирования (стандартные уровни `logging`) | +| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат строк лога | + +### Database (`DB_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_HOST` | string | `''` | Хост PostgreSQL | +| `DB_PORT` | int | `5432` | Порт PostgreSQL | +| `DB_DATABASE` | string | `''` | Имя базы данных | +| `DB_USERNAME` | string | `''` | Пользователь БД | +| `DB_PASSWORD` | string | `''` | Пароль пользователя БД | +| `DB_DIALECT` | string | `postgresql+psycopg` | Диалект/драйвер SQLAlchemy. Итоговый DSN собирается в `db.url` | + +### Kafka (`KAFKA_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_HOST` | string | `localhost` | Хост брокера | +| `KAFKA_PORT` | int | `9092` | Порт брокера | +| `KAFKA_USERNAME` | string \| null | `None` | Логин SASL | +| `KAFKA_PASSWORD` | string \| null | `None` | Пароль SASL | +| `KAFKA_SECURITY_PROTOCOL` | string | `PLAINTEXT` | Протокол безопасности: `PLAINTEXT`/`SSL`/`SASL_PLAINTEXT`/`SASL_SSL`. При `SSL`/`SASL_SSL` используется SSL-контекст | +| `KAFKA_SASL_MECHANISM` | string \| null | `None` | Механизм SASL. Обрабатываются `PLAINTEXT` и `SCRAM-SHA-512` | +| `KAFKA_SSL_CAFILE` | string \| null | `None` | Путь к CA-сертификату для SSL-контекста | + +### Redis / кеш (`CACHE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CACHE_HOST` | string | `localhost` | Хост Redis | +| `CACHE_PORT` | int | `6378` | Порт Redis | +| `CACHE_PASSWORD` | string \| null | `None` | Пароль Redis | +| `CACHE_SSL` | bool | `False` | Подключение по TLS (`rediss://`) | +| `CACHE_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату Redis | + +> Redis используется как менеджер состояния Socket.IO (`AsyncRedisManager`) и как хранилище присутствия пользователей в проектах. + +### S3 (`S3_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_HOST` | string | `https://storage.yandexcloud.net` | Endpoint S3 (`endpoint_url`) | +| `S3_LOGIN` | string | `''` | Access key | +| `S3_PASSWORD` | string | `''` | Secret key | +| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию | +| `SETTINGS_VERIFY_SSL` | bool | `True` | Проверять TLS-сертификат (общий флаг, см. App) | + +### HTTP-клиенты внешних сервисов + +Все клиенты наследуют общий класс `ServiceConfig` с полями `HOST`, `TIMEOUT` и общим флагом `VERIFY_SSL` (через alias `SETTINGS_VERIFY_SSL`). Значения по умолчанию: `HOST=http://localhost:8001`, `TIMEOUT=60`. + +| Секция / префикс | Назначение | +| --- | --- | +| `SAREX_*` | Sarex backend (получение токенов клиентов и пр.) | +| `PM_*` | PM backend (синхронизация задач, автопланирование) | +| `ISSUES_*` | Сервис issues (типы задач, модели статусов) | +| `BI_*` | BI backend (синхронизация значений аналитики) | +| `EAV_*` | EAV-сервис (ассеты и атрибуты) | +| `PDF_CONVERTER_*` | Конвертер HTML → PDF (export-project) | + +Для каждого — две переменные, напр. для PM: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `PM_HOST` | string | Базовый URL сервиса | +| `PM_TIMEOUT` | int | Таймаут запроса (сек) | + +### Mailer (`MAILER_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MAILER_HOST` | string | `http://localhost:8001` | Базовый URL сервиса рассылок | +| `MAILER_PREFIX` | string | `/api/v1` | Префикс маршрутов сервиса рассылок | +| `MAILER_TIMEOUT` | int | `60` | Таймаут запроса (сек) | + +## Переменные инфраструктуры, сборки и запуска + +Не читаются классами настроек приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `PYTHONPATH` | `docker-compose.yaml`, Helm `envs` | Каталог исходников (`src`) | +| `WORKERS` | `docker/entrypoint.sh`, Helm `envs` | Число воркеров gunicorn (по умолчанию `2`) | +| `WORKER_TIMEOUT` | `docker/entrypoint.sh`, Helm `envs` | Таймаут воркера gunicorn (`--timeout`) | +| `CI_COMMIT_SHORT_SHA` | `docker/Dockerfile` (build-arg через `BUILD_ARGS`) | Идентификатор сборки | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Сервис деплоится через зависимость `universal-chart`. Обычные значения задаются в блоке `universal-chart.services.message-hub.envs` для окружений `stage`/`preprod`/`production` (различаются адресами БД, Kafka, Redis, сервисов, именами топиков, числом реплик и таймаутами). + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default` / `production`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `KAFKA_USERNAME` | `message-hub-kafka-secret` / `kafka-secret` | `username` | +| `KAFKA_PASSWORD` | `message-hub-kafka-secret` / `kafka-secret` | `password` | +| `DB_USERNAME` | `pm-postgresql-secret` / `postgres-pm-secret` | `user` | +| `DB_PASSWORD` | `pm-postgresql-secret` / `postgres-pm-secret` | `password` | +| `CACHE_PASSWORD` | `cache-secret-pm` / `cache-secret` | `password` | +| `S3_LOGIN` | `planning-s3-secret` / `s3-secret` | `username` | +| `S3_PASSWORD` | `planning-s3-secret` / `s3-secret` | `password` | +| `S3_BUCKET` | `planning-s3-secret` / `s3-secret` | `bucket` | +| `S3_HOST` | `planning-s3-secret` / `s3-secret` | `host` | + +Помимо env, чарт монтирует CA-сертификат из секрета `kafka-secret` (ключ `ssl_cafile`) как файл `/opt/ssl/ca.pem` (том `kafka-ca-volume`, `readOnly`) — на него указывают `KAFKA_SSL_CAFILE` и `CACHE_SSL_CA_CERTS` в конфигурациях окружений. + +Health-пробы (`.helm/values.yaml`): liveness `GET /health/live`, readiness `GET /health/ready`, порт `8000`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `planning` | `0.0.1-stage` | +| ветка `master` | `preprod` | `message-hub-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `message-hub-prod` | `0.0.1-prod` | + +Ключевые переменные пайплайна: `SERVICE_NAME=message-hub`, `DOCKERFILE_PATH=./docker/Dockerfile`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE=app`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (`--set universal-chart...`), флаг `ENABLE_BUILD_IMAGE`. Отдельные job'ы `linter` (`ruff check` / `ruff format --check`) и `typechecker` (`mypy src`). + +## Замечания и потенциальные проблемы + +- Единая переменная `SETTINGS_VERIFY_SSL` управляет проверкой TLS сразу для S3 и всех шести HTTP-клиентов (alias у поля `VERIFY_SSL`). Отдельно на клиент это не настраивается. +- Поле `WORKER_TIMEOUT` есть и в классе `Settings` (читается как `SETTINGS_WORKER_TIMEOUT`, дефолт `30`), и как самостоятельная переменная `WORKER_TIMEOUT` для gunicorn (`entrypoint.sh`, Helm). Это разные переменные — не перепутайте. +- `SETTINGS_TOPICS` валидируется: допустимы только ключи `assets`, `planning`, `issues`. Прочие ключи вызывают ошибку старта. Если ключ отсутствует, соответствующий потребитель подписывается на пустое имя топика. +- Файл `.env.example` в репозитории сервиса не содержит части переменных (сервисы `PM_/ISSUES_/BI_/EAV_/PDF_CONVERTER_`, `MAILER_`, `LOG_`, ряд `SETTINGS_*`) — при реальном запуске задавайте их явно (полный перечень — в данном документе и в `.env.example` рядом). +- У большинства полей есть дефолты (локальные адреса), поэтому при пустом окружении сервис поднимется, но будет ходить на `localhost` — для рабочих окружений значения задаются через Helm. + +## Минимальный набор для локального запуска + +Kafka поднимается через `docker-compose.yaml` (сервисы `kafka`, `kafka-ui`); PostgreSQL и Redis — внешние. Минимально стоит задать (с учётом префиксов): + +- `SETTINGS_TOPICS` — карта логических топиков в реальные; +- `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`; +- `KAFKA_HOST`, `KAFKA_PORT` (для docker-compose — `kafka:9092`); +- `CACHE_HOST`, `CACHE_PORT` (+ `CACHE_PASSWORD`/`CACHE_SSL` при необходимости); +- `S3_HOST`, `S3_LOGIN`, `S3_PASSWORD`, `S3_BUCKET`; +- `HOST` для внешних сервисов, которые реально используются (`SAREX_`, `PM_`, `ISSUES_`, `BI_`, `EAV_`, `PDF_CONVERTER_`, `MAILER_`); +- `SETTINGS_VERIFY_SSL` (`0` локально, если сертификаты самоподписанные). + +Готовые значения-примеры приведены в `.env.example` рядом с этим документом. diff --git a/apps/message-hub/ENDPOINTS.md b/apps/message-hub/ENDPOINTS.md new file mode 100644 index 0000000..898f3ef --- /dev/null +++ b/apps/message-hub/ENDPOINTS.md @@ -0,0 +1,94 @@ +# Интерфейсы сервиса message-hub +# Версия: 0.1.0 + +Документ описывает интерфейсную поверхность сервиса: HTTP-эндпоинты, WebSocket (Socket.IO), потребляемые топики Kafka и исходящие HTTP-запросы к внешним сервисам. + +> В отличие от классических backend-сервисов, у `message-hub` нет публичного REST API и, соответственно, нет OpenAPI-схемы (health-роуты объявлены с `include_in_schema=False`). Основные интерфейсы — это Kafka-потребители и Socket.IO. Поэтому файла `openapi.yaml` для сервиса нет. + +## Как устроено взаимодействие + +Единое ASGI-приложение (`main:app`, `src/main.py`) объединяет три поверхности: + +- **HTTP** — health-проверки (`src/health.py`), обслуживаются FastStream ASGI; +- **WebSocket** — Socket.IO-сервер (`socketio.AsyncServer` + `AsyncRedisManager`), пространство имён `/project` (`src/ws/namespaces.py`); +- **Kafka** — потребители сообщений (`src/consumers/*.py`) на базе FastStream `KafkaRouter`. + +Исходящие вызовы к внешним сервисам выполняются через `httpx.AsyncClient` (`src/config/sarex.py`, `mailer.py`), базовый хост берётся из соответствующего `*_HOST` (см. `CONFIGURATION.md`). + +## HTTP-эндпоинты (входящие) + +| Метод | Путь | Ответ | Назначение | +| --- | --- | --- | --- | +| GET | `/health/live` | `204 No Content` | Liveness-проба (всегда 204, если процесс жив) | +| GET | `/health/ready` | `204` / `500` | Readiness-проба: проверяет доступность Kafka (`broker.ping`) и Redis (`ping`); `500`, если хотя бы один недоступен | + +Слушает `0.0.0.0:8000` (gunicorn + UvicornWorker). В Helm пробы настроены на `/health/live` и `/health/ready`, порт `8000`. + +## WebSocket (Socket.IO) + +Пространство имён: **`/project`** (`ProjectNamespace`). Менеджер состояния — Redis (`AsyncRedisManager`), CORS — `*`. + +Параметры подключения (query string при `connect`): `project_id` (int), `user_id` (int). Клиент помещается в комнату `project:{project_id}`. + +| Направление | Событие | Данные | Назначение | +| --- | --- | --- | --- | +| client → server | `connect` | query: `project_id`, `user_id` | Подключение; вход в комнату проекта, регистрация присутствия в Redis | +| client → server | `heartbeat` | — | Продление TTL присутствия пользователя в проекте | +| client → server | `disconnect` | — | Отключение; выход из комнаты, снятие присутствия | +| server → client | `connected_users` | `list[int]` (user_id) | Актуальный список пользователей, подключённых к проекту (рассылается в комнату `project:{project_id}` при connect/disconnect) | + +Ключи присутствия в Redis (TTL = `SETTINGS_CACHE_EXPIRATION`): `project:{project_id}:{user_id}`, `user_sids:{project_id}:{user_id}:{sid}`. + +## Kafka-потребители (входящие сообщения) + +Реальные имена топиков задаются переменной `SETTINGS_TOPICS` (маппинг логических имён `planning`/`assets`/`issues` в имена топиков). Формат сообщения — `MessageSchema` (`src/schemas/message.py`): поля `schema_version`, `model`, `sender`, `type`, `body`, `timestamp`, `xtraceId`, `user_id`, `tenants`, `tags`. + +| Топик (логич.) | `group_id` | Offset reset | Обработчик | Назначение | +| --- | --- | --- | --- | --- | +| `assets` | `assets_consumer` | earliest | `update_attributes_with_assets` | Обновление атрибутов по ассетам | +| `planning` | `planning` | earliest | диспетчер по `type` (см. ниже) | Обработка событий планирования | +| `issues` | `project_entity` | earliest | `create_or_update_entity` | Создание/обновление сущности проекта из issue | +| `issues` | `analytic_values` | latest | `proceed_entity_value` | Обработка значений аналитики по сущности | + +Диспетчеризация топика `planning` по полю `type` (`src/consumers/planning.py`): + +| `type` сообщения | Обработчик | Назначение | +| --- | --- | --- | +| `auto_scheduling` | `handle_auto_scheduling` | Автопланирование | +| `get_converted_file` | `handle_file_export` | Экспорт/конвертация файла | +| `system_log` | `handle_system_log` | Системный журнал изменений | +| `email_notifications` | `handle_email_notification` | Email-уведомления | +| `sync_entity_to_project` | `handle_project_entity_sync` | Синхронизация сущности в проект | +| `sync_detailed_tasks_attributes` | `handle_task_attributes_sync` | Синхронизация атрибутов детальных задач | +| `sync_tasks` | `handle_task_sync` | Синхронизация задач | +| `detailed_tasks_analytics` | `handle_task_analytics` | Аналитика по детальным задачам | +| `update_project` | — | Пропускается (в списке `PLANNING_SKIP_TYPES`) | + +Обработка обёрнута в `retry_handler` (`src/infrastructure/kafka/retry.py`): до `SETTINGS_MAX_RETRIES` попыток с экспоненциальной задержкой (старт `SETTINGS_RETRY_DELAY`), ручной `ack` после успеха либо исчерпания попыток. + +## Исходящие HTTP-запросы к внешним сервисам + +Базовый хост каждого сервиса — из соответствующего `*_HOST` (см. `CONFIGURATION.md`). Итоговый URL = `` + путь из таблицы. + +| Сервис (`config`) | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `bi` (`BI_HOST`) | POST | `/internal/values/sync_value/` | Синхронизация значений аналитики | +| `pm` (`PM_HOST`) | POST | `/internal/pm/detailed_tasks/` | Синхронизация атрибутов детальных задач | +| `pm` (`PM_HOST`) | POST | `/internal/pm/{endpoint}/` | Автопланирование (endpoint из тела сообщения) | +| `pm` (`PM_HOST`) | POST | `/internal/pm/sync_tasks/` | Синхронизация задач | +| `pdf` (`PDF_CONVERTER_HOST`) | POST | `/convert_to_pdf/` | Конвертация HTML → PDF | +| `mailer` (`MAILER_HOST`) | POST | `{MAILER_PREFIX}/emails/bulk` | Массовая отправка email | +| `issues` (`ISSUES_HOST`) | GET | `/api/issue-types/?company_id={id}` | Список типов issue компании | +| `issues` (`ISSUES_HOST`) | GET | `/api/companies/{company_id}/status-model/v2/?issue_type_id={id}` | Модель статусов по типу issue | +| `eav` (`EAV_HOST`) | POST | `/api/v4/assets/search/` | Поиск ассетов по идентификаторам | +| `eav` (`EAV_HOST`) | GET | `/api/v4/attribute/` | Список атрибутов | +| `sarex` (`SAREX_HOST`) | GET | `internal/client/token/{user_id}/` | Получение токена клиента | + +## Внешние зависимости (инфраструктура) + +| Зависимость | Назначение | +| --- | --- | +| Kafka | Источник сообщений (топики `planning`/`assets`/`issues`) | +| PostgreSQL | Хранилище данных (SQLAlchemy + psycopg) | +| Redis | Менеджер состояния Socket.IO и хранилище присутствия пользователей | +| S3 (Yandex Object Storage) | Файловое хранилище | diff --git a/apps/notes/.env.example b/apps/notes/.env.example new file mode 100644 index 0000000..dc7110a --- /dev/null +++ b/apps/notes/.env.example @@ -0,0 +1,56 @@ +# App +BASE_HOST=https://stage-api.sarex.io/notes +API_PREFIX=/api/v1 +# DEBUG влияет и на PostgresSettings (при true хост БД -> localhost:6432), и на Settings.debug +DEBUG=false +REGISTRY=cr.yandex/crp3ccidau046kdj8g9q/ +LOG_LEVEL=INFO + +# Database (префикс PG_) +PG_LOGIN=notes +PG_PASSWORD=notes +PG_DB=notes_db +PG_HOST=127.0.0.1 +PG_PORT=5432 +# В коде подключение к БД всегда идёт с sslmode=verify-full (см. замечания в CONFIGURATION.md) +PG_SSL_MODE=verify-full + +# Django (sarex-backend, префикс DJANGO_) +# DJANGO_USE=false — отключает проверку токена через Django (локальная разработка, тестовый пользователь) +DJANGO_USE=false +DJANGO_HOST=https://stage.sarex.io +DJANGO_TIMEOUT=10 +DJANGO_TOKEN=token + +# Documentations (префикс DOCUMENTATIONS_) +DOCUMENTATIONS_HOST=https://stage-api.sarex.io/documentations/api/v1 + +# Workflows (префикс WORKFLOW_) +WORKFLOW_HOST=https://stage-api.sarex.io/workflows/api/v1 +WORKFLOW_TAG=dev +WORKFLOW_TIMEOUT=30 + +# Attachments (префикс ATTACHMENT_) +ATTACHMENT_HOST=http://attachments-service.attachments-stage.svc.cluster.local:80/api/v1 +ATTACHMENT_TIMEOUT=30 + +# Внешние сервисы (top-level Settings) +FAAS_SERVICE=https://stage-api.sarex.io/lambdas +WORKSPACE_URL=https://stage-api.sarex.io/workspaces/api/v1 +RESOURCE_URL=https://stage-api.sarex.io/resources/api/v1 +# Включает вычисление resource_id по workspace/target при создании заметки +SYNC_RESOURCE_ID=false + +# ND-сервис (проксирование НД, префиксов нет) +ENABLE_ND=false +ND_JWT_ENABLE=false +ND_JWT_SECRET= +ND_JWT_ALGORITHM=HS256 +ND_ACCESS_TOKEN_EXPIRE_DAYS=30 + +# Logger (префикс LOG_) +# LOG_FORMAT='[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s' + +# Инфраструктура / запуск (не читаются кодом приложения) +# Таймаут воркеров gunicorn (entrypoint.sh) +TIMEOUT=120 diff --git a/apps/notes/CONFIGURATION.md b/apps/notes/CONFIGURATION.md new file mode 100644 index 0000000..81ea848 --- /dev/null +++ b/apps/notes/CONFIGURATION.md @@ -0,0 +1,184 @@ +# Конфигурация проекта notes-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/app/config.py` через библиотеку [`pydantic`](https://docs.pydantic.dev/) (`pydantic.BaseSettings`, pydantic v1). + +Конфигурация разбита на несколько классов настроек, каждый со своим префиксом (`Config.env_prefix`): + +- `PostgresSettings` — префикс `PG_`; +- `DjangoSettings` — префикс `DJANGO_`; +- `Documentations` — префикс `DOCUMENTATIONS_`; +- `WorkflowSettings` — префикс `WORKFLOW_`; +- `AttachmentSettings` — префикс `ATTACHMENT_`; +- `LoggerSettings` — префикс `LOG_`; +- корневой `Settings` — **без префикса** (поля читаются по имени в верхнем регистре, напр. `BASE_HOST`, `FAAS_SERVICE`). + +Каждый вложенный класс настроек инстанцируется отдельно и читает свои переменные из окружения по своему префиксу. Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (uvicorn/gunicorn) | Переменные окружения процесса. Приложение **не загружает `.env` автоматически** (в `config.py` нет `env_file`/`python-dotenv`) — переменные нужно экспортировать самому | +| Локально (контейнеры) | `docker-compose.yml`: блок `environment` для сервиса `notes` (`PG_HOST`, `PG_DB`, `PG_LOGIN`, `PG_PASSWORD`, `DJANGO_USE`, `TIMEOUT`) | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) — выбор окружения по ветке/тегу | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала выполняются миграции (`alembic upgrade head`), затем стартует gunicorn с воркерами `uvicorn.workers.UvicornWorker` на `0.0.0.0:8000` с таймаутом `$TIMEOUT`. + +## Переменные приложения + +Дефолт `—` означает, что явного значения по умолчанию нет (пустая строка/обязательно задать для реального окружения). + +### App / корневой `Settings` (без префикса) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `BASE_HOST` | string | `https://lk.sarex.io` | Внешний базовый URL сервиса | +| `API_PREFIX` | string | `/api/v1` | Префикс публичного API | +| `DEBUG` | bool | `False` | Режим отладки. Также влияет на `PostgresSettings` (см. ниже) и включает `ProfilingSqlQueryMiddleware` | +| `FAAS_SERVICE` | string | `https://stage-api.sarex.io/lambdas` | URL сервиса лямбд/FaaS | +| `WORKSPACE_URL` | string | `https://stage-api.sarex.io/workspaces/api/v1` | URL сервиса рабочих областей (для `SYNC_RESOURCE_ID`) | +| `RESOURCE_URL` | string | `https://stage-api.sarex.io/resources/api/v1` | URL сервиса ресурсов (для `SYNC_RESOURCE_ID`) | +| `SYNC_RESOURCE_ID` | bool | `False` | При `True` `resource_id` заметки вычисляется по workspace → target → resource | +| `REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q/` | Реестр образов (используется вспомогательно) | +| `ENABLE_ND` | bool | `False` | Подключить роутер `nd_service` (`/api/v1/nd/*`) | + +### ND-сервис (без префикса) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ND_JWT_ENABLE` | bool | `False` | Включить проверку JWT (`JWTBearer`) на части эндпоинтов НД | +| `ND_JWT_SECRET` | string | `""` | Секрет для подписи/проверки JWT | +| `ND_JWT_ALGORITHM` | string | `HS256` | Алгоритм JWT | +| `ND_ACCESS_TOKEN_EXPIRE_DAYS` | int | `30` | Срок жизни токена НД (дни) | + +### Database (`PG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PG_LOGIN` | string | `""` | Пользователь PostgreSQL | +| `PG_PASSWORD` | string | `""` | Пароль пользователя | +| `PG_DB` | string | `""` | Имя базы данных | +| `PG_HOST` | string | `""` | Хост PostgreSQL | +| `PG_PORT` | string | `5432` | Порт PostgreSQL | +| `PG_SSL_MODE` | string | `disable` | Поле `ssl_mode` настроек (см. замечание ниже — фактически подключение всегда `verify-full`) | +| `DEBUG` | bool | `False` | Через `Field(env='DEBUG')`. При `True` хост БД принудительно `localhost:6432` (pgbouncer) | + +> Итоговый DSN собирается в `PostgresSettings.url` как `postgresql://:@:/`. + +### Django / sarex-backend (`DJANGO_*`) + +Клиент к основному backend (Django). Используется middleware `DjangoUserMiddleware` для аутентификации пользователя (запрос `/client/settings/`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_USE` | bool | `True` | При `False` аутентификация через Django отключается, используется тестовый пользователь (`is_admin=True`) | +| `DJANGO_HOST` | string | `http://localhost:8000` | Базовый хост Django (к нему добавляется `/api`) | +| `DJANGO_TIMEOUT` | int | `10` | Таймаут HTTP-клиента (сек) | +| `DJANGO_TOKEN` | string | `token` | Токен для служебных (sync) запросов | + +### Documentations (`DOCUMENTATIONS_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATIONS_HOST` | string | `https://stage-api.sarex.io/documentations/api/v1` | URL сервиса документации | + +### Workflows (`WORKFLOW_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOW_HOST` | string | `https://stage-api.sarex.io/workflows/api/v1` | URL сервиса обработки процессов | +| `WORKFLOW_TAG` | string | `dev` | Тег/канал workflow (`dev`/`stable`) | +| `WORKFLOW_TIMEOUT` | int | `30` | Таймаут HTTP-клиента (сек) | + +### Attachments (`ATTACHMENT_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ATTACHMENT_HOST` | string | `http://attachments-service.attachments-stage.svc.cluster.local:80/api/v1` | URL сервиса вложений | +| `ATTACHMENT_TIMEOUT` | int | `30` | Таймаут HTTP-клиента (сек) | + +### Logger (`LOG_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `INFO` | Уровень логирования (`DEBUG`/`INFO`/…); при неизвестном значении используется `INFO` | +| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат сообщений лога | + +## Переменные инфраструктуры, сборки и вспомогательных утилит + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `TIMEOUT` | `docker-compose.yml`, `.helm/values.yaml`, `entrypoint.sh` | Таймаут воркеров gunicorn (`--timeout`) | +| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `docker-compose.yml` (сервис `database`) | Параметры локального контейнера Postgres | +| `PGADMIN_DEFAULT_EMAIL` / `PGADMIN_DEFAULT_PASSWORD` | `docker-compose.yml` (сервис `pgadmin`) | Учётные данные pgAdmin для локальной разработки | +| `NPM_NEXUS_TOKEN` | (для фронтенда) | Токен приватного npm-реестра — здесь не используется | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — `universal-chart` (зависимость в `.helm/Chart.yaml`). Обычные значения задаются в блоке `services.main.envs` и различаются по окружениям (`_default`/`stage`/`preprod`/`production`): + +| Переменная | `_default` (stage) | `preprod` | `production` | +| --- | --- | --- | --- | +| `PG_SSL_MODE` | `verify-full` | — | — | +| `PG_PORT` | `6432` | — | — | +| `DJANGO_HOST` | `https://stage.sarex.io` | `https://lk.preprod.sarex.io` | `https://lk.sarex.io` | +| `BASE_HOST` | `https://stage-api.sarex.io/notes` | `https://api.preprod.sarex.io/notes` | `https://api.sarex.io/notes` | +| `TIMEOUT` | `120` | — | — | +| `FAAS_SERVICE` | `https://stage-api.sarex.io/lambdas` | `https://api.preprod.sarex.io/lambdas` | `https://api.sarex.io/lambdas` | +| `WORKSPACE_URL` | `https://stage-api.sarex.io/workspaces/api/v1` | `https://api.preprod.sarex.io/workspaces/api/v1` | `https://api.sarex.io/workspaces/api/v1` | +| `WORKFLOW_HOST` | `https://stage-api.sarex.io/workflows/api/v1` | `https://api.preprod.sarex.io/workflows/api/v1` | `https://api.sarex.io/workflows/api/v1` | +| `WORKFLOW_TAG` | `dev` | `stable` | `stable` | +| `RESOURCE_URL` | `https://stage-api.sarex.io/resources/api/v1` | `https://api.preprod.sarex.io/resources/api/v1` | `https://api.sarex.io/resources/api/v1` | +| `SYNC_RESOURCE_ID` | `0` | — | — | +| `ENABLE_ND` | `1` | `0` | `0` | +| `ATTACHMENT_HOST` | `…attachments-stage…` | `…attachments-preprod…` | `…attachments-prod…` | + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`, stage) | Ключ (`secretKey`) | +| --- | --- | --- | +| `PG_DB` | `notes-postgresql-secret` | `database` | +| `PG_LOGIN` | `notes-postgresql-secret` | `username` | +| `PG_PASSWORD` | `notes-postgresql-secret` | `password` | +| `PG_HOST` | `notes-postgresql-secret` | `host` | +| `DJANGO_TOKEN` | `django-secret` | `token` | + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, порт `8000`, реплики, ресурсы, probes отключены), `image.*`, `service.*` (`notes-backend-service`, в production — `backend-service`), `imagePullSecrets` (`dockerhub`), `ingress.enabled: false`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `aero` | `0.0.1-stage` | +| ветка `master` | `preprod` | `notes-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `notes-prod` | `0.0.1-prod` | + +Ключевые переменные: `SERVICE_NAME=notes-backend`, `DOCKERFILE_PATH`, `RELEASE_NAME`, `CHART_NAME`, `HELM_SET_ARGS` (проброс образа/окружения в `universal-chart`). Джобы `linter` (flake8), `typechecker` (mypy), `rest-api` (docker-compose + newman/postman) выполняются на MR/ветках/тегах. + +## Замечания и потенциальные проблемы + +- **SSL к БД всегда `verify-full`.** Поле `PG_SSL_MODE` (по умолчанию `disable`) в код подключения не попадает: и `PostgresSettings.create_session`, и `DBSessionMiddleware` жёстко передают `connect_args={'sslmode': "verify-full"}`. CA-сертификат монтируется из образа: `Dockerfile` копирует `yandex_pg.pem` → `/root/.postgresql/root.crt`. +- **`DEBUG` — общая переменная.** Она читается и корневым `Settings.debug`, и `PostgresSettings.debug` (`Field(env='DEBUG')`). При `DEBUG=true` хост БД принудительно становится `localhost:6432`, а также включается `ProfilingSqlQueryMiddleware`. +- **Приложение не загружает `.env` автоматически** — переменные нужно экспортировать в окружение (или задавать через `--env`/compose/helm). +- **Аутентификация.** При `DJANGO_USE=true` каждый публичный запрос (кроме путей с `/nd`) проверяется через Django `/client/settings/` по заголовку `Authorization` (опционально `Identity` для Zitadel). При `DJANGO_USE=false` подставляется тестовый администратор — использовать только локально. +- **Роутер НД включается флагом `ENABLE_ND`.** На stage он включён (`1`), на preprod/production выключен (`0`). +- Значение `SYNC_RESOURCE_ID` требует доступности `WORKSPACE_URL` и `RESOURCE_URL`; клиент к ним создаётся с `verify=False`. + +## Минимальный набор для локального запуска + +Postgres и pgAdmin поднимаются через `docker-compose up -d database pgadmin`. Минимально необходимо задать: + +- `PG_LOGIN`, `PG_PASSWORD`, `PG_DB`, `PG_HOST`, `PG_PORT` +- `DJANGO_USE=false` (чтобы не требовать реальный Django-токен) +- при `ENABLE_ND=true` — `ND_JWT_*` при необходимости проверки токена + +Остальные значения имеют рабочие дефолты (см. `.env.example`). diff --git a/apps/notes/ENDPOINTS.md b/apps/notes/ENDPOINTS.md new file mode 100644 index 0000000..cfeb3a5 --- /dev/null +++ b/apps/notes/ENDPOINTS.md @@ -0,0 +1,76 @@ +# Эндпоинты, с которыми взаимодействует notes-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `notes-frontend`, remote-имя `srx_notes`). + +## Как устроено взаимодействие + +Все запросы собраны в объекте `notesApi` в `module/api/endpoints.ts`. Каждый метод вызывает `httpService` (`module/api/http-service.ts`, обёртка над `@sarex-team/sdk-js`) одним из методов `getRequest` / `postRequest` / `putRequest` / `deleteRequest`, передавая: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (относительно базового хоста сервиса); +- `data` — тело запроса (для POST/PUT). + +Базовый хост подставляется `httpService` из `module/api/hosts.ts` в зависимости от `BUILD_ENV` (`local`/`stage`/`preprod`/`prod`). `BUILD_ENV` задаётся через webpack `DefinePlugin` на этапе сборки (`build.config.js`). Итоговый URL = `<базовый хост сервиса>` + `url`. Удалённый модуль `documentations` (Module Federation) подключается отдельно через `module/api/modules-hosts.ts`. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `notes` | Бэкенд заметок (notes-backend) | `https://stage-api.sarex.io/notes` | `https://api.sarex.io/notes` | +| `sarexApi` | Gateway/API Sarex (`/eav`, `/notes`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `documentations` | Сервис документации (бандлы) | `https://stage-api.sarex.io/documentations/` | `https://api.sarex.io/documentations/` | +| `sarex` | Основной backend Sarex (`/api/core`) | `""` (относительные пути) | `""` | +| `workspaces` | Сервис рабочих областей | `https://stage-workspaces.sarex.io` | `https://workspaces.sarex.io` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local` и `preprod`. В `local` `sarex` указывает на `https://stage.sarex.io`, в остальных — пустая строка (относительные пути). Сервисы `workspaces` и `zitadel` объявлены в хостах, но напрямую из `endpoints.ts` не вызываются. Удалённый модуль `documentations` описан в `module/api/modules-hosts.ts` (`…/documentations/static/module/remoteEntry.js`). + +## Эндпоинты по сервисам + +### `notes` — Бэкенд заметок (notes-backend) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `createNote` | POST | `/api/v1/notes/` | Создать заметку | +| `getNote` | GET | `/api/v1/notes/{id}/` | Заметка по id | +| `updateNote` | PUT | `/api/v1/notes/{id}/` | Обновить заметку | +| `deleteNote` | DELETE | `/api/v1/notes/{id}/` | Удалить заметку | +| `getNoteAttachments` | GET | `/api/v1/notes/{noteId}/attachments/` | Вложения заметки | +| `createAttachmentsToNote` | POST | `/api/v1/notes/{noteId}/attachments/` | Загрузить вложения к заметке | +| `deleteAttachmentsFromNote` | DELETE | `/api/v1/attachments/{attachmentId}/` | Удалить вложение | +| `generateDocument` | POST | `/api/v1/notes/{noteId}/generate_document/` | Сгенерировать документ по заметке | +| `postScreen` | POST | `/api/v1/nd/bound-note/{noteId}/` | Привязать скриншот/файл к заметке (НД) | + +### `sarexApi` — Gateway/API Sarex + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getAttributes` | GET | `/eav/api/v0/attribute/` | Атрибуты (EAV) | +| `createLinkNote` | POST | `/notes/api/v1/links/` | Привязать ссылку к заметке (через gateway) | + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getBundle` | GET | `/api/v1/bundles/{id}` | Бандл по id | + +### `sarex` — Основной backend Sarex (`/api/core`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getCompanies` | GET | `/api/core/companies/` | Список компаний | +| `createLink` | POST | `/api/core/target-links/` | Создать ссылку у target | +| `updateLink` | PUT | `/api/core/target-links/{id}/` | Обновить ссылку | +| `deleteLink` | DELETE | `/api/core/target-links/{id}/` | Удалить ссылку | + +## Обработка ошибок + +Централизованного модуля обработки ошибок (аналога `errors.ts`) нет. Ответы `httpService` (`@sarex-team/sdk-js` поверх axios) обрабатываются в местах вызова — в MobX-сторах (`module/Notes/stores/notes.ts`, `sendScreen.ts`) через `try/catch`. + +## Замечания + +- Путь `postScreen` (`/api/v1/nd/bound-note/{noteId}/`) не совпадает с фактическим маршрутом бэкенда `/api/v1/nd/nd_proxy/{instance_id}/bound/` — при интеграции стоит свериться с актуальным API notes-backend. +- Часть создания/обновления ссылок идёт через сервис `sarex` (`/api/core/target-links/`), а привязка ссылки к заметке — через `sarexApi` (`/notes/api/v1/links/`). +- В `endpoints.ts` присутствует закомментированный устаревший вариант `updateLink` (декларативный стиль `service/method/path/body`) — актуальна функция-обёртка над `httpService`. diff --git a/apps/notes/openapi.yaml b/apps/notes/openapi.yaml new file mode 100644 index 0000000..9477b70 --- /dev/null +++ b/apps/notes/openapi.yaml @@ -0,0 +1,737 @@ +openapi: 3.0.3 + +info: + title: Notes Service API + version: "0.0.1" + description: | + REST API сервиса **notes-backend** (`aero/notes-backend`) — управление + заметками к сущностям (workspace), их ссылками, документами и вложениями, + а также генерацией документов через workflow. + + Сервис написан на Python (**FastAPI**, pydantic v1). Приложение собирается + фабрикой `get_app` в `src/app/main.py`. Публичный роутинг подключается с + префиксом `/api/v1` (`src/app/routers/__init__.py`) и включает группы + `notes`, `links`, `documents`, `attachments`. + + Опционально (при `ENABLE_ND=true`) подключается роутер `nd_service` с + префиксом `/api/v1/nd` (`src/app/nd_service/router.py`). + + ### Аутентификация + Аутентификация выполняется middleware `DjangoUserMiddleware` + (`src/app/middleware.py`). При `DJANGO_USE=true` каждый запрос (кроме путей, + содержащих `/nd`) должен содержать заголовок `Authorization` — токен + проверяется обращением к Django `/client/settings/`. Опционально + передаётся заголовок `Identity` (режим Zitadel). Если заголовок + `Authorization` отсутствует — возвращается `401`. + + При `DJANGO_USE=false` middleware подставляет тестового пользователя- + администратора (использовать только локально). + + Дополнительно, права проверяются зависимостью `PermissionManager`: + для не-админов метод сопоставляется с правом (`base.can_add_note` для POST, + `base.can_view_note` для GET, `base.can_change_note` для PUT/PATCH, + `base.can_delete_note` для DELETE); при отсутствии права — `403`. + + Часть эндпоинтов НД (`/api/v1/nd/nd_proxy/*` на запись) защищена + JWT (`JWTBearer`, включается флагом `ND_JWT_ENABLE`). + + ### Замечания (расхождения кода) + - Ошибки валидации тела/параметров (Pydantic) отдаются FastAPI в + стандартном формате `422`. + - Подключение к БД всегда идёт с `sslmode=verify-full` независимо от + значения `PG_SSL_MODE`. + - Многие пути завершаются слэшем (`/api/v1/notes/{id}/`). + + contact: + name: notes-backend + url: https://gitlab/aero/notes-backend + +servers: + - url: https://api.sarex.io/notes + description: Production (ingress, BASE_HOST) + - url: https://api.preprod.sarex.io/notes + description: Preprod (ingress, BASE_HOST) + - url: https://stage-api.sarex.io/notes + description: Stage (ingress, BASE_HOST) + - url: http://notes-backend-service:8000 + description: Внутрикластерный адрес (ClusterIP) + - url: http://localhost:8000 + description: Локальный запуск (gunicorn/docker, порт 8000) + +tags: + - name: notes + description: Заметки — создание, просмотр, обновление, поиск, документы и вложения + - name: links + description: Ссылки, привязанные к заметкам + - name: documents + description: Документы, привязанные к заметкам + - name: attachments + description: Вложения заметок + - name: nd_service + description: Проксирование НД (подключается при ENABLE_ND=true) + +security: + - bearerAuth: [] + +paths: + /api/v1/notes/: + post: + tags: [notes] + summary: Создать заметку + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BaseNote' + responses: + '201': + description: Создано + content: + application/json: + schema: + $ref: '#/components/schemas/Note' + '400': + description: Некорректный company_id + get: + tags: [notes] + summary: Список заметок + parameters: + - { name: search, in: query, schema: { type: string } } + - { name: limit, in: query, schema: { type: integer, default: 1000, minimum: 0 } } + - { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } } + - { name: created_from, in: query, schema: { type: string, format: date-time } } + - { name: created_to, in: query, schema: { type: string, format: date-time } } + - { name: time_start, in: query, schema: { type: string, format: date-time } } + - { name: time_end, in: query, schema: { type: string, format: date-time } } + - { name: author, in: query, schema: { type: string } } + - { name: description, in: query, schema: { type: boolean } } + - { name: document, in: query, schema: { type: boolean } } + - { name: resource_id, in: query, description: "Список UUID через запятую", schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Note' + + /api/v1/notes/{service}/{entity}/{instance_id}/: + get: + tags: [notes] + summary: Заметки по сервису/сущности/инстансу + parameters: + - { name: service, in: path, required: true, schema: { $ref: '#/components/schemas/Service' } } + - { name: entity, in: path, required: true, schema: { $ref: '#/components/schemas/Entity' } } + - { name: instance_id, in: path, required: true, schema: { type: string } } + - { name: full, in: query, description: "Вернуть расширенные заметки (ExtendedNote)", schema: { type: boolean, default: false } } + - { name: search, in: query, schema: { type: string } } + - { name: limit, in: query, schema: { type: integer, default: 1000 } } + - { name: offset, in: query, schema: { type: integer, default: 0 } } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Note' + - $ref: '#/components/schemas/ExtendedNote' + + /api/v1/notes/{instance_id}/: + get: + tags: [notes] + summary: Заметка по id + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Note' } + '404': + description: Не найдено + put: + tags: [notes] + summary: Обновить заметку + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseNote' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Note' } + '400': { description: Некорректный company_id } + '404': { description: Не найдено } + delete: + tags: [notes] + summary: Удалить заметку + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: Удалено (возвращает удалённый объект) + content: + application/json: + schema: { $ref: '#/components/schemas/Note' } + '404': { description: Не найдено } + + /api/v1/notes/{instance_id}/documents/: + post: + tags: [notes] + summary: Добавить документы к заметке + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/BaseDocument' } + responses: + '201': + description: Создано + content: + application/json: + schema: + type: array + items: { $ref: '#/components/schemas/Document' } + '404': { description: Заметка не найдена } + + /api/v1/notes/{instance_id}/attachments/: + post: + tags: [notes] + summary: Загрузить файлы-вложения к заметке + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + files: + type: array + items: { type: string, format: binary } + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: { type: string } + link: { type: string } + id: { type: integer } + attachment_type: { type: string } + '404': { description: Заметка не найдена } + get: + tags: [notes] + summary: Вложения заметки + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { type: object } } + '404': { description: Заметка не найдена } + + /api/v1/notes/{instance_id}/bound_attachments/: + post: + tags: [notes] + summary: Привязать существующее вложение к заметке + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseAttachment' } + responses: + '201': + description: Создано + content: + application/json: + schema: { $ref: '#/components/schemas/Attachment' } + '404': { description: Заметка не найдена } + + /api/v1/notes/{instance_id}/generate_document/: + post: + tags: [notes] + summary: Сгенерировать документ по заметке (через workflow) + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/DocumentCreation' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/WFResponse' } + '404': { description: Заметка не найдена } + + /api/v1/links/: + post: + tags: [links] + summary: Создать ссылку + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseLink' } + responses: + '201': + description: Создано + content: + application/json: + schema: { $ref: '#/components/schemas/Link' } + '400': { description: Некорректный note_id } + get: + tags: [links] + summary: Список ссылок + parameters: + - { name: note, in: query, description: "Фильтр по note_id", schema: { type: integer } } + - { name: limit, in: query, schema: { type: integer, default: 1000 } } + - { name: offset, in: query, schema: { type: integer, default: 0 } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/Link' } } + + /api/v1/links/{instance_id}/: + get: + tags: [links] + summary: Ссылка по id + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Link' } + '404': { description: Не найдено } + put: + tags: [links] + summary: Обновить ссылку + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BaseLink' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Link' } + '404': { description: Не найдено } + delete: + tags: [links] + summary: Удалить ссылку + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: Удалено + content: + application/json: + schema: { $ref: '#/components/schemas/Link' } + '404': { description: Не найдено } + + /api/v1/documents/{instance_id}/: + delete: + tags: [documents] + summary: Удалить документ заметки + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: Удалено + content: + application/json: + schema: { $ref: '#/components/schemas/Document' } + '404': { description: Не найдено } + + /api/v1/attachments/{instance_id}/: + delete: + tags: [attachments] + summary: Удалить вложение + parameters: + - { name: instance_id, in: path, required: true, description: "attachment_id", schema: { type: integer } } + responses: + '200': + description: Удалено + content: + application/json: + schema: { $ref: '#/components/schemas/Attachment' } + '404': { description: Не найдено } + + /api/v1/nd/nd_proxy/: + post: + tags: [nd_service] + summary: Создать НД-прокси + security: [{ bearerAuth: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxyCreate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxySchema' } + '400': { description: Нарушение целостности } + get: + tags: [nd_service] + summary: Список НД-прокси + security: [{ bearerAuth: [] }] + parameters: + - { name: is_bound, in: query, schema: { type: boolean } } + - { name: limit, in: query, schema: { type: integer, default: 1000 } } + - { name: offset, in: query, schema: { type: integer, default: 0 } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: array, items: { $ref: '#/components/schemas/NDProxySchema' } } + + /api/v1/nd/nd_proxy/{instance_id}/: + get: + tags: [nd_service] + summary: НД-прокси по коду + parameters: + - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxySchema' } + '404': { description: Не найдено } + put: + tags: [nd_service] + summary: Обновить НД-прокси + parameters: + - { name: instance_id, in: path, required: true, schema: { type: string } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxyCreate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxySchema' } + '404': { description: Не найдено } + patch: + tags: [nd_service] + summary: Частично обновить НД-прокси + parameters: + - { name: instance_id, in: path, required: true, schema: { type: string } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxyUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxySchema' } + '404': { description: Не найдено } + delete: + tags: [nd_service] + summary: Удалить НД-прокси + parameters: + - { name: instance_id, in: path, required: true, schema: { type: string } } + responses: + '200': + description: Удалено + content: + application/json: + schema: { $ref: '#/components/schemas/NDProxySchema' } + '404': { description: Не найдено } + + /api/v1/nd/nd_proxy/{instance_id}/bound/: + post: + tags: [nd_service] + summary: Привязать заметку и файл к НД-прокси + parameters: + - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } } + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + note_id: { type: integer } + name: { type: string } + file: { type: string, format: binary } + responses: + '200': + description: OK + '404': { description: Не найдено } + + /api/v1/nd/notes/{instance_id}/: + get: + tags: [nd_service] + summary: Заметка НД с изображением + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + responses: + '200': + description: OK + content: + application/json: + schema: { type: object } + '404': { description: Не найдено } + patch: + tags: [nd_service] + summary: Обновить время заметки (НД) + parameters: + - { name: instance_id, in: path, required: true, schema: { type: integer } } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NDUpdateTimeNote' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Note' } + '404': { description: Не найдено } + + /api/v1/nd/users/: + post: + tags: [nd_service] + summary: Проверить существование пользователя по username + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/User' } + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + exist: { type: boolean } + '400': { description: Ошибка запроса к Django } + + /api/v1/nd/token/: + get: + tags: [nd_service] + summary: Сгенерировать JWT для НД-сервиса + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + token: { type: string } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: | + Заголовок `Authorization` (проверяется через Django `/client/settings/`). + Опционально заголовок `Identity` для режима Zitadel. Эндпоинты `/api/v1/nd/*` + на запись используют собственный JWT (`JWTBearer`, флаг ND_JWT_ENABLE). + + schemas: + Service: + type: string + enum: [workspace] + Entity: + type: string + enum: [workspace] + + BaseNote: + type: object + required: [name, service, entity, instance_id, company_id] + properties: + name: { type: string } + body: { type: string, nullable: true } + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + service: { $ref: '#/components/schemas/Service' } + entity: { $ref: '#/components/schemas/Entity' } + instance_id: { type: string } + meta_data: { type: object, nullable: true } + height_base_plane: { type: number, format: float, nullable: true } + height_geom_prmtv: { type: number, format: float, nullable: true } + created_by: { type: integer, nullable: true, minimum: 1 } + company_id: { type: integer, minimum: 1 } + resource_id: { type: string, format: uuid, nullable: true } + + Note: + allOf: + - $ref: '#/components/schemas/BaseNote' + - type: object + required: [id, created_at, updated_at] + properties: + id: { type: integer } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + + ExtendedNote: + allOf: + - $ref: '#/components/schemas/Note' + - type: object + properties: + links: + type: array + items: { type: integer } + documents: + type: array + items: { $ref: '#/components/schemas/Document' } + + DocumentCreation: + type: object + required: [path, values, file_name, template] + properties: + path: { type: string } + values: { type: object, additionalProperties: true } + file_name: { type: string } + template: { type: string } + + WFResponse: + type: object + required: [context] + properties: + context: { type: object } + + BaseLink: + type: object + required: [link_id, note_id] + properties: + link_id: { type: integer, minimum: 1 } + note_id: { type: integer, minimum: 1 } + + Link: + allOf: + - $ref: '#/components/schemas/BaseLink' + - type: object + required: [id] + properties: + id: { type: integer } + + BaseDocument: + type: object + required: [document_id, bundle_id] + properties: + document_id: { type: integer } + bundle_id: { type: string } + note_id: { type: integer, nullable: true } + + Document: + allOf: + - $ref: '#/components/schemas/BaseDocument' + - type: object + required: [id] + properties: + id: { type: integer } + + BaseAttachment: + type: object + required: [attachment_id, object_name, attachment_type, is_state] + properties: + attachment_id: { type: integer, minimum: 1 } + note_id: { type: integer, nullable: true, minimum: 1 } + object_name: { type: string } + attachment_type: { type: string } + is_state: { type: boolean } + + Attachment: + allOf: + - $ref: '#/components/schemas/BaseAttachment' + - type: object + required: [id] + properties: + id: { type: integer } + + NDProxyCreate: + type: object + required: [nd_code, creator, is_locked] + properties: + nd_code: { type: string } + work_description: { type: string, nullable: true } + equipment_description: { type: string, nullable: true } + description: { type: string, nullable: true } + creator: { type: string } + is_locked: { type: boolean } + note_id: { type: integer, nullable: true } + + NDProxyUpdate: + type: object + properties: + nd_code: { type: string, nullable: true } + work_description: { type: string, nullable: true } + equipment_description: { type: string, nullable: true } + description: { type: string, nullable: true } + creator: { type: string, nullable: true } + is_locked: { type: boolean, nullable: true } + note_id: { type: integer, nullable: true } + + NDProxySchema: + allOf: + - $ref: '#/components/schemas/NDProxyCreate' + - type: object + required: [id] + properties: + id: { type: integer } + + NDUpdateTimeNote: + type: object + properties: + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + + User: + type: object + required: [username] + properties: + username: { type: string } diff --git a/apps/pm/.env.example b/apps/pm/.env.example new file mode 100644 index 0000000..a7d854d --- /dev/null +++ b/apps/pm/.env.example @@ -0,0 +1,134 @@ +# Server +SERVER_HOST="https://lk.sarex.io" +SERVER_API_HOST="https://api.sarex.io" +SERVER_DEBUG=false +SERVER_ENABLE_SILK=false +SERVER_ALLOWED_HOSTS=["*"] +SERVER_SECRET_KEY="secret" +SERVER_USE_OTEL=false +SERVER_VERIFY_SSL=true +SERVER_LOG_LEVEL=INFO +SERVER_ENABLE_SYNC_RESOURCES=false +# SERVER_MEDIA_ROOT=sarex/media +SERVER_DELETED_TASK_MAX_AGE_DAYS=30 +SERVER_EXPIRED_TASK_NOTIFICATION_HOUR=9 +SERVER_EXPIRED_TASK_NOTIFICATION_MAX_DAYS=7 +SERVER_SEND_UPDATED_PROJECTS_INFO_INTERVAL=5 + +# Auth +AUTH_ALGORITHM=RS512 +# Replace newlines with \n +AUTH_PUBLIC_KEY='' +AUTH_PUBLIC_TOKEN_URL="https://lk.sarex.io/api/token/public/" + +# Database (PostgreSQL) +DB_ENGINE="django.db.backends.postgresql" +DB_HOST="localhost" +DB_PORT=5432 +DB_DATABASE="sarex_db" +DB_USERNAME="sarex" +DB_PASSWORD="sarex" + +# S3 +S3_HOST="https://storage.yandexcloud.net" +S3_LOGIN="" +S3_PASSWORD="" +S3_BUCKET="sarex-media-storage" +S3_VERIFY="true" + +# Cache (Redis) +CACHE_ENABLE=0 +CACHE_EXPIRATION=300 +CACHE_HOST="localhost" +CACHE_PORT=6379 +CACHE_PASSWORD=None +CACHE_SSL=0 +CACHE_SSL_CA_CERTS=None +CACHE_QUEUE=default + +# ClickHouse +CLICKHOUSE_ENABLE=0 +CLICKHOUSE_HOST="localhost" +CLICKHOUSE_PORT=9000 +CLICKHOUSE_USER="" +CLICKHOUSE_PASSWORD="" +CLICKHOUSE_DATABASE="values_db" +CLICKHOUSE_TABLE="values" +CLICKHOUSE_SECURE=0 +CLICKHOUSE_VERIFY=0 +CLICKHOUSE_CERT="" + +# Kafka +KAFKA_ENABLE=0 +KAFKA_BOOTSTRAP_SERVERS=["localhost:9092"] +KAFKA_SECURITY_PROTOCOL="" +KAFKA_SASL_MECHANISM="" +KAFKA_SASL_PLAIN_USERNAME="user" +KAFKA_SASL_PLAIN_PASSWORD="password" +KAFKA_SSL_CAFILE="" +KAFKA_TOPICS={"planning": "message-hub-stage"} + +# Celery — RabbitMQ (broker) +CELERY_RABBITMQ_HOST='localhost' +CELERY_RABBITMQ_PORT=5672 +CELERY_RABBITMQ_USER='rabbit' +CELERY_RABBITMQ_PASSWORD='rabbit' +CELERY_RABBITMQ_VHOST="pm" + +# Celery — Redis (result backend) +CELERY_REDIS_HOST='redis-service.sarex-stage.svc.cluster.local' +CELERY_REDIS_PORT=6379 +CELERY_REDIS_DATABASE=0 +# CELERY_REDIS_PASSWORD= +CELERY_REDIS_SSL=false +# CELERY_REDIS_SSL_CA_CERTS= +CELERY_REDIS_SSL_CERT_REQS=required + +# Users service +USERS_HOST=https://lk.sarex.io +USERS_API_PREFIX=/api/core +USERS_INTERNAL_HOST=http://backend-service.sarex-stage.svc.cluster.local:8000 +USERS_INTERNAL_PREFIX=/internal +USERS_TIMEOUT=10 +USERS_ENABLE=true + +# Resources service (IAM/resources) +RESOURCES_INTERNAL_HOST=http://sarex-resources-service.resources-stage +RESOURCES_INTERNAL_PREFIX=/api/v1 +RESOURCES_TIMEOUT=10 +RESOURCES_ENABLE=true + +# EAV service +EAV_HOST=http://eav-service.eav-stage +EAV_API_PREFIX=/api/v0 +EAV_API_PREFIX_V1=/api/v1 +EAV_TIMEOUT=10 +EAV_ENABLE=true + +# Gateway service +# GATEWAY_HOST=https://api.sarex.io +GATEWAY_API_PREFIX=/gateway/api/v1 +GATEWAY_TIMEOUT=10 +GATEWAY_ENABLE=true + +# Documentation service +# DOCUMENTATION_HOST=https://api.sarex.io +DOCUMENTATION_API_PREFIX=/documentations/api/v1 +DOCUMENTATION_TIMEOUT=10 +DOCUMENTATION_ENABLE=true + +# Tracing (OpenTelemetry) — used when SERVER_USE_OTEL=true +TRACING_SERVICE_NAME=pm-backend.pm-pord +TRACING_ENDPOINT=localhost:4317 +TRACING_INSECURE=false +TRACING_ENVIRONMENT=prod +TRACING_MODULE=planning +TRACING_TEAM=team_planning +TRACING_COMPONENT=backend + +# Sentry +SENTRY_USE=true +SENTRY_HOST='' +SENTRY_ENVIRONMENT="" +SENTRY_TRACES_SAMPLE_RATE=1.0 +SENTRY_PROFILES_SAMPLE_RATE=0.1 diff --git a/apps/pm/CONFIGURATION.md b/apps/pm/CONFIGURATION.md new file mode 100644 index 0000000..7ffd5b9 --- /dev/null +++ b/apps/pm/CONFIGURATION.md @@ -0,0 +1,280 @@ +# Конфигурация проекта pm-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис — это Django-приложение (Django 5.1 + Django REST Framework), запускаемое как ASGI (`config.asgi_root:application`) через gunicorn с воркерами `uvicorn.workers.UvicornWorker`. Настройки читаются из переменных окружения через набор классов [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), объявленных в `config/settings/base.py` и `config/settings/deps/*`. + +Особенности разбора: + +- **у каждой секции свой префикс** (`env_prefix`), напр. `SERVER_`, `DB_`, `S3_`, `CACHE_`, `CLICKHOUSE_`, `KAFKA_`, `CELERY_RABBITMQ_`, `CELERY_REDIS_`, `AUTH_`, `GATEWAY_`, `EAV_`, `DOCUMENTATION_`, `USERS_`, `RESOURCES_`, `TRACING_`, `SENTRY_`; +- **вложенного делимитера нет** — каждая настройка задаётся плоской переменной вида ``, напр. `DB_HOST`, `CELERY_RABBITMQ_VHOST`; +- **`extra='ignore'`** — все классы игнорируют посторонние переменные, поэтому один общий `.env` без ошибок разбирается всеми секциями; +- **`env_file='.env'`** — в отличие от эталонного сервиса, здесь `.env` **загружается автоматически** каждым классом настроек (у `SentrySettings` — `.env.base` и `.env`). Значения из реального окружения процесса имеют приоритет над файлом. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. `DJANGO_SETTINGS_MODULE` по умолчанию — `config.settings.base` (см. `manage.py`). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (manage.py / gunicorn) | Переменные окружения процесса + файл `.env` в корне репозитория (загружается pydantic-settings) | +| Локально (docker-compose) | `docker-compose.yaml` поднимает зависимости (postgres, redis, rabbit, clickhouse, minio, pgadmin); переменные приложения задаются через окружение/`.env` | +| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `api` и `celery`; чарт-зависимость `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и job-ы `linter`/`typechecker`/`linter_src` | + +Способы запуска процессов: + +| Процесс | Точка входа | Назначение | +| --- | --- | --- | +| HTTP API | `docker/entrypoint.sh` → `gunicorn config.asgi_root:application` (uvicorn worker, порт 8000) | REST API | +| Celery worker/beat | `celery -A config worker -B -Q pm …` (см. `.helm/values.yaml`, сервис `celery`) | Фоновые задачи и периодические (beat) | +| `manage.py migrate` | `manage.py` | Миграции БД | +| `manage.py clean_db` / `import_data` | `manage.py` | Служебные команды (см. `README.md`) | + +## Переменные приложения + +Ниже перечислены все секции настроек с их префиксами. Дефолт `—` означает отсутствие значения по умолчанию в коде. + +### Server (`SERVER_*`) + +Класс `ServerSettings` (`config/settings/base.py`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SERVER_HOST` | string | `https://lk.sarex.io` | Внешний базовый URL (личный кабинет); из него формируется `SERVER_MEDIA_HOST` | +| `SERVER_API_HOST` | string | `https://api.sarex.io` | Базовый URL API-шлюза (используется внешними клиентами по умолчанию) | +| `SERVER_MEDIA_ROOT` | string | `sarex/media` | Каталог медиафайлов | +| `SERVER_DEBUG` | bool | `False` | Django DEBUG. При `True` также включает `FAKE_CELERY` и обход аутентификации в `JWTAuthentication` | +| `SERVER_ENABLE_SILK` | bool | `False` | Подключить профайлер django-silk (только при `DEBUG`) | +| `SERVER_ALLOWED_HOSTS` | list[str] (JSON) | `["*"]` | Django `ALLOWED_HOSTS` | +| `SERVER_SECRET_KEY` | string | `secret` | Django `SECRET_KEY` | +| `SERVER_USE_OTEL` | bool | `False` | Включить OpenTelemetry-трейсинг и OTel-логгер (см. секцию `TRACING_*`) | +| `SERVER_VERIFY_SSL` | bool | `True` | Проверять TLS-сертификаты при обращении к внешним сервисам | +| `SERVER_LOG_LEVEL` | enum | `INFO` | `DEBUG`/`INFO`/`WARNING`/`CRITICAL`/`FATAL` | +| `SERVER_ENABLE_SYNC_RESOURCES` | bool | `False` | Включить синхронизацию ресурсов | +| `SERVER_DELETED_TASK_MAX_AGE_DAYS` | int | `30` | Срок хранения удалённых задач (дней) | +| `SERVER_EXPIRED_TASK_NOTIFICATION_HOUR` | int | `9` | Час отправки уведомлений о просроченных задачах | +| `SERVER_EXPIRED_TASK_NOTIFICATION_MAX_DAYS` | int | `7` | Горизонт уведомлений о просрочке (дней) | +| `SERVER_SEND_UPDATED_PROJECTS_INFO_INTERVAL` | int | `5` | Интервал отправки информации об обновлённых проектах | + +### Auth (`AUTH_*`) + +Класс `AUTHSettings` (`config/settings/deps/auth.py`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_ALGORITHM` | string | `RS512` | Алгоритм проверки подписи JWT | +| `AUTH_PUBLIC_KEY` | string | `''` | Публичный RSA-ключ для проверки JWT в режиме sarex-backend | +| `AUTH_PUBLIC_TOKEN_URL` | string | `https://lk.sarex.io/api/token/public/` | URL получения публичного ключа/токена | + +Аутентификация DRF (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`) — по очереди `ZitadelJWTAuthentication`, затем `JWTAuthentication`; доступ по умолчанию `IsAuthenticated`. Zitadel-режим требует одновременно заголовки `Authorization` и `Identity`. + +### Database (`DB_*`) + +Класс `DBSettings` (`config/settings/deps/db.py`). PostgreSQL. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_ENGINE` | string | `django.db.backends.postgresql` | Движок Django ORM | +| `DB_HOST` | string | `localhost` | Хост PostgreSQL | +| `DB_PORT` | int | `5432` | Порт PostgreSQL | +| `DB_DATABASE` | string | `sarex_db` | Имя базы данных | +| `DB_USERNAME` | string | `sarex` | Пользователь БД | +| `DB_PASSWORD` | string | `sarex` | Пароль пользователя БД | + +### S3 (`S3_*`) + +Класс `S3Settings` (`config/settings/deps/s3.py`). Хранилище через django-storages (boto3), по умолчанию Yandex Object Storage. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_HOST` | string | `https://storage.yandexcloud.net` | Endpoint S3 | +| `S3_LOGIN` | string | `''` | Access key | +| `S3_PASSWORD` | string | `''` | Secret key | +| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию | +| `S3_VERIFY` | bool | `True` | Проверять TLS-сертификат | + +### Cache (`CACHE_*`) + +Класс `CacheSettings` (`config/settings/deps/cache.py`). Redis-кеш, включается отдельно. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CACHE_ENABLE` | bool | `False` | Включить кеш (иначе `CACHE_CLIENT=None`) | +| `CACHE_EXPIRATION` | int | `300` | TTL записей (сек) | +| `CACHE_HOST` | string | `localhost` | Хост Redis | +| `CACHE_PORT` | int | `6379` | Порт Redis | +| `CACHE_PASSWORD` | string \| null | `None` | Пароль | +| `CACHE_SSL` | bool | `False` | Подключение по TLS | +| `CACHE_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату | +| `CACHE_QUEUE` | string | `default` | Имя очереди кеша | + +### ClickHouse (`CLICKHOUSE_*`) + +Класс `ClickHouseSettings` (`config/settings/deps/click_house.py`). Хранилище значений, включается отдельно. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CLICKHOUSE_ENABLE` | bool | `False` | Включить ClickHouse | +| `CLICKHOUSE_HOST` | string | `rc1d-…​.mdb.yandexcloud.net` | Хост | +| `CLICKHOUSE_PORT` | int | `9000` | Порт | +| `CLICKHOUSE_USER` | string | `''` | Пользователь | +| `CLICKHOUSE_PASSWORD` | string | `''` | Пароль | +| `CLICKHOUSE_DATABASE` | string | `values_db` | База данных | +| `CLICKHOUSE_TABLE` | string | `values` | Таблица | +| `CLICKHOUSE_SECURE` | bool | `False` | Защищённое подключение | +| `CLICKHOUSE_VERIFY` | bool | `False` | Проверять сертификат | +| `CLICKHOUSE_CERT` | string | `''` | Путь к CA-сертификату | + +### Kafka (`KAFKA_*`) + +Класс `KafkaSettings` (`config/settings/deps/kafka.py`). Продюсер сообщений, включается отдельно. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_ENABLE` | bool | `False` | Включить продюсер (иначе `get_producer()` вернёт `None`) | +| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | `["localhost:9092"]` | Список брокеров | +| `KAFKA_SECURITY_PROTOCOL` | string | `''` | Протокол безопасности | +| `KAFKA_SASL_MECHANISM` | string | `''` | SASL-механизм | +| `KAFKA_SASL_PLAIN_USERNAME` | string | `user` | SASL-логин | +| `KAFKA_SASL_PLAIN_PASSWORD` | string | `password` | SASL-пароль | +| `KAFKA_SSL_CAFILE` | string | `''` | Путь к CA-сертификату | +| `KAFKA_TOPICS` | dict (JSON) | `{}` | Карта топиков, напр. `{"planning": "message-hub-stage"}` | + +### Celery — RabbitMQ (`CELERY_RABBITMQ_*`) + +Класс `CeleryRabbitMQ` (`config/settings/deps/celery.py`). Брокер задач; из полей собирается `BROKER_URL` (`amqp://…?heartbeat=30`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CELERY_RABBITMQ_HOST` | string | `localhost` | Хост | +| `CELERY_RABBITMQ_PORT` | int | `5672` | Порт | +| `CELERY_RABBITMQ_USER` | string | `rabbit` | Пользователь | +| `CELERY_RABBITMQ_PASSWORD` | string | `rabbit` | Пароль | +| `CELERY_RABBITMQ_VHOST` | string | `api` | Виртуальный хост (в `.env`/helm — `pm`) | + +### Celery — Redis (`CELERY_REDIS_*`) + +Класс `CeleryRedis` (`config/settings/deps/celery.py`). Result backend; при `SSL=true` используется `rediss://` и `ssl_cert_reqs`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CELERY_REDIS_HOST` | string | `redis` | Хост | +| `CELERY_REDIS_PORT` | int | `6379` | Порт | +| `CELERY_REDIS_DATABASE` | int | `0` | Номер БД Redis | +| `CELERY_REDIS_PASSWORD` | string \| null | `None` | Пароль (используется при SSL) | +| `CELERY_REDIS_SSL` | bool | `False` | Подключение по TLS (`rediss://`) | +| `CELERY_REDIS_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату | +| `CELERY_REDIS_SSL_CERT_REQS` | string \| null | `required` | Требования к сертификату | + +### HTTP-клиенты внешних сервисов + +Общий базовый класс `BaseApiServiceMixin` (`config/settings/base.py`): поля `host` (по умолчанию `SERVER_API_HOST`), `api_prefix`, `internal_host`, `internal_prefix`, `timeout` (`10`), `enable` (`True`). Наследники задают собственные префиксы и дефолтные значения prefix. + +| Секция / префикс | Класс | Назначение | Особенности | +| --- | --- | --- | --- | +| `GATEWAY_*` | `GateWaySetttings` | API-шлюз | `api_prefix=/gateway/api/v1` | +| `EAV_*` | `EAVSettings` | Сервис атрибутов (EAV) | `api_prefix=/eav/api/v0`, доп. `EAV_API_PREFIX_V1=/eav/api/v1` | +| `DOCUMENTATION_*` | `DocumentationSettings` | Сервис документаций | `api_prefix=/documentations/api/v1` | +| `USERS_*` | `UsersSettings` | Сервис пользователей (core) | `host=SERVER_HOST`, `api_prefix=/api/core`, `internal_host=http://localhost:8001`, `internal_prefix=/internal` | +| `RESOURCES_*` | `ResourceSettings` | Сервис ресурсов (IAM) | `internal_host=http://localhost:8001`, `internal_prefix=/api/v1` | + +Для каждого клиента доступны переменные `HOST`, `API_PREFIX`, `INTERNAL_HOST`, `INTERNAL_PREFIX`, `TIMEOUT`, `ENABLE` (плюс `EAV_API_PREFIX_V1`). + +### Tracing / OpenTelemetry (`TRACING_*`) + +Класс `TracingConfig` (`config/settings/base.py`). Применяется только при `SERVER_USE_OTEL=true`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACING_SERVICE_NAME` | string | `pm-backend.pm-pord` | Имя сервиса в трейсах | +| `TRACING_ENDPOINT` | string | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACING_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение | +| `TRACING_ENVIRONMENT` | string | `prod` | Окружение (атрибут трейса) | +| `TRACING_MODULE` | string | `planning` | Модуль (атрибут трейса) | +| `TRACING_TEAM` | string | `team_planning` | Команда (атрибут трейса) | +| `TRACING_COMPONENT` | string | `backend` | Компонент (атрибут трейса) | + +### Sentry (`SENTRY_*`) + +Класс `SentrySettings` (`config/settings/deps/sentry.py`). Читает `.env.base` и `.env`. Инициализируется при `SENTRY_USE=true`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SENTRY_USE` | bool | `True` | Включить Sentry | +| `SENTRY_HOST` | string | `''` | DSN Sentry | +| `SENTRY_ENVIRONMENT` | string | `''` | Окружение | +| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов | +| `SENTRY_PROFILES_SAMPLE_RATE` | float | `0.1` | Доля профилей | + +## Переменные инфраструктуры, сборки и деплоя + +Не читаются кодом приложения напрямую, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `GUNICORN_WORKERS` | `docker/entrypoint.sh` | Число воркеров gunicorn (по умолчанию `4`) | +| `TIMEOUT` | `docker/entrypoint.sh` | Таймаут воркера gunicorn (по умолчанию `60`) | +| `NEXUS_USERNAME`, `NEXUS_PASSWORD` | `docker/Dockerfile` (build-arg), `.gitlab-ci.yml` | Доступ к приватному индексу пакетов Nexus | +| `SETTINGS_BASE_HOST` | `.helm/values.yaml` (env) | Базовый хост окружения (`stage`/`preprod`/`lk`) | + +Порядок запуска контейнера (`docker/entrypoint.sh`): миграции закомментированы, сразу стартует gunicorn с ASGI-приложением `config.asgi_root:application`. + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт зависит от `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`). Значения задаются для двух сервисов — `api` и `celery` — с ключами по окружениям `_default`/`stage`/`preprod`/`production`. + +Обычные значения (блок `envs`) включают: `USERS_INTERNAL_HOST`, `CELERY_REDIS_HOST`, `RESOURCES_INTERNAL_HOST`, `EAV_HOST`, `EAV_API_PREFIX`, `EAV_API_PREFIX_V1`, `TRACING_ENDPOINT`, `TRACING_INSECURE`, `SERVER_ENABLE_SYNC_RESOURCES`, `SERVER_DELETED_TASK_MAX_AGE_DAYS`, `SERVER_EXPIRED_TASK_NOTIFICATION_HOUR`, `SETTINGS_BASE_HOST` (различаются адресами сервисов по окружениям). + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Секрет (`secretName`) | Переменные | +| --- | --- | +| `ya-pg-secret-pm` | `DB_USERNAME`, `DB_PASSWORD`, `DB_DATABASE`, `DB_HOST`, `DB_PORT` | +| `ya-s3-secret-pm` | `S3_HOST`, `S3_LOGIN`, `S3_PASSWORD`, `S3_BUCKET` | +| `cache-secret-pm` | `CACHE_HOST`, `CACHE_PORT`, `CACHE_PASSWORD`, `CACHE_SSL`, `CACHE_SSL_CA_CERTS`, `CACHE_ENABLE` | +| `clickhouse-secret-pm` | `CLICKHOUSE_HOST`, `CLICKHOUSE_PORT`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_TABLE`, `CLICKHOUSE_SECURE`, `CLICKHOUSE_VERIFY`, `CLICKHOUSE_CERT`, `CLICKHOUSE_ENABLE` | +| `ya-kafka-secret-pm` | `KAFKA_ENABLE`, `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_SECURITY_PROTOCOL`, `KAFKA_SASL_MECHANISM`, `KAFKA_SASL_PLAIN_USERNAME`, `KAFKA_SASL_PLAIN_PASSWORD`, `KAFKA_SSL_CAFILE`, `KAFKA_TOPICS` | +| `rabbit-secret-pm` | `CELERY_RABBITMQ_HOST`, `CELERY_RABBITMQ_PORT`, `CELERY_RABBITMQ_USER`, `CELERY_RABBITMQ_PASSWORD`, `CELERY_RABBITMQ_VHOST` | +| `server-secret-pm` | `AUTH_PUBLIC_TOKEN_URL`, `SERVER_HOST`, `SERVER_API_HOST`, `SERVER_DEBUG`, `SERVER_ALLOWED_HOSTS`, `SERVER_VERIFY_SSL`, `SERVER_LOG_LEVEL` | + +Дополнительно чарт монтирует CA-сертификат ClickHouse (configMap `ch-cert`, ключ `CA.pem`) как файл `/root/clickhouse/RootCA.crt` и `tmp-volume` в `/tmp`. Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, probes на `/api/health/`), `image.*`, `service.*`, `affinity`, `owner`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `planning` | `0.0.1-stage` | +| ветка `master` | `preprod` | `pm-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `pm-prod` | `0.0.1-prod` | +| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные пайплайна: `SERVICE_NAME=pm-backend`, `DOCKERFILE_PATH=docker/Dockerfile`, `RELEASE_NAME`, `CHART_NAME`, `K8S_HUSTLER_BRANCH`, `IMAGE_NAME`, `HELM_SET_ARGS` (`--set universal-chart.services.{api,celery}.image.name…` и метаданные коммита). Джобы стадии `test`: `linter` (flake8 по `sarex`), `typechecker` (mypy по `src`), `linter_src` (ruff check/format по `src`). + +## Замечания и потенциальные проблемы + +- При `SERVER_DEBUG=true` `JWTAuthentication` возвращает анонимного пользователя и **аутентификация обходится** — использовать только локально. +- Все секции читают `.env` автоматически (`env_file='.env'`), поэтому один общий `.env` в корне достаточен для локального запуска. `SentrySettings` дополнительно читает `.env.base`. +- Sentry инициализируется по умолчанию (`SENTRY_USE=true`), но при пустом `SENTRY_HOST` DSN не задан — задайте `SENTRY_USE=false` локально, чтобы отключить. +- `CELERY_RABBITMQ_VHOST` в коде по умолчанию `api`, тогда как в `.env.example`/helm используется `pm` — для корректной работы очереди значение должно совпадать с брокером. +- Переменные `CACHE_PASSWORD`/`CACHE_SSL_CA_CERTS`/`CELERY_REDIS_PASSWORD` допускают `None`; в `.env` для «пустого» значения используйте `None` или закомментируйте строку. +- Список-переменные (`SERVER_ALLOWED_HOSTS`, `KAFKA_BOOTSTRAP_SERVERS`) и dict (`KAFKA_TOPICS`) задаются в формате JSON. + +## Минимальный набор для локального запуска + +Зависимости (postgres, redis, rabbit, clickhouse, minio) поднимаются через `docker-compose up`. Минимально необходимо задать: + +- `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` +- `S3_HOST`, `S3_BUCKET`, `S3_LOGIN`, `S3_PASSWORD`, `S3_VERIFY` +- `CELERY_RABBITMQ_*` (host/port/user/password/vhost) и `CELERY_REDIS_HOST`/`CELERY_REDIS_PORT` +- `SERVER_DEBUG=true` (локально), `SERVER_ALLOWED_HOSTS`, `SERVER_LOG_LEVEL` +- `AUTH_PUBLIC_KEY` (можно пустой при `SERVER_DEBUG=true`) +- адреса внешних сервисов при необходимости: `USERS_INTERNAL_HOST`, `RESOURCES_INTERNAL_HOST`, `EAV_HOST` +- `SENTRY_USE=false`, `SERVER_USE_OTEL=false` — чтобы не подключать Sentry/OTel локально +- опциональные подсистемы по флагам: `CACHE_ENABLE`, `CLICKHOUSE_ENABLE`, `KAFKA_ENABLE` (`0` по умолчанию) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/pm/ENDPOINTS.md b/apps/pm/ENDPOINTS.md new file mode 100644 index 0000000..2a7a8ff --- /dev/null +++ b/apps/pm/ENDPOINTS.md @@ -0,0 +1,229 @@ +# Эндпоинты, с которыми взаимодействует pm-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `pm-frontend`). + +## Как устроено взаимодействие + +Все REST-запросы идут через единый `httpService` (`src/services/api/http-service.ts`, поверх `@sarex-team/sdk-js` + axios). API-модули объявлены декларативно в `src/store/api/*` и `src/services/api/*` и вызывают `httpService.Request({ service, url, data?, axiosConfig?, errorMessage? })`, где: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (обычно уже включает свой префикс, напр. `/api/pm/msp/...`); +- `data` — тело запроса; `errorMessage` — сообщение при ошибке. + +Базовый хост подставляется SDK-функцией `resolveHost(service)` по значению `hosts[ENDPOINT].hosts[service]` из `src/services/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url`. Прямых вызовов `axios.*`/`fetch()` в `src/` нет. + +Выбор окружения — сборочная переменная `process.env.ENDPOINT` (инъектируется webpack через `DefinePlugin`). Допустимые значения: `local`, `stage`, `prod`, `preprod`, `contour`; значение по умолчанию — `prod`. Для `local`/`stage` dev-сервер webpack (`configWebpack/buildDevServer.ts`) проксирует относительные префиксы (`/sarex-backend`, `/pm`, `/sarex-eav-v1`, `/sarex-gateway`, `/sarex-api` …) на stage-бэкенды. + +## Базовые хосты по сервисам и окружениям + +Значения из `src/services/api/hosts.ts`. Для сервиса `sarex` в удалённых окружениях хост пустой (`""`) — запросы идут относительно текущего origin (маршрутизируются ingress/gateway перед SPA). + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `sarex` | Монолит / PM REST (`/api/pm/...`, `/api/core/...`) | `""` (same-origin) | `""` (same-origin) | +| `pm` | PM-микросервис (`/api/v1/...`: интегрированные задачи, комментарии) | `https://stage-api.sarex.io/pm` | `https://api.sarex.io/pm` | +| `sarexApi` | API-шлюз для flows (reviews, документы, процессы) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `eavV1` | Сервис атрибутов EAV (`/api/v2`, `/api/v4`) | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` | +| `gateway` | Шлюз ресурсов (`/api/v1/resources`) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `notifications` | Лямбда уведомлений | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` | +| `bimv2` | BIM v2 | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` | +| `bim` | BIM v1 | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` | +| `analyticsV2` | Аналитика v2 | `https://stage-api.sarex.io/analytics-v2` | `https://api.sarex.io/analytics-v2` | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `documentations` | Сервис документаций | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `comparisons` | Сервис сравнений | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `projects` | Сервис проектов | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | + +> Также определены окружения `local` (относительные прокси-префиксы) и `preprod`/`contour`. Реально используются в коде только `sarex`, `pm`, `sarexApi`, `eavV1`, `gateway`; остальные сервисы объявлены в hosts, но REST-вызовов к ним в этом модуле нет. Кроме REST есть WebSocket (`src/store/stores/gantt/ganttWebsocket.ts`): `io(`${url}/project`, { path: "/message-hub/socket.io" })`, где `url` — `https://stage-api.sarex.io` (stage) / `https://api.sarex.io` (prod). + +## Эндпоинты по модулям + +### `store/api/api.ts` — ProjectsAPI (сервис `sarex`, если не указано иное) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getFolderById` | GET | `/api/pm/msp/folders/{folderId}` | Папка по id | +| `getProjectsAndFolders` | GET | `/api/pm/msp/projects/?{query}` | Список проектов и папок | +| `getProjectTemplates` | GET | `/api/pm/msp/projects/?{query}` | Список шаблонов проектов | +| `getAllProjects` | GET | `/api/pm/msp/projects/` | Все проекты | +| `getAllProjectsWithNotFolders` | GET | `/api/pm/msp/projects/?schema=tiny&is_folder=false&company_id={companyId}` | Проекты (без папок) по компании | +| `getProject` | GET | `/api/pm/msp/projects/{projectId}/?extend=true` | Проект (расширенный) | +| `getKeyMilestones` | GET | `/api/pm/msp/projects/{projectId}/key_milestones/` | Ключевые вехи проекта | +| `getResourcePlanning` | GET | `/api/pm/msp/resources/resource_planning/?projects={projects}&start={start}&end={end}&resource_type=human&scale={scale}` | Ресурсное планирование | +| `changeResourceForTask` | PATCH | `/api/pm/msp/resources-tasks/assign/` | Назначить ресурс на задачу | +| `getIntegratedProjects` | GET | `/api/v1/projects/{projectId}/integrated-tasks/` (сервис `pm`) | Интегрированные задачи проекта | +| `createProject` | POST | `/api/pm/msp/projects/` | Создать проект | +| `importProject` / `importProjectV2` | POST | `/api/pm/msp/projects/import/` | Импорт проекта | +| `updateProjectV2` | PATCH | `/api/pm/msp/projects/{projectId}/` | Обновить проект | +| `updateFolders` | GET | `/api/pm/msp/projects/?{idsQuery}` | Папки по id | +| `deleteProject` | DELETE | `/api/pm/msp/projects/{id}` | Удалить проект | +| `createProjectTemplate` | POST | `/api/pm/msp/projects/{projectId}/create_template/` | Создать шаблон из проекта | +| `getProjectStates` | GET | `/api/pm/msp/projects/{id}/states/` | Базовые планы проекта | +| `createProjectState` | POST | `/api/pm/msp/project-states/` | Создать базовый план | +| `updateProjectState` | PUT | `/api/pm/msp/project-states/{id}/` | Обновить базовый план | +| `deleteProjectState` | DELETE | `/api/pm/msp/project-states/{id}/` | Удалить базовый план | +| `patchProjectStateDifferenceData` | PATCH | `/api/pm/msp/project-states/{stateId}/edit_state/` | Изменить данные базового плана | +| `getProjectState` | GET | `/api/pm/msp/project-states/{id}/` | Базовый план по id | +| `getProjectStateData` | GET | `/api/pm/msp/project-states/{id}/data/` | Данные базового плана | +| `addTasksToBasicPlan` | POST | `/api/pm/msp/project-states/{planId}/data/` | Добавить задачи в базовый план | +| `getStatusImport` | GET | `/api/pm/msp/external-task-info/?task_id={uuid}` | Статус фоновой задачи импорта | +| `getTasks` | GET | `/api/pm/msp/projects/{id}/tasks/` | Задачи проекта | +| `taskIndex` | PATCH | `/api/pm/msp/tasks/task_index/` | Переиндексация задач | +| `bulkCreateTasks` | POST | `/api/pm/msp/projects/{project}/create_tasks/` | Массовое создание задач | +| `bulkUpdateTasks` | PATCH | `/api/pm/msp/projects/{project}/update_tasks/` | Массовое обновление задач | +| `bulkDeleteTasks` | DELETE | `/api/pm/msp/projects/{project}/delete_tasks/` | Массовое удаление задач | +| `copyPasteTasks` | POST | `/api/pm/msp/tasks/copy/` | Копирование задач | +| `getTaskDescription` | GET | `/api/pm/msp/tasks/{taskId}/descriptions/` | Описание задачи | +| `updateTaskDescription` | PATCH | `/api/pm/msp/tasks/{taskId}/descriptions/` | Обновить описание задачи | +| `createComment` | POST | `/api/v1/comments/` (сервис `pm`) | Создать комментарий к задаче | +| `getActualValues` | GET | `/api/pm/msp/values/?task={task}` | Фактические значения по задаче | +| `createActualValue` | POST | `/api/pm/msp/values/` | Создать фактическое значение | +| `updateActualValue` | PUT | `/api/pm/msp/values/{id}/` | Обновить фактическое значение | +| `deleteActualValues` | DELETE | `/api/pm/msp/values/{id}/` | Удалить фактическое значение | +| `getAllGanttLinks` | GET | `/api/pm/msp/task-relations/{params}` | Связи задач (Ганта) | +| `createGanttLinks` | POST | `/api/pm/msp/task-relations/` | Создать связи задач | +| `updateGanttLink` | PUT | `/api/pm/msp/task-relations/{id}/` | Обновить связь задач | +| `bulkDeleteRelation` | DELETE | `/api/pm/msp/task-relations/bulk_delete/` | Массовое удаление связей | +| `getResourcesTable` | GET | `/api/pm/msp/resources/?{query}` | Таблица ресурсов | +| `updateResources` | PUT | `/api/pm/msp/resources/{id}/` | Обновить ресурс | +| `deleteResource` | DELETE | `/api/pm/msp/resources/{id}/` | Удалить ресурс | +| `createVisualProfile` | POST | `/api/pm/msp/visual-profiles/` | Создать визуальный профиль | +| `editVisualProfile` | PATCH | `/api/pm/msp/visual-profiles/{id}` | Изменить визуальный профиль | +| `getVisualProfiles` | GET | `/api/pm/msp/projects/{projectId}/profiles/` | Визуальные профили проекта | +| `getMyTasks` | GET | `/api/pm/msp/tasks/?responsible=true&executors=true&{query}` | Мои задачи | +| `copyProject` | POST | `/api/pm/msp/projects/{projectId}/copy_project/` | Копировать проект | +| `createProjectDocument` | POST | `/api/pm/msp/projects/{projectId}/ksg_docs_sync/` | Синхронизация КСГ-документов | +| `getUsersByCompanyId` | GET | `/api/core/v2/users/?company={companyId}&{query}` | Пользователи компании | +| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={companyId}` | Отделы компании | +| `getPositionsV2` | GET | `/api/core/admin/positions/?company={companyId}` | Должности компании | +| `getDocumentStatus` | GET | `/flows/api/v1/documents/?full=true&document_ids={ids}` (сервис `sarexApi`) | Статус документов | +| `exportTasksPDF` | POST | `/api/pm/msp/projects/{id}/export_project_to_pdf/` | Экспорт проекта в PDF (+опрос `external-task-info`) | +| `projectExport` | POST | `/api/pm/msp/projects/{id}/project_export/` | Экспорт проекта (xlsx/xml) | + +### `store/api/attributes-api-v2.ts` — AttributesApiV2 + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getProjectAttributes` | GET | `/api/pm/msp/project-attribute/?project={projectId}` (сервис `sarex`) | Атрибуты проекта | +| `updateProjectAttribute` | PUT | `/api/pm/msp/project-attribute/{id}/` (сервис `sarex`) | Обновить атрибут проекта | +| `deleteProjectAttribute` | DELETE | `/api/pm/msp/project-attribute/{id}/` (сервис `sarex`) | Удалить атрибут проекта | +| `addAttributesToProject` | POST | `/api/pm/msp/project-attribute/` (сервис `sarex`) | Привязать атрибуты к проекту | +| `getTimeMarkersData` | GET | `/api/pm/msp/projects/{projectID}/time_markers_data/?attributes={ids}&with_hierarchy={flag}` (сервис `sarex`) | Данные временных маркеров | +| `getAttributesList` | GET | `/api/v4/attribute/?model_name=gantt-task&company_id={companyId}` (сервис `eavV1`) | Список атрибутов (EAV) | +| `getAssetsByAttribute` | GET | `/api/v4/assets/?path_contains={assetsId}` (сервис `eavV1`) | Ассеты по атрибуту | +| `getAttributesByAssetsParent` | GET | `/api/v4/assets/?tenant_id={companyId}&depth=0` (сервис `eavV1`) | Корневые ассеты компании | +| `createNewAttribute` | POST | `/api/v2/attribute/` (сервис `eavV1`) | Создать атрибут (EAV) | +| `updateAttribute` | PATCH | `/api/v2/attribute/{id}/` (сервис `eavV1`) | Обновить атрибут (EAV) | + +### `store/api/calculate-api.ts` — CalculatesAPI (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `postFormula` | POST | `/api/pm/msp/projects/{id}/formula/` | Пересчёт по формуле | + +### `store/api/calendarsApi.ts` — CalendarsApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getAllCalendars` | GET | `/api/pm/msp/calendars/` | Все календари | +| `getCalendarById` | GET | `/api/pm/msp/calendars/{id}/` | Календарь по id | +| `createCalendar` | POST | `/api/pm/msp/calendars/` | Создать календарь | +| `editCalendar` | PATCH | `/api/pm/msp/calendars/{id}/` | Изменить календарь | +| `deleteCalendar` | DELETE | `/api/pm/msp/calendars/{id}/` | Удалить календарь | + +### `store/api/issuesApi.ts` — IssuesApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getIssueTypes` | GET | `/api/pm/msp/entity-relations/?project_id={projectId}` | Типы связей/проблем проекта | +| `patchIssueType` | PATCH | `/api/pm/msp/entity-relations/{issueTypeId}/` | Обновить тип связи | +| `getIssueData` | GET | `/api/pm/msp/projects/{projectId}/issues_data/` | Данные проблем проекта | + +### `store/api/ksgStatesApi.ts` — KsgStatesApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getProjestStates` | GET | `/api/pm/msp/project-settings/?{query}` | Настройки/состояния КСГ | +| `createProjectState` | POST | `/api/pm/msp/project-settings/` | Создать состояние КСГ | +| `editProjectState` | PATCH | `/api/pm/msp/project-settings/{id}/` | Изменить состояние КСГ | +| `deleteProjectAttribute` | DELETE | `/api/pm/msp/project-settings/{id}/` | Удалить состояние КСГ | +| `checkApplyState` | POST | `/api/pm/msp/project-settings/{id}/apply/` | Применить состояние КСГ | + +### `store/api/permissionsApi.ts` — PermissionsApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getPermissions` | GET | `/api/pm/msp/projects/{projectId}/permissions/` | Права проекта | +| `getTaskPermissions` | GET | `/api/pm/msp/tasks/{taskId}/permissions/` | Права задачи | +| `getAllTaskPermissions` | GET | `/api/pm/msp/projects/{projectId}/all_permissions/` | Все права задач проекта | +| `savePermission` | POST | `/api/pm/msp/projects/{projectId}/permissions/` | Сохранить права проекта | +| `saveTaskPermission` | POST | `/api/pm/msp/tasks/{taskId}/permissions/` | Сохранить права задачи | + +### `store/api/relationApi.ts` — RelationsApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getProjectRelations` | GET | `/api/pm/msp/projects/{projectId}/relations/` | Связи/интеграции проекта | +| `postProjectIntegrations` | POST | `/api/pm/msp/projects/{id}/bulk_integration/` | Массовое создание интеграций | +| `updateProjectIntegration` | PATCH | `/api/pm/msp/project-relations/{id}/` | Обновить интеграцию | +| `deleteProjectIntegration` | DELETE | `/api/pm/msp/project-relations/{id}/` | Удалить интеграцию | + +### `store/api/reviewsApi.ts` — ReviewAPI (сервис `sarexApi`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `createReview` | POST | `/flows/api/v1/reviews/` | Создать ревью/согласование | +| `getProcesses` | GET | `/flows/api/v1/flows/?{query}` | Процессы/потоки согласования | + +### `store/api/systemLogApi.ts` — SystemLogApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getNewSystemLogs` | GET | `/api/pm/msp/projects/{projectId}/system-logs/?{query}` | Системные логи проекта | +| `getDetailsSystemLog` | GET | `/api/pm/msp/projects/{projectId}/system-log-detail/?log_id={logId}` | Детали записи лога | +| `rollBack` | POST | `/api/pm/msp/projects/{projectId}/rollback-to-record/` | Откат к записи лога | + +### `store/api/taskDetailingApi.ts` — TaskDetailingApi (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getRole` | GET | `/api/pm/msp/projects/{projectId}/rule/` | Правило детализации проекта | +| `createRule` | POST | `/api/pm/msp/projects/{projectId}/rule/` | Создать правило детализации | +| `getDetailTasks` | GET | `/api/pm/msp/detailed-tasks/?task={taskId}` | Детализированные задачи | +| `patchDetailTasks` | PATCH | `/api/pm/msp/detailed-tasks/{detailingTaskId}/` | Обновить детализированную задачу | + +### `store/api/workspaceApi.ts` — WorkspaceAPI (сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getResourcesByTaskId` | GET | `/api/pm/msp/resources-tasks/?tasks={task}` | Ресурсы по задаче | +| `getResourcesByIds` | GET | `/api/pm/msp/resources/?ids={resourcesIds}` | Ресурсы по id | +| `loadElementsByResourceIds` | GET | `/api/pm/msp/resources-elements/?resources={ids}` | Элементы ресурсов | +| `connectResourcesTasks` | POST | `/api/pm/msp/resources-tasks/` | Привязать ресурс к задаче | +| `editResourcesTasks` | PATCH | `/api/pm/msp/resources-tasks/bulk_update/` | Массово изменить связи ресурс-задача | +| `deleteResource` | DELETE | `/api/pm/msp/resources-tasks/bulk_delete/` | Массово удалить связи ресурс-задача | + +### `services/api/fetch/gateway.ts` — GatewayAPI (сервис `gateway`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchResources` | GET | `/api/v1/resources` | Ресурсы шлюза (доступы/фичи) | + +### Gantt-репозитории (`src/pages/TasksNew/GanttWorkspace/repositories/*`, сервис `sarex`) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `WorkspaceSelectedKSGProjectRepository` | GET | `/api/pm/msp/projects/?bundle_id={bundleID}&schema=tiny` | Проекты выбранного бандла КСГ | +| `TaskResourceConnectionBaseRepository` | GET | `/api/pm/msp/projects/{id}/profiles/` | Профили ресурсов проекта | +| `TaskResourcesConnectionsRepository` | GET | `/api/pm/msp/resources-tasks/?projects={project}` | Связи ресурс-задача по проекту | +| `TasksRepository` (список) | GET | `/api/pm/msp/tasks/?project={project}&{query}` | Задачи проекта | +| `TasksRepository` (одна) | GET | `/api/pm/msp/tasks/{id}/` | Одна задача | +| `uploadFileToServer` | POST (multipart) | `/api/pm/msp/descriptions/upload_file/` | Загрузка файла/изображения в описание | + +## Обработка ошибок + +Централизованного middleware (RTK Query `createApi`/`fetchBaseQuery` не используется) нет — API-модули оборачивают axios-based `httpService`. Типичные паттерны: большинство вызовов `.then(r => r.data)` и пробрасывают ошибку выше; часть — `try/catch` с `isAxiosError(error)`, где `403` даёт «Нет доступа»/«Доступ запрещён», а прочие ошибки — общее сообщение (напр. «Не удалось сохранить данные, попробуйте ещё раз»), нередко показываемое через `createToast(...)` и повторно выбрасываемое как `new Error(...)`. Некоторые читают `error.response.data.detail`. `getStatusImport` при ошибке возвращает `{ request_failed: true }`; `exportTasksPDF` опрашивает `external-task-info` каждые 2 с до `is_ready`. Часть вызовов передаёт в SDK опцию `errorMessage` (напр. «Некорректные данные»). diff --git a/apps/pm/openapi.yaml b/apps/pm/openapi.yaml new file mode 100644 index 0000000..2f84b2c --- /dev/null +++ b/apps/pm/openapi.yaml @@ -0,0 +1,1768 @@ +openapi: 3.0.3 + +info: + title: PM Backend API + version: "1.0.0" + description: | + REST API сервиса **pm-backend** (`planning/pm-backend`) — управление + проектами и папками, задачами (иерархия, связи, ресурсы), базовыми планами + (состояниями), атрибутами, календарями, настройками КСГ, детализацией задач, + системным журналом и экспортом. + + Сервис написан на Python (**Django 5.1 + Django REST Framework**) и запускается + как ASGI-приложение (`config.asgi_root:application`) через gunicorn с воркерами + `uvicorn.workers.UvicornWorker` (порт `8000`). Роутинг задан в `config/urls.py` + и состоит из трёх групп: + + - публичный API — префикс `/api/pm/msp/` (`sarex.pm.urls`); + - внешний API v2 — префикс `/api/pm/external/v2/` (`sarex.pm.urls_external`), + только чтение; + - внутренний API — префикс `/internal/pm/` (`sarex.pm.urls_internal`), + предназначен для вызовов внутри кластера, аутентификация не требуется. + + ### Аутентификация + Публичные (`/api/pm/msp/*`) и внешние (`/api/pm/external/v2/*`) эндпоинты + требуют аутентификации (DRF `IsAuthenticated`). Проверка выполняется по цепочке + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): + + 1. **Zitadel** (`ZitadelJWTAuthentication`) — требуются одновременно заголовки + `Authorization: Bearer ` и `Identity: Identity `. При отсутствии + любого из них — переход к следующему механизму/ошибка. + 2. **sarex-backend** (`JWTAuthentication`) — подпись токена `Authorization: + Bearer ` проверяется публичным RSA-ключом (`AUTH_PUBLIC_KEY`, + алгоритм `RS512`). При `SERVER_DEBUG=true` аутентификация обходится + (возвращается анонимный пользователь). + + Внутренние эндпоинты (`/internal/pm/*`) имеют `authentication_classes=[]` и + `AllowAny` — доступ ограничивается сетевым слоем. Отдельный публичный эндпоинт + `/api/pm/msp/external-task-info/` также открыт (`AllowAny`). + + ### Пагинация + По умолчанию используется DRF `LimitOffsetPagination` (`PAGE_SIZE=1000`). + Списочные ответы оборачиваются в объект `{ count, next, previous, results }`; + размер страницы задаётся query-параметром `limit`, смещение — `offset`. Часть + «тяжёлых» списков (задачи, привязки ресурсов, связи задач) использует + `ProjectTaskPagination` (`default_limit=30000`). + + ### Обработка ошибок + Ошибки возвращаются в стандартном формате DRF: ошибки валидации — `400` + (`{ "": [""] }` либо `{ "detail": "..." }`), нет прав — `403` + (`{ "detail": "..." }`), не аутентифицирован — `401`, не найдено — `404`. + Объектные права проверяются `PermissionMixin` (raw-SQL), суперпользователь + их обходит. + + ### Замечания (расхождения кода) + - Внешние вьюсеты v2 объявляют `http_method_names = ['get']`, поэтому доступны + только `list`/`retrieve` (и GET-`@action`), даже если в коде есть методы + записи. + - Ряд bulk-эндпоинтов принимает JSON-массив (list-сериализатор). + - Datetime-поля (`DateTimeWithoutTZFiled`) наивные — таймзона отбрасывается. + - Внутренние эндпоинты принимают/возвращают «сырые» словари, без модельных + сериализаторов. + + contact: + name: pm-backend + url: https://gitlab/planning/pm-backend + +servers: + - url: https://api.sarex.io + description: Production (ingress) + - url: https://stage-api.sarex.io + description: Stage (ingress) + - url: http://pm-backend-service.planning.svc.cluster.local:8000 + description: Внутрикластерный адрес (ClusterIP, порт 8000) — единственный способ достучаться до /internal/pm + - url: http://localhost:8000 + description: Локальный запуск (gunicorn/uvicorn, порт 8000) + +tags: + - name: projects + description: Проекты и папки — CRUD, копирование, экспорт, права, шаблоны + - name: tasks + description: Задачи проекта — CRUD, массовые операции, права, описания + - name: resources + description: Ресурсы и их привязки к задачам и элементам + - name: states + description: Состояния проекта (базовые планы) + - name: relations + description: Связи задач и проектов, атрибутивные связи + - name: attributes + description: Атрибуты проекта и значения атрибутов задач + - name: calendars + description: Календари + - name: settings + description: Настройки/состояния КСГ + - name: detailing + description: Детализация задач, визуальные профили + - name: comments + description: Комментарии к задачам + - name: system-log + description: Системный журнал и откаты + - name: external + description: Внешний API v2 (только чтение) + - name: internal + description: Внутренние эндпоинты (только внутри кластера, без аутентификации) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Projects (/api/pm/msp/projects) + # ========================================================================== + /api/pm/msp/projects/: + get: + tags: [projects] + summary: Список проектов и папок + operationId: listProjects + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: search, in: query, schema: { type: string } } + - { name: ids, in: query, description: CSV идентификаторов, schema: { type: string } } + - { name: resource_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: integer } } + - { name: parent_id, in: query, schema: { type: integer } } + - { name: only_children, in: query, schema: { type: boolean } } + - { name: templates, in: query, schema: { type: boolean } } + - { name: is_folder, in: query, schema: { type: boolean } } + - { name: strict, in: query, schema: { type: boolean } } + - { name: extend, in: query, schema: { type: boolean } } + - { name: schema, in: query, description: "напр. tiny/detail", schema: { type: string } } + responses: + '200': + description: Страница проектов + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Project' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + post: + tags: [projects] + summary: Создать проект/папку + operationId: createProject + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ProjectCreate' } + responses: + '201': + description: Проект создан + content: + application/json: + schema: { $ref: '#/components/schemas/Project' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + + /api/pm/msp/projects/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [projects] + summary: Проект по id + operationId: retrieveProject + parameters: + - { name: extend, in: query, schema: { type: boolean } } + - { name: schema, in: query, schema: { type: string } } + responses: + '200': + description: Проект + content: + application/json: + schema: { $ref: '#/components/schemas/Project' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [projects] + summary: Полное обновление проекта + operationId: updateProject + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [projects] + summary: Частичное обновление проекта + operationId: partialUpdateProject + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [projects] + summary: Удалить проект + operationId: deleteProject + responses: + '204': { description: Удалено } + '404': { $ref: '#/components/responses/NotFound' } + + /api/pm/msp/projects/root_folder/: + post: + tags: [projects] + summary: Создать корневую папку + operationId: createRootFolder + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/FolderCreate' } } } + responses: + '201': { description: Папка создана, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/rename/: + post: + tags: [projects] + summary: Переименовать проект + operationId: renameProject + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/import/: + post: + tags: [projects] + summary: Импорт задач в проект + operationId: importProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + pk: { type: integer } + from_type: { type: string, enum: [sarex, ms_project, primavera, template] } + update: { type: boolean } + template_id: { type: integer, nullable: true } + required: [pk, from_type] + responses: + '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } } + '404': { $ref: '#/components/responses/NotFound' } + + /api/pm/msp/projects/import-from-gasprom/: + post: + tags: [projects] + summary: Импорт проекта из Газпром ЦПС + operationId: importProjectFromGaspromCps + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_name: { type: string } + company_id: { type: integer } + file_url: { type: string } + required: [project_name, company_id, file_url] + responses: + '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/states/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Состояния (базовые планы) проекта + operationId: listProjectStates + responses: + '200': { description: Список состояний, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } + + /api/pm/msp/projects/{id}/permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Права проекта + operationId: getProjectPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + post: + tags: [projects] + summary: Сохранить права проекта + operationId: saveProjectPermissions + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/all_permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Все права проекта + operationId: getProjectAllPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/copy_project/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Копировать проект + operationId: copyProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + parent_id: { type: integer, nullable: true } + project_name: { type: string } + assets_mapping: { type: object } + with_resources: { type: boolean } + responses: + '201': { description: Проект скопирован, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/create_template/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Создать шаблон из проекта + operationId: createTemplateFromProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_name: { type: string } + description: { type: string, nullable: true } + required: [project_name] + responses: + '201': { description: Шаблон создан, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + + /api/pm/msp/projects/{id}/export_project_to_pdf/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Экспорт проекта в PDF + operationId: exportProjectToPdf + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ExportProject' } } } + responses: + '200': { description: Задача экспорта запущена } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/project_export/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Экспорт проекта (xlsx/xml) + operationId: projectExport + responses: + '200': { description: Экспорт запущен } + + /api/pm/msp/projects/{id}/key_milestones/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Ключевые вехи проекта + operationId: getKeyMilestones + responses: + '200': { description: Вехи, content: { application/json: { schema: { type: array, items: { type: object } } } } } + + /api/pm/msp/projects/{id}/profiles/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Визуальные профили проекта + operationId: getProjectProfiles + responses: + '200': { description: Профили, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } } + + /api/pm/msp/projects/{id}/time_markers_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Данные временных маркеров + operationId: getTimeMarkersData + parameters: + - { name: attributes, in: query, description: CSV идентификаторов атрибутов, schema: { type: string } } + - { name: with_hierarchy, in: query, schema: { type: boolean } } + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/relations/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Связи/интеграции проекта + operationId: getProjectRelations + responses: + '200': { description: Связи, content: { application/json: { schema: { type: array, items: { type: object } } } } } + + /api/pm/msp/projects/{id}/bulk_integration/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Массовое создание интеграций + operationId: bulkIntegration + responses: + '200': { description: OK } + + /api/pm/msp/projects/{id}/issues_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Данные проблем проекта + operationId: getIssuesData + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/formula/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Пересчёт по формуле + operationId: applyFormula + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/rule/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [detailing] + summary: Правило детализации проекта + operationId: getProjectRule + responses: + '200': { description: Правило, content: { application/json: { schema: { type: object } } } } + post: + tags: [detailing] + summary: Создать правило детализации + operationId: createProjectRule + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + attributes: { type: array, items: { type: integer } } + summary_fields: { type: array, items: { type: string } } + mode: { type: string } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Задачи проекта + operationId: getProjectTasks + responses: + '200': { description: Задачи, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } + + /api/pm/msp/projects/{id}/create_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [tasks] + summary: Массовое создание задач + operationId: bulkCreateTasks + requestBody: + required: true + content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/update_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [tasks] + summary: Массовое обновление задач + operationId: bulkUpdateTasks + requestBody: + required: true + content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskUpdate' } } } } + responses: + '200': { description: Обновлено } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/delete_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + delete: + tags: [tasks] + summary: Массовое удаление задач + operationId: bulkDeleteTasks + requestBody: + required: true + content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } + responses: + '204': { description: Удалено } + + /api/pm/msp/projects/{id}/system-logs/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [system-log] + summary: Системные логи проекта + operationId: getSystemLogs + parameters: + - { name: task_id, in: query, schema: { type: integer } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Страница логов + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: { results: { type: array, items: { $ref: '#/components/schemas/SystemLog' } } } + + /api/pm/msp/projects/{id}/system-log-detail/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [system-log] + summary: Деталь записи журнала + operationId: getSystemLogDetail + parameters: + - { name: log_id, in: query, required: true, schema: { type: integer } } + responses: + '200': { description: Деталь, content: { application/json: { schema: { $ref: '#/components/schemas/SystemLog' } } } } + + /api/pm/msp/projects/{id}/rollback-to-record/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [system-log] + summary: Откат к записи журнала + operationId: rollbackToRecord + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + log_id: { type: integer } + read_only: { type: boolean } + required: [log_id] + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + # ========================================================================== + # Tasks (/api/pm/msp/tasks) + # ========================================================================== + /api/pm/msp/tasks/: + get: + tags: [tasks] + summary: Список задач + operationId: listTasks + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: project, in: query, schema: { type: integer } } + - { name: ids, in: query, schema: { type: string } } + - { name: name__icontains, in: query, schema: { type: string } } + - { name: time_start, in: query, schema: { type: string, format: date-time } } + - { name: time_end, in: query, schema: { type: string, format: date-time } } + - { name: status, in: query, description: CSV статусов, schema: { type: string } } + - { name: responsible, in: query, schema: { type: string } } + - { name: executors, in: query, schema: { type: string } } + - { name: with_hierarchy, in: query, schema: { type: boolean, default: true } } + responses: + '200': + description: Страница задач + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: { results: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } + post: + tags: [tasks] + summary: Создать задачу + operationId: createTask + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Задача по id + operationId: retrieveTask + responses: + '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [tasks] + summary: Полное обновление задачи + operationId: updateTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + patch: + tags: [tasks] + summary: Частичное обновление задачи + operationId: partialUpdateTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + delete: + tags: [tasks] + summary: Удалить задачу + operationId: deleteTask + responses: + '204': { description: Удалено } + + /api/pm/msp/tasks/task_index/: + patch: + tags: [tasks] + summary: Переиндексация задач + operationId: updateTaskIndex + responses: + '200': { description: OK } + + /api/pm/msp/tasks/copy/: + post: + tags: [tasks] + summary: Копировать задачи + operationId: copyTasks + requestBody: + required: true + content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } + responses: + '200': { description: OK } + + /api/pm/msp/tasks/{id}/permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Права задачи + operationId: getTaskPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + post: + tags: [tasks] + summary: Сохранить права задачи + operationId: saveTaskPermissions + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + + /api/pm/msp/tasks/{id}/descriptions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Описание задачи + operationId: getTaskDescription + responses: + '200': { description: Описание, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + patch: + tags: [tasks] + summary: Обновить описание задачи + operationId: updateTaskDescription + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + + # ========================================================================== + # Detailed tasks / descriptions + # ========================================================================== + /api/pm/msp/detailed-tasks/: + get: + tags: [detailing] + summary: Список детализированных задач + operationId: listDetailedTasks + parameters: + - { name: task, in: query, schema: { type: integer } } + - { name: project, in: query, schema: { type: integer } } + responses: + '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/DetailedTask' } } } } } + post: + tags: [detailing] + summary: Создать детализированную задачу + operationId: createDetailedTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + + /api/pm/msp/detailed-tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [detailing] + summary: Обновить детализированную задачу + operationId: updateDetailedTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + delete: + tags: [detailing] + summary: Удалить детализированную задачу + operationId: deleteDetailedTask + responses: + '204': { description: Удалено } + + /api/pm/msp/descriptions/upload_file/: + post: + tags: [tasks] + summary: Загрузить файл описания + operationId: uploadDescriptionFile + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: { type: string, format: binary } + responses: + '200': { description: OK } + + # ========================================================================== + # Values (actual values) + # ========================================================================== + /api/pm/msp/values/: + get: + tags: [tasks] + summary: Фактические значения задач + operationId: listActualValues + parameters: + - { name: task, in: query, schema: { type: integer } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': { description: Значения, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } } + post: + tags: [tasks] + summary: Создать фактическое значение + operationId: createActualValue + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } + + /api/pm/msp/values/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Значение по id + operationId: retrieveActualValue + responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } }, '404': { $ref: '#/components/responses/NotFound' } } + put: + tags: [tasks] + summary: Обновить значение + operationId: updateActualValue + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } } + responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } } + delete: + tags: [tasks] + summary: Удалить значение + operationId: deleteActualValue + responses: { '204': { description: Удалено } } + + # ========================================================================== + # Project states (base plans) + # ========================================================================== + /api/pm/msp/project-states/: + get: + tags: [states] + summary: Список состояний + operationId: listStates + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } } + post: + tags: [states] + summary: Создать состояние (базовый план) + operationId: createState + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } } + responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } + + /api/pm/msp/project-states/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [states] + summary: Состояние по id + operationId: retrieveState + responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } }, '404': { $ref: '#/components/responses/NotFound' } } + put: + tags: [states] + summary: Обновить состояние + operationId: updateState + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } } + responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } + delete: + tags: [states] + summary: Удалить состояние + operationId: deleteState + responses: { '204': { description: Удалено } } + + /api/pm/msp/project-states/{id}/data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [states] + summary: Данные состояния (базового плана) + operationId: getStateData + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + post: + tags: [states] + summary: Добавить задачи в базовый план + operationId: addTasksToState + responses: { '200': { description: OK } } + + /api/pm/msp/project-states/{id}/edit_state/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [states] + summary: Редактировать состояние + operationId: editState + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: { type: integer } + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + progress: { type: number, nullable: true } + planned_value: { type: number, nullable: true } + unit: { type: string, nullable: true } + required: [id] + responses: { '200': { description: OK } } + + # ========================================================================== + # Resources + # ========================================================================== + /api/pm/msp/resources/: + get: + tags: [resources] + summary: Список ресурсов + operationId: listResources + parameters: + - { name: ids, in: query, schema: { type: string } } + - { name: projects, in: query, schema: { type: string } } + - { name: type, in: query, schema: { type: string } } + - { name: parent, in: query, schema: { type: integer } } + - { name: company, in: query, schema: { type: integer } } + - { name: full, in: query, schema: { type: boolean } } + - { name: extend, in: query, schema: { type: boolean } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Resource' } } } } } } + post: + tags: [resources] + summary: Создать ресурс + operationId: createResource + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } } + responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } + + /api/pm/msp/resources/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [resources], summary: Ресурс по id, operationId: retrieveResource, responses: { '200': { description: Ресурс, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } }, '404': { $ref: '#/components/responses/NotFound' } } } + put: { tags: [resources], summary: Обновить ресурс, operationId: updateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } } + patch: { tags: [resources], summary: Частичное обновление ресурса, operationId: partialUpdateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } } + delete: { tags: [resources], summary: Удалить ресурс, operationId: deleteResource, responses: { '204': { description: Удалено } } } + + /api/pm/msp/resources/bulk_create/: + post: + tags: [resources] + summary: Массовое создание ресурсов + operationId: bulkCreateResources + requestBody: { required: true, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceCreate' } } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/resources/resource_planning/: + get: + tags: [resources] + summary: Ресурсное планирование + operationId: resourcePlanning + parameters: + - { name: projects, in: query, schema: { type: string } } + - { name: resource_type, in: query, schema: { type: string, default: human } } + - { name: start, in: query, required: true, schema: { type: string, format: date-time } } + - { name: end, in: query, required: true, schema: { type: string, format: date-time } } + - { name: scale, in: query, schema: { type: string, default: D } } + - { name: group, in: query, schema: { type: string } } + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + /api/pm/msp/resources/{id}/bind_task/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: { tags: [resources], summary: Привязать задачу к ресурсу, operationId: bindTaskToResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/resources/{id}/bind_elements/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: { tags: [resources], summary: Привязать элементы к ресурсу, operationId: bindElementsToResource, responses: { '201': { description: Создано } } } + + # ---- Resource relations ---- + /api/pm/msp/resources-tasks/: + get: + tags: [resources] + summary: Список привязок ресурс-задача + operationId: listResourceTaskRelations + parameters: + - { name: projects, in: query, schema: { type: string } } + - { name: tasks, in: query, schema: { type: string } } + - { name: resources, in: query, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } } + post: { tags: [resources], summary: Создать привязку ресурс-задача, operationId: createResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/resources-tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [resources], summary: Привязка по id, operationId: retrieveResourceTaskRelation, responses: { '200': { description: Привязка, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } } + patch: { tags: [resources], summary: Обновить привязку, operationId: updateResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [resources], summary: Удалить привязку, operationId: deleteResourceTaskRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/resources-tasks/assign/: + patch: { tags: [resources], summary: Назначение ресурса на задачи, operationId: assignResourceToTasks, responses: { '200': { description: OK } } } + + /api/pm/msp/resources-tasks/bulk_update/: + patch: + tags: [resources] + summary: Массовое обновление привязок ресурс-задача + operationId: bulkUpdateResourceTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } }, profile: { type: integer } } } } } } + responses: { '200': { description: OK } } + + /api/pm/msp/resources-tasks/bulk_delete/: + delete: + tags: [resources] + summary: Массовое удаление привязок ресурс-задача + operationId: bulkDeleteResourceTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } } + responses: { '204': { description: Удалено } } + + /api/pm/msp/resources-elements/: + get: + tags: [resources] + summary: Список привязок ресурс-элемент + operationId: listResourceElementRelations + parameters: + - { name: resources, in: query, schema: { type: string } } + - { name: instances, in: query, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceElementRelation' } } } } } } + post: { tags: [resources], summary: Создать привязку ресурс-элемент, operationId: createResourceElementRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceElementRelation' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Relations (task/project) + # ========================================================================== + /api/pm/msp/task-relations/: + get: + tags: [relations] + summary: Список связей задач + operationId: listTaskRelations + parameters: [ { name: project, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Task2TaskRelation' } } } } } } + post: { tags: [relations], summary: Создать связь задач, operationId: createTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/task-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + put: { tags: [relations], summary: Обновить связь задач, operationId: updateTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить связь задач, operationId: deleteTaskRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/task-relations/bulk_delete/: + delete: + tags: [relations] + summary: Массовое удаление связей задач + operationId: bulkDeleteTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } } + responses: { '204': { description: Удалено } } + + /api/pm/msp/project-relations/: + get: { tags: [relations], summary: Список связей проектов, operationId: listProjectRelations, parameters: [ { name: project, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } } } } } } + post: + tags: [relations] + summary: Создать связь(и) проектов + operationId: createProjectRelation + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/Project2ProjectRelation' }, { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } ] } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/project-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [relations], summary: Обновить связь проектов, operationId: updateProjectRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Project2ProjectRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить связь проектов, operationId: deleteProjectRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/entity-relations/: + get: { tags: [relations], summary: Список сущностных связей, operationId: listEntityRelations, parameters: [ { name: project_id, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/EntityRelation' } } } } } } } + post: { tags: [relations], summary: Создать сущностную связь, operationId: createEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/entity-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [relations], summary: Обновить сущностную связь, operationId: updateEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить сущностную связь, operationId: deleteEntityRelation, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Attributes + # ========================================================================== + /api/pm/msp/project-attribute/: + get: + tags: [attributes] + summary: Атрибуты проекта + operationId: listProjectAttributes + parameters: [ { name: project, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } } } } + post: + tags: [attributes] + summary: Привязать атрибут(ы) к проекту + operationId: createProjectAttribute + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/ProjectAttributeRelation' }, { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } ] } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/project-attribute/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + put: { tags: [attributes], summary: Обновить атрибут проекта, operationId: updateProjectAttribute, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [attributes], summary: Удалить атрибут проекта, operationId: deleteProjectAttribute, responses: { '204': { description: Удалено } } } + + /api/pm/msp/task-value/: + get: + tags: [attributes] + summary: Значения атрибутов задач + operationId: listTaskValues + parameters: + - { name: tasks, in: query, description: CSV, schema: { type: string } } + - { name: attributes, in: query, description: CSV, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } } } } } + post: + tags: [attributes] + summary: Создать значение(я) атрибутов задач + operationId: createTaskValue + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/TaskAttributeValue' }, { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } ] } } } } + responses: { '201': { description: Создано } } + + # ========================================================================== + # Calendars + # ========================================================================== + /api/pm/msp/calendars/: + get: { tags: [calendars], summary: Список календарей, operationId: listCalendars, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Calendar' } } } } } } } + post: { tags: [calendars], summary: Создать календарь, operationId: createCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } } + + /api/pm/msp/calendars/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [calendars], summary: Календарь по id, operationId: retrieveCalendar, responses: { '200': { description: Календарь, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } } + patch: { tags: [calendars], summary: Изменить календарь, operationId: updateCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [calendars], summary: Удалить календарь, operationId: deleteCalendar, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Project settings (KSG) + # ========================================================================== + /api/pm/msp/project-settings/: + get: { tags: [settings], summary: Список настроек КСГ, operationId: listProjectSettings, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectSettings' } } } } } } } + post: { tags: [settings], summary: Создать настройку КСГ, operationId: createProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/project-settings/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [settings], summary: Настройка по id, operationId: retrieveProjectSettings, responses: { '200': { description: Настройка, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } } } } + patch: { tags: [settings], summary: Изменить настройку, operationId: updateProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [settings], summary: Удалить настройку, operationId: deleteProjectSettings, responses: { '204': { description: Удалено } } } + + /api/pm/msp/project-settings/{id}/apply/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [settings] + summary: Применить глобальную настройку + operationId: applyProjectSettings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_id: { type: integer } + connect_missing_attributes: { type: boolean } + required: [project_id] + responses: { '200': { description: OK } } + + # ========================================================================== + # Visual profiles + # ========================================================================== + /api/pm/msp/visual-profiles/: + get: { tags: [detailing], summary: Список визуальных профилей, operationId: listVisualProfiles, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } } } } + post: { tags: [detailing], summary: Создать визуальный профиль, operationId: createVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/visual-profiles/{id}: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [detailing], summary: Изменить визуальный профиль, operationId: updateVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [detailing], summary: Удалить визуальный профиль, operationId: deleteVisualProfile, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Comments + # ========================================================================== + /api/pm/msp/comments/: + get: { tags: [comments], summary: Список комментариев, operationId: listComments, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Comment' } } } } } } } + post: { tags: [comments], summary: Создать комментарий, operationId: createComment, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CommentCreate' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Descriptions resource + # ========================================================================== + /api/pm/msp/descriptions/: + get: { tags: [tasks], summary: Список описаний, operationId: listDescriptions, parameters: [ { name: task, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Description' } } } } } } } + post: { tags: [tasks], summary: Создать описание, operationId: createDescription, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Task info (public/service) + # ========================================================================== + /api/pm/msp/task-info/: + get: + tags: [tasks] + summary: Задачи по профилям (not_started/in_progress/completed) + operationId: getTaskInfo + parameters: + - { name: projects, in: query, required: true, description: CSV идентификаторов, schema: { type: string } } + - { name: date, in: query, required: true, schema: { type: string, format: date-time } } + - { name: document, in: query, required: true, schema: { type: string } } + - { name: base_plane, in: query, schema: { type: string } } + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + /api/pm/msp/external-task-info/: + get: + tags: [tasks] + summary: Инфо о фоновой задаче через брокер + operationId: getExternalTaskInfo + security: [] + parameters: [ { name: task_id, in: query, schema: { type: string, format: uuid } } ] + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + # ========================================================================== + # External API v2 (read-only) + # ========================================================================== + /api/pm/external/v2/projects/: + get: + tags: [external] + summary: Список проектов (внешний) + operationId: externalListProjects + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: search, in: query, schema: { type: string } } + - { name: ids, in: query, schema: { type: string } } + - { name: parent_id, in: query, schema: { type: integer } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project' } } } } } } + + /api/pm/external/v2/projects/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Проект (внешний), operationId: externalRetrieveProject, responses: { '200': { description: Проект, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }, '404': { $ref: '#/components/responses/NotFound' } } } + + /api/pm/external/v2/projects/{id}/project_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Данные проекта (внешний), operationId: externalProjectData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } } + + /api/pm/external/v2/tasks/: + get: + tags: [external] + summary: Список задач (внешний) + operationId: externalListTasks + parameters: + - { name: extend, in: query, schema: { type: boolean } } + - { name: ids, in: query, schema: { type: string } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } } + + /api/pm/external/v2/tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Задача (внешний), operationId: externalRetrieveTask, responses: { '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } } } + + /api/pm/external/v2/values/: + get: { tags: [external], summary: Фактические значения (внешний), operationId: externalListValues, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } } } } + + /api/pm/external/v2/values/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Значение (внешний), operationId: externalRetrieveValue, responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } } } + + /api/pm/external/v2/project-states/: + get: + tags: [external] + summary: Состояния проекта (внешний) + operationId: externalListStates + parameters: [ { name: project_id, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } } + + /api/pm/external/v2/project-states/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Состояние (внешний), operationId: externalRetrieveState, responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } } + + /api/pm/external/v2/project-states/{id}/data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Данные состояния (внешний), operationId: externalStateData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } } + + # ========================================================================== + # Internal API (no auth, cluster-only) + # ========================================================================== + /internal/pm/auto_scheduling/: + post: + tags: [internal] + summary: Интегрированное автопланирование + operationId: internalAutoScheduling + security: [] + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } } + responses: { '200': { description: OK } } + + /internal/pm/integrated_base_plan/: + post: + tags: [internal] + summary: Создать/обновить интегрированные состояния + operationId: internalIntegratedBasePlan + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_id: { type: integer } + user_id: { type: integer } + linked_project: { type: integer } + name: { type: string } + tasks: { type: array, items: { type: object } } + task_ids: { type: array, items: { type: integer } } + state_id: { type: integer } + responses: { '200': { description: OK } } + + /internal/pm/detailed_tasks/: + post: + tags: [internal] + summary: Обновить значения атрибутов задачи + operationId: internalDetailedTasks + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + task_id: { type: integer } + result_fields: { type: object } + company_id: { type: integer } + required: [task_id, company_id] + responses: { '200': { description: OK } } + + /internal/pm/sync_tasks/: + post: + tags: [internal] + summary: Синхронизация задач + автопланирование + operationId: internalSyncTasks + security: [] + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } } + responses: { '200': { description: OK } } + + /internal/pm/base_plans/: + get: + tags: [internal] + summary: Получить базовый план + operationId: internalGetBasePlan + security: [] + parameters: [ { name: base_plan_id, in: query, required: true, schema: { type: integer } } ] + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /internal/pm/tasks_dates/: + get: + tags: [internal] + summary: Даты и прогресс задач проекта + operationId: internalTasksDates + security: [] + parameters: [ { name: project_id, in: query, required: true, schema: { type: integer } } ] + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + '400': { $ref: '#/components/responses/BadRequest' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. В режиме sarex-backend + подпись проверяется публичным ключом (`RS512`); в режиме Zitadel + дополнительно требуется заголовок `identity`. + identityToken: + type: apiKey + in: header + name: identity + description: | + Заголовок `identity` (`Identity `) для режима Zitadel. При его + наличии полезная нагрузка берётся из этого токена. + + parameters: + PathId: + name: id + in: path + required: true + schema: { type: integer } + Limit: + name: limit + in: query + required: false + schema: { type: integer, minimum: 0, default: 1000 } + Offset: + name: offset + in: query + required: false + schema: { type: integer, minimum: 0, default: 0 } + + responses: + BadRequest: + description: Некорректный запрос / ошибка валидации + content: + application/json: + schema: { $ref: '#/components/schemas/ValidationError' } + Unauthorized: + description: Токен не предоставлен или невалиден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + + schemas: + # --- Общие ------------------------------------------------------------ + Detail: + type: object + properties: + detail: { type: string } + required: [detail] + + ValidationError: + type: object + description: | + Ошибка валидации DRF — либо словарь `{ "": ["", ...] }`, + либо `{ "detail": "..." }`. + additionalProperties: + type: array + items: { type: string } + + PaginatedResponse: + type: object + description: Обёртка DRF LimitOffsetPagination + properties: + count: { type: integer } + next: { type: string, format: uri, nullable: true } + previous: { type: string, format: uri, nullable: true } + results: { type: array, items: { type: object } } + required: [count, results] + + # --- Проекты ---------------------------------------------------------- + Project: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + parent: { type: integer, nullable: true } + is_folder: { type: boolean } + resource_id: { type: string, nullable: true } + document_id: { type: string, nullable: true } + bundle_id: { type: string, nullable: true } + description: { type: string, nullable: true } + settings: { type: object } + target_date: { type: string, format: date-time, nullable: true } + project_type: { type: string, nullable: true } + calendar: { type: integer, nullable: true } + status: { type: string, readOnly: true } + creator: { type: integer, readOnly: true } + permissions: { type: object, readOnly: true } + restrictions: { type: object, readOnly: true } + time_start: { type: string, format: date-time, readOnly: true, nullable: true } + time_end: { type: string, format: date-time, readOnly: true, nullable: true } + total_cost: { type: number, readOnly: true, nullable: true } + current_cost: { type: number, readOnly: true, nullable: true } + progress: { type: number, readOnly: true, nullable: true } + relations: { type: array, items: { type: object }, readOnly: true } + rule: { type: object, nullable: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, company] + + ProjectCreate: + type: object + properties: + name: { type: string } + company_id: { type: integer } + parent: { type: integer, nullable: true } + is_folder: { type: boolean } + resource_id: { type: string, nullable: true } + ms_file: { type: string, nullable: true } + description: { type: string, nullable: true } + document_id: { type: string, nullable: true } + bundle_id: { type: string, nullable: true } + settings: { type: object, default: {} } + target_date: { type: string, format: date-time, nullable: true } + project_type: { type: string, nullable: true } + calendar: { type: integer, nullable: true } + required: [name, company_id] + + FolderCreate: + type: object + properties: + company: { type: integer } + company_name: { type: string } + creator: { type: integer } + service_account: { type: string, format: uuid } + folder_name: { type: string, nullable: true } + required: [company, company_name, creator, service_account] + + PermissionsResponse: + type: object + properties: + direct_permissions: { type: array, items: { type: object } } + inherited_permissions: { type: array, items: { type: object } } + + # --- Задачи ----------------------------------------------------------- + ProjectTask: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + parent: { type: integer, nullable: true } + index_prefix: { type: string, nullable: true } + task_id: { type: string, nullable: true } + name: { type: string } + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + deadline: { type: string, format: date-time, nullable: true } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + task_type: { type: string } + unit: { type: string, nullable: true } + local_index: { type: integer, nullable: true } + constraint_date: { type: string, format: date-time, nullable: true } + constraint_type: { type: string, nullable: true } + cost: { type: number, nullable: true } + progress: { type: number, nullable: true } + status: { type: string, nullable: true } + duration: { type: number, nullable: true } + planned_duration: { type: number, nullable: true } + cipher: { type: string, nullable: true } + is_active: { type: boolean } + current_cost: { type: number, nullable: true } + planned_value: { type: number, nullable: true } + calendar: { type: integer, nullable: true } + distribution: { type: object, nullable: true } + actual_value: { type: number, readOnly: true, nullable: true } + total_cost: { type: number, readOnly: true, nullable: true } + attributes: { type: array, items: { type: object } } + permissions: { type: object } + + ProjectTaskCreate: + type: object + properties: + project: { type: integer } + parent: { type: integer, nullable: true } + name: { type: string } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + deadline: { type: string, format: date-time, nullable: true } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + task_type: { type: string, default: fix_value } + unit: { type: string, nullable: true } + cost: { type: number, nullable: true } + progress: { type: number, nullable: true } + status: { type: string, nullable: true } + is_active: { type: boolean, default: true } + calendar: { type: integer, nullable: true } + required: [project, name, time_start, time_end] + + ProjectTaskUpdate: + type: object + description: Плоский сериализатор для массового обновления + properties: + id: { type: integer } + name: { type: string } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + cost: { type: number } + progress: { type: number } + status: { type: string } + required: [id] + + ActualValue: + type: object + properties: + id: { type: integer, readOnly: true } + value: { type: number } + description: { type: string, nullable: true } + task: { type: integer } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + ActualValueCreate: + type: object + properties: + value: { type: number } + description: { type: string, nullable: true } + task: { type: integer } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + required: [value, task, time_start, time_end] + + DetailedTask: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + weight: { type: number, nullable: true } + data: { type: object } + + Description: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + text: { type: string } + required: [task, text] + + # --- Состояния -------------------------------------------------------- + ProjectState: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + project: { type: integer } + creator: { type: integer, readOnly: true } + is_locked: { type: boolean, readOnly: true } + + ProjectStateCreate: + type: object + properties: + name: { type: string } + project: { type: integer } + required: [name, project] + + # --- Ресурсы ---------------------------------------------------------- + Resource: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + calendar: { type: integer, nullable: true } + project: { type: integer, nullable: true } + resource_type: { type: string } + parent: { type: integer, nullable: true } + cost: { type: number, nullable: true } + path: { type: string, readOnly: true } + is_leaf: { type: boolean, readOnly: true } + elements: { type: array, items: { type: integer } } + projects: { type: array, items: { type: object } } + + ResourceCreate: + type: object + properties: + name: { type: string } + company: { type: integer } + calendar: { type: integer, nullable: true } + project: { type: integer, nullable: true } + resource_type: { type: string } + parent: { type: integer, nullable: true } + cost: { type: number, nullable: true } + required: [name, company] + + ResourceTaskRelation: + type: object + properties: + id: { type: integer, readOnly: true } + resource: { type: integer } + task: { type: integer } + profile: { type: integer, nullable: true } + + ResourceElementRelation: + type: object + properties: + id: { type: integer, readOnly: true } + resource: { type: integer } + instance: { type: string } + model_name: { type: string } + attributes: { type: object } + path: { type: string, nullable: true } + + # --- Связи ------------------------------------------------------------ + Task2TaskRelation: + type: object + properties: + id: { type: integer, readOnly: true } + target: { type: integer } + source: { type: integer } + type: { type: string } + lag: { type: number, nullable: true } + + Project2ProjectRelation: + type: object + properties: + id: { type: integer, readOnly: true } + successor: { type: integer } + predecessor: { type: integer } + mode: { type: string, nullable: true } + + EntityRelation: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + entity_name: { type: string } + attribute_ids: { type: array, items: { type: integer } } + settings: { type: object } + entity_type: { type: string } + + # --- Атрибуты --------------------------------------------------------- + ProjectAttributeRelation: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + asset_id: { type: string, nullable: true } + attribute_id: { type: integer } + is_system: { type: boolean } + default_value: { nullable: true } + mode: { type: string, nullable: true } + + TaskAttributeValue: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + attribute_id: { type: integer } + type: { type: string, description: "Тип значения (вход): integer/float/string/datetime/date" } + values: { description: Значение(я) атрибута } + integer_value: { type: integer, nullable: true } + float_value: { type: number, nullable: true } + string_value: { type: string, nullable: true } + datetime_value: { type: string, format: date-time, nullable: true } + date_value: { type: string, format: date, nullable: true } + assets: { type: array, items: { type: string } } + + # --- Календари -------------------------------------------------------- + Calendar: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + mask: { type: array, items: { type: integer } } + work_time_start: { type: string } + work_time_end: { type: string } + work_day_duration: { type: number, readOnly: true } + is_system: { type: boolean } + holidays: { type: array, items: { type: string, format: date } } + exception_holidays: { type: array, items: { type: string, format: date } } + creator: { type: integer, readOnly: true } + projects_count: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, mask, work_time_start, work_time_end] + + # --- Настройки -------------------------------------------------------- + ProjectSettings: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + project: { type: integer, nullable: true } + state: { type: integer, nullable: true } + image: { type: string, nullable: true } + is_default: { type: boolean } + visibility: { type: string, enum: [global, private, public] } + company_id: { type: integer, nullable: true } + service_accounts: { type: array, items: { type: string } } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + VisualProfile: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + color: { type: string, description: "HEX-цвет, напр. #RRGGBB" } + project: { type: integer } + behavior: { type: object } + + Comment: + type: object + properties: + id: { type: integer, readOnly: true } + text: { type: string } + task: { type: integer } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + CommentCreate: + type: object + properties: + text: { type: string } + task: { type: integer } + required: [text, task] + + # --- Системный журнал ------------------------------------------------- + SystemLog: + type: object + properties: + id: { type: integer, readOnly: true } + project_id: { type: integer } + user_id: { type: integer, nullable: true } + created_at: { type: string, format: date-time, readOnly: true } + message_data: { type: object } + action: { type: string } + entity: { type: string } + changes: { type: object, description: "Только в detail-варианте" } + + # --- Экспорт ---------------------------------------------------------- + ExportProject: + type: object + properties: + export_fields: { type: array, items: { type: string } } + export_attrs: { type: array, items: { type: integer } } + fields_order: { type: array, items: { type: string } } + data: { type: array, items: { type: object } } + page_format: { type: string } + page_size: { type: string } + left_table_percent_width: { type: number, minimum: 0, maximum: 100 } + base_plan_ids: { type: array, items: { type: integer } } + margins: { type: array, items: { type: number } } + expanded_tasks: { type: array, items: { type: integer } } + header_text: { type: string } + footer_text: { type: string } + display_relations: { type: boolean } + date_from: { type: string, format: date } + date_to: { type: string, format: date } + fields_width: { type: object } + project_name: { type: string } + show_counter: { type: boolean } + font_size: { type: integer } + conditional_formatting: { type: array, items: { type: object } } + required: [export_fields, data] diff --git a/apps/prescriptions/.env.example b/apps/prescriptions/.env.example new file mode 100644 index 0000000..8b53450 --- /dev/null +++ b/apps/prescriptions/.env.example @@ -0,0 +1,21 @@ +# prescriptions-frontend — переменные СБОРКИ и CI +# +# ВАЖНО: у фронтенда нет рантайм-.env. Собранный бандл — статика, которую +# раздаёт nginx. Все переменные ниже используются на этапе СБОРКИ образа +# (Dockerfile ARG / --build-arg в .gitlab-ci.yml) и локального запуска. +# Значение BUILD_ENV валидируется в env.js и внедряется в бандл через +# webpack DefinePlugin. Подробности — в CONFIGURATION.md. + +# Build (обязательно). Одно из: local | stage | preprod | prod | contour +BUILD_ENV=stage + +# Флаг сборки под Storybook: "true" | "false" +STORYBOOK=false + +# Токен приватного npm-реестра nexus.infra.sarex.io (см. .npmrc) +NPM_TOKEN= + +# --- Cypress e2e --- +# Задаются в cypress.env.json (пример — cypress.env-example.json), не в .env: +# SRX_LOGIN= +# SRX_PASSWORD= diff --git a/apps/prescriptions/CONFIGURATION.md b/apps/prescriptions/CONFIGURATION.md new file mode 100644 index 0000000..9aead94 --- /dev/null +++ b/apps/prescriptions/CONFIGURATION.md @@ -0,0 +1,127 @@ +# Конфигурация проекта prescriptions-frontend + +Документ описывает способы конфигурирования микрофронтенда `prescriptions-frontend` (Module Federation `srx_prescriptions`, экспонирует `./PrescriptionsPage`), его переменные сборки/CI и параметры деплоя. + +## Способы конфигурирования + +В отличие от backend-сервисов, у фронтенда **нет рантайм-конфигурации через `.env`**: собранный бандл — статические файлы, которые раздаёт nginx. Всё поведение задаётся **на этапе сборки** одной переменной — `BUILD_ENV`. + +- `env.js` читает `process.env.BUILD_ENV` и валидирует его против списка `local`/`stage`/`prod`/`contour`/`preprod` (иначе — ошибка сборки `set BUILD_ENV one of ...`); +- `webpack.config.js` через `DefinePlugin` внедряет глобальные константы `BUILD_ENV` и `STORYBOOK` в бандл; +- `build.config.js` по `BUILD_ENV` выбирает режим сборки (`mode`/`devtool`); +- `module/api/hosts.ts` и `module/api/module-hosts.ts` содержат карты хостов по окружениям; SDK (`@sarex-team/sdk-js`) на рантайме выбирает нужный хост по глобальной константе `BUILD_ENV` (по умолчанию `prod`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +### Значения `BUILD_ENV` + +| `BUILD_ENV` | `mode` | `devtool` | Назначение | +| --- | --- | --- | --- | +| `local` | `development` | `eval-source-map` | Локальная разработка (dev-server, storybook), `httpService` → `original` | +| `stage` | `development` | `eval-source-map` | Стенд stage | +| `preprod` | `production` | `source-map` | Предпрод | +| `prod` | `production` | `source-map` | Прод | +| `contour` | `production` | `source-map` | Изолированный контур (относительные пути хостов) | + +## Переменные сборки и CI + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `BUILD_ENV` | `env.js`, `webpack.config.js`, `Dockerfile` (ARG), `.gitlab-ci.yml` (`--build-arg`) | Целевое окружение сборки. Обязательна | +| `NPM_TOKEN` | `.npmrc`, `Dockerfile` (ARG), `.gitlab-ci.yml` (`--build-arg`) | Токен доступа к приватному npm-реестру `nexus.infra.sarex.io` | +| `STORYBOOK` | `webpack.config.js` (`DefinePlugin`), `module/pages/index.tsx` | Флаг сборки под Storybook (`"true"`/`"false"`) | + +Cypress-тесты берут учётные данные из `cypress.env.json` (пример — `cypress.env-example.json`): `SRX_LOGIN`, `SRX_PASSWORD`. + +Версия Node для разработки — `v20.16.0` (`.nvmrc`). + +## Хосты по окружениям + +Базовые API-хосты подставляются из `module/api/hosts.ts` по `BUILD_ENV` (детальная разбивка по сервисам — в `ENDPOINTS.md`): + +| Окружение | Базовый API | Пример (`documentations`) | +| --- | --- | --- | +| `local` / `stage` | `https://stage-api.sarex.io` | `https://stage-api.sarex.io/documentations/api/v1` | +| `preprod` | `https://api.preprod.sarex.io` | `https://api.preprod.sarex.io/documentations/api/v1` | +| `prod` | `https://api.sarex.io` | `https://api.sarex.io/documentations/api/v1` | +| `contour` | относительные пути | `/documentations/api/v1` | + +Удалённый модуль (Module Federation) `documentations` подключается по `module/api/module-hosts.ts` (`remoteEntry.js`). + +## Запуск и скрипты (`package.json`) + +| Команда | Назначение | +| --- | --- | +| `npm run serve-module` | Dev-server (`webpack.dev.js`, порт `9001`, https, proxy `/api`, `/admin` на `appUrl`), `BUILD_ENV=local` | +| `npm run storybook` | Storybook (порт `9000`, https), `BUILD_ENV=local` | +| `npm run start` / `test:dev` | Параллельный запуск serve-module + storybook (+ cypress в `test:dev`) | +| `npm run build-module` | Продакшн-сборка модуля (`webpack --config webpack.config.js`) | +| `npm run build-storybook` | Сборка статики Storybook | +| `npm run cypress:open` | Запуск e2e-тестов Cypress | +| `npm run lint` / `lint:ts` | Prettier / проверка типов `tsc --noEmit` | + +## Сборка образа (`Dockerfile`) + +Двухстадийная сборка: + +1. `node:15` — установка зависимостей (`npm i --legacy-peer-deps` с `NPM_TOKEN`), `npm run lint`, `BUILD_ENV=$BUILD_ENV npm run build-module` → `/app/dist`; +2. `nginx:mainline-alpine-otel` — копирование `dist` в `/dist` и конфига `nginx/nginx.conf`. + +Build-args: `BUILD_ENV`, `NPM_TOKEN`. + +nginx (`nginx/nginx.conf`) раздаёт статику из `/dist`, отдаёт `/ping` → `{"result": "ok"}` (healthcheck) и запрещает кеширование `/module/remoteEntry.js` (`Cache-Control: no-store`). + +## CI/CD (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). `SERVICE_NAME=prescriptions-frontend`. Окружение переключается по ветке/тегу (`workflow.rules`): + +| Условие | STAND | NAMESPACE | BUILD_ENV | CHART_VERSION | +| --- | --- | --- | --- | --- | +| ветка `stage` | `stage` | `proc` | `stage` | `1.0.0-stage` | +| ветка `master` | `preprod` | `prescriptions-preprod` | `preprod` | `1.0.0-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `prescriptions-prod` | `prod` | `1.0.0-prod` | +| merge request | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Деплой параметризуется через `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`). + +## Helm-чарт проекта (`.helm`) + +`Chart.yaml`: зависимость `universal-chart` (`oci://cr.yandex/.../charts`, версия `0.1.7`). + +`values.yaml` (`universal-chart.services.frontend`): + +- `deployment.name = prescription-frontend`, `replicaCount = 1`, `port = 80`, `revisionHistoryLimit = 10` (prod — `15`); +- `image.name = cr.yandex/crp3ccidau046kdj8g9q/prescription-frontend`, `pullPolicy = IfNotPresent`; +- `service.name = prescriptions-frontend-service`, `type = ClusterIP`, `port/targetPort = 80`; +- `imagePullSecrets = dockerhub`; +- probes (`liveness`/`readiness`) на `/ping:80` заданы, но **выключены** (`enabled: false`); +- `envs: []`, `secretEnvs: []` — переменных окружения контейнеру не передаётся; +- ресурсы: `requests` `memory 100Mi`, `cpu 100m`. + +## Инфраструктура (`iac/apps/prescriptions`) + +Разворачивается через Kustomize. Состав каталога: + +| Путь | Назначение | +| --- | --- | +| `base/namespace.yaml` | Namespace `prescriptions` с `istio-injection: enabled` | +| `base/deployment.yaml` | Deployment `frontend`, образ `cr.yandex/.../prescriptions-frontend:production_...`, порт `80`, `requests` `cpu 25m`/`memory 100Mi`, `imagePullSecrets: regcred` | +| `base/service.yaml` | Service `frontend-service`, `ClusterIP`, порт `80` | +| `base/kustomization.yaml` | Сборка base (namespace + deployment + service) | +| `yc-k8s-test/` | Оверлей поверх `../base` (патч `replicas.yaml` закомментирован) | +| `brusnika-prod/`, `brusnika-stage/` | Оверлеи (см. замечание ниже) | + +## Замечания и потенциальные проблемы + +- **Оверлеи `brusnika-prod` / `brusnika-stage`** сейчас содержат `HelmRelease` бэкенда `measurements` (namespace `measurements`, образ `documentations`), не относящийся к prescriptions — похоже на копипаст-заготовку, которую нужно заменить на конфигурацию prescriptions-frontend либо удалить. +- **Два способа описания деплоя**: helm-чарт в репозитории фронтенда (`.helm`, `universal-chart`) и Kustomize-манифесты в инфраструктуре (`iac/apps/prescriptions`) описывают один и тот же сервис по-разному — стоит зафиксировать единый источник истины. +- **Несогласованные имена**: `SERVICE_NAME=prescriptions-frontend` (мн. ч.), а `deployment.name`/`image.name` в `.helm` — `prescription-frontend` (ед. ч.). В инфра-манифестах deployment называется просто `frontend`. +- **Версии Node расходятся**: разработка — `v20.16.0` (`.nvmrc`), сборка образа — `node:15` (`Dockerfile`). +- **Мёртвая конфигурация**: экспорт `hosts` в `networking.config.js` (внедрение `___host`) и `module/Env` (`IEnv`, `process.env as IEnv`) в коде модуля не используются; `dotenv-webpack` присутствует в devDependencies, но не подключён в `webpack.config.js`. `networking.config.js` реально используется только в `webpack.dev.js` (прокси dev-сервера). + +## Минимальный набор для сборки + +- `BUILD_ENV` — одно из `local`/`stage`/`preprod`/`prod`/`contour` (обязательно); +- `NPM_TOKEN` — для установки приватных пакетов `@sarex-team/*` из nexus; +- (опционально) `STORYBOOK=true` — при сборке/запуске Storybook; +- (для e2e) `SRX_LOGIN`, `SRX_PASSWORD` в `cypress.env.json`. diff --git a/apps/prescriptions/ENDPOINTS.md b/apps/prescriptions/ENDPOINTS.md new file mode 100644 index 0000000..eb09e39 --- /dev/null +++ b/apps/prescriptions/ENDPOINTS.md @@ -0,0 +1,166 @@ +# Эндпоинты, с которыми взаимодействует prescriptions-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `prescriptions-frontend`, Module Federation `srx_prescriptions`, экспонирует `./PrescriptionsPage`). + +## Как устроено взаимодействие + +Запросы выполняются через единый `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из [`@sarex-team/sdk-js`](https://www.npmjs.com/) поверх `axios`. Каждый вызов задаётся объектом с полями: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (дописывается к базовому хосту сервиса); +- `data` — тело запроса (для `POST`/`PUT`/`PATCH`); +- `queryKey`, `axiosConfig` (в т.ч. `responseType: "blob"` для файлов), `params` — опции кеширования/повторов и параметры запроса. + +Метод HTTP определяется вызываемой функцией `httpService`: `getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`. + +Базовый хост подставляется SDK по паре (`BUILD_ENV`, `service`) из реестра `module/api/hosts.ts`. Значение `BUILD_ENV` задаётся на этапе сборки (`webpack.config.js` → `DefinePlugin`, глобальная константа `BUILD_ENV`), по умолчанию — `prod` (`http-service.ts`: `BUILD_ENV ?? "prod"`). В окружении `local` тип сервиса переключается на `original` (`setTypeOfHttpService("original")`). + +Итоговый URL = `<базовый хост сервиса>` + `url`. + +Определения вызовов сосредоточены в `module/api/*` (`index.ts`, `contractsApi.ts`, `resourcesApi.ts`, `templatesApi.ts`, `marks.ts`) и частично в сторах (`module/store/stores/resources.ts`, `module/store/stores/users.ts`). + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Помимо `stage`/`prod` определены окружения `local`, `preprod` и `contour` (в `contour` — относительные пути для изолированного контура). + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `prescriptions` | Предписания (поверх issues) | `https://stage-api.sarex.io/issues/api/prescriptions` | `https://api.sarex.io/issues/api/prescriptions` | +| `issues` | Сервис замечаний/issues | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` | +| `documentations` | Сервис документации (документы, бандлы, диски) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | +| `gateway_api_v1` | Gateway API v1 (ресурсы, документы, шаблоны) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` | +| `gateway_api_v2` | Gateway API v2 (пользователи, ресурсы) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` | +| `sarex` | Основной backend (core/client) | `https://stage.sarex.io` | `https://lk.sarex.io` | +| `sarexApi` | API Sarex (contracts) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `eav_api_v0` | Сервис атрибутов (EAV) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` | +| `orchestrator` | Оркестратор процессов (маркировка, подпись) | `https://stage-api.sarex.io/orchestrator` | `https://api.sarex.io/orchestrator/api` | +| `files` | Сервис файлов (скачивание) | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` | +| `lambdas` | Лямбды (экспорт reviews) | `https://stage-api.sarex.io/lambdas` | `https://api.sarex.io/lambdas` | +| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Подключаемый удалённый модуль (Module Federation) описан отдельно в `module/api/module-hosts.ts`: `documentations` → `remoteEntry.js` (stage: `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js`, prod: `https://modules.sarex.io/documentations/static/module/remoteEntry.js`). Хост выбирается функцией `getModuleHost(moduleName)` по `BUILD_ENV`. + +## Эндпоинты по сервисам + +### `prescriptions` — Предписания + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getPrescriptions` | GET | `/?{query}` | Список предписаний (фильтры/поиск, сериализация в `serializePrescriptionParams`) | +| `createPrescription` | POST | `/prescription/` | Создать предписание ⚠ вызывается с `service: "prescription"` (см. замечания) | +| `getPrescriptionById` | GET | `/{id}/` | Предписание по id | +| `editPrescription` | PATCH | `/{id}/` | Редактировать предписание | +| `deletePrescription` | DELETE | `/{id}/` | Удалить предписание | +| `exportPrescriptionById` | GET | `/{id}/export/?file_format={docx\|pdf}` | Экспорт предписания в docx/pdf | +| `getStatusCount` | GET | `/status-count/?{query}` | Счётчики по статусам | +| `getHistoryByCompanyId` | GET | `/history/?company_id={id}` | История предписаний компании | +| `getHistoryByPrescriptionId` | GET | `/{id}/history/` | История конкретного предписания | + +### `issues` — Замечания / статусы + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getStatusModels` | GET | `/prescription-status-models/?company_id={id}` | Модели статусов предписаний | +| `getCompanyStatuses` | GET | `/prescription-statuses/?company_id={id}` | Статусы предписаний компании | +| `getIssues` | GET | `/issues/?{params}` | Список замечаний | +| `getCustomStatuses` | GET | `/companies/{companyId}/status-model/v2/` | Кастомная модель статусов компании | + +### `documentations` — Сервис документации + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDocument` | GET | `/documents/{id}` | Документ по id | +| `getDisks` | GET | `/disks` | Список дисков (используется в `DocumentAPI` и `TemplatesApi`) | +| `mark` | PUT | `/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи в бандл | +| `sign` | POST | `/bundles/{bundleId}/sign` | Подписать бандл | +| `downloadFile` | GET | `/bundles/{bundleId}/{key}/download` | Скачать файл бандла (`responseType: blob`) | + +### `gateway_api_v1` — Gateway API v1 + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getResources` (store) | GET | `/resources/?company_id={id}` | Список ресурсов компании | +| `fetchParentDocumentByResourceId` | GET | `/resources-rpc/parent-document-by-resource-id/{resourceId}/` | Родительский документ по resource id | +| `fetchDocumentsBundleVersions` | POST | `/documents/bundle_versions` | Версии бандлов документов | +| `getDocumentAncestors` | POST | `/documents/ancestors` | Предки документов | +| `getTemplates` | GET | `/disks/{diskId}/flat_documents/?type={type}` | Шаблоны диска (плоский список) | + +### `gateway_api_v2` — Gateway API v2 + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getUsersByResourceId` | GET | `/users/?{query}` | Пользователи по фильтру ресурса | +| `getResourceFullInfo` | GET | `/resources/{resourceId}/` | Полная информация о ресурсе | + +### `sarex` — Основной backend (core/client) + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getUsersByCompanyId` | GET | `/api/core/users/?company={id}&{query}` | Пользователи компании | +| `getDepartments` | GET | `/api/core/admin/departments/?company={id}` | Отделы компании | +| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={id}&{query}` | Отделы компании (с доп. query) | +| `getPositions` | GET | `/api/core/admin/positions/?company={id}` | Должности компании | +| `getPositionsV2` | GET | `/api/core/admin/positions/?company={id}&{query}` | Должности компании (с доп. query) | +| `getSettings` (store) | GET | `/api/client/settings/` | Клиентские настройки | + +### `sarexApi` — API Sarex (contracts) + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getContracts` | GET | `/contracts/api/v0/contracts/?tenant_id={companyId}` | Договоры компании | +| `getContractsByContractorId` | GET | `/contracts/api/v0/contracts/?tenant_id={companyId}&contractor_id={id}` | Договоры по контрагенту | + +### `eav_api_v0` — Сервис атрибутов (EAV) + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getAttributes` | GET | `/attribute/?company_id={id}` | Атрибуты компании | + +### `orchestrator` — Оркестратор процессов + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `createMarkFlow` | POST | `/process` | Запустить процесс маркировки | +| `getMarkFlow` | GET | `/process/{id}` | Процесс по id | +| `startSign` | POST | `/sign` | Запустить подписание | + +### `files` — Сервис файлов + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `downloadDocuments` | POST | `/documents/` | Скачать документы по `bundle_ids` (`responseType: blob`) | + +### `lambdas` — Лямбды (экспорт) + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchExportReviewsByResourceIDs` | GET | `/export-reviews/{params}` | Экспорт reviews (xlsx) | +| `fetchExportReview` | GET | `/export-reviews/{reviewId}/report/` | Отчёт по review (pdf) | + +### `flows` — Процессы (⚠ сервис не задан в hosts.ts) + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `changeCopyPaths` | PATCH | `/documents/change-copy-paths/` | Изменить пути копий документов | + +## Обработка ошибок + +Отдельного модуля-маппера ошибок (`errors.ts`) в проекте нет — обработка распределена: + +- часть обёрток (`ContractsApi`, `ResourcesApi`, `TemplatesApi`) при ошибке пробрасывают `throw new Error(error)`; +- часть функций (`fetchParentDocumentByResourceId`, `fetchExportReview*`) гасят ошибку через `console.error` и не пробрасывают её; +- тип ответа об ошибке — `ErrorResponse` (`module/api/types.ts`): читается `response.data.detail`; +- статусы запроса в сторах: `RequestStatus` — `init`/`loading`/`success`/`fetching`/`error`/`permissionError`. + +## Права доступа (`module/api/permissions.ts`) + +Модуль оперирует правами `core.*`: `can_view_prescription`, `can_add_prescription`, `can_edit_prescription`, `can_delete_prescription`, `can_view_all_prescriptions`, `can_admin_prescription`. Группы (`FG_PERMISSIONS`): `ADMIN` (все права), `AUTHOR` (просмотр + создание), `RESPONSIBLE` и `VIEW_ALL` (просмотр). + +## Замечания и потенциальные проблемы + +- **`prescription` (единственное число)** — `createPrescription` вызывается с `service: "prescription"`, но такого ключа в `module/api/hosts.ts` нет (есть только `prescriptions`). Базовый хост не резолвится корректно — вероятно опечатка, следует использовать `prescriptions`. +- **`flows`** — `changeCopyPaths` использует `service: "flows"`, который также не задан в `hosts.ts`. Ключ нужно добавить в реестр либо исправить. +- **`contour`** — в окружении `contour` не определён сервис `sarexApi`, поэтому `getContracts`/`getContractsByContractorId` в этом контуре работать не будут. +- **`orchestrator`** — в `prod` базовый URL с суффиксом `/api` (`.../orchestrator/api`), а в `stage`/`local`/`preprod` — без него. Пути эндпоинтов (`/process`, `/sign`) следует проверять с учётом этого различия. +- **`checklists` и `zitadel`** заданы в `hosts.ts`, но напрямую через `httpService` в модуле не вызываются (`zitadel` — IdP, используется SDK для авторизации; `checklists` в текущем коде модуля не используется). diff --git a/apps/processing/workflows-api.CONFIGURATION.md b/apps/processing/workflows-api.CONFIGURATION.md new file mode 100644 index 0000000..a86d666 --- /dev/null +++ b/apps/processing/workflows-api.CONFIGURATION.md @@ -0,0 +1,246 @@ +# Конфигурация проекта workflows-api + +`workflows-api` — HTTP-сервис (Go 1.24, фреймворк [Fiber v2](https://github.com/gofiber/fiber)) для работы с workflow: создание, чтение, перезапуск задач, отмена запусков, приоритизация. Хранилище — PostgreSQL. Трейсинг — OpenTelemetry (через внешнюю библиотеку `gitlab.sarex.io/infra/golang-fiber-otel-tools`). + +## Способы конфигурирования + +Конфигурация читается **только из переменных окружения**. Используется библиотека [`github.com/ilyakaznacheev/cleanenv`](https://github.com/ilyakaznacheev/cleanenv) (`cleanenv.ReadEnv`). Файлы конфигурации (`.yaml`, `.json`) не читаются — вызывается именно `ReadEnv`, а не `ReadConfig`. + +- **Префикс** у переменных отсутствует — используются «плоские» имена (`POSTGRES_ADDRESS`, `HTTP_HOST` и т. п.). +- **Вложенность** структуры `Config` описывается через встроенные (embedded) структуры (`App`, `Log`, `HTTP`, `pgxconnection.Postgres`, `TRACER`, `Execution`), но на имена переменных это не влияет — теги `env` заданы плоско. +- Значения по умолчанию задаются тегом `env-default`. +- Булевы значения cleanenv принимает как `true/false`, а также `1/0` (в Helm используется числовая форма). + +Точка сборки конфигурации — `config/config.go`, функция `config.New()`. Отдельно, для запуска миграций, вторая структура `pkg/postgres/gopg.Postgres` читается своим вызовом `cleanenv.ReadEnv` в `gopg.GetPgConnectionWithoutConfig()` — **у неё те же имена переменных, но частично другие значения по умолчанию** (см. «Замечания»). + +### Способы запуска + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Бинарь `httpserver` (production, `Dockerfile` `ENTRYPOINT ["/httpserver", "migrate"]`) | Переменные окружения контейнера (в k8s — из Helm-чарта: блоки `envs` и `secretEnvs`) | +| Бинарь `migrations` (отдельный ранер миграций) | Переменные окружения контейнера | +| Локальный запуск через `air` (`.air.toml`, hot-reload, сборка `./cmd/httpserver/main.go`) | Переменные окружения оболочки / `.env` (подхватываются вручную), значения по умолчанию из кода | +| `docker-compose up` (`docker-compose.yaml`) | `environment:` в compose + значения по умолчанию из кода | + +### Точки входа и вспомогательные скрипты + +| Файл / скрипт | Назначение | +| --- | --- | +| `cmd/httpserver/main.go` | Основная точка входа. Если передан хотя бы один аргумент (например `migrate`) — сначала выполняет миграции (`go-pg-migrations`), затем поднимает Fiber-сервер | +| `cmd/migrations/main.go` | Отдельный бинарь только для миграций (без запуска сервера) | +| `Dockerfile` | Multi-stage сборка: собирает `httpserver` и `migrations`, `ENTRYPOINT ["/httpserver", "migrate"]` | +| `entrypoint.sh` | Скрипт-обёртка (`/go/bin/migrations migrate` → `/go/bin/httpserver`). **Не используется** Dockerfile и ссылается на несуществующие пути бинарей — устаревший артефакт (см. «Замечания») | +| `.air.toml` | Конфиг hot-reload `air` для локальной разработки | +| `docker-compose.yaml` | Локальный стенд: PostgreSQL 14-alpine + сборка API. Блок `migrations` закомментирован | +| `Makefile` | Юнит-тесты, генерация моков, поднятие/сборка контейнера БД (`.docker/postgres`) | +| `.docker/postgres/Dockerfile` | Образ локальной БД для `make container-run-deps` | + +### Порядок старта контейнера (production) + +1. Контейнер стартует с `ENTRYPOINT ["/httpserver", "migrate"]`. +2. `httpserver` видит аргумент `migrate` (`len(os.Args) > 1`) → открывает подключение к БД через `go-pg` (`gopg.GetPgConnectionWithoutConfig`, читает env заново) и прогоняет миграции из `cmd/migrations/migrationfiles`. +3. После миграций поднимается Fiber-приложение (`server.New` → `server.Run`), подключается пул `pgx` (`pgxconnection.GetPgConnection`), при `TRACER_USE=true` инициализируется трейсер/otel-логгер. +4. Сервер слушает адрес из `HTTP_HOST`. Health-check — `GET /ping`. + +## Переменные приложения + +Ниже — переменные, которые **реально читает код** (`config/config.go` + `pkg/postgres/pgxconnection/postgres.go`). + +### App (`config/config.go`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `APP_NAME` | string | `workflows-api` | Имя приложения (`App.Name`) | +| `APP_VERSION` | string | `v1` | Версия приложения (`App.Version`) | + +### Log + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `info` | Уровень логирования (`logging.NewLogger`) | + +### HTTP + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_HOST` | string | `0.0.0.0:8000` | Адрес и порт прослушивания Fiber-сервера | +| `PUBLIC_KEY` | string | — (пусто) | PEM-публичный ключ (PKIX) для проверки JWT Sarex. **Обязателен**: при пустом значении `auth.New` вызывает `panic` на старте | +| `HTTP_BODY_LIMIT` | int | `268435456` (256 MiB) | Максимальный размер тела запроса (`fiber.Config.BodyLimit`) | +| `HTTP_READ_BUFFER_SIZE` | int | `98304` (96 KiB) | Размер буфера чтения (`fiber.Config.ReadBufferSize`) | + +### Database (`pkg/postgres/pgxconnection`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | `localhost` | Хост PostgreSQL | +| `POSTGRES_DB` | string | `processing_db` | Имя базы данных | +| `POSTGRES_USER` | string | `sarex` | Пользователь БД | +| `POSTGRES_PASSWORD` | string | `sarex` | Пароль БД | +| `POSTGRES_PORT` | string | `5432` | Порт PostgreSQL | +| `POSTGRES_POOL_SIZE` | int | `3` | Размер пула (используется только для логирования; фактический размер пула pgx задаётся строкой подключения) | +| `ENABLE_SQL_QUERY` | bool | `true` | Флаг логирования SQL. **Читается в конфиг, но нигде не используется** (см. «Замечания») | +| `YC-PG-CERTIFICATE` | string | — (пусто) | CA-сертификат (PEM) для TLS-подключения к БД. Непустое значение включает TLS | +| `POSTGRES_SSL_USE` | bool | `false` | Включение TLS-подключения к БД | + +### Tracer (`config.TRACER`, OpenTelemetry) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACER_USE` | bool | `false` | Включить трейсинг/otel-логгер и otelfiber-middleware | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора (gRPC) | +| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `SERVICE_NAME` | string | `wf-test-db` | Имя сервиса в трейсах | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя otel-логгера | + +### Execution (лимиты ресурсов задач, `config.Execution`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MAX_CPU_REQUESTS` | string | `25` | Максимально допустимый CPU-request в конфиге execution задачи (валидация через `k8s.io/apimachinery/resource`) | +| `MAX_MEMORY_REQUESTS` | string | `300Gi` | Максимально допустимый memory-request в конфиге execution задачи | + +## Переменные инфраструктуры, сборки и вспомогательных утилит + +Переменные `Makefile` (значения по умолчанию, переопределяются через `make VAR=...`): + +| Переменная | Значение по умолчанию | Назначение | +| --- | --- | --- | +| `OCI` | `docker` | Контейнерный движок (`docker`/`podman`) | +| `WF_API_CONTAINER__NETWORK_NAME` | `wf-api-network` | Имя bridge-сети для локальных контейнеров | +| `WF_API_DATABASE_IMAGE__TAG` | `wf-api-database` | Тег образа локальной БД | +| `WF_API_DATABASE_CONTAINER__NAME` | `wf-api-database-c` | Имя контейнера БД | +| `WF_API_DATABASE__VOLUME_NAME` | `wf-api-data` | Имя тома данных БД | +| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` / `POSTGRES_PORT` | берутся из окружения | Прокидываются в контейнер БД при `container-run-database` | + +Переменные сборки образа (`Dockerfile`): + +| Переменная | Значение | Назначение | +| --- | --- | --- | +| `CGO_ENABLED` | `0` | Статическая сборка Go | +| `GOOS` | `linux` | Целевая ОС | +| `GOARCH` | `amd64` | Целевая архитектура | + +Переменные `.docker/postgres/Dockerfile` и `docker-compose.yaml` (локальный стенд): + +| Переменная | Значение | Назначение | +| --- | --- | --- | +| `POSTGRES_DB` | `processing_db` | БД локального PostgreSQL | +| `POSTGRES_USER` | `processing` | Пользователь локального PostgreSQL | +| `POSTGRES_PASSWORD` | `processing` | Пароль локального PostgreSQL | +| `POSTGRES_ADDRESS` | `database` (в compose для сервиса `api`) | Хост БД внутри сети compose | +| `POSTGRES_SSL_USE` | `false` | Отключение TLS локально | + +## Переменные из Helm-чарта + +Деплой выполняется зависимостью-чартом `universal-chart` (`.helm/Chart.yaml`, версия `0.1.7`), значения — в `.helm/values.yaml`. Значения даются по стендам через ключи `_default / stage / preprod / production`. + +### Обычные переменные (`services.workflows-api.envs`) + +| Переменная | Значение (`_default`) | Значения по стендам / примечание | +| --- | --- | --- | +| `POD_NAME` | `$(K8S_POD_NAME)` | Имя пода. **Кодом не читается** | +| `POSTGRES_POOL_SIZE` | `3` | Размер пула (логирование) | +| `HTTP_HOST` | `0.0.0.0:8080` | Адрес прослушивания в k8s (порт 8080) | +| `S3_SERVICE_ACCOUNT` | `/etc/sarex/yc-s3/yc-s3-service-account.json` | **Кодом не читается** | +| `DJANGO_HOST` | `https://stage.sarex.io` | stage: `stage.sarex.io`, preprod: `preprod.sarex.io`, production: `lk.sarex.io`. **Кодом не читается** | +| `OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT` | `opentelemetry-collector.observability.svc.cluster.local:4318` | Одинаково на всех стендах. **Кодом не читается** (легаси-пакет `observavility` не подключён) | +| `ENABLE_SQL_QUERY` | `0` | Читается в конфиг, но не используется | +| `POSTGRES_SSL_USE` | `1` | preprod: `true`, остальные `1` | +| `ENABLE_OBSERVABILITY` | `1` | stage `1`, preprod `0`, production `1`. **Кодом не читается** | +| `TRACER_USE` | `1` | Включает трейсинг | +| `TRACER_HOST` | `signoz-otel-collector.signoz.svc.cluster.local:4317` | preprod/production: `signoz-otel-collector-external.signoz.svc.cluster.local:4317` | +| `SERVICE_NAME` | `workflows-api.processing-stage` | stage: `workflows-api.platform`, preprod: `workflows-api.processing-preprod`, production: `workflows-api.processing-prod` | +| `TRACER_USE_INSECURE` | `1` | Небезопасное подключение к коллектору | +| `TRACER_LOGGER_NAME` | `tracer_logger` | Имя otel-логгера | +| `MAX_CPU_REQUESTS` | `25` | Лимит CPU-request задач | +| `MAX_MEMORY_REQUESTS` | `300Gi` | Лимит memory-request задач | + +### Секретные переменные (`services.workflows-api.secretEnvs`) + +| Переменная | Секрет (secret_name) | Ключ (secret_key) | +| --- | --- | --- | +| `POSTGRES_ADDRESS` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `host` | +| `POSTGRES_PORT` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `port` | +| `POSTGRES_DB` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `database` | +| `POSTGRES_USER` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `_default`/`stage`: `user`; `preprod`/`production`: `username` | +| `POSTGRES_PASSWORD` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `password` | +| `PUBLIC_KEY` | `_default`/`stage`: `jwt-secret`; `preprod`/`production`: `public-key` | `_default`/`stage`: `public_key`; `preprod`/`production`: `key` | +| `YC-PG-CERTIFICATE` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `yc-pg-certificate` | `_default`/`stage`: `ca.crt`; `preprod`/`production`: `certificate` | + +### Прочие параметры чарта + +- **Порт деплоймента**: `8080`; сервис `ClusterIP`, `targetPort: 8080`, `port: 80` (stage: `8000`). +- **Имя сервиса**: `workflows-service` (stage: `workflows-api-service`). +- **Реплики**: `_default`/`stage` — 1, preprod/production — 2. +- **Ресурсы пода**: requests `_default` 100Mi / 100m, preprod/production 200Mi / 200m. +- **Пробы**: liveness и readiness — `httpGet /ping` на порту 8080. +- **serviceAccount**: `workflows-api-sa`. +- **imagePullSecrets**: `dockerhub`. **Образ**: `cr.yandex/crp3ccidau046kdj8g9q/workflows-api`. + +## Переменные в CI + +`.gitlab-ci.yml` подключает общие пайплайны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`). Глобальные переменные: + +| Переменная | Значение | Назначение | +| --- | --- | --- | +| `SERVICE_NAME` | `workflows-api` | Имя сервиса в пайплайне | +| `DOCKERFILE_PATH` | `Dockerfile` | Путь к Dockerfile | +| `BUILD_ARGS` | `--build-arg CI_COMMIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}` | Аргументы сборки образа | +| `CI_TRIGGER_SOURCE` | `app` | Источник триггера | + +Маппинг ветка/тег → стенд и namespace (`workflow.rules`): + +| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | env (universal-chart.global.env) | +| --- | --- | --- | --- | --- | --- | +| `CI_COMMIT_BRANCH == "stage"` | `stage` | `platform` | `0.0.1-stage` | `universal-chart-stage` | `stage` | +| `CI_COMMIT_BRANCH == "master"` | `preprod` | `processing-preprod` | `0.0.1-preprod` | `universal-chart-preprod` | `preprod` | +| `CI_COMMIT_TAG` (любой тег) | `production` | `processing-prod` | `0.0.1-prod` | `universal-chart-production` | `production` | +| `CI_PIPELINE_SOURCE == "merge_request_event"` | — | — | — | — | сборка образа выключена (`ENABLE_BUILD_IMAGE=false`) | + +Во всех деплой-правилах через `HELM_SET_ARGS` пробрасываются `IMAGE_NAME`, `commitSha=${CI_COMMIT_SHA}`, `gitlabUri`, `gitlabJobUrl`, `owner`. `RELEASE_NAME`/`CHART_NAME` — `workflows-api`. + +Джоба `unittest` (`stage: test`, образ `golang:1.24`, `make unit-tests`) запускается на любых ветках/тегах и MR, `allow_failure: true`. + +## Замечания и потенциальные проблемы + +1. **Две разные структуры конфигурации БД с разными дефолтами.** Сервер использует `pkg/postgres/pgxconnection.Postgres` (дефолты `POSTGRES_USER=sarex`, `POSTGRES_PASSWORD=sarex`), а миграции — `pkg/postgres/gopg.Postgres` (дефолты `processing`/`processing`). При запуске без явно заданных переменных сервер и миграции подключались бы под разными кредами. В production это не проявляется, т. к. все переменные приходят из секретов. +2. **`ENABLE_SQL_QUERY` не используется.** Поле читается в обе структуры (`EnableSQLQuery`), но нигде в коде не применяется — флаг «мёртвый». +3. **`OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT`, `ENABLE_OBSERVABILITY` не используются.** Пакет `pkg/observavility` (с `SetupOTelSDK`) нигде не импортируется — это легаси. Реальный трейсинг настраивается переменными `TRACER_*` через внешнюю библиотеку `golang-fiber-otel-tools`. +4. **`POD_NAME`, `S3_SERVICE_ACCOUNT`, `DJANGO_HOST` из Helm кодом не читаются** — либо задел на будущее, либо устаревшие переменные. +5. **`entrypoint.sh` устарел и не используется.** Он ссылается на `/go/bin/migrations` и `/go/bin/httpserver`, тогда как в образе бинари лежат в `/httpserver` и `/migrations`, а `ENTRYPOINT` задан в `Dockerfile` напрямую (`/httpserver migrate`). +6. **Опечатка в mount-пути internal-middleware.** В `server.go` middleware, выставляющий `is_internal=true`, монтируется как `app.Use("./internal", ...)` (с ведущей точкой) вместо `"/internal"`. Из-за этого для маршрутов группы `/internal` флаг `is_internal` может не выставляться; в контроллерах `nil`-значение трактуется как «внутренний/доверенный запрос» (проверки принадлежности к компании пропускаются). Логически поведение сохраняется, но путь выглядит как баг. +7. **`PUBLIC_KEY` обязателен.** При пустом значении `auth.New` делает `panic("failed to parse PEM block ...")` — сервис не стартует. Дефолта нет. +8. **Расхождение по порту.** Дефолт кода `HTTP_HOST=0.0.0.0:8000`, docker-compose — `8000`, а в k8s (Helm) — `8080`. Локально сервис слушает 8000, в кластере — 8080. +9. **`BUILD_ARGS` передаёт `CI_COMMIT_SHORT_SHA`, но `Dockerfile` не объявляет соответствующий `ARG`** — build-arg игнорируется. +10. **`POSTGRES_SSL_USE` в Helm задаётся то как `1`, то как `true`** (preprod). cleanenv корректно парсит обе формы, но единообразия нет. + +## Минимальный набор для локального запуска + +Для запуска сервера локально (например, БД поднята через `make container-run-deps` или `docker-compose`) достаточно: + +```env +# БД +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=processing_db +POSTGRES_USER=processing +POSTGRES_PASSWORD=processing +POSTGRES_SSL_USE=false + +# HTTP +HTTP_HOST=0.0.0.0:8000 + +# Обязательно: PEM публичный ключ (PKIX) для проверки JWT +PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" + +# Трейсинг можно выключить +TRACER_USE=false +``` + +Минимальный сценарий: +1. Поднять PostgreSQL: `make container-run-deps` (или `docker-compose up database`). +2. Экспортировать переменные выше (особенно валидный `PUBLIC_KEY`, иначе `panic`). +3. Прогнать миграции и запустить сервер: `go run ./cmd/httpserver migrate` (аргумент `migrate` включает миграции), либо `air` для hot-reload. +4. Проверить: `GET http://localhost:8000/ping` → `{"status":"ready"}`. + +> Через `docker-compose up` сервис поднимается на `:8000`, БД — `processing/processing/processing_db`, но `PUBLIC_KEY` в compose не задан — для полноценной работы API его нужно добавить. diff --git a/apps/processing/workflows-api.env.example b/apps/processing/workflows-api.env.example new file mode 100644 index 0000000..434d7ff --- /dev/null +++ b/apps/processing/workflows-api.env.example @@ -0,0 +1,68 @@ +# ============================================================================ +# workflows-api — пример переменных окружения +# Конфигурация читается через cleanenv (github.com/ilyakaznacheev/cleanenv) +# из переменных окружения. Префикса нет. Значения по умолчанию — из кода. +# ============================================================================ + +# ----- App ----- +APP_NAME=workflows-api +APP_VERSION=v1 + +# ----- Logging ----- +# Уровень логирования: debug | info | warn | error +LOG_LEVEL=info + +# ----- HTTP ----- +# Адрес и порт прослушивания (в k8s задаётся 0.0.0.0:8080) +HTTP_HOST=0.0.0.0:8000 +# PEM публичный ключ (PKIX) для проверки JWT Sarex. ОБЯЗАТЕЛЕН: +# при пустом значении сервис падает с panic на старте. +PUBLIC_KEY= +# Максимальный размер тела запроса, байт (по умолчанию 256 MiB) +HTTP_BODY_LIMIT=268435456 +# Размер буфера чтения, байт (по умолчанию 96 KiB) +HTTP_READ_BUFFER_SIZE=98304 + +# ----- Database (PostgreSQL) ----- +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=processing_db +POSTGRES_USER=processing +POSTGRES_PASSWORD=processing +# Размер пула (используется только для лога) +POSTGRES_POOL_SIZE=3 +# Включить TLS-подключение к БД +POSTGRES_SSL_USE=false +# CA-сертификат (PEM) для TLS. Непустое значение включает TLS автоматически. +YC-PG-CERTIFICATE= +# ВНИМАНИЕ: переменная читается в конфиг, но кодом НЕ используется (мёртвый флаг). +ENABLE_SQL_QUERY=true + +# ----- Tracing (OpenTelemetry) ----- +# Включает трейсинг, otel-логгер и otelfiber-middleware +TRACER_USE=false +# Адрес OTLP-коллектора (gRPC) +TRACER_HOST=localhost:4317 +# Небезопасное (без TLS) подключение к коллектору +TRACER_USE_INSECURE=true +# Имя сервиса в трейсах +SERVICE_NAME=workflows-api +# Имя otel-логгера +TRACER_LOGGER_NAME=tracer_logger + +# ----- Execution (лимиты ресурсов задач) ----- +# Максимально допустимый CPU-request в execution-конфиге задачи +MAX_CPU_REQUESTS=25 +# Максимально допустимый memory-request в execution-конфиге задачи +MAX_MEMORY_REQUESTS=300Gi + +# ============================================================================ +# Переменные ниже присутствуют в .helm/values.yaml / старом .example.env, +# но кодом НЕ читаются (легаси). Оставлены для справки, включать не нужно: +# OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT — пакет observavility не подключён +# ENABLE_OBSERVABILITY +# POD_NAME +# S3_SERVICE_ACCOUNT +# DJANGO_HOST +# ENABLE_SSL — опечатка старого .example.env; корректное имя POSTGRES_SSL_USE +# ============================================================================ diff --git a/apps/processing/workflows-api.openapi.yaml b/apps/processing/workflows-api.openapi.yaml new file mode 100644 index 0000000..6393cbf --- /dev/null +++ b/apps/processing/workflows-api.openapi.yaml @@ -0,0 +1,755 @@ +openapi: 3.0.3 +info: + title: Workflows API + version: "1.0.0" + description: | + REST API сервиса **workflows-api** для работы с workflow (пайплайнами задач): + создание, получение, перезапуск задач, отмена запусков, приоритизация и получение + позиции в очереди. + + ### Технологии + - Язык: Go 1.24, HTTP-фреймворк **Fiber v2**. + - JSON-сериализация: `bytedance/sonic`. + - Хранилище: PostgreSQL (пул `pgx/v5`). + - Трейсинг: OpenTelemetry (`otelfiber`), включается переменной `TRACER_USE`. + + ### Группы маршрутов + Все обработчики регистрируются дважды — в двух группах с одинаковым набором путей + (см. `internal/server/server.go` и `internal/controller/http/v1/workflows/routes.go`): + + - **`/api/v1/...`** — публичная группа. На префикс `/api` навешен middleware аутентификации + (`auth.AuthMiddleware`): требуется валидный JWT. Для запросов выставляется `is_internal=false` + и проверяется принадлежность пользователя к компании. + - **`/internal/v1/...`** — внутренняя группа (сервис-сервис) без аутентификации; трактуется как + доверенная (`is_internal=true`), проверки принадлежности к компании пропускаются. + + Отдельно, вне групп и без авторизации, доступен health-check **`GET /ping`**. + + В этом документе пути описаны относительно базового префикса группы `/api/v1` + (см. `servers`). Те же пути доступны и под `/internal/v1`. + + ### Аутентификация + Группа `/api` защищена middleware, который принимает один из двух токенов: + - **`Authorization: Bearer `** — токен Sarex, подпись проверяется публичным ключом + из переменной `PUBLIC_KEY` (RS/PKIX). Из claims извлекаются `user_id`, `company_ids`, + `is_superuser`. + - **`Identity: Bearer `** — токен Zitadel (проверяется без верификации подписи, + `ParseUnverified`); данные пользователя берутся из claim + `urn:zitadel:iam:user:metadata` (поля `id`, `company_ids`, `is_superuser`). + + Если присутствует заголовок `Identity`, используется он; иначе — `Authorization`. + Часть операций (`prioritize`, `move_to_super_high_resources`) доступна только суперпользователю + (`is_superuser=true`). + + ### Пагинация + Метод `GET /workflows` поддерживает `limit` и `offset` (query-параметры, строки). + Ответ содержит `items`, а также `limit`, `offset`, `total`. + + ### Обработка ошибок + Ошибки возвращаются в JSON вида: + ```json + { "message": "human readable message", "error_code": "WF-0001" } + ``` + Коды (`internal/app_errors`): `WF-0000` (system, 500), `WF-0001` (not found, 404), + `WF-0002` (no auth, 401), `WF-0003` (no access), `WF-0004` (invalid, 400), + `WF-0005` (workflow config error / forbidden, 400/403). + HTTP-статус выбирается в `ErrorHandler` фреймворка по типу ошибки: 400 (ошибки парсинга/валидации/ + конфигурации workflow), 401 (нет/некорректный токен), 403 (forbidden), 404 (не найдено), 500 (прочее). + + ### Замечания (расхождения кода и существующей схемы) + Документ приведён в соответствие с реальными маршрутами `routes.go`. Отличия от старого + `openapi_schema.yaml`: + - **Добавлены** отсутствовавшие маршруты: `POST /workflows/batch`, `GET /workflows/{workflow_id}/position`, + `POST /workflows/{workflow_id}/prioritize`, `POST /tasks/{task_id}/move_to_super_high_resources`. + - **Удалён** маршрут `GET /tasks-runs/{task_run_id}/logs` — в коде такого обработчика нет. + (Внимание: `workflows-frontend` этот эндпоинт вызывает — см. `workflows-frontend.ENDPOINTS.md`.) + - `POST /tasks-runs/{task_run_id}/cancel` фактически возвращает пустой объект `{}` (в коде + `c.JSON(&struct{}{})`), а не объект task_run. + - У задачи (`Task`) в модели есть поля `execution`, `services`, `layer`, `id`, `workflow_id`, + а у workflow — `document_id`, которые в старой схеме отсутствовали. +servers: + - url: 'http://localhost:8000/api/v1' + description: Локальный сервер (дефолт HTTP_HOST / docker-compose) + - url: 'http://workflows-service/api/v1' + description: Внутрикластерный сервис (preprod/production; stage — workflows-api-service:8000) + - url: 'https://stage-api.sarex.io/workflows/api/v1' + description: Публичный stage (через ingress, префикс /workflows) + - url: 'https://api.sarex.io/workflows/api/v1' + description: Публичный production (через ingress, префикс /workflows) +tags: + - name: workflows + description: Операции с workflow + - name: tasks + description: Операции с задачами workflow + - name: task-runs + description: Операции с запусками задач + - name: health + description: Проверка доступности сервиса +paths: + /ping: + get: + tags: [health] + summary: Health-check + description: > + Проверка готовности сервиса. Зарегистрирован вне групп `/api` и `/internal`, + без аутентификации (реальный путь — `/ping`, без префикса `/api/v1`). + operationId: Ping + responses: + '200': + description: Сервис готов + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ready + + /companies/{company_id}/workflows: + post: + tags: [workflows] + summary: Создать workflow + description: > + Создаёт workflow для компании. Для группы `/api` пользователь должен принадлежать + компании `company_id`. Задачи не должны содержать поле `services` — используется + `service_requests`. Если у задачи не задан `backoff_limit`, он проставляется равным 5. + Поле `valid_until` должно быть в будущем. + operationId: CreateWorkflow + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: company_id + in: path + required: true + description: Идентификатор компании + schema: + type: integer + example: 1 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkflowRequest' + responses: + '200': + description: Созданный workflow + content: + application/json: + schema: + $ref: '#/components/schemas/Workflow' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalError' + + /workflows: + get: + tags: [workflows] + summary: Список workflow по компаниям + description: > + Возвращает список workflow для указанных компаний. Параметр `company_ids` — + обязательная строка с идентификаторами через запятую. Для группы `/api` + не-суперпользователю возвращаются только его компании. + operationId: ListByCompanyID + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: company_ids + in: query + required: true + description: Идентификаторы компаний через запятую + schema: + type: string + example: "1,2,3" + - name: limit + in: query + required: false + schema: + type: integer + example: 100 + - name: offset + in: query + required: false + schema: + type: integer + example: 0 + responses: + '200': + description: Список workflow + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowsResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalError' + + /workflows/batch: + post: + tags: [workflows] + summary: Получить workflow пачкой + description: Возвращает workflow по списку идентификаторов. + operationId: GetWorkflows + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetBatchWorkflowsRequest' + responses: + '200': + description: Найденные workflow + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowsBatchResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalError' + + /workflows/{id}: + get: + tags: [workflows] + summary: Получить workflow по ID + operationId: GetWorkflow + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: id + in: path + required: true + description: UUID workflow + schema: + type: string + format: uuid + responses: + '200': + description: Workflow + content: + application/json: + schema: + $ref: '#/components/schemas/Workflow' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /workflows/{workflows_ids}/state: + get: + tags: [workflows] + summary: Состояния нескольких workflow + description: > + Возвращает отображение `workflow_id -> state` для списка workflow. + `workflows_ids` — UUID через запятую. + operationId: GetStates + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: workflows_ids + in: path + required: true + description: UUID workflow через запятую + schema: + type: string + example: "3fa85f64-5717-4562-b3fc-2c963f66afa6,4fa85f64-5717-4562-b3fc-2c963f66afa6" + responses: + '200': + description: Отображение id -> состояние + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/CompletenessState' + example: { "3fa85f64-5717-4562-b3fc-2c963f66afa6": "done" } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalError' + + /workflows/{workflow_id}/position: + get: + tags: [workflows] + summary: Позиция workflow в очереди + description: Возвращает позицию workflow в очереди на выполнение (ограничение 100). + operationId: GetWorkflowQueuePosition + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Позиция в очереди + content: + application/json: + schema: + type: object + properties: + position: + type: string + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /workflows/{workflow_id}/prioritize: + post: + tags: [workflows] + summary: Приоритизировать workflow + description: Повышает приоритет workflow. Доступно только суперпользователю. + operationId: PrioritizeWorkflow + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: workflow_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Успешно, тело отсутствует + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalError' + + /tasks/{task_id}/restart: + post: + tags: [tasks] + summary: Перезапустить задачу + description: > + Перезапускает задачу, опционально переопределяя `inputs`, `outputs`, `parameters`, + `valid_until`. Возвращает обновлённый workflow. + operationId: RestartTask + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: task_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RestartTaskRequest' + responses: + '200': + description: Обновлённый workflow + content: + application/json: + schema: + $ref: '#/components/schemas/Workflow' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /tasks/{task_id}/move_to_super_high_resources: + post: + tags: [tasks] + summary: Перевести задачу на super-high-resources + description: Перемещает задачу на пул ресурсов super-high-resources. Только суперпользователь. + operationId: MoveTaskToSuperHighResources + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: task_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Успешно, тело отсутствует + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /tasks-runs/{task_run_id}/cancel: + post: + tags: [task-runs] + summary: Отменить запуск задачи + description: Отменяет запуск задачи (task run). Возвращает пустой объект. + operationId: CancelTaskRun + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + - name: task_run_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Успешно (пустой объект) + content: + application/json: + schema: + type: object + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + +components: + parameters: + AuthorizationHeader: + name: Authorization + in: header + required: true + description: > + `Bearer ` — токен Sarex. Альтернативно можно передать заголовок + `Identity: Bearer ` (токен Zitadel). Не требуется для группы `/internal`. + schema: + type: string + example: "Bearer eyJhbGciOi..." + + responses: + BadRequest: + description: Некорректный запрос (парсинг/валидация/конфигурация workflow) + content: + application/json: + schema: + $ref: '#/components/schemas/AppError' + Unauthorized: + description: Не авторизован (нет/некорректный токен) + content: + application/json: + schema: + $ref: '#/components/schemas/AppError' + Forbidden: + description: Доступ запрещён + content: + application/json: + schema: + $ref: '#/components/schemas/AppError' + NotFound: + description: Ресурс не найден + content: + application/json: + schema: + $ref: '#/components/schemas/AppError' + InternalError: + description: Внутренняя ошибка + content: + application/json: + schema: + $ref: '#/components/schemas/AppError' + + schemas: + AppError: + type: object + properties: + message: + type: string + example: "not found workflow" + error_code: + type: string + description: "Код ошибки: WF-0000..WF-0005" + example: "WF-0001" + + CompletenessState: + type: string + description: Состояние workflow + enum: [done, running, error] + + ExecutionState: + type: string + description: Состояние запуска задачи (task run) + enum: [pending, done, canceling, canceled, idle, running, error, lost] + + Description: + type: object + description: Описание входа/выхода задачи (источник/приёмник данных) + required: [type, path] + properties: + type: + type: string + maxLength: 32 + description: "Тип хранилища (валидация: google, local, s3, s3v2, srx-tmp, url, pdm)" + example: s3 + path: + type: string + maxLength: 512 + + ServicePublicDescription: + type: object + properties: + type: + type: string + kind: + type: string + + Resources: + type: object + properties: + cpu_limits: + type: string + memory_limits: + type: string + cpu_requests: + type: string + memory_requests: + type: string + + Execution: + type: object + properties: + executor: + type: string + description: "Исполнитель задачи (допустимые: k8s, amqp)" + enum: [k8s, amqp] + resources: + $ref: '#/components/schemas/Resources' + + Task: + type: object + properties: + id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + slug: + type: string + docker_image: + type: string + backoff_limit: + type: integer + format: int32 + description: "По умолчанию 5, если не задан" + inputs: + type: object + additionalProperties: + $ref: '#/components/schemas/Description' + outputs: + type: object + additionalProperties: + $ref: '#/components/schemas/Description' + parameters: + type: object + additionalProperties: true + service_requests: + type: array + items: + type: string + services: + type: array + items: + $ref: '#/components/schemas/ServicePublicDescription' + execution: + $ref: '#/components/schemas/Execution' + needs: + type: array + items: + type: string + layer: + type: integer + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + TaskRun: + type: object + properties: + id: + type: string + format: uuid + task_id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + state: + $ref: '#/components/schemas/ExecutionState' + reason: + type: string + nullable: true + progress: + type: integer + format: int32 + nullable: true + total_time: + type: integer + format: int32 + nullable: true + logs_paths: + type: array + items: + type: string + logs_storage: + type: string + nullable: true + logs_last_gathered: + type: string + format: date-time + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + Workflow: + type: object + properties: + id: + type: string + format: uuid + company_id: + type: integer + state: + $ref: '#/components/schemas/CompletenessState' + name: + type: string + valid_until: + type: string + format: date-time + document_id: + type: integer + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + task_runs: + type: array + items: + $ref: '#/components/schemas/TaskRun' + + CreateWorkflowRequest: + type: object + required: [name, valid_until] + properties: + name: + type: string + valid_until: + type: string + format: date-time + document_id: + type: integer + nullable: true + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + + RestartTaskRequest: + type: object + properties: + inputs: + type: object + additionalProperties: + $ref: '#/components/schemas/Description' + outputs: + type: object + additionalProperties: + $ref: '#/components/schemas/Description' + parameters: + type: object + additionalProperties: true + valid_until: + type: string + format: date-time + + GetBatchWorkflowsRequest: + type: object + required: [workflows_ids] + properties: + workflows_ids: + type: array + items: + type: string + format: uuid + + WorkflowsBatchResponse: + type: object + properties: + workflows: + type: array + items: + $ref: '#/components/schemas/Workflow' + + WorkflowsResponse: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/Workflow' + limit: + type: integer + offset: + type: integer + total: + type: integer diff --git a/apps/processing/workflows-engine.CONFIGURATION.md b/apps/processing/workflows-engine.CONFIGURATION.md new file mode 100644 index 0000000..a9556a1 --- /dev/null +++ b/apps/processing/workflows-engine.CONFIGURATION.md @@ -0,0 +1,363 @@ +# Конфигурация проекта workflows-engine + +`workflows-engine` (внутреннее имя образа — `kubernetes-engine`) — это фоновый сервис-оркестратор (демон без REST API), который вычитывает workflow/задачи из PostgreSQL и запускает их либо как Job'ы в Kubernetes, либо публикует в RabbitMQ (AMQP-исполнитель). Конфигурируется исключительно через переменные окружения. + +## Способы конфигурирования + +- **Библиотека парсинга:** [`github.com/ilyakaznacheev/cleanenv`](https://github.com/ilyakaznacheev/cleanenv). +- **Точка входа конфигурации:** `config/config.go`, функция `config.New()` вызывает `cleanenv.ReadEnv(cfg)` и заполняет структуру `Config` из переменных окружения процесса. +- **Формат тегов:** каждое поле помечено тегом `env:"ИМЯ"`; значение по умолчанию задаётся тегом `env-default:"..."`. Префикса имён переменных нет. +- **Составная структура `Config`** собирается из встроенных (embedded) структур: + - `App`, `Log`, `Executor`, `Kubernetes`, `Resources`, `Storages`, `Pooling`, `Workflows`, `Security` — объявлены в `config/config.go`; + - `pgxconnection.Postgres` — объявлена в `pkg/pgxconnection/postgres.go` (переменные `POSTGRES_*`); + - `rabbitmq.RabbitMQ` — объявлена в `pkg/rabbitmq/config.go` (переменные `RABBITMQ_*`). +- **Кастомный парсинг:** поле `WORKFLOW_PRIORITY` имеет тип `entity.WorkflowPriority` с методом `SetValue` (`internal/entity/workflow.go`); допустимые значения: `default`, `high`, `low` (регистр не важен). +- **Важно:** часть переменных из `config.env` и Helm-чарта **не читается** структурой конфигурации. Они либо читаются напрямую через `os.Getenv(...)` в `pkg/kube_services/services.go` и **пробрасываются в env запускаемых Job-подов** (а не потребляются самим engine), либо не используются кодом вовсе (см. раздел «Замечания и потенциальные проблемы»). + +### Режимы запуска / исполнители + +Бинарь один (`cmd/engine`), «режим» определяется набором включённых исполнителей и приоритетом. + +| Режим | Как включается | Назначение | +| --- | --- | --- | +| Kubernetes-исполнитель | `ENABLE_KUBERNETES_EXECUTOR=1` (по умолчанию `true`) | Запуск задач как `batchv1.Job` в кластере Kubernetes | +| AMQP-исполнитель | `ENABLE_AMQP_EXECUTOR=1` (по умолчанию `false`) | Публикация задач в RabbitMQ и приём результатов | +| backend (обычный приоритет) | Helm-деплой `backend`, `WORKFLOW_PRIORITY=low` | Обработка обычных workflow | +| backend-high-priority | Helm-деплой `backend-high-priority`, `WORKFLOW_PRIORITY=high` | Обработка высокоприоритетных workflow | + +Оба Helm-деплоя разворачивают один и тот же образ; отличаются только значением `WORKFLOW_PRIORITY` (и `MAX_WORKFLOWS_LIMIT`). + +### Точки входа (entrypoints) + +| Путь | Тип | Описание | +| --- | --- | --- | +| `cmd/engine/main.go` | main-пакет | Единственная точка входа. Поднимает pprof-сервер на `:8081`, читает конфиг, создаёт `engine.New(cfg, logger)` и вызывает `Run()` | +| `internal/apps/engine/engine.go` | приложение | Основная логика: подключение к Postgres, инициализация исполнителей (K8s/AMQP), pooling-контроллер и оркестратор, graceful shutdown по SIGINT | + +### Запуск контейнера + +- **Dockerfile:** multi-stage сборка на `golang:1.20-buster`, финальный образ `FROM scratch`. +- **CMD/ENTRYPOINT:** `ENTRYPOINT ["/engine"]` — запускается собранный бинарь напрямую, без shell-скрипта/entrypoint-обёртки. +- Внутри процесса дополнительно поднимается HTTP-сервер `net/http/pprof` на порту `:8081` (профилирование). Полноценного REST API нет (см. раздел про `API_ADDRESS`). + +## Переменные приложения + +### Приложение и логирование (`App`, `Log`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `APP_NAME` | string | `kubernetes-engine` | Имя приложения (в Helm переопределяется на `kubernetes-engine`/`kubernetes-engine-high-priority`) | +| `APP_VERSION` | string | `v1` | Версия приложения | +| `LOG_LEVEL` | string | `info` | Уровень логирования | + +### PostgreSQL (`pkg/pgxconnection`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | `localhost` | Хост БД | +| `POSTGRES_DB` | string | `processing_db` | Имя базы данных | +| `POSTGRES_USER` | string | `user` | Пользователь БД | +| `POSTGRES_PASSWORD` | string | `password` | Пароль БД | +| `POSTGRES_PORT` | string | `5432` | Порт БД | +| `POSTGRES_POOL_SIZE` | int | `20` | Размер пула соединений | +| `ENABLE_SQL_QUERY` | bool | `true` | Логирование SQL-запросов | +| `YC-PG-CERTIFICATE` | string | `""` | TLS-сертификат CA для подключения к БД (значение сертификата) | +| `POSTGRES_SSL_USE` | bool | `false` | Использовать SSL при подключении к БД | + +### RabbitMQ (`pkg/rabbitmq`) — используется только при `ENABLE_AMQP_EXECUTOR=1` + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `RABBITMQ_HOST` | string | — | Хост брокера | +| `RABBITMQ_PORT` | string | — | Порт брокера | +| `RABBITMQ_USER` | string | — | Пользователь | +| `RABBITMQ_PASS` | string | — | Пароль | +| `RABBITMQ_VHOST` | string | — | Виртуальный хост | +| `RABBITMQ_USE_SSL` | bool | — | Использовать TLS | +| `RABBITMQ_CERTIFICATE` | string | — | TLS-сертификат | +| `RABBITMQ_CREATE_EXCHANGE` | string | — | Exchange для отправки задач на запуск | +| `RABBITMQ_CANCEL_EXCHANGE` | string | — | Exchange для отмены задач | +| `RABBITMQ_CREATE_ROUTING_KEY` | string | — | Routing key для запуска | +| `RABBITMQ_CANCEL_TOPIC` | string | — | Topic/ключ отмены | +| `RABBITMQ_COMPLETENESS_EXCHANGE` | string | — | Exchange для получения результатов выполнения | +| `RABBITMQ_COMPLETENESS_TOPIC` | string | — | Topic результатов выполнения | + +> Примечание: в `config.env` присутствуют также `RABBITMQ_CREATE_TOPIC` — переменной `RABBITMQ_CREATE_TOPIC` в коде нет (в структуре есть только `CreateRoutingKey`). + +### Исполнители и воркеры (`Executor`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `COUNT_RUNNING_WORKERS` | int | — (0) | Кол-во воркеров запуска задач | +| `COUNT_CANCELING_WORKERS` | int | — (0) | Кол-во воркеров отмены задач | +| `COUNT_HANDLE_JOB_WORKERS` | int | — (0) | Кол-во воркеров обработки Job'ов | +| `ENABLE_AMQP_EXECUTOR` | bool | `false` | Включить AMQP-исполнитель (RabbitMQ) | +| `ENABLE_KUBERNETES_EXECUTOR` | bool | `true` | Включить Kubernetes-исполнитель | + +### Kubernetes (`Kubernetes`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JOBS_NAMESPACE` | string | — | Namespace, в котором создаются Job'ы задач | +| `KUBE_CONFIG` | string | — | Путь к kubeconfig. Если **пусто** — используется in-cluster конфигурация; если задан — out-of-cluster | +| `KUBE_CONTEXT` | string | — | Контекст kubeconfig (для out-of-cluster) | +| `KUBE_ADDR` | string | — | Адрес API-сервера кластера (для out-of-cluster; используется с `InsecureSkipTLSVerify`) | + +### Ресурсы и планирование подов (`Resources`) + +Управляют NodeSelector/Tolerations/requests для запускаемых Job'ов в зависимости от `service_requests` задачи (см. `README.md`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `CPU_COUNT` | int | — | CPU requests для `high-resources`/`persistent` | +| `MEMORY_GI` | int | — | Memory (Gi) requests для `high-resources`/`persistent` | +| `CPU_COUNT_LOW_RESOURCES` | int | — | CPU для `low-resources` | +| `MEMORY_GI_LOW_RESOURCES` | int | — | Memory (Gi) для `low-resources` | +| `CPU_COUNT_HIGH_MEM` | int | — | CPU для `super-high-resources` | +| `MEMORY_GI_HIGH_MEM` | int | — | Memory (Gi) для `super-high-resources` | +| `ENABLE_TOLERATION` | bool | — | Включить набор сервисов ресурсов (toleration/nodeselector) | +| `TOLERATION_KEY` | string | — | Ключ toleration/nodeselector для high/low ресурсов | +| `TOLERATION_VALUE` | string | — | Значение toleration/nodeselector для high/low ресурсов | +| `TOLERATION_KEY_HIGH_MEM` | string | — | Ключ toleration для `super-high-resources` | +| `TOLERATION_VALUE_HIGH_MEM` | string | — | Значение toleration для `super-high-resources` | +| `TOLERATION_KEY_PERSISTENT` | string | — | Ключ toleration для `persistent` | +| `TOLERATION_VALUE_PERSISTENT` | string | — | Значение toleration для `persistent` | +| `DJANGO_BASIC_AUTH` | string | — | Базовая авторизация Django (поле присутствует в конфиге, но не используется в бизнес-логике; в под задачи прокидывается хардкод-путь `DJANGO_BASIC_AUTH_PATH`) | +| `DEFAULT_TOLERATION_KEY` | string | — | Ключ toleration по умолчанию для всех подов | +| `DEFAULT_TOLERATION_VALUE` | string | — | Значение toleration по умолчанию | +| `DEFAULT_NODE_SELECTOR_KEY` | string | — | Ключ NodeSelector по умолчанию | +| `DEFAULT_NODE_SELECTOR_VALUE` | string | — | Значение NodeSelector по умолчанию | +| `DEFAULT_IMAGE_PULL_POLICY` | string | `always` | ImagePullPolicy для подов задач | +| `DEFAULT_CPU_REQUESTS` | string | `100m` | CPU requests по умолчанию | +| `DEFAULT_MEMORY_REQUESTS` | string | `64Mi` | Memory requests по умолчанию | + +### Хранилища и внешние сервисы (`Storages`) + +Флаги `ENABLE_*` включают соответствующий «сервис» (набор env/секретов/volume), который добавляется в под задачи в зависимости от `service_requests`. URL'ы прокидываются в поды mesh/pdm-сервисов. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `S3_SERVICE_ACCOUNT` | string | — | Путь к service account JSON для S3 (в коде для подов путь по факту хардкодится) | +| `ENABLE_S3_STORAGE` | bool | — | Включить сервис `s3` | +| `ENABLE_S3V2_STORAGE` | bool | — | Включить сервис `s3v2` | +| `ENABLE_PDM_STORAGE` | bool | — | Включить сервис `pdm` | +| `ENABLE_URL_STORAGE` | bool | — | Включить сервис `url` | +| `ENABLE_LOCAL` | bool | — | Включить локальное хранилище | +| `ENABLE_BIM_API_DB` | bool | — | Включить сервис `bim_api_db` | +| `ENABLE_BIM_API_CH` | bool | — | Включить сервис `bim_api_ch` (ClickHouse) | +| `ENABLE_BIM_API_V2_DB` | bool | — | Включить сервисы `bim_api_v2_db` (+ db_2/3/4) | +| `ENABLE_PDM_API_DB` | bool | — | Включить сервис `pdm_api_db` | +| `ENABLE_COMPARISONS_API_DB` | bool | — | Включить сервис `comparison-api-db` | +| `ENABLE_ISSUE_API_DB` | bool | — | Включить сервис `issues-api-db` | +| `ENABLE_RESOURCES_API` | bool | — | Включить сервис `resources-api` | +| `ENABLE_MAIL_GUN` | bool | — | Включить сервис `mailgun` | +| `ENABLE_SMTP` | bool | — | Включить сервис `smtp` | +| `ENABLE_WORKSPACE_API_DB` | bool | — | Включить сервис `workspace_api_db` | +| `ENABLE_CROSS_SECTION_API_DB` | bool | — | Включить сервис `cross-section-api-db` | +| `BIM_API_DEBUG` | bool | — | Флаг debug для BIM API (также пробрасывается в под, см. ниже) | +| `COMPARISONS_API_DEBUG` | string | — | Флаг debug для Comparisons API (также пробрасывается в под) | +| `INTERNAL_PDM_URL` | string | — | Внутренний URL PDM | +| `EXTERNAL_PDM_URL` | string | — | Внешний URL PDM | +| `INTERNAL_FILESTREAM_URL` | string | — | Внутренний URL filestream | +| `EXTERNAL_FILESTREAM_URL` | string | — | Внешний URL filestream | +| `INTERNAL_REMARK_URL` | string | — | Внутренний URL remarks | +| `EXTERNAL_REMARK_URL` | string | — | Внешний URL remarks | +| `DJANGO_HOST` | string | — | URL Django-хоста (ЛК) | +| `INTERNAL_WORKSPACE_URL` | string | — | Внутренний URL workspaces | +| `EXTERNAL_WORKSPACE_URL` | string | — | Внешний URL workspaces | + +### Пулинг (`Pooling`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MAX_WORKFLOWS_LIMIT` | uint64 | — (0) | Лимит одновременно обрабатываемых workflow при выборке | +| `MAX_RUNNING_JOBS` | int64 | `200` | Максимум одновременно выполняющихся Job'ов | + +### Workflow (`Workflows`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DEFAULT_GET_SINCE_LAST_HOURS` | uint64 | `168` | Глубина выборки задач в часах (нужно для FIFO; 168 ч = 7 дней) | +| `WORKFLOW_PRIORITY` | enum (`default`/`high`/`low`) | `default` | Приоритет обрабатываемых workflow (задаёт «роль» инстанса) | + +### Безопасность (`Security`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `ENABLE_RUNASUSER` | bool | `false` | Запускать контейнеры задач под фиксированным UID | +| `USER_RUNASUSER` | int64 | `1000` | UID для `runAsUser` | + +### Переменные, читаемые через `os.Getenv` и пробрасываемые в под'ы задач + +Эти переменные **не входят** в структуру `Config` и **не потребляются** самим engine. Они читаются напрямую в `pkg/kube_services/services.go` и добавляются как env в контейнер запускаемой задачи (Job), когда включён соответствующий `ENABLE_*`-флаг. Тип — всегда строка (передаётся «как есть»). + +| Переменная | Куда прокидывается (условие) | Назначение | +| --- | --- | --- | +| `BIM_API_DB` | сервис `bim_api_db` (при `ENABLE_BIM_API_DB`) | Путь к JSON-конфигу БД BIM API | +| `BIM_API_DEBUG` | сервис `bim_api_db` | Флаг debug | +| `BIM_API_CH` | сервис `bim_api_ch` (при `ENABLE_BIM_API_CH`) | Путь к JSON-конфигу ClickHouse | +| `BIM_API_CH_DEBUG` | сервис `bim_api_ch` | Флаг debug | +| `BIM_API_V2_DB` | сервис `bim_api_v2_db` (при `ENABLE_BIM_API_V2_DB`) — в под прокидывается под именем `BIM_API_DB` | Путь к JSON-конфигу БД BIM API v2 | +| `BIM_API_V2_DEBUG` | сервис `bim_api_v2_db` | Флаг debug | +| `BIM_API_V2_DB_2` | сервис `bim_api_v2_db_2` — в под как `BIM_API_DB_2` | Путь к JSON-конфигу 2-й БД BIM v2 | +| `BIM_API_V2_DB_3` | сервис `bim_api_v2_db_3` — в под как `BIM_API_DB_3` | Путь к JSON-конфигу 3-й БД BIM v2 | +| `BIM_API_V2_DB_4` | сервис `bim_api_v2_db_4` — в под как `BIM_API_DB_4` | Путь к JSON-конфигу 4-й БД BIM v2 | +| `COMPARISONS_API_DB` | сервис `comparison-api-db` (при `ENABLE_COMPARISONS_API_DB`) | Путь к JSON-конфигу БД Comparisons | +| `COMPARISONS_API_DEBUG` | сервис `comparison-api-db` | Флаг debug | +| `ISSUE_API_DB` | сервис `issues-api-db` (при `ENABLE_ISSUE_API_DB`) | Путь к JSON-конфигу БД Issues | +| `ISSUE_API_DEBUG` | сервис `issues-api-db` | Флаг debug | +| `RESOURCES_API_INTERNAL_HOST` | сервис `resources-api` (при `ENABLE_RESOURCES_API`) | Внутренний хост Resources API | +| `SMTP` | сервис `smtp` (при `ENABLE_SMTP`) — в под как `SMTP_CONFIG_PATH` | Путь к JSON-конфигу SMTP | +| `PDM_API_DB` | сервис `pdm_api_db` (при `ENABLE_PDM_API_DB`) | Путь к JSON-конфигу БД PDM | +| `PDM_API_DEBUG` | сервис `pdm_api_db` | Флаг debug | +| `WORKSPACE_API_DB` | сервис `workspace_api_db` (при `ENABLE_WORKSPACE_API_DB`) | Путь к JSON-конфигу БД Workspace | +| `WORKSPACE_API_DEBUG` | сервис `workspace_api_db` | Флаг debug | +| `CROSS_SECTION_API_DB` | сервис `cross-section-api-db` (при `ENABLE_CROSS_SECTION_API_DB`) | Путь к JSON-конфигу БД Cross-section | +| `CROSS_SECTION_API_DEBUG` | сервис `cross-section-api-db` | Флаг debug | +| `WORKFLOWS_SENTRY_DSN` | сервис `Default` (всегда) | Sentry DSN для подов задач | +| `WORKFLOWS_SENTRY_DEBUG` | сервис `Default` (всегда) | Флаг debug Sentry | +| `ENVIRONMENT` | сервис `Default` (всегда) | Имя окружения для подов задач | + +> Пути `BIM_API_V2_DB_2/3/4`, `MAILGUN`, `SMTP` и т.п. задаются в Helm-чарте (`.helm/values.yaml`) — в `config.env` присутствуют не все из них. + +## Переменные инфраструктуры/сборки + +| Переменная | Где | Назначение | +| --- | --- | --- | +| `CGO_ENABLED=0`, `GOOS=linux`, `GOARCH=amd64` | Dockerfile (build stage) | Статическая сборка бинаря под Linux/amd64 | +| `CI_COMMIT_SHORT_SHA` | `.gitlab-ci.yml` → build-arg | Прокидывается в сборку образа | +| `SERVICE_NAME=workflows-engine` | `.gitlab-ci.yml` | Имя сервиса/чарта в CI | +| `DOCKERFILE_PATH=Dockerfile` | `.gitlab-ci.yml` | Путь к Dockerfile | +| `BUILD_ARGS` | `.gitlab-ci.yml` | Аргументы сборки образа | +| `CI_TRIGGER_SOURCE=app` | `.gitlab-ci.yml` | Источник триггера пайплайна | +| `IMAGE_NAME`, `CI_COMMIT_SHA`, `CI_PROJECT_URL`, `CI_JOB_URL`, `CI_PROJECT_NAMESPACE` | `.gitlab-ci.yml` → `HELM_SET_ARGS` | Метаданные деплоя, пробрасываются в universal-chart | + +Сборка/деплой наследуются из внешних CI-шаблонов проекта `generic/common-ci` (`universal-pipeline.yaml`, `common-security-scan.yaml`). Юнит-тесты: стадия `test`, образ `golang:1.20`, команда `make unit-tests`. + +## Переменные из Helm-чарта + +Чарт (`.helm/values.yaml`) построен на `universal-chart`; секреты монтируются как env (`secretEnvs`) и как volume'ы. Ниже — маппинг секретных env (общий для `backend` и `backend-high-priority`). Имя секрета зависит от окружения (`_default` / `stage` / `preprod` / `production`). + +| Переменная | Секрет (secret_name) | Ключ (secret_key) | +| --- | --- | --- | +| `POSTGRES_ADDRESS` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `host` | +| `POSTGRES_PORT` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `port` | +| `POSTGRES_DB` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `database` | +| `POSTGRES_USER` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `username` | +| `POSTGRES_PASSWORD` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `password` | +| `RABBITMQ_USER` | `rabbitmq-secret` | `username` | +| `RABBITMQ_PASS` | `rabbitmq-secret` | `password` | +| `YC-PG-CERTIFICATE` | `yc-pg-certificate` (stage: `processing-postgresql-secret`) | `certificate` (stage: `ca.crt`, preprod/prod: `certificate`) | + +Секреты, монтируемые как volume'ы (файлы JSON-конфигов внешних БД и т.п.): + +| Volume / секрет | mountPath | +| --- | --- | +| `yc-s3` | `/etc/sarex/yc-s3` | +| `bim-api-db` | `/etc/sarex` | +| `bim-api-v2-db-2` | `/etc/sarex/second-bim-bd` | +| `bim-api-v2-db-3` | `/etc/sarex/third-bim-bd` | +| `bim-api-v2-db-4` | `/etc/sarex/fourth-bim-bd` | +| `comparison-api-db` | `/etc/comparisons` | +| `pdm-api-db` | `/etc/pdm` | +| `ws-api-db` | `/etc/ws` | +| `mailgun-secret` | `/etc/mailgun-secret` | +| `smtp-secret` | `/etc/smtp-secret` | +| `issues-api-db` | `/etc/issues` | +| `cross-section-api-db` | `/etc/cross_section` | +| `tmp-volume` (emptyDir) | `/tmp` | + +Прочие значимые переменные окружения задаются в `.helm/values.yaml` секцией `envs` для каждого сервиса (значения зависят от окружения `_default/stage/preprod/production`) — в т.ч. пути к JSON-конфигам (`BIM_API_DB`, `PDM_API_DB`, …), URL'ы сервисов, `POD_NAME=$(K8S_POD_NAME)`, `WORKFLOW_PRIORITY` (`low` для `backend`, `high` для `backend-high-priority`). Также в чарте описан `meshConfig` (Istio) — включён только для production. + +## Переменные в CI + +Определяются в `workflow.rules` файла `.gitlab-ci.yml` по ветке/тегу: + +| Триггер | STAND | NAMESPACE | universal-chart env | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | --- | +| ветка `stage` | `stage` | `platform` | `stage` | `0.0.1-stage` | `universal-chart-stage` | +| ветка `master` | `preprod` | `processing-preprod` | `preprod` | `0.0.1-preprod` | `universal-chart-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `processing-prod` | `production` | `0.0.1-prod` | `universal-chart-production` | +| Merge Request | — (сборка образа выключена: `ENABLE_BUILD_IMAGE=false`) | — | — | — | — | + +`RELEASE_NAME` во всех случаях — `workflows-engine`. Для каждого стенда деплоятся оба сервиса: `backend` и `backend-high-priority`. + +## Замечания и потенциальные проблемы + +1. **Нет HTTP REST API, но `API_ADDRESS` присутствует.** Переменная `API_ADDRESS=0.0.0.0:8080` есть в `config.env` и в Helm (`envs`), но **не читается ни одной строкой Go-кода**. Реальный HTTP-сервер — только `net/http/pprof` на `:8081` (`cmd/engine/main.go`). В Helm у сервисов `service.enabled: false`, liveness/readiness-пробы выключены. Переменную стоит либо удалить, либо реализовать сервер. По этой причине `openapi.yaml` для сервиса не создаётся. +2. **Дубликаты в `config.env`:** + - `ENABLE_BIM_API_V2_DB=1` указана дважды (строки 18 и 19); + - `JOBS_NAMESPACE` задаётся дважды с разными значениями: `processing-stage` и `processing-testing` — побеждает последнее; + - `POD_NAME` задаётся дважды: `fieldRef(v1:metadata.name)` и `workflows-backend-869584d795-b7p2b` — второе значение это «замороженное» имя конкретного пода, что явно ошибочно для шаблона. +3. **Хардкод локальных путей разработчика в `config.env`:** + - `S3_SERVICE_ACCOUNT=/Users/khannanov/sarex/yc-s3-service-account.json`; + - `KUBE_CONFIG=/Users/khannanov/.kube/config`. + Эти пути специфичны для машины конкретного разработчика и не должны попадать в общий конфиг (в Helm `S3_SERVICE_ACCOUNT` корректно указывает на `/etc/sarex/yc-s3/...`). +4. **Переменные из `config.env`/Helm, которые не читаются кодом `engine` вообще** (кандидаты на удаление либо потребляются исключительно другими компонентами): `API_ADDRESS`, `CONTROL_PLANE_PERIOD`, `ENABLE_GOOGLE_STORAGE`, `GOOGLE_STORAGE_BUCKET`, `GOOGLE_STORAGE_PROJECT`, `ENABLE_SRX_TMP`, `MAX_TASKS_IN_PERIOD`, `ENABLE_PDM_DB`, `POD_NAME`. Ни `Getenv`, ни struct-тега для них нет. +5. **`config.env` частично устарел относительно `config/config.go` и Helm.** Ряд переменных, реально используемых кодом/чартом, в `config.env` отсутствует (`WORKFLOW_PRIORITY`, `DEFAULT_TOLERATION_*`, `DEFAULT_NODE_SELECTOR_*`, `ENABLE_ISSUE_API_DB`, `ENABLE_RESOURCES_API`, `ENABLE_WORKSPACE_API_DB`, `ENABLE_CROSS_SECTION_API_DB`, `RESOURCES_API_INTERNAL_HOST`, `WORKFLOWS_SENTRY_DSN`, пути `*_API_DB` для issues/ws/cross_section и т.д.). Источником истины следует считать `config/config.go` + `.helm/values.yaml`, а не `config.env`. +6. **Расхождение `env-default` и реальных значений.** У ряда флагов (`ENABLE_S3_STORAGE`, `ENABLE_TOLERATION`, `CPU_COUNT`, воркеры и др.) в коде нет `env-default`, поэтому при отсутствии переменной поле останется нулевым (`0`/`false`/`""`) — сервис молча стартует с «пустой» конфигурацией планирования. Значения обязательно должны задаваться через окружение (Helm). +7. **`DJANGO_BASIC_AUTH` / `COMPARISONS_API_DEBUG` / `BIM_API_DEBUG`** объявлены в структуре конфига, но фактически в бизнес-логике engine не задействованы (debug-флаги повторно читаются через `os.Getenv` и прокидываются в поды; `DJANGO_BASIC_AUTH` не используется — в под задачи попадает хардкод-путь `DJANGO_BASIC_AUTH_PATH=/etc/sarex/django-auth.json`). +8. **`RABBITMQ_CREATE_TOPIC`** присутствует в `config.env`, но в конфиге RabbitMQ такого поля нет (используется `RABBITMQ_CREATE_ROUTING_KEY`) — переменная не читается. + +## Минимальный набор для локального запуска + +Для локального старта достаточно поднять PostgreSQL и указать доступ к нему; исполнители и внешние интеграции можно выключить. + +```env +# Логирование +LOG_LEVEL=debug + +# PostgreSQL (обязательно — engine сразу подключается к БД) +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=processing_db +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_POOL_SIZE=5 +POSTGRES_SSL_USE=false +ENABLE_SQL_QUERY=true + +# Исполнители: выключаем AMQP; для Kubernetes нужен доступ к кластеру +ENABLE_AMQP_EXECUTOR=0 +ENABLE_KUBERNETES_EXECUTOR=1 +KUBE_CONFIG=/path/to/your/.kube/config # если пусто — используется in-cluster +KUBE_CONTEXT=your-context +JOBS_NAMESPACE=processing-testing + +# Воркеры +COUNT_RUNNING_WORKERS=1 +COUNT_CANCELING_WORKERS=1 +COUNT_HANDLE_JOB_WORKERS=1 + +# Пулинг / выборка +MAX_WORKFLOWS_LIMIT=10 +MAX_RUNNING_JOBS=10 +DEFAULT_GET_SINCE_LAST_HOURS=168 +WORKFLOW_PRIORITY=default + +# Дефолты планирования подов (иначе будут пустыми) +DEFAULT_IMAGE_PULL_POLICY=IfNotPresent +DEFAULT_CPU_REQUESTS=100m +DEFAULT_MEMORY_REQUESTS=64Mi + +# Хранилища/внешние БД можно отключить для минимального запуска +ENABLE_S3_STORAGE=0 +ENABLE_S3V2_STORAGE=0 +ENABLE_PDM_STORAGE=0 +ENABLE_URL_STORAGE=0 +ENABLE_LOCAL=0 +ENABLE_BIM_API_DB=0 +ENABLE_BIM_API_CH=0 +ENABLE_BIM_API_V2_DB=0 +ENABLE_PDM_API_DB=0 +ENABLE_COMPARISONS_API_DB=0 +ENABLE_ISSUE_API_DB=0 +ENABLE_RESOURCES_API=0 +ENABLE_MAIL_GUN=0 +ENABLE_SMTP=0 +ENABLE_WORKSPACE_API_DB=0 +ENABLE_CROSS_SECTION_API_DB=0 +ENABLE_TOLERATION=0 +``` + +Запуск: `go run ./cmd/engine`. Если Kubernetes-исполнитель не нужен вовсе — установите `ENABLE_KUBERNETES_EXECUTOR=0` (тогда `KUBE_*`/`JOBS_NAMESPACE` не требуются), но учтите, что без единого включённого исполнителя сервис не будет запускать задачи. Профилирование доступно на `http://localhost:8081/debug/pprof/`. diff --git a/apps/processing/workflows-engine.env.example b/apps/processing/workflows-engine.env.example new file mode 100644 index 0000000..da8daf2 --- /dev/null +++ b/apps/processing/workflows-engine.env.example @@ -0,0 +1,185 @@ +# ============================================================================ +# workflows-engine (kubernetes-engine) — пример переменных окружения +# Конфигурация читается через cleanenv (github.com/ilyakaznacheev/cleanenv) +# из переменных окружения. Префикса нет. Значения по умолчанию — из кода. +# Сервис — фоновый оркестратор без REST API (только pprof на :8081). +# ============================================================================ + +# ----- App / Logging ----- +APP_NAME=kubernetes-engine +APP_VERSION=v1 +# Уровень логирования: debug | info | warn | error +LOG_LEVEL=info + +# ----- Database (PostgreSQL) ----- +POSTGRES_ADDRESS=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=processing_db +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_POOL_SIZE=20 +# Включить TLS-подключение к БД +POSTGRES_SSL_USE=false +# CA-сертификат (PEM) для TLS-подключения к БД (значение сертификата) +YC-PG-CERTIFICATE= +# Логирование SQL-запросов +ENABLE_SQL_QUERY=true + +# ----- Executors / Workers ----- +# Включить AMQP-исполнитель (RabbitMQ). Требует секцию RabbitMQ ниже. +ENABLE_AMQP_EXECUTOR=0 +# Включить Kubernetes-исполнитель (запуск задач как Job'ов). По умолчанию true. +ENABLE_KUBERNETES_EXECUTOR=1 +COUNT_RUNNING_WORKERS=1 +COUNT_CANCELING_WORKERS=1 +COUNT_HANDLE_JOB_WORKERS=1 + +# ----- Kubernetes ----- +# Namespace, в котором создаются Job'ы задач +JOBS_NAMESPACE=processing-testing +# Путь к kubeconfig. Если ПУСТО — используется in-cluster конфигурация. +KUBE_CONFIG= +# Контекст kubeconfig (для out-of-cluster) +KUBE_CONTEXT= +# Адрес API-сервера кластера (для out-of-cluster, с InsecureSkipTLSVerify) +KUBE_ADDR= + +# ----- RabbitMQ (только при ENABLE_AMQP_EXECUTOR=1) ----- +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USER=sarex +RABBITMQ_PASS=sarex +RABBITMQ_VHOST=/ +RABBITMQ_USE_SSL=false +RABBITMQ_CERTIFICATE= +RABBITMQ_CREATE_EXCHANGE=autodesk.inputMessage +RABBITMQ_CANCEL_EXCHANGE=autodesk.cancelMessage +RABBITMQ_CREATE_ROUTING_KEY=converting +RABBITMQ_CANCEL_TOPIC=cancel +RABBITMQ_COMPLETENESS_EXCHANGE=autodesk.outputMessage +RABBITMQ_COMPLETENESS_TOPIC=output + +# ----- Pooling / Workflows ----- +# Лимит одновременно обрабатываемых workflow при выборке +MAX_WORKFLOWS_LIMIT=100 +# Максимум одновременно выполняющихся Job'ов +MAX_RUNNING_JOBS=200 +# Глубина выборки задач в часах (нужно для FIFO; 168 ч = 7 дней) +DEFAULT_GET_SINCE_LAST_HOURS=168 +# Приоритет обрабатываемых workflow: default | high | low (задаёт «роль» инстанса) +WORKFLOW_PRIORITY=default + +# ----- Планирование подов (NodeSelector / Tolerations / requests) ----- +# CPU/Memory для high-resources / persistent +CPU_COUNT=5 +MEMORY_GI=20 +# CPU/Memory для low-resources +CPU_COUNT_LOW_RESOURCES=2 +MEMORY_GI_LOW_RESOURCES=10 +# CPU/Memory для super-high-resources +CPU_COUNT_HIGH_MEM=10 +MEMORY_GI_HIGH_MEM=40 +# Включить набор сервисов ресурсов (toleration/nodeselector) +ENABLE_TOLERATION=1 +# Toleration/NodeSelector для high/low ресурсов +TOLERATION_KEY=dedicated +TOLERATION_VALUE=processing +# Toleration для super-high-resources +TOLERATION_KEY_HIGH_MEM=dedicated +TOLERATION_VALUE_HIGH_MEM=high-mem +# Toleration для persistent +TOLERATION_KEY_PERSISTENT= +TOLERATION_VALUE_PERSISTENT= +# Значения по умолчанию для всех подов +DEFAULT_TOLERATION_KEY= +DEFAULT_TOLERATION_VALUE= +DEFAULT_NODE_SELECTOR_KEY= +DEFAULT_NODE_SELECTOR_VALUE= +DEFAULT_IMAGE_PULL_POLICY=always +DEFAULT_CPU_REQUESTS=100m +DEFAULT_MEMORY_REQUESTS=64Mi + +# ----- Хранилища и внешние сервисы (флаги ENABLE_*) ----- +# Путь к service account JSON для S3 (в проде задаётся Helm'ом, +# напр. /etc/sarex/yc-s3/yc-s3-service-account.json) +S3_SERVICE_ACCOUNT= +ENABLE_S3_STORAGE=1 +ENABLE_S3V2_STORAGE=1 +ENABLE_PDM_STORAGE=1 +ENABLE_URL_STORAGE=1 +ENABLE_LOCAL=0 +ENABLE_BIM_API_DB=1 +ENABLE_BIM_API_CH=1 +ENABLE_BIM_API_V2_DB=1 +ENABLE_PDM_API_DB=1 +ENABLE_COMPARISONS_API_DB=1 +ENABLE_ISSUE_API_DB=0 +ENABLE_RESOURCES_API=0 +ENABLE_MAIL_GUN=1 +ENABLE_SMTP=0 +ENABLE_WORKSPACE_API_DB=0 +ENABLE_CROSS_SECTION_API_DB=0 +# Debug-флаги (также пробрасываются в под задачи) +BIM_API_DEBUG=0 +COMPARISONS_API_DEBUG=0 + +# ----- URL'ы внешних сервисов (internal/external) ----- +INTERNAL_PDM_URL=http://api-service.documentations-stage +EXTERNAL_PDM_URL=https://stage-api.sarex.io/documentations +INTERNAL_FILESTREAM_URL=http://filestream-service.documentations-stage +EXTERNAL_FILESTREAM_URL=https://stage-api.sarex.io/files +INTERNAL_REMARK_URL=http://remarks-service.remarks-stage +EXTERNAL_REMARK_URL=https://stage-api.sarex.io/remarks +INTERNAL_WORKSPACE_URL=http://workspaces-service.workspaces-stage +EXTERNAL_WORKSPACE_URL=https://stage-api.sarex.io/workspaces +DJANGO_HOST=https://stage.sarex.io + +# ----- Security ----- +# Запускать контейнеры задач под фиксированным UID +ENABLE_RUNASUSER=false +USER_RUNASUSER=1000 + +# ============================================================================ +# Переменные ниже читаются НЕ конфигом engine, а напрямую через os.Getenv +# (pkg/kube_services/services.go) и пробрасываются в env запускаемых Job-подов +# при включённом соответствующем ENABLE_*-флаге. Значения — пути к JSON-конфигам +# внутри контейнера (в проде монтируются Helm'ом как секреты-volume). +# ============================================================================ +BIM_API_DB=/etc/sarex/bim-api-db-stage.json +BIM_API_CH=/etc/sarex/bim-api-ch.json +BIM_API_CH_DEBUG=0 +BIM_API_V2_DB=/etc/sarex/bim-api-v2-db-stage.json +BIM_API_V2_DEBUG=0 +BIM_API_V2_DB_2=/etc/sarex/second-bim-bd/db.json +BIM_API_V2_DB_3=/etc/sarex/third-bim-bd/db.json +BIM_API_V2_DB_4=/etc/sarex/fourth-bim-bd/db.json +COMPARISONS_API_DB=/etc/comparisons/comparisons-db-stage.json +ISSUE_API_DB=/etc/issues/issues-db.json +ISSUE_API_DEBUG=0 +RESOURCES_API_INTERNAL_HOST= +SMTP=/etc/smtp-secret/env.json +PDM_API_DB=/etc/pdm/pdm-api-db-stage.json +PDM_API_DEBUG=0 +WORKSPACE_API_DB=/etc/ws/ws-api-db.json +WORKSPACE_API_DEBUG=0 +CROSS_SECTION_API_DB=/etc/cross_section/db.json +CROSS_SECTION_API_DEBUG=0 +# Прокидываются в под всегда (сервис Default) +WORKFLOWS_SENTRY_DSN= +WORKFLOWS_SENTRY_DEBUG=0 +ENVIRONMENT=stage +# Mailgun-конфиг (при ENABLE_MAIL_GUN) +MAILGUN=/etc/mailgun-secret/env.json + +# ============================================================================ +# Переменные ниже присутствуют в config.env / Helm, но кодом engine НЕ читаются +# (легаси / задел). Оставлены для справки, включать не обязательно: +# API_ADDRESS — REST API отсутствует, реальный HTTP только pprof на :8081 +# CONTROL_PLANE_PERIOD +# ENABLE_GOOGLE_STORAGE, GOOGLE_STORAGE_BUCKET, GOOGLE_STORAGE_PROJECT +# ENABLE_SRX_TMP +# MAX_TASKS_IN_PERIOD +# ENABLE_PDM_DB +# POD_NAME +# RABBITMQ_CREATE_TOPIC — в конфиге нет такого поля (см. RABBITMQ_CREATE_ROUTING_KEY) +# ============================================================================ diff --git a/apps/processing/workflows-frontend.ENDPOINTS.md b/apps/processing/workflows-frontend.ENDPOINTS.md new file mode 100644 index 0000000..b5af211 --- /dev/null +++ b/apps/processing/workflows-frontend.ENDPOINTS.md @@ -0,0 +1,48 @@ +# Эндпоинты, с которыми взаимодействует workflows-frontend + +Микрофронтенд `workflows-frontend` обращается к внешним HTTP-сервисам через тонкий слой API, построенный поверх SDK `@sarex-team/sdk-js`. В отличие от `transmittal-frontend`, здесь нет реестра-объекта `endpoints` с полями `service/method/path/body/cache`. Вместо этого каждый вызов оформлен отдельной функцией-обёрткой (`fetch*`), которая напрямую вызывает `httpService.getRequest` / `httpService.postRequest` с указанием сервиса, URL и параметров. Все явные запросы фронтенда идут в один сервис — `workflows`. Хосты `sarex` и `zitadel` объявлены в конфигурации, но используются самим SDK (аутентификация через `AuthProvider`), а не прикладным кодом модуля. + +## Как устроено взаимодействие + +- **Слой API-функций** — `module/shared/api/fetch/workflows.api.ts`. Здесь объявлены все обёртки над HTTP-запросами. Каждая функция принимает типизированные аргументы (`id`, `taskId`, `taskRunId`, `workflowId`, `ids`, пагинация, фильтры) и возвращает промис от `httpService`. Ключевые поля запроса: + - `service` — логическое имя сервиса (везде `"workflows"`), по которому SDK выбирает базовый хост; + - `url` — путь эндпоинта, собирается через шаблонные строки с подстановкой параметров; + - `data` — тело POST-запроса (например, `updates` при рестарте задачи); + - `axiosConfig.params` — query-параметры (`limit`, `offset`, `company_ids`). +- **Единая точка запроса** — `httpService`, создаётся в `module/shared/api/http-service.ts` через `createHttpService({ axiosConfig, config, buildEnv, hosts })` из `@sarex-team/sdk-js`. Методы `getRequest`/`postRequest` этого сервиса — единственный способ выполнить внешний вызов. Под капотом SDK использует axios. +- **Выбор окружения и хоста** — окружение берётся из глобальной константы сборки `__BUILD_ENV__` (`const endpoint = (__BUILD_ENV__ as TypeEnvironment) || "prod"`). Для `local` дополнительно включается режим `setTypeOfHttpService("original")`. SDK по имени `service` и текущему `buildEnv` находит базовый хост в объекте `hosts` (`module/shared/api/hosts.ts`) и подставляет его перед `url`. +- **Реэкспорт** — `module/shared/api/index.ts` реэкспортирует весь набор функций как `workflowsApi` и сам `httpService`. +- **Использование** — вызовы происходят из MobX-стора (`module/workflows/store/*.ts`: `workflows.ts`, `workflow.ts`, `task.ts`, `taskRun.ts`, `watchList.ts`). +- **Обработка ошибок** — отдельного файла `errors.ts` в проекте нет; маппинг и перехват ошибок HTTP выполняется внутри SDK `@sarex-team/sdk-js` и обрабатывается в сторах через try/catch с отображением состояний ошибки (`module/components/States/ErrorState.tsx`). Аутентификация и редиректы на страницу логина реализованы в `module/Auth/AuthProvider.tsx` через компонент `AuthProvider` из SDK. + +## Базовые хосты по сервисам и окружениям + +Источник: `module/shared/api/hosts.ts`. В коде определены окружения `local`, `stage`, `prod`, `preprod` и `cps` (закрытый контур Газпрома). Прикладной код обращается только к сервису `workflows`; хосты `sarex` и `zitadel` используются SDK для основного бэкенда и системы аутентификации (Zitadel). + +| Сервис (service) | Назначение | local | stage | prod | preprod | cps (контур Газпром) | +| --- | --- | --- | --- | --- | --- | --- | +| workflows | API оркестрации воркфлоу и задач | `/sarex-workflows` | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | `https://api.preprod.sarex.io/workflows` | `https://api.aeromonitoring.codm.gazprom.loc/workflows/` | +| sarex | Основной бэкенд Sarex (через SDK) | `/sarex-backend` | `/` | `/` | `/` | `https://aeromonitoring.codm.gazprom.loc/` | +| zitadel | Сервис аутентификации Zitadel (через SDK) | `/zitadel` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | `https://login.preprod.sarex.io` | — (не задан) | + +Примечание: в окружении `local` пути относительные (проксируются через dev-сервер), в `stage/prod/preprod` — абсолютные URL. Для контура `cps` сервис `zitadel` не объявлен. + +## Эндпоинты по сервисам + +### workflows — оркестрация воркфлоу и задач + +Все функции определены в `module/shared/api/fetch/workflows.api.ts`, `service: "workflows"`. + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| fetchGetWorkflows | GET | `/api/v1/workflows?limit={limit}&offset={offset}&company_ids={companyIds}` | Список воркфлоу с пагинацией и фильтром по компаниям (`company_ids` — id через запятую) | +| fetchGetWorkflowById | GET | `/api/v1/workflows/{id}` | Получить один воркфлоу по идентификатору | +| fetchGetWorkflowsStates | GET | `/api/v1/workflows/{ids}/state` | Получить состояния набора воркфлоу (`ids` — идентификаторы через запятую), используется для отслеживаемого списка | +| fetchGetWorkflowPosition | GET | `/api/v1/workflows/{workflowId}/position` | Позиция воркфлоу в очереди обработки | +| fetchPrioritizeWorkflow | POST | `/api/v1/workflows/{workflowId}/prioritize` | Повысить приоритет воркфлоу | +| fetchGetLogs | GET | `/api/v1/tasks-runs/{taskRunId}/logs` | Получить логи конкретного запуска задачи | +| fetchCancelTaskRun | POST | `/api/v1/tasks-runs/{taskRunId}/cancel` | Отменить запуск задачи | +| fetchRestartTask | POST | `/api/v1/tasks/{taskId}/restart` | Перезапустить задачу; тело запроса — объект `updates` (изменённые параметры/объекты/сервис-реквесты) | +| fetchMoveTaskToSuperHighResources | POST | `/api/v1/tasks/{taskId}/move_to_super_high_resources` | Перевести задачу на пул сверхвысоких ресурсов | + +> Примечание: фронтенд вызывает `GET /api/v1/tasks-runs/{taskRunId}/logs` (`fetchGetLogs`), однако в текущем коде `workflows-api` соответствующий обработчик отсутствует (см. `workflows-api.openapi.yaml`, раздел «Замечания»). Это расхождение стоит проверить: либо эндпоинт реализуется другим сервисом/ingress, либо один из репозиториев устарел. diff --git a/apps/projects/CONFIGURATION.md b/apps/projects/CONFIGURATION.md new file mode 100644 index 0000000..627c17f --- /dev/null +++ b/apps/projects/CONFIGURATION.md @@ -0,0 +1,133 @@ +# Конфигурация проекта projects-frontend + +Документ описывает, как конфигурируется микрофронтенд `projects-frontend`: переменные сборки, способы запуска, параметры Docker/nginx, Helm-чарта и CI. + +## Способы конфигурирования + +`projects-frontend` — это клиентский микрофронтенд (React 17 + MobX, сборка Webpack 5, Module Federation). У приложения **нет runtime-конфигурации и файла `.env`**: всё поведение, зависящее от окружения, определяется **на этапе сборки** одной переменной `BUILD_ENV`. + +Разбор выполняется в `configWebpack/config/build.config.ts` (`extractBuildOptions`): значение `process.env.BUILD_ENV` (одно из `local` / `stage` / `prod` / `preprod` / `contour`, по умолчанию `prod`) определяет режим сборки (`development`/`production`) и «endpoint». Полученный `endPoint` через `DefinePlugin` (`configWebpack/buildPlugins.ts`) подставляется в бандл как глобальная константа `__ENDPOINT__`: + +```js +new DefinePlugin({ __ENDPOINT__: JSON.stringify(endPoint) }) +``` + +Константа `__ENDPOINT__` используется в рантайме бандла для выбора: + +- карты хостов API — `src/shared/api/http-service.ts` (`const endpoint = (__ENDPOINT__ as TypeEnvironment) || "prod"`) поверх `src/shared/api/hosts.ts`; +- URL удалённого модуля timeline — `src/widgets/remote-timeline/ui/timelineProxy.tsx`; +- локального провайдера разработки — `src/app/bootstrap.tsx` (`__ENDPOINT__ === "local" ? :
`). + +Отдельного конфиг-файла (yaml/env) у приложения нет. + +## Переменные сборки и запуска + +| Переменная | Где используется | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `BUILD_ENV` | `webpack.config.ts`, `configWebpack/config/build.config.ts`, `Dockerfile` (build-arg) | `prod` | Целевое окружение сборки: `local`/`stage`/`prod`/`preprod`/`contour`. Определяет режим (`development`/`production`), карту хостов и URL удалённых модулей | +| `NPM_NEXUS_TOKEN` | `.npmrc`, `Dockerfile` (build-arg) | — | Токен доступа к приватному npm-реестру Nexus (`https://nexus.infra.sarex.io/repository/npm/`) для установки пакетов `@sarex-team/*` | +| `PORT` | `webpack.config.ts` | `9001` | Порт dev-сервера Webpack | + +Версия Node фиксирована в `.nvmrc` — `v16.0.0`. Приватный реестр и авторизация заданы в `.npmrc`: + +``` +@sarex-team:registry=https://nexus.infra.sarex.io/repository/npm/ +//nexus.infra.sarex.io/repository/npm/:_authToken=${NPM_NEXUS_TOKEN} +``` + +### npm-скрипты (`package.json`) + +| Команда | Действие | +| --- | --- | +| `npm run dev` | Локальная разработка: `BUILD_ENV=local webpack serve --config webpack.config.ts` | +| `npm run build-module` | Сборка модуля: `webpack --config webpack.config.ts` (окружение — из `BUILD_ENV`) | +| `npm run build:start` | Раздача собранного бандла: `serve -s ./dist -l 9001` | +| `npm run lint` | Форматирование Prettier: `npx prettier --write .` | + +## Сборка Webpack и Module Federation + +Точка входа — `src/app/index.ts` (`webpack.config.ts`). Для всех окружений, кроме `local`, добавляется `ModuleFederationPlugin`: + +- `name`: `srx_projects`; +- `filename`: `module/remoteEntry.js`; +- `exposes`: `./ProjectsPage` → `./src/app/App.tsx`; +- `shared` (singleton, `requiredVersion: false`): `react`, `react-dom`, `@material-ui/core`, `@sarex-team/sdk-js`. + +Плагины (`configWebpack/buildPlugins.ts`): `HtmlWebpackPlugin`, `DefinePlugin`. В режиме `local` дополнительно `ProgressPlugin`, `ForkTsCheckerWebpackPlugin`, `ReactRefreshWebpackPlugin`; вне `local` — `MiniCssExtractPlugin` (хеши в именах файлов). + +### Dev-сервер (`configWebpack/buildDevServer.ts`) + +HTTPS, порт `9001`, `historyApiFallback`, `hot`. Прокси на stage-окружение с `pathRewrite`: + +| Префикс | Target | +| --- | --- | +| `/sarex-backend` | `https://stage.sarex.io` | +| `/sarex-gateway` | `https://stage-api.sarex.io/gateway` | +| `/sarex-documentations` | `https://stage-api.sarex.io/documentations` | +| `/sarex-api` | `https://stage-api.sarex.io` | + +Заголовки CORS dev-сервера разрешают любой origin, методы `GET, POST, PUT, DELETE, PATCH, OPTIONS` и заголовки `X-Requested-With, content-type, Authorization`. + +## Docker + +Многоступенчатая сборка (`Dockerfile`): + +1. **Стадия сборки** (`node:16`): установка зависимостей (`npm i` с `NPM_NEXUS_TOKEN`), `npm run lint`, `BUILD_ENV=$BUILD_ENV npm run build-module` → `dist`. +2. **Стадия раздачи** (`nginx:1.19.6`): копирование `dist` в `/dist` и `nginx/nginx.conf` в `/etc/nginx/nginx.conf`. + +Build-args: `BUILD_ENV`, `NPM_NEXUS_TOKEN`. + +### nginx (`nginx/nginx.conf`) + +Статика раздаётся с `root /dist` на порту `80`. Особенности: + +- `location = /ping` → возвращает `200 {"result": "ok"}` (используется как liveness/readiness-проба); +- `location = /module/remoteEntry.js` → `Cache-Control: no-store, no-cache, must-revalidate…`, `expires off` (точка входа Module Federation не кешируется); +- `gzip on`, логи в `stdout`/`stderr`. + +## Helm-чарт (`.helm`) + +Чарт `projects-frontend` (`Chart.yaml`, `type: application`, `version: 0.1.0`, `appVersion: 1.16.0`) разворачивает статику как `Deployment` + `Service` (`templates/static.yaml`) и публикует её через Istio `VirtualService` (`templates/mesh-config.yaml`). + +Значения задаются в `values-.yaml` (блок `static`): + +| Параметр | `stage` | `preprod` | `production` | +| --- | --- | --- | --- | +| `static.host` | `stage-modules.sarex.io` | `modules.preprod.sarex.io` | `modules.sarex.io` | +| `static.replicas` | `1` | `2` | `2` | +| `static.path` | `/projects/static/` | `/projects/static/` | `/projects/static/` | +| `static.image` | `sarex/projects-frontend-static:latest` | то же | то же | +| `static.port` / `static.service_port` | `80` / `80` | `80` / `80` | `80` / `80` | +| `static.requests` | `memory: 100Mi`, `cpu: 100m` | то же | то же | +| `imagePullSecrets` | `dockerhub` | `dockerhub` | `dockerhub` | + +Общее для всех окружений: `static.name: projects-frontend-static`, `static.service_name: projects-frontend-static-service`, `static.version: stable`. Пробы `livenessProbe`/`readinessProbe` бьют в `/ping` (`templates/static.yaml`). + +`VirtualService` (`templates/mesh-config.yaml`): хост `static.host`, шлюз `gateway/modules-gateway`, матч по префиксу `static.path` (`/projects/static/`) с `rewrite` на `/`. Политика CORS: `allowOrigins` по регулярке `(https://.*\.sarex\.io)|(https://localhost:.*)`, `allowMethods: [GET, POST, PUT, PATCH, HEAD, DELETE]`, `allowHeaders: [Authorization, Content-Type]`, `maxAge: 24h`. + +## CI/CD (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | `BUILD_ENV` | +| --- | --- | --- | --- | +| ветка `master` | `preprod` | `projects-preprod` | `preprod` | +| ветка `stage` | `stage` | `projects-stage` | `stage` | +| тег (`CI_COMMIT_TAG`) | `prod` | `projects-prod` | `prod` | + +Ключевые переменные пайплайна (общие для всех окружений): `RELEASE_NAME`/`CHART_NAME` = `projects-frontend`, `IMAGE_PATH` = `static.image`, `HELM_SET_ARGS` = `--set static.image=${IMAGE_NAME}`, `BUILD_ARGS` = `--build-arg BUILD_ENV= --build-arg NPM_NEXUS_TOKEN=${NPM_NEXUS_TOKEN}`, `DOCKERFILE_PATH` = `Dockerfile`. Флаги этапов: `ENABLE_LINTER` (`false`), `ENABLE_BUILD_CHART`, `ENABLE_BUILD_IMAGE`, `ENABLE_STATE_UPDATE`, `ENABLE_DEPLOY` (`true`). `CHART_VERSION` задаётся как `0.0.1-`. Стадии: `linter → test → unittest → prebuild-secscan → build → state-update → deploy`. + +## Замечания и потенциальные проблемы + +- **Нет runtime-конфигурации.** Всё, что зависит от окружения, «зашивается» в бандл на этапе сборки через `BUILD_ENV`/`__ENDPOINT__`. Пересборка под другое окружение обязательна — переопределить хосты в рантайме нельзя. +- **Значение по умолчанию `BUILD_ENV=prod`.** Если переменная не задана при сборке, собирается production-вариант (`build.config.ts`, `webpack.config.ts`, `http-service.ts`). Для локальной разработки нужно явно `BUILD_ENV=local` (скрипт `npm run dev` это делает). +- **Окружение `contour` неполно сконфигурировано.** Оно перечислено в `build.config.ts` и типах, но в `src/shared/api/hosts.ts` карта хостов для `contour` отсутствует, а URL удалённого модуля timeline для `contour` пуст (`timelineProxy.tsx`). Сборка с `BUILD_ENV=contour` не сможет разрешить хосты API. +- **README не совпадает с `package.json`.** В `README.md` указаны `nvm use 16.0.0` и `npm run start:local`, однако скрипта `start:local` в `package.json` нет — локальный запуск выполняется командой `npm run dev`. `.nvmrc` фиксирует `v16.0.0`. +- **Версии чарта расходятся.** В `Chart.yaml` — `version: 0.1.0` / `appVersion: 1.16.0`, тогда как в CI `CHART_VERSION` задаётся как `0.0.1-`. Значения не синхронизированы. +- **Приватный реестр.** Установка зависимостей требует валидный `NPM_NEXUS_TOKEN` (`.npmrc`); без него `npm i` (в т.ч. в Docker-сборке) завершится ошибкой авторизации. + +## Минимальный набор для локального запуска + +1. `nvm use` (Node `v16.0.0` из `.nvmrc`). +2. Экспортировать `NPM_NEXUS_TOKEN` (доступ к Nexus) и выполнить `npm i`. +3. `npm run dev` — соберёт с `BUILD_ENV=local` и поднимет HTTPS dev-сервер на `https://localhost:9001` (запросы к backend проксируются на stage через `/sarex-backend`, `/sarex-gateway`, `/sarex-documentations`, `/sarex-api`). diff --git a/apps/projects/ENDPOINTS.md b/apps/projects/ENDPOINTS.md new file mode 100644 index 0000000..b6775e2 --- /dev/null +++ b/apps/projects/ENDPOINTS.md @@ -0,0 +1,94 @@ +# Эндпоинты, с которыми взаимодействует projects-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `projects-frontend`), а также удалённые модули, которые он подключает и экспортирует через Module Federation. + +## Как устроено взаимодействие + +Запросы описаны в слое `src/shared/api`. Функции запросов сгруппированы по доменам в каталоге `src/shared/api/fetch/*.api.ts` и реэкспортируются из `src/shared/api/index.ts` (`targetsApi`, `resourcesApi`, `projectsApi`, `userApi`, `companyApi`). + +Каждый запрос выполняется через единый `httpService` (`src/shared/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). У сервиса есть методы `getRequest` / `postRequest` / `putRequest` / `deleteRequest`, принимающие объект с полями: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (относительно базового хоста сервиса); +- `data` — тело запроса (для POST/PUT); +- `axiosConfig.params` — query-параметры; +- `isCSRF` — признак необходимости передать CSRF-токен (для сервиса `sarex`); +- `cache`, `queryOptions`, `queryKey` — опции кеширования и ключи кеша. + +Базовый хост подставляется по `service` из карты хостов `src/shared/api/hosts.ts` в зависимости от окружения сборки. Окружение задаётся значением `__ENDPOINT__`, которое подставляется на этапе сборки Webpack (`DefinePlugin`) из переменной `BUILD_ENV` и по умолчанию равно `prod` (`const endpoint = (__ENDPOINT__ as TypeEnvironment) || "prod"`). + +## Базовые хосты по сервисам и окружениям + +Значения из `src/shared/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта. + +| Сервис (`service`) | Назначение | `local` | `stage` | `prod` | +| --- | --- | --- | --- | --- | +| `sarex` | Основной backend (core, pm) | `/sarex-backend` (прокси dev-сервера → `https://stage.sarex.io`) | `/` (относительные пути, тот же origin) | `/` | +| `gateway` | Gateway/API Sarex | `/sarex-gateway` (прокси → `https://stage-api.sarex.io/gateway`) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `documentations` | Сервис документации | `/sarex-documentations` (прокси → `https://stage-api.sarex.io/documentations`) | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `sarexApi` | API Sarex (корень) | `/sarex-api` (прокси → `https://stage-api.sarex.io`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `workspaces` | Сервис рабочих областей | `""` | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `workflows` | Сервис обработки документов | `""` | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `comparisons` | Сервис сравнений | `""` | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `remarks` | Сервис замечаний | `""` | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `projects` | Сервис проектов | `""` | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` | +| `eavV1` | EAV (атрибуты) | — | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` | +| `notifications` | Сервис уведомлений (lambdas) | — | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` | +| `bim` / `bimv2` | BIM-API | `""` | `https://stage-api.sarex.io/bim` / `/bimv2` | `https://api.sarex.io/bim` / `/bimv2` | +| `google` | Временное хранилище (GCS) | `""` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определено окружение `preprod` (`https://api.preprod.sarex.io/*`, `zitadel` → `https://login.preprod.sarex.io`). В `local` часть сервисов проксируется dev-сервером Webpack (`configWebpack/buildDevServer.ts`): `/sarex-backend`, `/sarex-gateway`, `/sarex-documentations`, `/sarex-api`. Остальные сервисы в `local` заданы пустой строкой (относительные пути). Окружение `contour` присутствует в конфигурации сборки (`build.config.ts`), но в `hosts.ts` для него карта хостов не задана. + +## Эндпоинты по сервисам + +Реально вызываются эндпоинты двух сервисов — `sarex` и `gateway`. + +### `sarex` — Основной backend (core, pm) + +Все запросы к `sarex` выполняются с `isCSRF: true`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchTargetLinks` | GET | `/api/core/target-links/` | Список ссылок таргета (кешируется, `stateTime: 20`) | +| `fetchCreateTargetLink` | POST | `/api/core/target-links/` | Создать ссылку (`name`, `link`, `target`, `type`) | +| `fetchUpdateTargetLink` | PUT | `/api/core/target-links/{id}/` | Обновить ссылку | +| `fetchDeleteTargetLink` | DELETE | `/api/core/target-links/{id}/` | Удалить ссылку | +| `fetchMilestonesByProjectId` | GET | `/api/pm/msp/projects/{projectId}/key_milestones/` | Ключевые вехи проекта | +| `fetchUsersByCompanyId` | GET | `/api/core/users/?companies={companyId}&limit=10000&id={usersIds}` | Пользователи компании по списку id | +| `fetchCompanyById` | GET | `/api/core/companies/{companyId}/` | Компания по id (кешируется) | + +### `gateway` — Gateway/API Sarex + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchResources` | GET | `/api/v1/resources?company_id={companyId}` | Список ресурсов компании (кешируется, `stateTime: 20`) | +| `fetchProjectsByCompanyId` | GET | `/api/v2/resources?company_id={companyId}&limit=10000` | Проекты компании (ресурсы v2, кешируется) | + +## Удалённые модули (Module Federation) + +Помимо HTTP-запросов, модуль взаимодействует с другими микрофронтендами через Webpack Module Federation. + +### Подключаемый удалённый модуль + +`src/widgets/remote-timeline` динамически загружает удалённый модуль `srx_pm`, экспонированный модуль `./Timeline` (`src/widgets/remote-timeline/ui/timelineProxy.tsx`, загрузка через `src/widgets/remote-timeline/lib/loader.ts`). + +| Окружение | URL `remoteEntry.js` | +| --- | --- | +| `stage` | `https://stage-modules.sarex.io/pm/module/remoteEntry.js` | +| `local` | `https://stage-modules.sarex.io/pm/module/remoteEntry.js` | +| `prod` | `https://modules.sarex.io/pm/module/remoteEntry.js` | +| `preprod` | `https://modules.sarex.io/pm/module/remoteEntry.js` | +| `contour` | `""` (не задан) | + +### Экспортируемый модуль + +Сам `projects-frontend` при сборке (для всех окружений, кроме `local`) публикует себя как удалённый модуль `srx_projects` (`webpack.config.ts`, `ModuleFederationPlugin`): + +- `filename`: `module/remoteEntry.js`; +- `exposes`: `./ProjectsPage` → `./src/app/App.tsx`; +- `shared` (singleton): `react`, `react-dom`, `@material-ui/core`, `@sarex-team/sdk-js`. + +## Обработка ошибок + +Отдельного модуля маппинга ошибок в `projects-frontend` нет (в отличие от некоторых других фронтендов) — обработка ответов и ошибок делегирована `httpService` из `@sarex-team/sdk-js`. Заголовки CORS для запросов в режиме разработки задаются dev-сервером Webpack, а в кластере — политикой CORS Istio `VirtualService` (`.helm/templates/mesh-config.yaml`): разрешённые источники по регулярке `(https://.*\.sarex\.io)|(https://localhost:.*)`, методы `GET, POST, PUT, PATCH, HEAD, DELETE`, заголовки `Authorization`, `Content-Type`. diff --git a/apps/remarks/ENDPOINTS.md b/apps/remarks/ENDPOINTS.md new file mode 100644 index 0000000..a2d65fb --- /dev/null +++ b/apps/remarks/ENDPOINTS.md @@ -0,0 +1,114 @@ +# Эндпоинты, с которыми взаимодействует remarks-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `remarks-frontend`). + +## Как устроено взаимодействие + +В отличие от `transmittal-frontend`, в `remarks-frontend` **нет декларативного реестра эндпоинтов** (`endpoints.ts`). Запросы формируются по месту — в API-объектах (`module/api/*.ts`) и в MobX-сторах (`module/store/*.ts`) — прямыми вызовами методов `httpService`: + +- `httpService.getRequest(options)` +- `httpService.postRequest(options)` +- `httpService.putRequest(options)` (объявлен, в коде не используется) +- `httpService.patchRequest(options)` +- `httpService.deleteRequest(options)` + +Каждый вызов задаётся объектом-параметром: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже), определяет базовый хост; +- `url` — путь запроса (часто шаблонная строка с подстановкой id/query); +- `data` — тело запроса (для `POST`/`PATCH`/`PUT`); +- `axiosConfig` — доп. настройки axios (`params` для query-параметров, `responseType: "blob"` для файлов и т.п.); +- `showErrorNotification` — включает показ уведомления об ошибке средствами SDK. + +`httpService` (`module/api/http-client.ts`) — это `Proxy` поверх базового `baseHttpService` (`module/api/http-service.ts`, создаётся через `createHttpService` из `@sarex-team/sdk-js`). Прокси добавляет единую обработку ответа `403`: показывает toast с текстом `response.data.detail` либо сообщением «У вас недостаточно прав для выполнения данного действия.». Базовый хост подставляется SDK по значению `service` и текущему окружению `BUILD_ENV` (значения — из `module/api/hosts.ts`, по умолчанию `prod`). В окружении `local` тип HTTP-сервиса переключается на `zitadel` (`setTypeOfHttpService("zitadel")`). + +Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `sarexApi` | Gateway/API Sarex (`/issues`, `/flows`, `/eav`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `gateway` | Gateway Sarex | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `documentations` | Сервис документации | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `eavV4` | Сервис атрибутов/ассетов (EAV v4) | `https://stage-api.sarex.io/eav/api/v4` | `https://api.sarex.io/eav/api/v4` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local` и `preprod`. В `preprod` хосты указывают на `https://api.preprod.sarex.io/*` (для `zitadel` — `https://login.preprod.sarex.io`). В `local` сервис `sarex` проксируется на `/sarex-backend`, а остальные сервисы указывают на `stage`-хосты (`https://stage-api.sarex.io/*`, `zitadel` → `https://idp.dev.stage.sarex.io`). Сервис `zitadel` явно в коде не вызывается — используется SDK для аутентификации. + +## Эндпоинты по сервисам + +### `sarexApi` — Gateway/API Sarex (issues / flows / eav) + +Базовый хост — корневой (`https://-api.sarex.io`); маршрутизация задаётся префиксами пути (`/issues/api`, `/flows/api`, `/eav/api`). + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| POST | `/issues/api/issues/filter/` | Список/фильтрация замечаний (пагинация `limit`/`offset`) | `api/issues/issues.ts` | +| POST | `/issues/api/issues/` | Создать замечание | `store/collectionRemarks.ts` | +| GET | `/issues/api/issues/{uuid}/` | Замечание по uuid | `store/collectionRemarks.ts` | +| PATCH | `/issues/api/issues/{uuid}/` | Обновить замечание | `store/collectionRemarks.ts`, `store/remarks.ts` | +| POST | `/issues/api/issues/export/?{params}` | Экспорт замечаний (ответ `blob`) | `store/remarks.ts` | +| GET | `/issues/api/issues/daterange/` | Диапазон дат замечаний | `store/filters.ts` | +| POST | `/issues/api/issues/filter-options/?{params}` | Опции фильтра замечаний | `store/filters.ts` | +| GET | `/issues/api/issue-changes/?issue_id={uuid}` | История изменений замечания | `store/remark.ts` | +| POST | `/issues/api/comments/` | Создать комментарий | `store/collectionRemarks.ts` | +| DELETE | `/issues/api/comments/{id}/` | Удалить комментарий | `store/collectionRemarks.ts` | +| GET | `/issues/api/attachments/{fileId}/` | Получить вложение | `store/collectionRemarks.ts` | +| POST | `/issues/api/attachments/` | Загрузить вложение | `store/collectionRemarks.ts` | +| DELETE | `/issues/api/attachments/{id}/` | Удалить вложение | `store/collectionRemarks.ts` | +| GET | `/issues/api/companies/{companyId}/status-model/v2/` | Модель статусов компании | `store/remarks.ts` | +| GET | `/flows/api/v1/documents/` | Документы (flows) | `store/filters.ts` | +| GET | `/flows/api/v1/documents/?{query}&offset=0&limit=100000` | Документы (flows, полная выборка) | `store/remarks.ts` | +| POST | `/flows/api/v1/flows/filter/` | Фильтрация flows | `store/filters.ts` | +| GET | `/eav/api/v0/attribute/?company_id={params}` | Атрибуты компании (EAV v0) | `store/remarks.ts` | + +### `remarks` — Сервис замечаний + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| GET | `/api/v1/remarks?target=true{params}` | Список замечаний по target | `store/remarks.ts` | +| DELETE | `/api/v1/remarks/{uuid}` | Удалить замечание | `store/collectionRemarks.ts` | + +### `gateway` — Gateway Sarex + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| GET | `/api/v2/users/?{query}` | Пользователи (с фильтрами прав/таргета) | `store/remarks.ts` | +| GET | `/api/v2/users/` | Пользователи (без фильтров) | `store/remarks.ts` | +| GET | `/api/v1/documents/{docId}/attributes/` | Атрибуты документа | `store/remarks.ts` | +| GET | `/api/v1/resources/` | Список ресурсов | `store/resourcesStore.ts` | + +### `documentations` — Сервис документации + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| GET | `/api/v1/documents/{docId}?extend=bundles` | Документ с бандлами | `store/remark.ts` | + +### `eavV4` — Сервис ассетов (EAV v4) + +Базовый хост уже включает префикс `/eav/api/v4`. + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| POST | `/assets/search/` | Поиск ассетов по набору id | `api/assets.ts` | +| GET | `/assets/` | Список ассетов (фильтры, пагинация, `tag`) | `api/assets.ts` | + +### `sarex` — Локальный сервис данных + +| Метод | Путь | Назначение | Где вызывается | +| --- | --- | --- | --- | +| GET | `/api/client/settings/` | Клиентские настройки | `store/remarks.ts` | +| GET | `/api/core/admin/departments/?company={companyId}` | Отделы компании (пагинация по `next`) | `store/remarks.ts` | +| GET | `/api/core/admin/positions/?company={companyId}` | Должности компании (пагинация по `next`) | `store/remarks.ts` | + +## Обработка ошибок + +Отдельного файла-маппера ошибок (аналога `module/api/errors.ts` в `transmittal-frontend`) в проекте нет. Обработка сосредоточена в двух местах: + +- `module/api/http-client.ts` — прокси перехватывает ответ `403` и показывает toast (`react-toastify`) с текстом `response.data.detail` или сообщением по умолчанию «У вас недостаточно прав для выполнения данного действия.»; +- SDK `@sarex-team/sdk-js` — при `showErrorNotification: true` показывает стандартное уведомление об ошибке; в сторах ряд операций дополнительно оборачивается в `try/catch` с собственными toast-сообщениями (напр. «Замечание успешно создано!» / «Произошла ошибка при создании замечания»). diff --git a/apps/resources/.env.example b/apps/resources/.env.example new file mode 100644 index 0000000..6d927f2 --- /dev/null +++ b/apps/resources/.env.example @@ -0,0 +1,58 @@ +# Django settings module +# wsgi.py / manage.py по умолчанию используют config.settings.production +DJANGO_SETTINGS_MODULE=config.settings.production + +# Django +# Читаются только при DEBUG (в base.py) и в production.py (ALLOWED_HOSTS зашиты в коде) +# SECRET_KEY и DEBUG заданы в самих settings-модулях, через окружение не переопределяются + +# Environment +# Метка окружения (используется в атрибутах трейсинга): stage / preprod / prod +ENVIRONMENT=prod + +# Database (PostGIS) +# Читаются в config/settings/production.py; ENGINE фиксирован: django.contrib.gis.db.backends.postgis +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 +DATABASE_NAME=resources +DATABASE_USER=postgres +DATABASE_PASSWORD=password + +# Database (тесты) +# Читаются только в config/settings/test.py (запуск pytest) +DJANGO_POSTGRES_DB=resources +DJANGO_POSTGRES_USER=postgres +DJANGO_POSTGRES_PASSWORD=12345678dev +DJANGO_POSTGRES_HOST=test_postgres +DJANGO_POSTGRES_PORT=5432 + +# S3 (django-storages / boto3, Yandex Object Storage) +# Маппятся на AWS_* в settings; DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage +YC_S3_ACCESS_KEY_ID= +YC_S3_SECRET_ACCESS_KEY= +YC_S3_BUCKET_NAME=resources +YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net + +# Sarex backend (интеграция с ядром Sarex) +# SAREX_HOST -> SAREX_BASE_HOST; используется для получения токена и списка пользователей +SAREX_HOST=https://lk.sarex.io +SAREX_ADMIN_USERNAME= +SAREX_ADMIN_PASSWORD= +# Определяется в production.py, но кодом приложения напрямую не читается +SERVICE_ACCOUNTS_HOST=https://lk.sarex.io/api/core + +# OpenTelemetry (django-otel-tools) +# ВНИМАНИЕ: трейсинг включается по любой непустой строке (os.getenv('USE_OTEL', False)), +# поэтому USE_OTEL=False строкой ТОЖЕ включит его. Для выключения переменную не задавать. +USE_OTEL=True +SERVICE_NAME=resources-backend.sarex-resources +TRACER_ENDPOINT=localhost:4375 +# То же поведение "любая строка = True", что и у USE_OTEL +USE_INSECURE=True +MODULE=resources +TEAM=platform_team +COMPONENT=backend + +# Uwsgi / инфраструктура +# Прокидывается в Helm/k8s, но кодом приложения не читается (порт задан в uwsgi.ini) +API_ADDRESS=8000 diff --git a/apps/resources/CONFIGURATION.md b/apps/resources/CONFIGURATION.md new file mode 100644 index 0000000..2e237ce --- /dev/null +++ b/apps/resources/CONFIGURATION.md @@ -0,0 +1,203 @@ +# Конфигурация проекта sarex-resources + +Документ описывает все переменные окружения и способы конфигурирования сервиса ресурсов (`sarex-resources`, backend модуля «Ресурсы/Проекты»). + +## Способы конфигурирования + +Сервис — приложение на **Django 4.1 + Django REST Framework** (GeoDjango/PostGIS). В отличие от сервисов на `pydantic-settings`, конфигурация задаётся **классическими Django settings-модулями** в `server/config/settings/`, а не единым классом настроек: + +- `base.py` — общие настройки (приложения, middleware, DRF, S3, интеграция с Sarex, OpenTelemetry); +- `production.py` — наследует `base.py` (`from .base import *`), задаёт `DEBUG=False`, `ALLOWED_HOSTS`, БД, CORS, S3, статику; +- `test.py` — наследует `base.py`, отдельная тестовая БД, снятие permission-классов DRF. + +Активный модуль выбирается переменной **`DJANGO_SETTINGS_MODULE`**. По умолчанию (`server/config/wsgi.py`, `server/manage.py`) — `config.settings.production`. + +Особенности разбора: + +- **префикса у переменных нет** — имена плоские (`DATABASE_HOST`, `YC_S3_BUCKET_NAME` и т.п.); +- значения читаются напрямую через `os.getenv(...)`; вложенных секций/делимитеров (как `__` в pydantic-сервисах) нет; +- **типизация и валидация окружения отсутствуют** — всё приходит строками; булевы флаги (`USE_OTEL`, `USE_INSECURE`) проверяются на «truthy», поэтому **любая непустая строка, включая `"False"`, считается истиной** (см. «Замечания»); +- `.env`-файл приложением **не загружается автоматически** — переменные должны быть в окружении процесса (docker-compose `environment`, k8s `env`/`secretKeyRef`, либо `set -a && . ./.env && set +a`). + +> **Важно для прод-развёртывания.** В кластере файл `server/config/settings/production.py` **подменяется** содержимым ConfigMap `django-configmap` (монтируется на `/server/config/settings/production.py`, см. `infra/iac/apps/resources/base/django-configmap.yaml` и `.helm/templates/api.yaml`). Именно версия из ConfigMap определяет фактические `ALLOWED_HOSTS`, `CORS_*`, `SERVICE_ACCOUNTS_HOST`, cookie-имена и часть значений Sarex. Репозиторный `production.py` — это шаблон/локальный вариант. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | `docker-compose.yml` — сервисы `postgres`/`api`; переменные Postgres задаются в `environment`, приложение читает окружение контейнера | +| Локально (тесты) | `docker-compose.test.yml` + `pytest.ini` (`DJANGO_SETTINGS_MODULE=config.settings.test`), переменные `DJANGO_POSTGRES_*` | +| Kubernetes (Helm-чарт репозитория) | `.helm/values-.yaml`: блоки `envs` (обычные значения) и `secrets` (из k8s-секретов); шаблон `.helm/templates/api.yaml` | +| Kubernetes (infra, GitOps) | `infra/iac/apps/resources/*` — kustomize `base` (Vault-инъекция env в аннотациях Deployment + ConfigMaps) и оверлеи `brusnika-stage`/`brusnika-prod` (`helmrelease.yaml`, universal-chart) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: подключает общие шаблоны `generic/common-ci`, переключает окружение по ветке/тегу через `workflow.rules` | + +Способы запуска процессов: + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `uwsgi --ini /opt/server/uwsgi.ini` | `config.wsgi:application` | HTTP API (uWSGI, порт 8000) | +| `python manage.py migrate` | `manage.py` | Применение миграций (в `entrypoint.sh` перед стартом uWSGI) | +| `python manage.py ` | `manage.py` | Служебные Django-команды (`createsuperuser`, `shell`, миграции и т.п.) | +| `pytest` | `config.settings.test` | Юнит-тесты (`compose/test_server/entrypoint.sh`) | + +Порядок запуска в контейнере (`compose/server/entrypoint.sh`): сначала `python manage.py migrate`, затем `opentelemetry-instrument uwsgi --plugin python3 --ini /opt/server/uwsgi.ini` с `DJANGO_SETTINGS_MODULE=config.settings.production`. + +## Переменные приложения + +Все переменные плоские (без префикса). Дефолт `—` означает, что значение при обращении вернёт `None`/пусто (Django/psycopg2 или интеграции могут упасть уже в рантайме, а не на старте). + +### Выбор настроек + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.production` | Модуль настроек Django. Значения: `config.settings.production` / `config.settings.test` | + +### Окружение и трейсинг (`base.py`) + +Блок OpenTelemetry активируется целиком по `USE_OTEL` (`django-otel-tools`): добавляется `OtelMiddleware`, настраивается трейсер и OTEL-логгер. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool-ish | `False` (выкл.) | Включение трейсинга. Truthy-проверка: любая непустая строка включает | +| `SERVICE_NAME` | string | `resources-backend.sarex-resources` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool-ish | `False` | Небезопасное (без TLS) подключение к коллектору; та же truthy-проверка | +| `ENVIRONMENT` | string | `prod` | Метка окружения в атрибутах трейсинга (`stage`/`preprod`/`prod`) | +| `MODULE` | string | `resources` | Атрибут `module` в трейсинге | +| `TEAM` | string | `platform_team` | Атрибут `team` в трейсинге | +| `COMPONENT` | string | `backend` | Атрибут `component` в трейсинге | + +### База данных (`production.py`, `django.contrib.gis.db.backends.postgis`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_HOST` | string | — | Хост PostgreSQL/PostGIS | +| `DATABASE_PORT` | int (string) | — | Порт PostgreSQL | +| `DATABASE_NAME` | string | — | Имя базы данных | +| `DATABASE_USER` | string | — | Пользователь БД | +| `DATABASE_PASSWORD` | string | — | Пароль пользователя БД | + +> БД требует расширения PostGIS (образ `postgis/postgis`), т.к. используются гео-поля (`Location.geometry`/`geography`) и `django.contrib.gis`. TLS к БД настраивается на уровне libpq: в Helm-чарте секрет `yc-pg-certificate` монтируется как `/root/.postgresql/root.crt` (в settings отдельного `sslmode` нет). + +### База данных для тестов (`test.py`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_POSTGRES_DB` | string | `resources` | Имя тестовой БД | +| `DJANGO_POSTGRES_USER` | string | `postgres` | Пользователь тестовой БД | +| `DJANGO_POSTGRES_PASSWORD` | string | `12345678dev` | Пароль тестовой БД | +| `DJANGO_POSTGRES_HOST` | string | `test_postgres` | Хост тестовой БД | +| `DJANGO_POSTGRES_PORT` | int (string) | `5432` | Порт тестовой БД | + +Дополнительно `test.py` выставляет `DJANGO_ALLOW_ASYNC_UNSAFE=true` в коде. + +### S3 / объектное хранилище (`base.py` и `production.py`, `django-storages` + `boto3`) + +Файлы (`ResourcePhoto.image` и т.п.) хранятся в S3-совместимом хранилище; `DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL="public-read"`. + +| Переменная | Тип | Значение по умолчанию | Назначение (Django-настройка) | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | — | `AWS_ACCESS_KEY_ID` | +| `YC_S3_SECRET_ACCESS_KEY` | string | — | `AWS_SECRET_ACCESS_KEY` | +| `YC_S3_BUCKET_NAME` | string | — | `AWS_STORAGE_BUCKET_NAME` | +| `YC_S3_ENDPOINT_URL` | string | — | `AWS_S3_ENDPOINT_URL` | + +### Интеграция с ядром Sarex (`base.py`) + +Используется для получения токена (`/api/token/`) и списка пользователей (`/api/core/users/`) в эндпоинтах группировки пользователей/ресурсов (`users-grouped-by-resource`, `users-with-resources`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SAREX_HOST` | string | `https://lk.sarex.io` | Базовый хост Sarex (`SAREX_BASE_HOST`) | +| `SAREX_ADMIN_USERNAME` | string | — | Логин сервисной учётки Sarex | +| `SAREX_ADMIN_PASSWORD` | string | — | Пароль сервисной учётки Sarex | +| `SERVICE_ACCOUNTS_HOST` | string | `https://lk.sarex.io/api/core` | Определяется в `production.py`; кодом приложения напрямую не читается (см. «Замечания») | + +## Переменные инфраструктуры, сборки и вспомогательных утилит + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_ADDRESS` | `.helm/values-*.yaml`, `infra/.../backend-deployment.yaml` | Задаётся в окружении (`8000`), кодом не читается — порт фактически берётся из `uwsgi.ini` (`http = 0.0.0.0:8000`) | +| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `docker-compose.yml`, `docker-compose.test.yml` | Инициализация контейнера Postgres (не путать с `DATABASE_*`, которые читает Django) | +| `PYTHONUNBUFFERED` | `Dockerfile` | Небуферизованный stdout/stderr | +| build-настройки uWSGI | `compose/server/uwsgi.ini` | `processes=8`, `http=0.0.0.0:8000`, `harakiri`, `buffer-size`, `static-map` для `/static` и `/media` | + +Приватный индекс пакетов для сборки (`django-otel-tools`) задан прямо в `requirements/base.txt` через `--extra-index-url` (nexus.infra.sarex.io). + +## Переменные из Helm-чарта репозитория (`.helm/values-.yaml`) + +Обычные значения — блок `envs`, секреты — блок `secrets` (монтируются как env через `secretKeyRef`). Значения различаются по окружениям (`stage`/`preprod`/`production`): адрес БД (`DATABASE_HOST`), `SERVICE_NAME`, `TRACER_ENDPOINT`, `ENVIRONMENT`, число реплик. + +Значения из секретов: + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `DATABASE_USER` | `ya-pg-secret` | `user` | +| `DATABASE_PASSWORD` | `ya-pg-secret` | `password` | +| `YC_S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` | +| `YC_S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` | +| `YC_S3_BUCKET_NAME` | `yc-s3-secret` | `storage_bucket_name` | +| `YC_S3_ENDPOINT_URL` | `yc-s3-secret` | `endpoint_url` | +| `SAREX_ADMIN_USERNAME` | `sarex-auth-secret` | `username` | +| `SAREX_ADMIN_PASSWORD` | `sarex-auth-secret` | `password` | + +Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `yc-pg-certificate` (ключ `certificate`) как файл `/root/.postgresql/root.crt`, а также ConfigMap `uwsgi-configmap` (файл `uwsgi.ini`). Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы), `service_*`, `api_host` (istio `VirtualService`, prefix `/resource-management`). + +## Переменные из infra (GitOps, kustomize `base`) + +В `infra/iac/apps/resources/base/backend-deployment.yaml` секреты БД и S3 подаются через **HashiCorp Vault Agent** (аннотации `vault.hashicorp.com/*`), который рендерит файлы `/vault/secrets/resources-db` и `/vault/secrets/resources-s3`; они подгружаются в окружение в `args` контейнера (`set -a && . /vault/secrets/... && set +a`) перед запуском `entrypoint.sh`. + +| Переменная | Источник (Vault path / значение) | +| --- | --- | +| `DATABASE_HOST` | `postgresql.resources.svc.cluster.local` (шаблон Vault) | +| `DATABASE_PORT` | `5432` | +| `DATABASE_NAME` | `resources_db` | +| `DATABASE_USER` | `secrets/data/postgresql/apps/resources` → `username` | +| `DATABASE_PASSWORD` | `secrets/data/postgresql/apps/resources` → `password` | +| `YC_S3_ENDPOINT_URL` | `secrets/data/minio/apps/resources` → `client.endpoint` | +| `YC_S3_BUCKET_NAME` | `resources` | +| `YC_S3_ACCESS_KEY_ID` | `secrets/data/minio/apps/resources` → `access_key` | +| `YC_S3_SECRET_ACCESS_KEY` | `secrets/data/minio/apps/resources` → `secret_key` | +| `DJANGO_SETTINGS_MODULE` | `config.settings.production` (env Deployment) | +| `API_ADDRESS` | `8000` (env Deployment) | + +Оверлеи `brusnika-stage`/`brusnika-prod` используют `HelmRelease` (universal-chart): env `DJANGO_SETTINGS_MODULE`, `DATABASE_HOST/PORT/NAME`, `API_ADDRESS`, `YC_S3_ENDPOINT_URL`, `YC_S3_BUCKET_NAME`; секреты `DATABASE_USER/PASSWORD` (`postgres-secret`), `YC_S3_ACCESS_KEY_ID/SECRET_ACCESS_KEY` (`yc-s3-secret`, ключи `key-id`/`access-key`). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу: + +| Условие | STAND | Namespace | RELEASE_NAME | +| --- | --- | --- | --- | +| ветка `master` | `preprod` | `resources-preprod` | `resources` | +| ветка `stage` | `stage` | `resources-stage` | `sarex-resources` | +| тег (`CI_COMMIT_TAG`) | `prod` | `resources-prod` | `sarex-resources` | + +Ключевые переменные пайплайна: `CHART_NAME=sarex-resources`, `CHART_VERSION`, `DOCKERFILE_PATH=./compose/server/Dockerfile`, `IMAGE_PATH=deployment.image`, `HELM_SET_ARGS` (`--set deployment.image=…`), флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY`. Job `linter` запускает `flake8` (образ `python:3.8-buster`). + +## Замечания и потенциальные проблемы + +- **Truthy-флаги.** `USE_OTEL` и `USE_INSECURE` читаются как `os.getenv(name, False)` без приведения типа. Любая непустая строка — истина, включая `"False"`, `"0"`, `"no"`. Чтобы **выключить** трейсинг, переменную `USE_OTEL` нужно **не задавать вовсе** (а не ставить в `False`). В Helm-values она задана строкой `"True"`. +- **`.env` не загружается автоматически.** В settings нет `python-dotenv`/`env_file`; переменные должны попадать в окружение процесса (compose `environment`, k8s `env`/Vault, ручной `export`). +- **`production.py` подменяется в кластере.** Репозиторный `production.py` содержит хардкод `ALLOWED_HOSTS`, `CORS_*`, тестовый `SECRET_KEY` и др.; в проде используется версия из ConfigMap `django-configmap`. Различия: `ALLOWED_HOSTS=['*']`, другой `SERVICE_ACCOUNTS_HOST`, хардкод `SAREX_ADMIN_*`/`SAREX_BASE_HOST`, cookie-имена `resource-sessionid`/`resource-csrftoken`. +- **`SECRET_KEY` захардкожен** в `base.py`/`production.py` (в т.ч. пометка `# Delete after Test`) — секрет не берётся из окружения. Для реального прода его следует вынести в секрет. +- **`SERVICE_ACCOUNTS_HOST`** определяется, но напрямую в коде не используется (интеграции ходят по `SAREX_BASE_HOST`); переменная задаётся «на вырост». +- **`DATABASE_*` без дефолтов.** При отсутствии переменных Django получит `None` и подключение к БД упадёт в рантайме (не на импортстарте). Пять переменных БД обязательны для рабочего запуска. +- **Аутентификация DRF отключена.** `DEFAULT_AUTHENTICATION_CLASSES=[]`, permission-классы не заданы (эффективно `AllowAny`) — доступ ограничивается только сетевым слоем/istio. Учитывать при публикации. +- **Имя релиза различается между окружениями** (`resources` на preprod vs `sarex-resources` на stage/prod) — при работе с Helm/namespace это легко перепутать. + +## Минимальный набор для локального запуска + +Postgres (PostGIS) поднимается через `docker-compose up postgres`, приложение — через сервис `api` (`docker-compose.yml`) или локально `uwsgi`/`manage.py runserver`. Минимально необходимо задать: + +- `DJANGO_SETTINGS_MODULE=config.settings.production` (или свой dev-модуль) +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `YC_S3_*` — если задействуется загрузка файлов/статики в S3 (иначе операции с файлами упадут) +- `SAREX_HOST`, `SAREX_ADMIN_USERNAME`, `SAREX_ADMIN_PASSWORD` — если нужны эндпоинты интеграции с пользователями Sarex +- OTEL — по умолчанию не задавать (`USE_OTEL` отсутствует); при включении — `USE_OTEL=1`, `SERVICE_NAME`, `TRACER_ENDPOINT`, при необходимости `USE_INSECURE` + +Для тестов достаточно поднять `docker-compose.test.yml` — переменные `DJANGO_POSTGRES_*` имеют рабочие дефолты, `pytest.ini` уже указывает `config.settings.test`. + +Готовые значения-примеры для всех переменных приведены в `.env.example`. diff --git a/apps/resources/openapi.yaml b/apps/resources/openapi.yaml new file mode 100644 index 0000000..321e9fc --- /dev/null +++ b/apps/resources/openapi.yaml @@ -0,0 +1,841 @@ +openapi: 3.0.3 + +info: + title: Sarex Resources API + version: "1.0.0" + description: | + REST API сервиса **sarex-resources** (`platform/sarex-resources`) — управление + ресурсами/проектами, их типами (иерархия), локациями, а также разрешениями + сервисных аккаунтов на ресурсы и на компании. + + Сервис написан на Python (**Django 4.1 + Django REST Framework**, GeoDjango/PostGIS). + Роутинг задаётся в `server/config/urls.py`: + + - `resource-management/` — Django-admin; + - `api/v1/` — основной API (`apps.resource.urls`); + - `api/v2/` — расширенный API ресурсов (`apps.resource.urls_v2`). + + Списочные и CRUD-эндпоинты строятся `rest_framework.routers.DefaultRouter` + поверh `ModelViewSet`; часть операций — отдельные `APIView`. + + ### Аутентификация + На уровне приложения аутентификация и permission-классы DRF **отключены** + (`DEFAULT_AUTHENTICATION_CLASSES = []`, permission-классы не заданы — + эффективно `AllowAny`). Ограничение доступа обеспечивается сетевым слоем + (istio/ingress, внутрикластерный доступ к `sarex-resources-service`). + + ### Пагинация + Используется `rest_framework.pagination.LimitOffsetPagination` с очень большим + `PAGE_SIZE` (100000). Списочные ответы оборачиваются в + `{ count, next, previous, results }`; постранично управляется параметрами + `limit` и `offset`. + + ### Идентификаторы ресурсов + У ресурса есть внутренний `id` (int, в API почти не используется), публичный + `public_id`/`id` (UUID — основной идентификатор в URL detail-эндпоинтов v1/v2) + и устаревший `target_id` (`_target_id`, int) для обратной совместимости. + Удаление — «мягкое» (`deleted=True`), объекты с `deleted=True` из выборок + исключаются. + + ### Замечания (расхождения кода) + - Detail-роуты `resource` в v1/v2 ищут объект по `public_id` (UUID), а не по + первичному ключу; путь при этом выглядит как `/api/v1/resource/{public_id}/`. + - Ряд аналитических эндпоинтов (`users-grouped-by-resource`, + `users-with-resources`, `resources-grouped-by-sa`) возвращают + «сырые» структуры (`dict`/списки), не обёрнутые в пагинацию. + - `bulk_delete/resource_permission/` и `permissions-bulk/` выполняют + **жёсткое** удаление разрешений (`.delete()`), в отличие от «мягкого» + удаления сущностей. + - `RetrieveResourceByTargetIdAPIView` при отсутствии ресурса возвращает + `404` без тела. + + contact: + name: sarex-resources + url: https://gitlab/platform/sarex-resources + +servers: + - url: https://api.sarex.io/resource-management + description: Production admin (istio VirtualService, prefix /resource-management) + - url: http://sarex-resources-service.resources-prod + description: Внутрикластерный адрес (ClusterIP), production namespace + - url: http://sarex-resources-service.resources-stage + description: Внутрикластерный адрес (ClusterIP), stage namespace + - url: http://localhost:8888 + description: Локальный запуск (docker-compose, 8888 → 8000) + +tags: + - name: resources + description: Ресурсы (проекты) + - name: resources_v2 + description: Расширенные ресурсы (api/v2) + - name: resource_types + description: Типы ресурсов (иерархия) + - name: locations + description: Локации (гео-данные) + - name: resource_permissions + description: Разрешения сервисных аккаунтов на ресурсы + - name: company_permissions + description: Разрешения сервисных аккаунтов на компании + - name: analytics + description: Служебные/аналитические эндпоинты (группировки, интеграция с Sarex) + +paths: + + /api/v1/resource/: + get: + tags: [resources] + summary: Список ресурсов + parameters: + - { name: tenant_id, in: query, schema: { type: string }, description: "ID компании; поддерживает список через запятую" } + - { name: type, in: query, schema: { type: integer } } + - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву (ltree)" } + - { name: show_in_overview, in: query, schema: { type: boolean } } + - { name: id, in: query, schema: { type: string }, description: "public_id (UUID); список через запятую" } + - { name: target_id, in: query, schema: { type: string }, description: "устаревший target_id; список через запятую" } + - { name: service_accounts, in: query, schema: { type: string }, description: "UUID сервисных аккаунтов через запятую (доступные ресурсы)" } + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedResourceList" + post: + tags: [resources] + summary: Создать ресурс + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceWrite" } + responses: + "201": + description: Created + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceWrite" } + + /api/v1/resource/{public_id}/: + parameters: + - { name: public_id, in: path, required: true, schema: { type: string, format: uuid } } + get: + tags: [resources] + summary: Получить ресурс по public_id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceRetrieve" } + "404": { description: Не найден } + delete: + tags: [resources] + summary: Мягкое удаление ресурса (deleted=true) + responses: + "204": { description: No Content } + + /api/v1/resource_type/: + get: + tags: [resource_types] + summary: Список типов ресурсов + parameters: + - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву" } + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedResourceTypeList" } + post: + tags: [resource_types] + summary: Создать тип ресурса + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceTypeWrite" } + responses: + "201": + description: Created + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceTypeWrite" } + + /api/v1/resource_type/filters/: + get: + tags: [resource_types] + summary: "Значения фильтра типов ресурсов (потомки родителя)" + parameters: + - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; вернёт его потомков" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + parent: + type: array + items: + type: object + properties: + id: { type: integer } + name: { type: string } + + /api/v1/resource_type/{id}/: + parameters: + - { name: id, in: path, required: true, schema: { type: integer } } + get: + tags: [resource_types] + summary: Получить тип ресурса + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceTypeRetrieve" } + delete: + tags: [resource_types] + summary: Мягкое удаление типа ресурса + responses: + "204": { description: No Content } + + /api/v1/location/: + get: + tags: [locations] + summary: Список локаций + parameters: + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedLocationList" } + post: + tags: [locations] + summary: Создать локацию + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/LocationWrite" } + responses: + "201": + description: Created + content: + application/json: + schema: { $ref: "#/components/schemas/LocationWrite" } + + /api/v1/location/{id}/: + parameters: + - { name: id, in: path, required: true, schema: { type: integer } } + get: + tags: [locations] + summary: Получить локацию + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/LocationRetrieve" } + delete: + tags: [locations] + summary: Мягкое удаление локации + responses: + "204": { description: No Content } + + /api/v1/resource_permission/: + get: + tags: [resource_permissions] + summary: Список разрешений на ресурсы + parameters: + - { name: public_resource_id, in: query, schema: { type: string }, description: "UUID через запятую" } + - { name: resource_id, in: query, schema: { type: string }, description: "внутренние id через запятую" } + - { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" } + - { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую (по resource.tenant_id)" } + - { name: company_id, in: query, schema: { type: string }, description: "синоним tenant_id" } + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedResourcePermissionList" } + post: + tags: [resource_permissions] + summary: "Создать разрешение(я) на ресурс" + description: "Принимает один объект или массив (bulk). Поле resource определяется по public_resource_id." + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ResourcePermissionWrite" + - type: array + items: { $ref: "#/components/schemas/ResourcePermissionWrite" } + responses: + "201": + description: Created + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ResourcePermissionWrite" + - type: array + items: { $ref: "#/components/schemas/ResourcePermissionWrite" } + + /api/v1/resource_permission/{id}/: + parameters: + - { name: id, in: path, required: true, schema: { type: integer } } + get: + tags: [resource_permissions] + summary: Получить разрешение на ресурс + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourcePermissionRetrieve" } + delete: + tags: [resource_permissions] + summary: Мягкое удаление разрешения + responses: + "204": { description: No Content } + + /api/v1/company_resource_permission/: + get: + tags: [company_permissions] + summary: Список разрешений на компании + parameters: + - { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" } + - { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую" } + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedCompanyPermissionList" } + post: + tags: [company_permissions] + summary: "Создать разрешение(я) на компанию" + description: "Принимает один объект или массив (bulk_create)." + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/CompanyPermission" + - type: array + items: { $ref: "#/components/schemas/CompanyPermission" } + responses: + "201": + description: Created + + /api/v1/company_resource_permission/{id}/: + parameters: + - { name: id, in: path, required: true, schema: { type: integer } } + get: + tags: [company_permissions] + summary: Получить разрешение на компанию + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/CompanyPermission" } + delete: + tags: [company_permissions] + summary: Мягкое удаление разрешения на компанию + responses: + "204": { description: No Content } + + /api/v1/targets/{target_id}/resource/: + get: + tags: [resources] + summary: Получить ресурс по устаревшему target_id + parameters: + - { name: target_id, in: path, required: true, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceRetrieve" } + "404": { description: Не найден } + + /api/v1/bulk_delete/resource_permission/: + post: + tags: [resource_permissions] + summary: Массовое (жёсткое) удаление разрешений на ресурсы + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: array + items: { type: integer } + required: [id] + responses: + "200": { description: OK } + "400": { description: "id не передан или содержит не-int" } + + /api/v1/service-accounts/: + get: + tags: [analytics] + summary: Сервисные аккаунты, имеющие доступ к ресурсу + description: "Нужно указать ровно один из resource_id (UUID) или target_id (int)." + parameters: + - { name: resource_id, in: query, schema: { type: string, format: uuid } } + - { name: target_id, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: { type: integer } + results: + type: array + items: { type: string, format: uuid } + "400": { description: "не указан либо указаны оба параметра / ошибка парсинга" } + "404": { description: Ресурс не найден } + + /api/v1/users-grouped-by-resource/: + get: + tags: [analytics] + summary: "Пользователи, сгруппированные по ресурсам" + description: "Обращается к ядру Sarex за списком пользователей. Возвращает map resource_id → [user_id]." + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: { type: integer } + + /api/v1/resources-grouped-by-sa/: + get: + tags: [analytics] + summary: "Ресурсы, сгруппированные по сервисным аккаунтам" + description: "Учитывает прямые (read) и компанейские разрешения. Возвращает map service_account → [public_resource_id]." + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: { type: string, format: uuid } + + /api/v1/users-with-resources/: + post: + tags: [analytics] + summary: "Доступные ресурсы для списка пользователей в рамках компании" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: array + items: { type: integer } + tenant_id: { type: integer } + required: [id, tenant_id] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + id: { type: integer } + unrestricted_access: { type: boolean } + resources: + type: array + items: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + "400": { description: "не передан id или tenant_id / некорректный tenant_id" } + + /api/v1/permissions-bulk/: + patch: + tags: [resource_permissions] + summary: "Синхронизация (bulk) разрешений на ресурсы и компании" + description: | + Приводит разрешения к переданному состоянию: создаёт недостающие, + удаляет лишние (жёстко). `permissions` — map service_account → [resource_id], + `unrestricted_permissions` — map service_account → bool (доступ ко всей компании). + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tenant_id: { type: integer } + permissions: + type: object + additionalProperties: + type: array + items: { type: string, format: uuid } + unrestricted_permissions: + type: object + additionalProperties: { type: boolean } + required: [tenant_id, permissions] + responses: + "200": { description: OK } + + /api/v2/resource/: + get: + tags: [resources_v2] + summary: Список расширенных ресурсов + parameters: + - { name: tenant_id, in: query, schema: { type: string }, description: "список через запятую" } + - { name: type, in: query, schema: { type: integer } } + - { name: parent, in: query, schema: { type: integer } } + - { name: show_in_overview, in: query, schema: { type: boolean } } + - { name: id, in: query, schema: { type: string }, description: "public_id (UUID) через запятую" } + - { name: target_id, in: query, schema: { type: integer } } + - { name: service_accounts, in: query, schema: { type: string } } + - { name: limit, in: query, schema: { type: integer } } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedResourceExpandedList" } + post: + tags: [resources_v2] + summary: Создать расширенный ресурс + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceExpandedWrite" } + responses: + "201": + description: Created + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceExpandedWrite" } + + /api/v2/resource/{public_id}/: + parameters: + - { name: public_id, in: path, required: true, schema: { type: string, format: uuid } } + - { name: tenant_id, in: query, schema: { type: string }, description: "фильтр по компаниям (через запятую) при retrieve" } + get: + tags: [resources_v2] + summary: Получить расширенный ресурс по public_id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceExpandedRetrieve" } + "404": { description: "there is no resource with provided id" } + patch: + tags: [resources_v2] + summary: Частичное обновление расширенного ресурса + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" } + delete: + tags: [resources_v2] + summary: Мягкое удаление расширенного ресурса + responses: + "204": { description: No Content } + +components: + + schemas: + + PermissionType: + type: integer + description: "0=read, 1=write, 2=delete, 3=admin" + enum: [0, 1, 2, 3] + + ResourceRetrieve: + type: object + properties: + id: { type: string, format: uuid, description: "public_id" } + name: { type: string } + type: { type: integer, nullable: true } + tenant_id: { type: integer } + location: { type: integer, nullable: true } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + target_id: { type: integer, nullable: true } + code: { type: string } + + ResourceWrite: + type: object + properties: + id: { type: string, format: uuid, nullable: true, readOnly: true } + name: { type: string } + type: { type: integer, nullable: true } + tenant_id: { type: integer } + location: { type: integer, nullable: true } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + target_id: { type: integer, nullable: true } + code: { type: string } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, tenant_id, target_id] + + PaginatedResourceList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/ResourceRetrieve" } + + ResourceTypeRetrieve: + type: object + properties: + id: { type: integer } + name: { type: string } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + + ResourceTypeWrite: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name] + + PaginatedResourceTypeList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/ResourceTypeRetrieve" } + + LocationRetrieve: + type: object + properties: + id: { type: integer } + name: { type: string } + coordinate_system: { type: integer } + geometry: { type: object, description: "GeoJSON-геометрия" } + geography: { type: object, description: "GeoJSON-геометрия (geography)" } + latitude: { type: string, format: decimal } + longitude: { type: string, format: decimal } + created_by: { type: integer } + + LocationWrite: + allOf: + - $ref: "#/components/schemas/LocationRetrieve" + - type: object + properties: + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, coordinate_system, geometry, geography, latitude, longitude] + + PaginatedLocationList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/LocationRetrieve" } + + ResourcePermissionRetrieve: + type: object + properties: + id: { type: integer } + service_account: { type: string, format: uuid } + public_resource_id: { type: string, format: uuid } + type: { $ref: "#/components/schemas/PermissionType" } + created_by: { type: integer } + + ResourcePermissionWrite: + type: object + properties: + id: { type: integer, readOnly: true } + service_account: { type: string, format: uuid } + public_resource_id: { type: string, format: uuid } + type: { $ref: "#/components/schemas/PermissionType" } + created_by: { type: integer } + resource: { type: integer, description: "заполняется сервером по public_resource_id" } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [service_account, public_resource_id] + + PaginatedResourcePermissionList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/ResourcePermissionRetrieve" } + + CompanyPermission: + type: object + properties: + id: { type: integer, readOnly: true } + service_account: { type: string, format: uuid } + tenant_id: { type: integer } + created_by: { type: integer } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [service_account, tenant_id] + + PaginatedCompanyPermissionList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/CompanyPermission" } + + ResourcePhoto: + type: object + properties: + id: { type: integer } + author_id: { type: integer } + created_at: { type: string, format: date-time } + image: { type: string, format: uri } + + ResourceExpandedRead: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + type: { type: integer, nullable: true } + tenant_id: { type: integer } + location: { type: integer, nullable: true } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + target_id: { type: integer, nullable: true } + code: { type: string } + description: { type: string } + location_verbose: { type: string } + latitude: { type: number, format: float } + longitude: { type: number, format: float } + photos: + type: array + items: { $ref: "#/components/schemas/ResourcePhoto" } + attributes: { type: object } + planning_widget_id: { type: integer, nullable: true } + work_schedule_project_id: { type: integer, nullable: true } + show_in_overview: { type: boolean } + widgets: { type: object } + meta: { type: object } + + ResourceExpandedRetrieve: + allOf: + - $ref: "#/components/schemas/ResourceExpandedRead" + + PaginatedResourceExpandedList: + type: object + properties: + count: { type: integer } + next: { type: string, nullable: true } + previous: { type: string, nullable: true } + results: + type: array + items: { $ref: "#/components/schemas/ResourceExpandedRead" } + + ResourceExpandedWrite: + type: object + properties: + id: { type: string, format: uuid, nullable: true, readOnly: true } + name: { type: string } + type: { type: integer, nullable: true } + tenant_id: { type: integer } + parent_id: { type: integer, nullable: true } + created_by: { type: integer } + target_id: { type: integer, nullable: true } + description: { type: string } + location_verbose: { type: string } + latitude: { type: number, format: float } + longitude: { type: number, format: float } + attributes: { type: object } + planning_widget_id: { type: integer, nullable: true } + work_schedule_project_id: { type: integer, nullable: true } + code: { type: string } + show_in_overview: { type: boolean } + widgets: { type: object } + meta: { type: object } + required: [name, tenant_id, target_id] + + ResourceExpandedPartialUpdate: + type: object + properties: + id: { type: string, format: uuid, readOnly: true } + name: { type: string } + type: { type: integer, nullable: true } + tenant_id: { type: integer, readOnly: true } + location: { type: integer, nullable: true } + parent_id: { type: integer, nullable: true } + created_by: { type: integer, readOnly: true } + target_id: { type: integer, readOnly: true } + code: { type: string } + description: { type: string } + location_verbose: { type: string } + latitude: { type: number, format: float } + longitude: { type: number, format: float } + attributes: { type: object } + planning_widget_id: { type: integer, nullable: true } + show_in_overview: { type: boolean } + widgets: { type: object } + meta: { type: object } diff --git a/apps/reviews/ENDPOINTS.md b/apps/reviews/ENDPOINTS.md new file mode 100644 index 0000000..e14cd23 --- /dev/null +++ b/apps/reviews/ENDPOINTS.md @@ -0,0 +1,226 @@ +# Эндпоинты, с которыми взаимодействует reviews-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `reviews-frontend` — страница обзора/согласования с проектами). + +## Как устроено взаимодействие + +В отличие от декларативного реестра `endpoints.ts`, запросы в `reviews-frontend` описаны императивно: в виде методов API-объектов и отдельных функций в каталоге `module/api/` (а также в нескольких сторах/страницах). Каждый вызов идёт через единый `httpService` (`module/api/http-service.ts`), созданный `createHttpService(...)` из `@sarex-team/sdk-js` (поверх `axios`). + +Вызов задаётся объектом со следующими полями: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- метод определяется функцией `httpService` (`getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`); +- `url` — путь запроса **относительно базового хоста сервиса** (базовый хост уже включает версионный префикс, напр. `/api/v1`); +- `data` — тело запроса (для POST/PUT/PATCH); +- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer`, `responseType: "blob"`, `timeout` и т.п.); +- `controller` — `AbortController` для отмены запроса; +- `showErrorNotification` — показывать ли уведомление об ошибке (обрабатывается на стороне SDK). + +Базовый хост подставляется SDK по имени `service` в зависимости от `BUILD_ENV` (см. `module/api/hosts.ts`). Итоговый URL = `<базовый хост сервиса>` + `url`. + +Основные точки, где выполняются запросы: + +| Файл | Экспорт | Назначение | +| --- | --- | --- | +| `module/api/index.ts` | `ReviewAPI`, `DocumentAPI`, `IssuesAPI` + отдельные функции (`getUsersByResourceId`, `getUsersByCompanyId`, `getDepartments*`, `getPositions*`, `getMrpaList`, `fetchParentDocumentByResourceId`, `fetchDocumentsBundleVersions`, `getDocumentAncestors`, `getDiskDocumentsPath`, `fetchExportReviews*`) | Ядро API: reviews, задачи, документы, справочники, экспорт | +| `module/api/agents.ts` | `AgentsAPI` | AI-агент подбора путей копирования (router-agent) | +| `module/api/checklists.ts` | `ChecklistsAPI` | Чек-листы и их результаты | +| `module/api/documentations.ts` | `DocumentationsAPI` | Дети папок с активными процессами | +| `module/api/marks.ts` | `MarksAPI` | Оркестрация штампов/подписей | +| `module/api/tranmittals.ts` | `TransmittalsAPI` | Создание трансмитталов и работа с шаблонами | +| `module/pages/Review/CheckList/AiCheck/api.ts` | `AiCheckAPI` | AI-проверка документов по чек-листу | +| `module/store/stores/resources.ts` | `ResourcesStore.fetchResources` | Список ресурсов компании | +| `module/store/stores/users.ts` | `Users.fetchSettings` | Клиентские настройки пользователя | + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Базовый хост уже включает версионный префикс сервиса, поэтому в таблицах эндпоинтов ниже указан только `url` (без него). + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `flows` | Сервис рабочих процессов (reviews, задачи, документы review) | `https://stage-api.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | +| `documentations` | Сервис документации (бандлы, файлы, штампы, подписи) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | +| `gateway_api_v1` | Gateway API v1 (ресурсы, документы, версии бандлов) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` | +| `gateway_api_v2` | Gateway API v2 (пользователи по ресурсу) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` | +| `eav_api_v0` | Сервис атрибутов (EAV) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` | +| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` | +| `transmittals` | Сервис передачи документации (трансмитталы, шаблоны) | `https://stage-api.sarex.io/transmittals/api/v1` | `https://api.sarex.io/transmittals/api/v1` | +| `issues` | Сервис замечаний | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` | +| `orchestrator` | Оркестратор процессов (штампы/подписи) | `https://stage-api.sarex.io/orchestrator` | `https://api.sarex.io/orchestrator/api` | +| `files` | Сервис файлов (скачивание бандлов) | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` | +| `lambdas` | Сервис экспорта (lambda-функции) | `https://stage-api.sarex.io/lambdas` | `https://api.sarex.io/lambdas` | +| `sarexAgents` | Сервис AI-агентов (проверка, подбор путей, загрузка файлов) | `https://sarex-agents.dev.stage.sarex.io/api/v1` | `https://agents.sarex.tech/api/v1` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local`, `preprod` и `contour`. В `contour` все хосты — относительные пути (изолированный контур), а `zitadel` пуст. В `local` сервис `sarex` указывает на `https://stage.sarex.io`, а `httpService` переключается в режим `zitadel` (`setTypeOfHttpService("zitadel")` в `http-service.ts`). Подключаемый удалённый модуль `documentations` (Module Federation) описан отдельно в `module/api/module-hosts.ts`. + +## Эндпоинты по сервисам + +### `flows` — Сервис рабочих процессов (reviews) + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `ReviewAPI.getReviews` | POST | `/reviews/filter/` | Список reviews с фильтрами (пагинация `limit`/`offset` в query) | +| `ReviewAPI.getReview` | GET | `/reviews/{id}/` | Review по id | +| `ReviewAPI.getReviewsByDocumentIds` | GET | `/documents/?document_ids={ids}&full=true&review_status=completed,canceled&limit=100000` | Документы review по id документов | +| `ReviewAPI.getReviewsByBundleCopiedIds` | GET | `/documents/?bundle_copied_ids={ids}&full=true&review_status=completed,canceled&limit=100000` | Документы review по id скопированных бандлов | +| `ReviewAPI.fetchCurrentTasks` | GET | `/tasks/` | Текущие задачи (фильтры `reviewer_id`, `is_active`, `resource_id`, пагинация) | +| `ReviewAPI.changePriorityTask` | PATCH | `/tasks/{id}/change-priority/` | Изменить приоритет задачи | +| `ReviewAPI.changeDurationTask` | PATCH | `/tasks/{id}/change-duration/` | Изменить длительность задачи | +| `ReviewAPI.getTasksEndDates` | GET | `/tasks/reviewers-max-end-dates/?{query}` | Макс. даты завершения по проверяющим | +| `ReviewAPI.getCountByResourceId` | POST | `/reviews/count_by_resource_id/` | Количество reviews по ресурсам | +| `ReviewAPI.getCountByReviewers` | POST | `/reviews/count_by_reviewer_id/` | Количество reviews по проверяющим | +| `ReviewAPI.getNextStepReviewers` | GET | `/steps/{stepId}/get_reviewers/?review_id={reviewId}` | Проверяющие следующего шага | +| `ReviewAPI.getReviewDocuments` | GET | `/reviews/{id}/documents/` | Документы review | +| `ReviewAPI.updateReviewDocument` | PUT | `/documents/{id}/` | Обновить документ review | +| `ReviewAPI.bulkUpdateReviewDocument` | PUT | `/reviews/{reviewId}/documents/` | Массовое обновление документов review | +| `ReviewAPI.setStatus` | PATCH | `/documents/set-status/?document_ids={ids}` | Проставить статус документам | +| `ReviewAPI.createReview` | POST | `/reviews/` | Создать review | +| `ReviewAPI.changeReviewers` | PATCH | `/reviews/{reviewId}/change_reviewers/` | Сменить проверяющих | +| `ReviewAPI.changeMinReviewers` | PATCH | `/reviews/{reviewId}/change-min-reviewers/` | Изменить мин. число проверяющих | +| `ReviewAPI.getTimeTrackerInfo` | GET | `/reviews/{reviewId}/time-tracking/` | Данные тайм-трекинга review | +| `ReviewAPI.startReview` | PATCH | `/reviews/{reviewId}/start/` | Запустить review | +| `ReviewAPI.patchReview` | PATCH | `/reviews/{id}/` | Обновить атрибуты review | +| `ReviewAPI.deleteReview` | DELETE | `/reviews/{id}/` | Удалить review | +| `ReviewAPI.activateReview` / `ReviewAPI.passReview` | PATCH | `/reviews/{id}/approve/` | Утвердить/пройти review (с комментарием и статусом) | +| `ReviewAPI.setReviewStep` | PATCH | `/reviews/{review_id}/set-step/{step_id}/` | Установить шаг review | +| `ReviewAPI.reviewUpdateBundles` | PATCH | `/reviews/{id}/update-bundles/` | Обновить бандлы review | +| `ReviewAPI.userAction` | POST | `/user-actions/` | Записать действие пользователя | +| `ReviewAPI.writeTransmittalCreated` | POST | `/reviews/{review_id}/transmittal-created/` | Отметить создание трансмиттала для review | +| `ReviewAPI.getProcesses` | GET | `/flows/light/?{query}` | Список процессов (облегчённый) | +| `ReviewAPI.getProcessById` | GET | `/flows/{id}/` | Процесс по id | +| `DocumentAPI.changeCopyPaths` | PATCH | `/documents/change-copy-paths/` | Изменить пути копирования документов | +| `ChecklistsAPI.createReviewsChecklistResult` | PATCH | `/reviews/{reviewId}/checklist-results/` | Результаты чек-листа для review | + +### `documentations` — Сервис документации + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `DocumentAPI.getDocumentsBatch` | POST | `/documents/batch` | Пакетное получение документов | +| `DocumentAPI.mark` | PUT | `/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи | +| `DocumentAPI.sign` | POST | `/bundles/{bundleId}/sign` | Подписать бандл | +| `DocumentAPI.downloadFile` | GET | `/bundles/{bundleId}/{key}/download` | Скачать файл (ответ `blob`) | +| `DocumentAPI.getDisks` | GET | `/disks` | Список дисков | +| `DocumentationsAPI.getFolderChildrenWithActiveProcesses` | POST | `/documents/flows` | Дети папок с активными процессами | +| `AiCheckAPI.getBundlePresignedUrl` | GET | `/bundles/{bundleDocumentId}/presigned_url?key=pdf` | Presigned-URL PDF бандла | + +### `gateway_api_v1` — Gateway API v1 + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `fetchResources` (`ResourcesStore`) | GET | `/resources/?company_id={companyId}` | Список ресурсов компании | +| `fetchParentDocumentByResourceId` | GET | `/resources-rpc/parent-document-by-resource-id/{resourceId}/` | Родительский документ по resource id | +| `fetchDocumentsBundleVersions` | POST | `/documents/bundle_versions` | Версии бандлов документов | +| `getDocumentAncestors` | POST | `/documents/ancestors` | Предки документов | +| `getDiskDocumentsPath` | GET | `/disks/{diskId}/documents?child_id={childId}` | Путь документа на диске | + +### `gateway_api_v2` — Gateway API v2 + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `getUsersByResourceId` | GET | `/users/?{query}` | Пользователи по ресурсу (с правами) | + +### `sarex` — Локальный сервис данных + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `Users.fetchSettings` | GET | `/api/client/settings/` | Клиентские настройки пользователя | +| `getUsersByCompanyId` | GET | `/api/core/users/?company={companyId}&{query}` | Пользователи компании | +| `getDepartments` | GET | `/api/core/admin/departments/` | Отделы | +| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={companyId}&{query}` | Отделы компании (пагинация) | +| `getPositions` | GET | `/api/core/admin/positions/` | Должности | +| `getPositionsV2` | GET | `/api/core/admin/positions/?company={companyId}&{query}` | Должности компании (пагинация) | +| `getMrpaList` | POST | `/api/core/mrpa/list/` | Список MRPA | + +### `eav_api_v0` — Сервис атрибутов (EAV) + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `ReviewAPI.getAttributes` | GET | `/schema/?model_name=flow&company_id={companyId}` | Схема атрибутов модели `flow` | + +### `checklists` — Сервис чек-листов + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `ChecklistsAPI.getChecklist` | GET | `/checklists/{id}/` | Чек-лист по id | +| `ChecklistsAPI.getChecklistResults` | GET | `/results/` | Результаты чек-листов (фильтры в query) | +| `ChecklistsAPI.createChecklistResult` | POST | `/results/` | Создать результат чек-листа | +| `ChecklistsAPI.updateChecklistResult` | PATCH | `/results/{id}/` | Обновить результат чек-листа | + +### `transmittals` — Сервис передачи документации + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `TransmittalsAPI.createTransmittal` | POST | `/transmittals/create` | Создать трансмиттал | +| `TransmittalsAPI.getTransmittals` | POST | `/transmittals` | Список трансмитталов ресурса (пагинация по `bookmark`) | +| `TransmittalsAPI.getTemplate` | GET | `/transmittal_templates/{templateId}?resource={resourceId}` | Шаблон трансмиттала по id | +| `TransmittalsAPI.getSelectTemplates` | GET | `/transmittal_templates/select?resource={resourceId}` | Список шаблонов для выбора | + +### `issues` — Сервис замечаний + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `IssuesAPI.getIssues` | POST | `/issues/filter/` | Список замечаний с фильтрами (пагинация в query) | +| `IssuesAPI.getIssuesTypesStatusModelsByCompanyId` | GET | `/status-models/?company_id={companyId}` | Модели статусов замечаний компании | + +### `orchestrator` — Оркестратор процессов + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `MarksAPI.createMarkFlow` | POST | `/process` | Запустить процесс маркировки | +| `MarksAPI.getMarkFlow` | GET | `/process/{id}` | Процесс маркировки по id | +| `MarksAPI.startSign` | POST | `/sign` | Запустить подписание | + +### `files` — Сервис файлов + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `DocumentAPI.downloadDocuments` | POST | `/documents/` | Скачать документы по `bundle_ids` (ответ `blob`) | + +### `lambdas` — Сервис экспорта + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `fetchExportReviewsByResourceIDs` | GET | `/export-reviews/{params}` | Экспорт reviews в XLSX (ответ `blob`) | +| `fetchExportReview` | GET | `/export-reviews/{reviewId}/report/` | Экспорт отчёта по review в PDF (ответ `blob`) | + +### `sarexAgents` — Сервис AI-агентов + +| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение | +| --- | --- | --- | --- | +| `AgentsAPI.suggestCopyPaths` | POST | `/runs/wait` | Запуск `router-agent` для подбора путей копирования | +| `AiCheckAPI.runValidator` | POST | `/runs/wait` | Запуск AI-проверки документов по чек-листу | +| `AiCheckAPI.getDocumentStorageStatus` | GET | `/files/documents/{documentId}/storage-status?tenant_id={tenantId}` | Статус загрузки документа в хранилище | +| `AiCheckAPI.uploadFileByUrl` | POST | `/files/upload/url` | Загрузить файл по URL в RAG-каталог | +| `AiCheckAPI.getEntitled` | GET | `/internal/tenant-agent-entitlements/{tenantId}/agents-status?user_id={userId}` | Доступность AI-агента для тенанта | + +> Запросы `/runs/wait` выполняются с увеличенным таймаутом `RUN_WAIT_TIMEOUT_MS = 600000` мс (10 минут) и с `showErrorNotification: false`. + +## Удалённый модуль (Module Federation) + +Помимо HTTP-API, `reviews-frontend` подключает удалённый микрофронтенд `documentations` через Module Federation (`module/api/module-hosts.ts`, функция `getModuleHost`): + +| Модуль | `stage` | `prod` | +| --- | --- | --- | +| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | + +В `contour` путь относительный (`/documentations/static/module/remoteEntry.js`), в `preprod` — `https://modules.preprod.sarex.io/...`. + +## Аутентификация + +Токен и режим аутентификации обеспечиваются `@sarex-team/sdk-js`. В окружении `local` `httpService` переводится в режim `zitadel` (`setTypeOfHttpService("zitadel")`), а хост IdP берётся из `hosts.zitadel` (`https://idp.dev.stage.sarex.io` для stage, `https://login.sarex.io` для prod). В остальных окружениях используется режим по умолчанию SDK. + +## Обработка ошибок + +Отдельного модуля маппинга ошибок (аналогичного `errors.ts`) в `reviews-frontend` нет. Обработка ошибок выполняется в двух местах: + +- **SDK `@sarex-team/sdk-js`** — при `showErrorNotification: true` (значение по умолчанию для большинства запросов) показывает пользователю уведомление об ошибке. Для «тихих» запросов (AI-агенты, presigned-URL, часть фоновых вызовов) явно задаётся `showErrorNotification: false`. +- **Локальные `try/catch`** — в сторах (`resources.ts`, `users.ts`) и функциях экспорта (`fetchExportReviews*`) ошибки перехватываются и логируются через `console.error`, без проброса наверх. + +## Замечания + +- Пути (`url`) указываются **относительно** базового хоста сервиса, который уже содержит версионный префикс (`/api/v1`, `/api/v0` и т.п.). Это отличается от реестра `endpoints.ts` в некоторых других микрофронтендах, где префикс включается в путь эндпоинта. +- Файл `module/api/tranmittals.ts` назван с опечаткой (`tranmittals` вместо `transmittals`); экспорт при этом называется `TransmittalsAPI`. +- Сервис `sarex` в окружениях `stage`/`prod`/`preprod`/`contour` имеет пустой базовый хост (`""`) — запросы идут по относительным путям (через тот же origin/реверс-прокси); в `local` он указывает на `https://stage.sarex.io`. +- Хост сервиса `orchestrator` в `prod` содержит суффикс `/api` (`.../orchestrator/api`), тогда как в `stage`/`preprod` — без него (`.../orchestrator`); пути методов (`/process`, `/sign`) одинаковы. diff --git a/apps/rfi/.env.example b/apps/rfi/.env.example new file mode 100644 index 0000000..4e9630f --- /dev/null +++ b/apps/rfi/.env.example @@ -0,0 +1,57 @@ +# PYTHONPATH (обязательно указывает на каталог с исходниками) +PYTHONPATH=src + +# Django +DJANGO_SECRET_KEY="django-insecure-a2o70(=hkv486m(3l-(6d52y5-1&98qokc06%43_uj_gmr^v-a" + +# Database (PostgreSQL) +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=postgres +DB_USER=postgres +DB_PASSWORD=postgres + +# Auth +# false — включён DefaultUserAuthentication (демо-пользователь для локальной разработки), +# true — Zitadel + JWT (проверка подписи RS512) +JWT_AUTH_ENABLE=false + +# Sarex backend (core/users, users_by_sa) — Basic-auth +SAREX_BACKEND_URL=https://stage.sarex.io +SAREX_BACKEND_AUTH=base64(username:password) +SAREX_BACKEND_TIMEOUT=30 + +# EAV (сервис атрибутов) +EAV_URL=http://eav-service.eav-stage + +# Gateway (resources) +GATEWAY_URL=https://stage-api.sarex.io/gateway + +# Resources API (IAM/resources) — используется в проде через Helm +# RESOURCES_API_HOST=http://iams.platform.svc.cluster.local:8080 + +# S3 (Yandex Object Storage) — используется в prod-настройках (config.settings.prod) +YC_S3_ACCESS_KEY_ID="" +YC_S3_SECRET_ACCESS_KEY="" +YC_S3_BUCKET_NAME="" +YC_S3_ENDPOINT_URL="" + +# Mailer service +MAILER_URL="http://mailer-service.mailer:8000" +MAILER_TIMEOUT=30 + +# Notifications +NOTIFICATIONS_ENABLE=True +NOTIFICATIONS_EMAIL_FROM=hello@sarex.io +NOTIFICATIONS_SERVICE_URL="https://stage.sarex.io/rfi" + +# RabbitMQ (брокер Celery) — дефолты заданы в settings/base.py +# RABBITMQ_USERNAME=mcc +# RABBITMQ_PASSWORD=mcc +# RABBITMQ_HOST=rabbitmq-service +# RABBITMQ_PORT=5672 +# RABBITMQ_VHOST=api + +# Celery +# True — уведомления отправляются асинхронно (apply_async), False — синхронно в процессе запроса +USE_ASYNC_FUNCTIONS=True diff --git a/apps/rfi/CONFIGURATION.md b/apps/rfi/CONFIGURATION.md new file mode 100644 index 0000000..9ab7102 --- /dev/null +++ b/apps/rfi/CONFIGURATION.md @@ -0,0 +1,211 @@ +# Конфигурация проекта rfi-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис — это Django-приложение (Django 5.1, Django REST Framework). Настройки лежат в пакете `src/config/settings/`: + +- `base.py` — общие настройки (`DEBUG = True`), читает переменные через `os.getenv(...)`; +- `prod.py` — наследует `base.py` (`from .base import *`), выключает `DEBUG` и добавляет S3-хранилище (`django-storages` + `boto3`). Активируется через `DJANGO_SETTINGS_MODULE=config.settings.prod` (см. `docker/http/entrypoint.sh`). + +Часть настроек уведомлений вынесена в отдельные классы `pydantic-settings` (`src/notifications/config.py`): `MailerConfig` (`env_prefix="MAILER_"`), `SarexBackendConfig` (`env_prefix="SAREX_BACKEND_"`), `NotificationsConfig` (`env_prefix="NOTIFICATIONS_"`). Эти классы имеют значения по умолчанию и переопределяются переменными окружения с соответствующим префиксом. + +Отдельного конфиг-файла (yaml/toml) для приложения нет. Все настройки — переменные окружения процесса. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально | `Makefile` через `SET_ENV`: `set -a; source .env; set +a`. Файл `.env` создаётся из `.env.template` вручную. `PYTHONPATH=src` обязателен | +| Docker (prod) | `docker/http/Dockerfile` + `entrypoint.sh`: миграции и `uwsgi` под `DJANGO_SETTINGS_MODULE=config.settings.prod`. Переменные пробрасываются рантаймом (Helm) | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `services.api.envs` / `services.api.secretEnvs` (и аналогичные для `services.celery`) universal-chart. Значения различаются по окружению (`_default`/`stage`/`preprod`/`production`) | +| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` переключают `STAND`/`NAMESPACE`, job `lint` гоняет ruff | + +Точки входа и процессы: + +| Процесс | Команда | Назначение | +| --- | --- | --- | +| HTTP API | `uv run src/manage.py runserver` (`make run`) / `uwsgi --ini uwsgi.ini` (prod) | Django REST API | +| Celery worker | `uv run celery -A config worker -l info` | Асинхронные задачи уведомлений (`notifications.tasks`) | +| Миграции | `uv run src/manage.py migrate` (`make migrate`) | Применение миграций (в контейнере — в `entrypoint.sh` перед стартом uwsgi) | + +Приложение по умолчанию слушает порт **8000** (uwsgi `http = 0.0.0.0:8000`; Django `runserver` — тоже 8000). Внутрикластерный Service слушает порт 80 → targetPort 8000. + +## Переменные приложения + +Дефолт `—` означает, что значение обязательно (читается `os.getenv` без дефолта; при отсутствии будет `None`, что приведёт к ошибке подключения/старта соответствующей подсистемы). + +### Django core + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `PYTHONPATH` | string | — | Должна быть `src` — корень пакета исходников | +| `DJANGO_SECRET_KEY` | string | — | Секретный ключ Django (`SECRET_KEY`) | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.base` | Модуль настроек. В контейнере — `config.settings.prod` | + +Значения `DEBUG`, `ALLOWED_HOSTS = ["*"]`, `LANGUAGE_CODE = ru-ru`, `TIME_ZONE = UTC` заданы в коде и не читаются из окружения. + +### Auth (`JWT_AUTH_ENABLE`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_AUTH_ENABLE` | bool (`True`/`true`) | `false` | Режим аутентификации. `true` — включаются `ZitadelJWTStatelessUserAuthentication` и `JWTStatelessUserAuthentication`; `false` — `DefaultUserAuthentication` (демо-пользователь `DefaultUser`, только для локальной разработки) | + +Алгоритм JWT фиксирован в `SIMPLE_JWT` — `RS512`. Класс пользователя токена — `core.auth.User`. + +### Database (`DB_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_HOST` | string | — | Хост PostgreSQL | +| `DB_PORT` | int | — | Порт PostgreSQL | +| `DB_NAME` | string | — | Имя базы данных | +| `DB_USER` | string | — | Пользователь БД | +| `DB_PASSWORD` | string | — | Пароль пользователя БД | + +Движок фиксирован: `django.db.backends.postgresql`. + +### Sarex backend (`SAREX_BACKEND_*`) + +Используется в `core/services.py` (получение пользователей `core/users/`, `users_by_sa/`) и в `notifications` (`SarexBackendConfig`). Авторизация — Basic (`Authorization: Basic `). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SAREX_BACKEND_URL` | string | `https://stage.sarex.io` | Базовый URL sarex-backend | +| `SAREX_BACKEND_AUTH` | string (base64) | — | Basic-auth в виде `base64(username:password)` | +| `SAREX_BACKEND_TIMEOUT` | int | `30` | Таймаут запроса (используется `SarexBackendConfig`) | + +### Внешние сервисы (EAV, Gateway, Resources) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `EAV_URL` | string | — | Базовый URL EAV-сервиса атрибутов (`core/services.get_attributes`) | +| `GATEWAY_URL` | string | — | Базовый URL gateway (`resources/`) для получения имени/списка ресурсов | +| `RESOURCES_API_HOST` | string | — | Адрес IAM/resources API. Задаётся в Helm (в `.env.template` отсутствует) | + +### S3 / Object Storage (`YC_S3_*`) + +Читаются только в `config/settings/prod.py` (backend хранилища — `storages.backends.s3boto3.S3Boto3Storage`, ACL `public-read`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | — | Access key (`AWS_ACCESS_KEY_ID`) | +| `YC_S3_SECRET_ACCESS_KEY` | string | — | Secret key (`AWS_SECRET_ACCESS_KEY`) | +| `YC_S3_BUCKET_NAME` | string | — | Бакет (`AWS_STORAGE_BUCKET_NAME`) | +| `YC_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 (`AWS_S3_ENDPOINT_URL`) | + +### Mailer (`MAILER_*`) + +Настройки клиента почтового сервиса (`MailerConfig`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `MAILER_URL` | string | `http://mailer-service.mailer:8000` | URL mailer-сервиса | +| `MAILER_TIMEOUT` | int | `30` | Таймаут запроса (сек) | + +### Notifications (`NOTIFICATIONS_*`) + +Настройки подсистемы уведомлений (`NotificationsConfig`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `NOTIFICATIONS_ENABLE` | bool | `True` | Включить отправку уведомлений | +| `NOTIFICATIONS_EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя | +| `NOTIFICATIONS_SERVICE_URL` | string | `https://stage.sarex.io/rfi` | Внешний URL сервиса (для ссылок в письмах) | + +### RabbitMQ (`RABBITMQ_*`) + +Используются при сборке `CELERY_BROKER_URL` в `settings/base.py`. У всех заданы дефолты. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `RABBITMQ_USERNAME` | string | `mcc` | Пользователь | +| `RABBITMQ_PASSWORD` | string | `mcc` | Пароль | +| `RABBITMQ_HOST` | string | `rabbitmq-service` | Хост | +| `RABBITMQ_PORT` | int | `5672` | Порт (задаётся в Helm через `envs`) | +| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост | + +К итоговому URL добавляется `?heartbeat=30` (`BROKER_HEARTBEAT`). Прочие параметры брокера/Celery заданы в коде: `BROKER_POOL_LIMIT=10`, очередь `default`, `CELERY_TASK_SERIALIZER=json`, `CELERY_TIMEZONE=Europe/Moscow`, `CELERY_IMPORTS=("notifications.tasks",)`. + +### Celery-поведение (`USE_ASYNC_FUNCTIONS`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_ASYNC_FUNCTIONS` | bool | `True` | `True` — уведомления отправляются через `apply_async` (нужен работающий воркер и брокер); `False` — синхронно внутри запроса | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Чарт — обёртка над `universal-chart` (`oci://cr.yandex/.../charts`, версия 0.1.7). Описаны два сервиса: `api` и `celery` (одинаковый образ `cr.yandex/crp3ccidau046kdj8g9q/rfi-backend`, разные команды и ресурсы). Значения выбираются по ключу `global.env` (`_default`/`stage`/`preprod`/`production`). + +Обычные значения (`services..envs`): `JWT_AUTH_ENABLE`, `SAREX_BACKEND_URL`, `EAV_URL`, `GATEWAY_URL`, `NOTIFICATIONS_ENABLE`, `NOTIFICATIONS_EMAIL_FROM`, `NOTIFICATIONS_SERVICE_URL`, `RABBITMQ_PORT`, `RABBITMQ_HOST`, `RESOURCES_API_HOST`. + +Значения из секретов (`services..secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default`) | Секрет (`production`) | Ключ | +| --- | --- | --- | --- | +| `DJANGO_SECRET_KEY` | `rfi-backend-api-django-secret` | `django-secret` | `django_secret_key` | +| `DB_HOST` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `host` | +| `DB_PORT` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `port` | +| `DB_NAME` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `dbname` | +| `DB_USER` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `user` | +| `DB_PASSWORD` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `password` | +| `SAREX_BACKEND_AUTH` | `django-secret` | `django-secret` | `token` | +| `YC_S3_ACCESS_KEY_ID` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `key_id` | +| `YC_S3_SECRET_ACCESS_KEY` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `access_key` | +| `YC_S3_BUCKET_NAME` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `storage_bucket_name` | +| `YC_S3_ENDPOINT_URL` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `endpoint_url` | +| `RABBITMQ_VHOST` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `vhost` | +| `RABBITMQ_USERNAME` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `username` | +| `RABBITMQ_PASSWORD` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `password` | + +Значения `envs` по окружениям (ключевые различия): + +| Переменная | stage | preprod | production | +| --- | --- | --- | --- | +| `SAREX_BACKEND_URL` | `https://stage.sarex.io` | `https://perprod.sarex.io` | `https://lk.sarex.io` | +| `EAV_URL` | `http://eav-service.eav-stage` | `http://eav-service.eav-preprod` | `http://eav-service.eav-prod` | +| `GATEWAY_URL` | `https://stage-api.sarex.io/gateway` | `https://api.perprod.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `NOTIFICATIONS_SERVICE_URL` | `https://stage.sarex.io/rfi` | `https://perprod.sarex.io/rfi` | `https://lk.sarex.io/rfi` | +| `RABBITMQ_HOST` | `rabbitmq.rabbitmq.svc.cluster.local` | `rabbitmq.rabbitmq.svc.cluster.local` | `default-rabbit-cluster.rabbitmq.svc` | +| `RESOURCES_API_HOST` | `http://iams.platform.svc.cluster.local:8080` | `http://sarex-resources-service.resources-preprod` | `http://iams.iam.svc.cluster.local:8080` | + +Прочие параметры чарта (не переменные приложения): `deployment.*` (имя, реплики — prod: api 2 / celery 2, ресурсы — api 1024Mi/1cpu, celery 512Mi/1cpu), `image.*`, `service.*` (ClusterIP, 80→8000), `imagePullSecrets: dockerhub`, `probes` (liveness/readiness выключены). + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Общие переменные: `SERVICE_NAME=rfi-backend`, `DOCKERFILE_PATH=./docker/http/Dockerfile`, `CI_TRIGGER_SOURCE=app`. + +Переключение окружения (`workflow.rules`): + +| Условие | STAND | NAMESPACE | CHART_VERSION | +| --- | --- | --- | --- | +| ветка `stage` | `stage` | `proc` | `0.0.1-stage` | +| ветка `master` | `preprod` | `rfi-preprod` | `0.0.1-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `rfi-prod` | `0.0.1-prod` | +| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Через `HELM_SET_ARGS` в чарт прокидываются образ (`services.api.image.name.`, `services.celery.image.name.`), `global.env`, а также метаданные коммита/джобы (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`). Job `lint` (образ `ghcr.io/astral-sh/uv`) прогоняет `ruff check` и `ruff format --check`. + +## Замечания и потенциальные проблемы + +- `PYTHONPATH=src` обязателен: без него не находятся пакеты (`config`, `core`, `request` и т.д.). Задан в `.env.template` и в `Dockerfile` (`ENV PYTHONPATH=src`). +- Приложение не загружает `.env` автоматически: Django-настройки читают `os.getenv`, а `.env` подхватывается только через `Makefile` (`set -a; source .env`). При ручном запуске переменные нужно экспортировать самому. +- `JWT_AUTH_ENABLE=false` включает `DefaultUser` с захардкоженными `id=356`, `company_ids=[1, 38]` и полным набором прав — это только для локальной отладки, в проде должно быть `true`. +- S3 (`YC_S3_*`) читается только в `config.settings.prod`. При запуске с `config.settings.base` (локально) хранилище S3 не используется — файлы (аватар RFI) сохраняются локально. +- В `.env.template` переменные `RABBITMQ_*` и `RESOURCES_API_HOST` отсутствуют — для Celery используются дефолты из кода (`mcc`/`rabbitmq-service`/`api`), в кластере они приходят из Helm (`envs` + `secretEnvs`). +- В values.yaml значения preprod для sarex/gateway записаны как `perprod` (`https://perprod.sarex.io`, `.../api.perprod.sarex.io/...`) — так в исходнике; при необходимости свериться с фактическими адресами. +- `USE_ASYNC_FUNCTIONS` через `os.getenv("USE_ASYNC_FUNCTIONS", True)` возвращает строку при заданной переменной; пустая/незаданная даёт `True`. Любая непустая строка (в т.ч. `"False"`) трактуется как истинное значение в условии `if settings.USE_ASYNC_FUNCTIONS`. + +## Минимальный набор для локального запуска + +С включённым `config.settings.base` и `JWT_AUTH_ENABLE=false` минимально необходимо: + +- `PYTHONPATH=src`, `DJANGO_SECRET_KEY` +- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` (поднять PostgreSQL) +- `SAREX_BACKEND_URL`, `SAREX_BACKEND_AUTH` (для обращений к sarex-backend) +- `EAV_URL`, `GATEWAY_URL` (для атрибутов и ресурсов) +- `NOTIFICATIONS_ENABLE=False` — чтобы не требовался mailer/брокер; либо `True` + `MAILER_URL` и работающий RabbitMQ +- `USE_ASYNC_FUNCTIONS=False` — чтобы уведомления шли синхронно без Celery-воркера + +Порядок: `make migrate` → `make run` (API) и при необходимости `uv run celery -A config worker -l info` (воркер). Готовые значения-примеры — в `.env.example`. diff --git a/apps/rfi/ENDPOINTS.md b/apps/rfi/ENDPOINTS.md new file mode 100644 index 0000000..4bcf6b0 --- /dev/null +++ b/apps/rfi/ENDPOINTS.md @@ -0,0 +1,100 @@ +# Эндпоинты, с которыми взаимодействует rfi-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `rfi-frontend`). + +## Как устроено взаимодействие + +Все запросы собраны в реестре `module/api/index.ts` и сгруппированы по объектам-«API»: `RfiAPI`, `AttachmentsAPI`, `CoreAPI`, `ResourcesAPI` и функция `getAttributes`. Каждый вызов идёт через единый `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх axios). + +Вызов задаётся объектом: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (дописывается к базовому хосту сервиса); +- `data` — тело запроса (для POST/PUT/PATCH); +- `axiosConfig` — доп. настройки axios, чаще всего `params` (query-параметры: `limit`, `offset`, `company_id` и т.п.); +- `showErrorNotification` — показывать ли уведомление об ошибке; +- `controller` — `AbortController` для отмены запроса. + +Методы `httpService`: `getRequest`, `postRequest`, `putRequest`, `patchRequest`, `deleteRequest`. Базовый хост подставляется по `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV`. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url`. Использовано пять сервисов (остальные ключи в `hosts.ts` объявлены, но модулем не вызываются). + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `rfi` | Собственный backend RFI (этот сервис) | `https://stage-api.sarex.io/rfi/api/v1` | `https://api.sarex.io/rfi/api/v1` | +| `sarexApi` | Gateway/API Sarex (attachments, users v2) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `sarex` | Локальный backend Sarex (`/api/core`, `/api/client`) | `https://stage.sarex.io` | `https://lk.sarex.io` | +| `gateway_api_v1` | Gateway API v1 (resources) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` | +| `eav_api_v0` | EAV — сервис атрибутов/схем | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` | + +> Также определены окружения `local`, `preprod` и `contour`. В `local` сервис `sarex` проксируется на `/sarex-backend`; в `contour` используются относительные пути. Подключаемый удалённый модуль documentations описан отдельно в `module/api/module-hosts.ts` (Module Federation `remoteEntry.js`). + +## Эндпоинты по сервисам + +### `rfi` — Backend RFI (этот сервис) + +`RfiAPI` из `module/api/index.ts`. Пути указаны относительно базы `.../rfi/api/v1`. + +| Метод (`RfiAPI`) | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `getRfi` | POST | `/rfi/filter/` | Список RFI по фильтру (query `limit`/`offset`, тело — фильтры) | +| `postRfi` | POST | `/rfi/` | Создать RFI | +| `putRfi` | PUT | `/rfi/{rfiId}/` | Полное обновление RFI | +| `patchRfi` | PATCH | `/rfi/{rfiId}/` | Частичное обновление RFI | +| `copyRfi` | POST | `/rfi/{rfiId}/copy/` | Скопировать RFI (тело `{ name }`) | +| `getRfiById` | GET | `/rfi/{id}/` | RFI по id | +| `deleteRfi` | DELETE | `/rfi/{id}/` | Удалить RFI (soft-delete) | +| `getHistory` | GET | `/rfi/history/` | История изменений по списку RFI (query-параметры фильтра) | +| `getStatusCount` | POST | `/rfi/status-count/` | Количество RFI по статусам (по `resource_id`) | +| `getPriorityCount` | POST | `/rfi/priority-count/` | Количество RFI по приоритетам (по `resource_id`) | +| `createRfiMessage` | POST | `/messages/` | Создать сообщение в RFI | +| `getMessagesByRfiId` | GET | `/messages/?request_id={id}` | Сообщения по id запроса | +| `patchRfiMessage` | PATCH | `/messages/{id}/` | Отметить сообщение решением (`is_solution`) | +| `getStatuses` | GET | `/statuses/` | Список статусов (query `company_id`, `limit`, `offset`) | +| `getStatusModels` | GET | `/status-models/` | Модели статусов компании (query `company_id`) | +| `getPriorities` | GET | `/priorities/` | Список приоритетов (query `company_id`, `limit`, `offset`) | +| `getPriorityModels` | GET | `/priority-models/` | Модели приоритетов компании (query `company_id`) | + +### `sarexApi` — Gateway/API Sarex + +`AttachmentsAPI` и `CoreAPI.getUsersV2`. Пути указаны относительно базы `https://(stage-)api.sarex.io`. + +| Метод | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `AttachmentsAPI.uploadFiles` | POST | `/gateway/api/v1/attachments/` | Загрузить файлы (`multipart/form-data`) | +| `AttachmentsAPI.deleteFile` | DELETE | `/gateway/api/v1/attachments/{id}` | Удалить файл | +| `AttachmentsAPI.getFilesByRfiId` | GET | `/gateway/api/v1/attachments/?company_id={companyId}&instance_id={rfiId}&model_name={ATTACHMENTS_MODEL_NAME}` | Файлы, привязанные к RFI | +| `CoreAPI.getUsersV2` | GET | `/gateway/api/v2/users/` | Пользователи (query `company_id`, `permissions`, `resource_id`, `limit`, `offset`) | + +### `sarex` — Backend Sarex + +`CoreAPI`. Пути указаны относительно базы `https://stage.sarex.io` / `https://lk.sarex.io`. + +| Метод | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `CoreAPI.getUsers` | GET | `/api/core/users/` | Пользователи (query `company`, `perm`, `resource_id`, `limit`, `offset`) | +| `CoreAPI.getDepartments` | GET | `/api/core/admin/departments/?company={companyId}&{query}` | Отделы компании | +| `CoreAPI.getPositions` | GET | `/api/core/admin/positions/?company={companyId}&{query}` | Должности компании | +| `CoreAPI.getSettings` | GET | `/api/client/settings/` | Клиентские настройки | + +### `gateway_api_v1` — Gateway API v1 + +`ResourcesAPI`. База уже включает `/gateway/api/v1`. + +| Метод | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `ResourcesAPI.getResources` | GET | `/resources/` | Список ресурсов (query `company_id`) | + +### `eav_api_v0` — EAV (атрибуты) + +Функция `getAttributes`. База уже включает `/eav/api/v0`. + +| Метод | HTTP | Путь | Назначение | +| --- | --- | --- | --- | +| `getAttributes` | GET | `/schema/?model_name=flow&company_id={companyId}` | Схема атрибутов по компании | + +## Обработка ошибок + +Ошибки обрабатываются в `httpService` (`@sarex-team/sdk-js`). Тип ответа с ошибкой описан в `module/api/types.ts` (`ErrorResponse` — `{ response?.data?.detail }`). Часть запросов включает показ уведомления об ошибке флагом `showErrorNotification: true` (например `getSettings`, `AttachmentsAPI.*`). Права доступа (`core.can_*_RFI`) описаны в `module/api/permissions.ts` и проверяются на стороне backend RFI (`RFITokenBasedPermission`). diff --git a/apps/rfi/openapi.yaml b/apps/rfi/openapi.yaml new file mode 100644 index 0000000..185d7bd --- /dev/null +++ b/apps/rfi/openapi.yaml @@ -0,0 +1,1218 @@ +openapi: 3.0.3 + +info: + title: RFI project API + version: "0.0.1" + description: | + REST API сервиса **rfi-backend** (`proc/rfi-backend`) — управление + запросами RFI (Request For Information), сообщениями к ним, статусами и + приоритетами (и их моделями), а также журналом изменений. + + Сервис написан на Python (**Django 5.1 / Django REST Framework**). Роутинг + строится `DefaultRouter` (`src/config/api/v1/router.py`), в который + собираются роутеры приложений `request`, `message`, `status`, `priority`, + `change_history`. Все ресурсы доступны под префиксом `/api/v1/` + (`config/urls.py` → `config/api/urls.py` → `config/api/v1/urls.py`). + + Схема OpenAPI генерируется `drf-spectacular` и доступна по `/api/schema/`, + Swagger UI — по `/api/schema/swagger-ui/`, ReDoc — по `/api/schema/redoc/`. + Django-admin — по `/admin/`. + + ### Аутентификация + Все эндпоинты требуют аутентификации (`DEFAULT_PERMISSION_CLASSES = + IsAuthenticated`). Режим задаётся переменной `JWT_AUTH_ENABLE` + (`config/settings/base.py`): + + 1. **JWT включён** (`JWT_AUTH_ENABLE=true`) — активны + `ZitadelJWTStatelessUserAuthentication` и `JWTStatelessUserAuthentication`. + Основной токен передаётся заголовком `Authorization: Bearer ` + (алгоритм `RS512`). Если дополнительно передан заголовок + `Identity: Bearer `, полезная нагрузка берётся из него + (`urn:zitadel:iam:user:metadata`); подпись при этом сервисом не + проверяется (доверие обеспечивается сетевым слоем/Istio). + 2. **JWT выключен** (`JWT_AUTH_ENABLE=false`) — `DefaultUserAuthentication` + подставляет демо-пользователя `DefaultUser` (только для локальной + разработки). + + ### Авторизация (права) + Эндпоинты `rfi` и `messages` используют `RFITokenBasedPermission`: + проверяется принадлежность к компании (`company_id ∈ user.company_ids`) и + модульные права из токена (`core.can_view_RFI`, `core.can_create_RFI`, + `core.can_edit_RFI`, `core.can_delete_RFI`, `core.can_copy_RFI`). + Редактирование доступно автору или ответственному (service account), + либо администратору. + + ### Пагинация + Списочные ответы используют `LimitOffsetPagination` (`PAGE_SIZE = 100`). + Ответ оборачивается в объект `{ count, next, previous, results }`. + Параметры — `limit` и `offset`. + + ### Фильтрация + Списки RFI поддерживают фильтрацию как через query-параметры (GET), так и + через тело запроса (POST `/api/v1/rfi/filter/`) — за счёт + `GetAndPostDjangoFilterBackend`. Дополнительно `CompanyFilterBackend` + неявно ограничивает выборку компаниями пользователя, если `company_id` + не передан явно. + + ### Особенности + - Удаление объектов — «мягкое» (soft-delete через `deleted_at`), базовый + класс `core.models.BaseModel`. `DELETE` возвращает `204`. + - Создание/обновление RFI принимает `multipart/form-data` (есть поле-файл + `image`); ответ формируется сериализатором чтения (`RFIRetrieveSerializer`). + - При создании RFI и сообщений отправляются уведомления (Celery-задачи), + если включено (`USE_ASYNC_FUNCTIONS`, `NOTIFICATIONS_ENABLE`). + + contact: + name: rfi-backend + url: https://gitlab/proc/rfi-backend + +servers: + - url: https://api.sarex.io/rfi + description: Production (ingress, префикс /rfi) + - url: https://stage-api.sarex.io/rfi + description: Stage (ingress, префикс /rfi) + - url: http://rfi-backend.rfi-prod.svc.cluster.local + description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000) + - url: http://localhost:8000 + description: Локальный запуск (uwsgi / runserver, порт 8000) + +tags: + - name: rfi + description: Запросы RFI — CRUD, копирование, фильтрация, счётчики, история + - name: messages + description: Сообщения в запросах RFI + - name: statuses + description: Статусы + - name: status-models + description: Модели статусов + - name: priorities + description: Приоритеты + - name: priority-models + description: Модели приоритетов + - name: change-history + description: Журнал изменений RFI (только чтение) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # RFI + # ========================================================================== + /api/v1/rfi/: + get: + tags: [rfi] + summary: Список RFI + description: Постраничный список RFI с фильтрацией через query-параметры. + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: id, in: query, schema: { type: string }, description: "Один или несколько id (повторяющийся параметр)" } + - { name: resource_id, in: query, schema: { type: string }, description: "UUID ресурса (повторяющийся)" } + - { name: author_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: string } } + - { name: status_id, in: query, schema: { type: string } } + - { name: priority_id, in: query, schema: { type: string } } + - { name: responsible_users, in: query, schema: { type: string }, description: "UUID service account (overlap)" } + - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } + - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } + - { name: message_at_gte, in: query, schema: { type: string, format: date-time }, description: "Есть сообщение с created_at >= значения" } + - { name: message_at_lte, in: query, schema: { type: string, format: date-time } } + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedRFIList" + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [rfi] + summary: Создать RFI + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/RFIWrite" + application/json: + schema: + $ref: "#/components/schemas/RFIWrite" + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/RFI" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/rfi/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [rfi] + summary: Получить RFI по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/RFI" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [rfi] + summary: Полное обновление RFI + requestBody: + required: true + content: + multipart/form-data: + schema: { $ref: "#/components/schemas/RFIWrite" } + application/json: + schema: { $ref: "#/components/schemas/RFIWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/RFI" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [rfi] + summary: Частичное обновление RFI + requestBody: + required: true + content: + multipart/form-data: + schema: { $ref: "#/components/schemas/RFIWrite" } + application/json: + schema: { $ref: "#/components/schemas/RFIWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/RFI" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [rfi] + summary: Удалить RFI (soft-delete) + responses: + "204": { description: Удалено } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + /api/v1/rfi/filter/: + post: + tags: [rfi] + summary: Список RFI по фильтру (POST) + description: | + Аналог `GET /api/v1/rfi/`, но фильтры передаются в теле запроса. + Тело валидируется `RFIFilterRequestSerializer`. Пагинация — через + query-параметры `limit`/`offset`. + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + requestBody: + required: false + content: + application/json: + schema: { $ref: "#/components/schemas/RFIFilterRequest" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedRFIList" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/rfi/history/: + get: + tags: [rfi] + summary: История изменений по списку RFI + description: | + Журнал изменений для всех RFI, попадающих под те же фильтры, что и + список (`GET /api/v1/rfi/`). Отсортирован по `created_at` убыв. + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: resource_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/rfi/{id}/history/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [rfi] + summary: История изменений одного RFI + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + /api/v1/rfi/status-count/: + get: + tags: [rfi] + summary: Количество RFI по статусам + description: Для каждого `resource_id` возвращает его статусы и количество RFI. + parameters: + - { name: resource_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/RFIStatusCount" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [rfi] + summary: Количество RFI по статусам (POST-фильтр) + requestBody: + required: false + content: + application/json: + schema: { $ref: "#/components/schemas/RFIFilterRequest" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/RFIStatusCount" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/rfi/priority-count/: + get: + tags: [rfi] + summary: Количество RFI по приоритетам + parameters: + - { name: resource_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/RFIPriorityCount" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [rfi] + summary: Количество RFI по приоритетам (POST-фильтр) + requestBody: + required: false + content: + application/json: + schema: { $ref: "#/components/schemas/RFIFilterRequest" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/RFIPriorityCount" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/rfi/{id}/copy/: + parameters: + - $ref: "#/components/parameters/PathId" + post: + tags: [rfi] + summary: Копировать RFI + description: Создаёт копию RFI с новым именем. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CopyName" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/RFI" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Messages + # ========================================================================== + /api/v1/messages/: + get: + tags: [messages] + summary: Список сообщений + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: id, in: query, schema: { type: string } } + - { name: author_id, in: query, schema: { type: string } } + - { name: request_id, in: query, schema: { type: string } } + - { name: reply_to_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: string } } + - { name: resource_id, in: query, schema: { type: string, format: uuid } } + - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } + - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedMessageList" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [messages] + summary: Создать сообщение + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/messages/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [messages] + summary: Сообщение по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/Message" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [messages] + summary: Обновить сообщение + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [messages] + summary: Частичное обновление сообщения (напр. отметить решением) + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/MessageWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [messages] + summary: Удалить сообщение (soft-delete) + responses: + "204": { description: Удалено } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Statuses + # ========================================================================== + /api/v1/statuses/: + get: + tags: [statuses] + summary: Список статусов + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: company_id, in: query, schema: { type: string } } + - { name: status_model, in: query, schema: { type: integer }, description: "id модели статусов (повторяющийся)" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedStatusList" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [statuses] + summary: Создать статус + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/statuses/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [statuses] + summary: Статус по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/Status" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [statuses] + summary: Обновить статус + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + "400": { $ref: "#/components/responses/ValidationError" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [statuses] + summary: Частичное обновление статуса + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/StatusCreate" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [statuses] + summary: Удалить статус (soft-delete) + responses: + "204": { description: Удалено } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Status models + # ========================================================================== + /api/v1/status-models/: + get: + tags: [status-models] + summary: Список моделей статусов + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: company_id, in: query, schema: { type: integer } } + - { name: resource_id, in: query, schema: { type: string, format: uuid } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedStatusModelList" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [status-models] + summary: Создать модель статусов + description: | + При создании первой модели статусов для компании автоматически + создаётся набор статусов по умолчанию (`DEFAULT_STATUSES`). + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/status-models/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [status-models] + summary: Модель статусов по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelDetail" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [status-models] + summary: Обновить модель статусов + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [status-models] + summary: Частичное обновление модели статусов + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/StatusModelWrite" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [status-models] + summary: Удалить модель статусов (soft-delete) + responses: + "204": { description: Удалено } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Priorities + # ========================================================================== + /api/v1/priorities/: + get: + tags: [priorities] + summary: Список приоритетов + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: company_id, in: query, schema: { type: string } } + - { name: priority_model, in: query, schema: { type: integer }, description: "id модели приоритетов (повторяющийся)" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedPriorityList" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [priorities] + summary: Создать приоритет + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/priorities/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [priorities] + summary: Приоритет по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/Priority" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [priorities] + summary: Обновить приоритет + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [priorities] + summary: Частичное обновление приоритета + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityWrite" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [priorities] + summary: Удалить приоритет (soft-delete) + responses: + "204": { description: Удалено } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Priority models + # ========================================================================== + /api/v1/priority-models/: + get: + tags: [priority-models] + summary: Список моделей приоритетов + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: company_id, in: query, schema: { type: integer } } + - { name: resource_id, in: query, schema: { type: string, format: uuid } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedPriorityModelList" } + "403": { $ref: "#/components/responses/Forbidden" } + post: + tags: [priority-models] + summary: Создать модель приоритетов + description: | + При создании первой модели приоритетов для компании автоматически + создаётся набор приоритетов по умолчанию (`DEFAULT_PRIORITIES`). + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + responses: + "201": + description: Создано + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + "400": { $ref: "#/components/responses/ValidationError" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/priority-models/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [priority-models] + summary: Модель приоритетов по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModel" } + "404": { $ref: "#/components/responses/NotFound" } + put: + tags: [priority-models] + summary: Обновить модель приоритетов + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + "404": { $ref: "#/components/responses/NotFound" } + patch: + tags: [priority-models] + summary: Частичное обновление модели приоритетов + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PriorityModelWrite" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [priority-models] + summary: Удалить модель приоритетов (soft-delete) + responses: + "204": { description: Удалено } + "404": { $ref: "#/components/responses/NotFound" } + + # ========================================================================== + # Change history (read-only) + # ========================================================================== + /api/v1/change-history/: + get: + tags: [change-history] + summary: Журнал изменений RFI + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Offset" + - { name: request_id, in: query, schema: { type: integer } } + - { name: resource_id, in: query, schema: { type: string, format: uuid } } + - { name: company_id, in: query, schema: { type: string } } + - { name: created_at_gte, in: query, schema: { type: string, format: date-time } } + - { name: created_at_lte, in: query, schema: { type: string, format: date-time } } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/PaginatedRFIChangeRecordList" } + "403": { $ref: "#/components/responses/Forbidden" } + + /api/v1/change-history/{id}/: + parameters: + - $ref: "#/components/parameters/PathId" + get: + tags: [change-history] + summary: Запись журнала изменений по id + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/RFIChangeRecord" } + "404": { $ref: "#/components/responses/NotFound" } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + `Authorization: Bearer ` (RS512). Опционально — + `Identity: Bearer ` для режима Zitadel. + + parameters: + Limit: + name: limit + in: query + required: false + schema: { type: integer, default: 100 } + description: Количество элементов на странице (LimitOffsetPagination). + Offset: + name: offset + in: query + required: false + schema: { type: integer } + description: Смещение от начала выборки. + PathId: + name: id + in: path + required: true + schema: { type: integer } + + responses: + ValidationError: + description: Ошибка валидации (DRF) + content: + application/json: + schema: + type: object + additionalProperties: true + example: { field_name: ["Обязательное поле."] } + Forbidden: + description: Недостаточно прав / не аутентифицирован + content: + application/json: + schema: { $ref: "#/components/schemas/Detail" } + NotFound: + description: Не найдено + content: + application/json: + schema: { $ref: "#/components/schemas/Detail" } + + schemas: + Detail: + type: object + properties: + detail: { type: string } + + Link: + type: object + properties: + name: { type: string, maxLength: 255 } + link: { type: string, format: uri } + required: [name, link] + + RFI: + description: Чтение RFI (RFIListSerializer / RFIRetrieveSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 255 } + author_id: { type: integer } + description: { type: string, nullable: true } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + responsible_users: + type: array + items: { type: string, format: uuid } + completion_date: { type: string, format: date-time, nullable: true } + image: { type: string, nullable: true, description: "URL изображения (по умолчанию rfi-default.png)" } + attributes: { type: object, additionalProperties: true } + links: + type: array + items: { $ref: "#/components/schemas/Link" } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + status: { type: integer, description: "id статуса (status_id)" } + priority: { type: integer, description: "id приоритета (priority_id)" } + + RFIWrite: + description: Запись RFI (RFIWriteSerializer). + type: object + properties: + name: { type: string, maxLength: 255 } + author_id: { type: integer } + description: { type: string, nullable: true } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + responsible_users: + type: array + items: { type: string, format: uuid } + completion_date: { type: string, format: date-time, nullable: true } + image: { type: string, format: binary, nullable: true } + attributes: { type: object, additionalProperties: true, default: {} } + links: + type: array + items: { $ref: "#/components/schemas/Link" } + status_id: { type: integer, description: "PK статуса" } + priority_id: { type: integer, description: "PK приоритета" } + required: [name, author_id, company_id, responsible_users, status_id, priority_id] + + RFIFilterRequest: + description: Тело фильтра для POST /rfi/filter/, /rfi/status-count/, /rfi/priority-count/. + type: object + properties: + id: { type: array, items: { type: string } } + resource_id: { type: array, items: { type: string } } + author_id: { type: array, items: { type: string } } + company_id: { type: integer } + status_id: { type: array, items: { type: string } } + priority_id: { type: array, items: { type: string } } + responsible_users: { type: array, items: { type: string } } + created_at_gte: { type: string, format: date-time } + created_at_lte: { type: string, format: date-time } + message_at_gte: { type: string, format: date-time } + message_at_lte: { type: string, format: date-time } + + CopyName: + type: object + properties: + name: { type: string, maxLength: 255 } + required: [name] + + Message: + description: Чтение сообщения (MessageReadSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + author_id: { type: integer } + text: { type: string } + is_solution: { type: boolean } + request: { type: integer, description: "id запроса (request_id)" } + reply_to: { type: integer, nullable: true, description: "id родительского сообщения" } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + MessageWrite: + description: Запись сообщения (MessageWriteSerializer). + type: object + properties: + author_id: { type: integer } + text: { type: string } + is_solution: { type: boolean } + request_id: { type: integer } + reply_to_id: { type: integer, nullable: true } + required: [author_id, text, request_id] + + Status: + description: Чтение статуса (StatusReadSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 512 } + label: { type: string, maxLength: 512 } + color: { type: string, description: "HEX-цвет", example: "#FFFFFF" } + status_model: { type: integer, description: "id модели статусов" } + + StatusShort: + type: object + properties: + id: { type: integer } + name: { type: string } + label: { type: string } + color: { type: string } + + StatusCreate: + description: Создание/обновление статуса (StatusCreateSerializer). + type: object + properties: + name: { type: string, maxLength: 512 } + label: { type: string, maxLength: 512 } + color: { type: string, example: "#FFFFFF" } + status_model: { type: integer, description: "PK модели статусов" } + required: [name, label, status_model] + + StatusModelList: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 512 } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + statuses: + type: array + items: { $ref: "#/components/schemas/StatusShort" } + + StatusModelDetail: + allOf: + - $ref: "#/components/schemas/StatusModelList" + - type: object + properties: + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + StatusModelWrite: + type: object + properties: + name: { type: string, maxLength: 512 } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + required: [name, company_id] + + Priority: + description: Чтение приоритета (PriorityReadSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 512 } + label: { type: string, maxLength: 512 } + color: { type: string, example: "#FFFFFF" } + priority_model: { type: integer, description: "id модели приоритетов" } + + PriorityShort: + type: object + properties: + id: { type: integer } + name: { type: string } + label: { type: string } + color: { type: string } + + PriorityWrite: + description: Создание/обновление приоритета (PriorityWriteSerializer). + type: object + properties: + name: { type: string, maxLength: 512 } + label: { type: string, maxLength: 512 } + color: { type: string, example: "#FFFFFF" } + priority_model_id: { type: integer, description: "PK модели приоритетов (write-only)" } + required: [name, label, priority_model_id] + + PriorityModel: + description: Чтение модели приоритетов (PriorityModelReadSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 512 } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + priorities: + type: array + items: { $ref: "#/components/schemas/PriorityShort" } + + PriorityModelWrite: + type: object + properties: + name: { type: string, maxLength: 512 } + company_id: { type: integer } + resource_id: { type: string, format: uuid, nullable: true } + required: [name, company_id] + + CountItem: + type: object + properties: + id: { type: integer } + name: { type: string } + label: { type: string } + color: { type: string } + count: { type: integer } + + RFIStatusCount: + type: object + properties: + resource_id: { type: string, format: uuid } + statuses: + type: array + items: { $ref: "#/components/schemas/CountItem" } + + RFIPriorityCount: + type: object + properties: + resource_id: { type: string, format: uuid } + priorities: + type: array + items: { $ref: "#/components/schemas/CountItem" } + + RFIChangeRecord: + description: Запись журнала изменений (RFIChangeRecordSerializer). + type: object + properties: + id: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + request_id: { type: integer, readOnly: true } + created_by: { type: integer, description: "ID пользователя, совершившего изменение" } + field_name: { type: string, maxLength: 128 } + attribute_name: { type: string, maxLength: 256, nullable: true } + was: { type: string, description: "Старое значение (raw)" } + became: { type: string, description: "Новое значение (raw)" } + was_text: { type: string, description: "Человекочитаемое старое значение" } + became_text: { type: string, description: "Человекочитаемое новое значение" } + + # ---- Пагинированные обёртки (LimitOffsetPagination) ---- + PaginatedBase: + type: object + properties: + count: { type: integer } + next: { type: string, format: uri, nullable: true } + previous: { type: string, format: uri, nullable: true } + + PaginatedRFIList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/RFI" } + + PaginatedMessageList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/Message" } + + PaginatedStatusList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/Status" } + + PaginatedStatusModelList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/StatusModelList" } + + PaginatedPriorityList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/Priority" } + + PaginatedPriorityModelList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/PriorityModel" } + + PaginatedRFIChangeRecordList: + allOf: + - $ref: "#/components/schemas/PaginatedBase" + - type: object + properties: + results: + type: array + items: { $ref: "#/components/schemas/RFIChangeRecord" } diff --git a/apps/stamp-verification/.env.example b/apps/stamp-verification/.env.example new file mode 100644 index 0000000..8aa1df0 --- /dev/null +++ b/apps/stamp-verification/.env.example @@ -0,0 +1,70 @@ +# ============================================================================= +# stamp-verification-frontend — пример переменных окружения +# ============================================================================= +# +# ВАЖНО: само приложение (статическая страница на nginx) НЕ читает переменные +# окружения во время выполнения. Конфигурация «зашита» в код: +# - базовый URL API выбирается по window.location.host (см. static/js/index.js); +# - nginx слушает порт 8080 (см. nginx.conf); +# - health-check отдаётся статически по пути /ping. +# +# Поэтому файл .env НЕ подхватывается образом. Перечисленные ниже переменные +# участвуют только в СБОРКЕ и ДЕПЛОЕ (GitLab CI + Helm/universal-chart) и +# приведены здесь как справочный шаблон значений для каждого окружения. +# ----------------------------------------------------------------------------- + + +# --- GitLab CI (.gitlab-ci.yml) --------------------------------------------- +# Задаются пайплайном/переменными проекта, переключаются по ветке/тегу. +SERVICE_NAME=stamp-verification-frontend +DOCKERFILE_PATH=Dockerfile +BUILD_ARGS= +CI_TRIGGER_SOURCE=app + +# STAND / NAMESPACE выставляются автоматически по правилам workflow: +# ветка stage -> STAND=stage NAMESPACE=documentations +# ветка master -> STAND=preprod NAMESPACE=stamp-verification-preprod +# тег (release) -> STAND=production NAMESPACE=stamp-verification-prod +STAND=stage +NAMESPACE=documentations +RELEASE_NAME=stamp-verification-frontend +CHART_NAME=stamp-verification-frontend +CHART_VERSION=0.0.1-stage +K8S_HUSTLER_BRANCH=universal-chart-stage + +# Имя собранного образа (подставляется в HELM_SET_ARGS как +# universal-chart.services.frontend.image.name.) +IMAGE_NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest + + +# --- Helm / universal-chart (.helm/values.yaml) ----------------------------- +# Значения чарта universal-chart (dependency 0.1.7). env выбирает набор *.env. +UNIVERSAL_CHART__GLOBAL__ENV=stage # _default | stage | preprod | production +UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__NAME=stamp-verification-frontend +UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__PORT=8080 +UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__REPLICA_COUNT=1 +UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest +UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__PULL_POLICY=IfNotPresent +UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__NAME=frontend-service +UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__PORT=8080 # production: 80 +UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TARGET_PORT=8080 +UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TYPE=ClusterIP +UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE_PULL_SECRETS__NAME=dockerhub + + +# --- Docker build (Dockerfile) ---------------------------------------------- +# Базовый образ фиксирован в Dockerfile: nginx:mainline-alpine-otel. +# BUILD_ARGS пуст — build-time аргументов у образа нет. + + +# --- «Runtime»-конфигурация приложения (справочно, НЕ через env) ------------- +# Значения ниже НЕ настраиваются переменными окружения — они захардкожены в +# static/js/index.js и приведены только для понимания поведения: +# +# window.location.host == "stamp-verification.sarex.io" -> API = api.sarex.io (prod) +# иначе (напр. stamp-verification.stage.sarex.io) -> API = stage-api.sarex.io (stage) +# +# ВНИМАНИЕ (безопасность): в static/js/index.js в заголовок Authorization +# захардкожен Bearer-JWT (истёк в 2023 г.). Это должно быть удалено — +# публичный эндпоинт QR не требует пользовательского токена. Секрета для env +# здесь нет и быть не должно. diff --git a/apps/stamp-verification/CONFIGURATION.md b/apps/stamp-verification/CONFIGURATION.md new file mode 100644 index 0000000..a963a08 --- /dev/null +++ b/apps/stamp-verification/CONFIGURATION.md @@ -0,0 +1,132 @@ +# Конфигурация проекта stamp-verification-frontend + +Документ описывает, как конфигурируется и разворачивается модуль **stamp-verification-frontend** — публичная статическая страница проверки штампа (QR) на документе. + +## Что это за сервис + +`stamp-verification-frontend` — это статический фронтенд (обычный HTML + ванильный JavaScript + `moment.js`), который раздаётся через **nginx**. По QR-коду со штампа на PDF пользователь попадает на страницу с параметрами `?id=&page_number=`, страница запрашивает данные документа у backend-сервиса `documentations` и отображает информацию о версии, авторе, дате загрузки, статусе согласования и ссылке на документ в Sarex. + +Ключевое отличие от backend-сервисов: **у приложения нет разбора переменных окружения**. Здесь нет `pydantic-settings`, нет `.env`, нет секций конфигурации. Всё «runtime»-поведение либо статично, либо определяется по хосту в браузере (`window.location.host`). Конфигурируется только **сборка и деплой** (Docker, Helm/`universal-chart`, GitLab CI). + +## Способы конфигурирования + +| Слой | Где задаётся | Что настраивает | +| --- | --- | --- | +| Runtime (браузер) | `static/js/index.js` | Выбор базового URL API по `window.location.host`; параметры запроса берутся из query-строки (`id`, `page_number`, `v`) | +| Веб-сервер | `nginx.conf` | Порт прослушивания `8080`, корень `/dist`, health-check `GET /ping` → `200 {"result": "ok"}`, gzip, логи в stdout/stderr | +| Образ | `Dockerfile` | Базовый образ `nginx:mainline-alpine-otel`, копирование `static/` и `index.html` в `/dist`, `EXPOSE 8080` | +| Helm | `.helm/values.yaml`, `.helm/Chart.yaml` | Значения чарта-зависимости `universal-chart` (deployment, image, service, imagePullSecrets) | +| CI/CD | `.gitlab-ci.yml` | Выбор окружения по ветке/тегу, имя релиза/чарта, `HELM_SET_ARGS` | +| IaC (kustomize) | `iac/apps/stamp-verification/**` | Namespace, Deployment, Service и оверлеи кластеров (этот репозиторий) | + +Отдельного конфиг-файла (yaml/toml/env) приложение не читает. + +## Runtime-поведение (`static/js/index.js`) + +Скрипт выполняется на `DOMContentLoaded`. Логика: + +- читает query-параметры: `id` (идентификатор QR), `page_number`, `v` (пока не используется); +- запрос выполняется только если заданы одновременно `id` и `page_number`; +- выбирает базовый URL API: + + | `window.location.host` | Базовый URL API | Окружение | + | --- | --- | --- | + | `stamp-verification.sarex.io` | `api.sarex.io` | prod | + | любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage | + +- делает `fetch` `GET https:///documentations/api/v1/public/qr/{id}/document_info?number_page={page_number}` с `mode: cors`, `credentials: include`, `cache: no-cache`; +- по ответу заполняет поля страницы (название, версия, номер страницы, автор, дата загрузки), блок изменений (changelog), индикаторы актуальности/аннулирования версии, статус согласования и ссылку на документ. + +Локаль дат — `ru` (`moment.locale("ru")`), формат отображения даты — `DD.MM.YYYY / HH:mm`. + +Полный перечень внешних вызовов см. в `ENDPOINTS.md`, контракт эндпоинта — в `openapi.yaml`. + +## nginx (`nginx.conf`) + +| Параметр | Значение | Назначение | +| --- | --- | --- | +| `listen` | `8080` | Порт HTTP | +| `root` | `/dist` | Корень статики (туда Dockerfile кладёт `static/` и `index.html`) | +| `location = /ping` | `return 200 '{"result": "ok"}'` | Health-check для k8s-проб/балансировщика | +| `access_log` | `/dev/stdout` | Логи доступа в stdout | +| `error_log` | `stderr warn` | Логи ошибок в stderr | +| `gzip` | `on` | Сжатие ответов | +| `expires` | `off` | Без заголовков кеширования | + +## Docker (`Dockerfile`, `docker-compose.yml`) + +Образ на базе `nginx:mainline-alpine-otel`. Сборка копирует `nginx.conf` в `/etc/nginx/nginx.conf`, каталог `static` и `index.html` в `/dist`, выставляет права на `/var` и `/run`, объявляет `EXPOSE 8080` и запускает `nginx -g "daemon off;"`. Build-time аргументов нет (`BUILD_ARGS=""`). + +`docker-compose.yml` — только для локального прогона (`image: sarex/landing:latest`, публикует порт `8000:8000`; обратите внимание, что nginx внутри слушает `8080` — маппинг в compose оставлен историческим). + +## Helm / universal-chart (`.helm/`) + +`Chart.yaml` подключает зависимость `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`). Значения задаются в `values.yaml` под ключом `universal-chart` и разбиты по окружениям через суффиксы (`_default`, `stage`, `preprod`, `production`). + +| Ключ (`universal-chart.services.frontend.*`) | Значение (`_default`) | Назначение | +| --- | --- | --- | +| `deployment.enabled` | `true` | Создавать Deployment | +| `deployment.name` | `stamp-verification-frontend` | Имя Deployment | +| `deployment.port` | `8080` | Порт контейнера | +| `deployment.replicaCount` | `1` | Число реплик | +| `image.name` | `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest` | Образ | +| `image.pullPolicy` | `IfNotPresent` | Политика загрузки образа | +| `service.enabled` | `true` | Создавать Service | +| `service.name` | `frontend-service` | Имя Service | +| `service.port` | `8080` (в `production` — `80`) | Порт сервиса | +| `service.targetPort` | `8080` | Целевой порт | +| `service.type` | `ClusterIP` | Тип сервиса | +| `imagePullSecrets.name` | `dockerhub` | Секрет для доступа к реестру | + +`global.env` (`_default`) выбирает активный набор значений по окружению. + +## CI/CD (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Общие переменные: `SERVICE_NAME=stamp-verification-frontend`, `DOCKERFILE_PATH=Dockerfile`, `BUILD_ARGS=""`, `CI_TRIGGER_SOURCE=app`. + +Окружение переключается правилами `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | +| `merge_request_event` | — | — | — | `ENABLE_BUILD_IMAGE="false"` (только проверки, без сборки образа) | +| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | `universal-chart-stage` | +| ветка `master` | `preprod` | `stamp-verification-preprod` | `0.0.1-preprod` | `universal-chart-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `stamp-verification-prod` | `0.0.1-prod` | `universal-chart-production` | +| иначе | `when: never` | | | | + +Для каждого деплой-окружения `HELM_SET_ARGS` передаёт: образ (`universal-chart.services.frontend.image.name.=${IMAGE_NAME}`), `universal-chart.global.env=` и метаданные коммита (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`). + +## Инфраструктура (kustomize, этот репозиторий) + +Каталог `iac/apps/stamp-verification/` содержит kustomize-манифесты (альтернатива/дополнение к Helm-деплою из CI): + +- `base/namespace.yaml` — namespace `stamp-verification` с `istio-injection: enabled`; +- `base/deployment.yaml` — Deployment `frontend` (образ `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:`, `imagePullPolicy: IfNotPresent`, `containerPort: 80`, requests `cpu: 25m`/`memory: 100Mi`, `imagePullSecrets: regcred`); +- `base/service.yaml` — Service `frontend-service` (`ClusterIP`, `port: 80` → `targetPort: 80`); +- `base/kustomization.yaml` — сборка base (namespace + deployment + service); +- `yc-k8s-test/kustomization.yaml` — оверлей кластера (`resources: ../base`, патчи закомментированы); +- `yc-k8s-test/replicas.yaml` — заготовка патча реплик (`replicas: 1`). + +## Замечания и потенциальные проблемы + +- **Захардкоженный JWT.** В `static/js/index.js` в заголовок `Authorization: Bearer …` вшит истёкший (2023 г.) токен. Публичный эндпоинт `/public/qr/...` не должен требовать пользовательский токен — строку следует удалить. Держать секреты в статике недопустимо. +- **Рассогласование портов.** nginx и Helm/`universal-chart` используют порт **8080**, а kustomize-манифесты в этом репозитории (`base/deployment.yaml`, `base/service.yaml`) — порт **80**. Нужно привести к одному значению (образ слушает 8080), иначе проброс/пробы могут не сходиться. +- **Дублирование деплоя.** Приложение может разворачиваться и через Helm (CI, чарт `universal-chart`), и через kustomize (этот репозиторий). Следует зафиксировать единый источник истины, чтобы образ/порт/namespace не расходились. +- **NAMESPACE для stage.** На ветке `stage` деплой идёт в namespace `documentations` (общий), тогда как preprod/prod — в выделенные `stamp-verification-*`. Это осознанное решение или наследие — стоит проверить. +- **Конфиг только по хосту.** Выбор prod/stage жёстко завязан на строку `stamp-verification.sarex.io`. Любой новый прод-домен потребует правки кода, а не конфигурации. +- **Mock-данные в бандле.** В `index.js` присутствуют объекты `mockData`/`mockChangeLog` — тестовые данные, оставшиеся в продовом бандле. На поведение не влияют (не используются), но их стоит убрать. + +## Минимальный набор для локального запуска + +Отдельная конфигурация не требуется — приложение статично: + +``` +# сборка и запуск образа +docker build -t stamp-verification-frontend . +docker run --rm -p 8080:8080 stamp-verification-frontend +# проверка +curl http://localhost:8080/ping # {"result": "ok"} +# страница: http://localhost:8080/?id=&page_number=1 +``` + +Для реальных данных нужен доступный backend `documentations` (по умолчанию для не-prod хоста используется `https://stage-api.sarex.io`). diff --git a/apps/stamp-verification/ENDPOINTS.md b/apps/stamp-verification/ENDPOINTS.md new file mode 100644 index 0000000..febeb76 --- /dev/null +++ b/apps/stamp-verification/ENDPOINTS.md @@ -0,0 +1,69 @@ +# Эндпоинты, с которыми взаимодействует stamp-verification-frontend + +Документ описывает все внешние HTTP-вызовы, которые выполняет статическая страница проверки штампа (`stamp-verification-frontend`). + +## Как устроено взаимодействие + +В отличие от микрофронтендов с декларативным реестром эндпоинтов, здесь весь сетевой код сосредоточен в одном файле — `static/js/index.js`. Страница обычным `fetch` обращается к **одному** публичному эндпоинту backend-сервиса `documentations`, а также подгружает сторонние ресурсы аналитики и статику. + +Логика вызова: + +1. из query-строки берутся параметры `id` (идентификатор QR) и `page_number`; запрос выполняется только если оба заданы; +2. базовый хост API выбирается по `window.location.host`; +3. выполняется `GET`-запрос с `mode: "cors"`, `credentials: "include"`, `cache: "no-cache"`; +4. ответ (JSON) маппится на поля страницы. + +## Базовые хосты по окружениям + +Значение определяется в `index.js` по хосту страницы. Итоговый URL = `https://<базовый хост>` + путь эндпоинта. + +| Хост страницы (`window.location.host`) | Базовый хост API | Окружение | +| --- | --- | --- | +| `stamp-verification.sarex.io` | `api.sarex.io` | prod | +| любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage | + +> Промежуточных окружений (preprod/local) в коде не предусмотрено: всё, что не prod-домен, трактуется как stage. + +## Эндпоинты по сервисам + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getQrDocumentInfo` | GET | `/documentations/api/v1/public/qr/{qrId}/document_info?number_page={pageNumber}` | Публичная информация о документе по QR-коду штампа: название, версия, автор, дата загрузки, актуальность/аннулирование версии, статус согласования, changelog и ссылка на документ в Sarex | + +Параметры запроса: + +| Параметр | Расположение | Обязателен | Назначение | +| --- | --- | --- | --- | +| `qrId` | path (`{qrId}`) | да | Идентификатор QR-кода (из query `?id=`) | +| `number_page` | query | да | Номер страницы документа (из query `?page_number=`) | + +Заголовки запроса (как в текущем коде): `Content-Type: application/json` и `Authorization: Bearer `. **Токен захардкожен и истёк** — см. раздел «Замечания». Запрос идёт с `credentials: "include"` (куки/учётные данные), `redirect: "follow"`, `referrerPolicy: "no-referrer"`. + +Используемые поля ответа (по `index.js`): `document_name`, `version_number`, `page_number`, `author`, `upload_date`, `is_actual_version`, `is_canceled`, `does_bundle_have_flows`, `is_actual_version_approved`, `document_review_status`, `document_url`, `is_latest_changelog`, `changelog` (`no`, `description`, `author` и др.). Полная схема — в `openapi.yaml`. + +Формирование ссылки на документ: при наличии `document_url` ссылка на странице собирается как `${data.document_url}&page_number=${data.page_number}` и открывается в Sarex. + +## Внешние (сторонние) ресурсы + +Не относятся к API Sarex, но выполняются страницей: + +| Ресурс | Метод | URL | Назначение | +| --- | --- | --- | --- | +| Яндекс.Метрика (tag.js) | GET | `https://mc.yandex.ru/metrika/tag.js` | Загрузка счётчика аналитики (id `93441026`) | +| Яндекс.Метрика (noscript) | GET | `https://mc.yandex.ru/watch/93441026` | Пиксель для окружения без JS | +| Статика приложения | GET | `static/css/*`, `static/js/*`, `static/icons/*`, `static/fonts/*`, `static/img/*` | CSS, `moment-with-locales.min.js`, иконки, шрифты, фон — раздаются самим nginx | + +Health-check (со стороны инфраструктуры, не самой страницы): `GET /ping` → `200 {"result": "ok"}` (отдаётся nginx). + +## Обработка ошибок + +Специализированного маппинга кодов ошибок в человекочитаемые сообщения (как в backend-сервисах) в коде нет. Ошибки обрабатываются минимально: `fetch` обёрнут в `.catch(...)`, ошибки логируются в консоль (`console.error`). При пустом/отсутствующем ответе поля страницы заполняются плейсхолдером `–––` (`PLACEHOLDER_STR`). Пользовательских уведомлений об ошибке нет. + +## Замечания + +- **Захардкоженный JWT.** В заголовке `Authorization` в `index.js` вшит истёкший (2023 г.) Bearer-токен. Эндпоинт публичный (`/public/qr/...`) — заголовок с токеном следует удалить; хранить токены в статике недопустимо. +- **`credentials: "include"`** при кросс-доменном запросе к `*-api.sarex.io` требует корректной CORS-конфигурации на стороне `documentations` (`Access-Control-Allow-Credentials` + конкретный `Access-Control-Allow-Origin`). +- **Параметр `v`** (версия) читается из query, но пока не используется. +- **Выбор окружения только по домену** — новый prod-домен потребует правки кода. diff --git a/apps/stamp-verification/openapi.yaml b/apps/stamp-verification/openapi.yaml new file mode 100644 index 0000000..123ff4e --- /dev/null +++ b/apps/stamp-verification/openapi.yaml @@ -0,0 +1,216 @@ +openapi: 3.0.3 + +info: + title: Stamp Verification — Public QR API (consumed) + version: "1.0.0" + description: | + Контракт публичного эндпоинта, который потребляет фронтенд + **stamp-verification-frontend** (`pdm/stamp-verification-frontend`). + + Сам фронтенд — статическая страница на nginx (ванильный JS, `static/js/index.js`) + и **не предоставляет** собственного HTTP API. Данная спецификация описывает + единственный внешний эндпоинт backend-сервиса **documentations**, к которому + страница обращается для получения информации о документе по QR-коду штампа. + Спецификация восстановлена по клиентскому коду (`index.js`) и служит + справочным контрактом (потребитель, а не поставщик). + + ### Базовый хост + Выбирается на клиенте по `window.location.host`: + - `stamp-verification.sarex.io` → `https://api.sarex.io` (prod); + - любой другой хост → `https://stage-api.sarex.io` (stage). + + ### Аутентификация + Эндпоинт публичный (сегмент пути `/public/`). Текущий клиент тем не менее + отправляет заголовок `Authorization: Bearer ` с **захардкоженным и + истёкшим** токеном — это дефект клиента, а не требование API (см. замечания + в `ENDPOINTS.md`). Запрос выполняется с `credentials: include` (CORS с + учётными данными). + + ### Формат + Ответ — `application/json`. Все поля опциональны/nullable: клиент подставляет + плейсхолдер `–––` при отсутствии значения. + +servers: + - url: https://api.sarex.io + description: prod + - url: https://stage-api.sarex.io + description: stage + +paths: + /documentations/api/v1/public/qr/{qr_id}/document_info: + get: + operationId: getQrDocumentInfo + summary: Информация о документе по QR-коду штампа + description: | + Возвращает публичную информацию о документе, на который указывает QR-код + штампа: название, версию, автора, дату загрузки, актуальность и статус + согласования версии, сведения об изменениях (changelog) и ссылку на + документ в Sarex. + tags: + - public-qr + parameters: + - name: qr_id + in: path + required: true + description: Идентификатор QR-кода (на клиенте — query-параметр `id`). + schema: + type: string + example: "b46d6b82-9e51-48e3-8dce-a38dfc6b2a26" + - name: number_page + in: query + required: true + description: Номер страницы документа (на клиенте — query-параметр `page_number`). + schema: + type: integer + minimum: 1 + example: 1 + responses: + "200": + description: Информация о документе + content: + application/json: + schema: + $ref: "#/components/schemas/QrDocumentInfo" + "404": + description: Документ/QR не найден + "422": + description: Некорректные параметры запроса + "500": + description: Внутренняя ошибка сервиса + +components: + schemas: + QrDocumentInfo: + type: object + description: | + Набор полей, используемых клиентом (`static/js/index.js`). Поля + опциональны — при отсутствии клиент отображает плейсхолдер `–––`. + properties: + document_name: + type: string + nullable: true + description: Имя файла документа + example: some-file-01.pdf + version_number: + type: string + nullable: true + description: Номер версии документа + example: v2 + page_number: + type: integer + nullable: true + description: Номер страницы (эхо параметра запроса); участвует в сборке ссылки на документ + example: 1 + author: + type: string + nullable: true + description: Автор версии (отображаемое имя) + example: Андрей Алешков + upload_date: + type: string + format: date-time + nullable: true + description: Дата и время загрузки версии (ISO 8601); на клиенте форматируется как `DD.MM.YYYY / HH:mm` + example: "2023-05-23T20:01:55.496768Z" + is_actual_version: + type: boolean + nullable: true + description: >- + `true` — последняя версия; `false` — не последняя; отсутствие/`null` — + актуальность неизвестна + example: true + is_canceled: + type: boolean + nullable: true + description: >- + `true` — QR-код/версия аннулированы (клиент показывает предупреждение + «QR-код аннулирован») + example: false + does_bundle_have_flows: + type: boolean + nullable: true + description: Есть ли у бандла процессы согласования (управляет показом блока статуса согласования) + example: false + is_actual_version_approved: + type: boolean + nullable: true + description: Согласована ли текущая версия (при `does_bundle_have_flows = true`) + example: true + document_review_status: + type: string + nullable: true + description: Человекочитаемый статус документа (показывается при согласованной актуальной версии) + example: Согласован + document_url: + type: string + format: uri + nullable: true + description: >- + Ссылка на документ в Sarex. Клиент открывает её как + `${document_url}&page_number=${page_number}` + example: https://stage.sarex.io/workspaces-v2/0244d9f9-8f32-4b65-80c7-7c403b85c81e?type=pdf + is_latest_changelog: + type: boolean + nullable: true + description: >- + Является ли `changelog` последним. Если `false` при наличии + `changelog` — клиент показывает предупреждение «Была выпущена новая + версия изменения» + example: true + changelog: + $ref: "#/components/schemas/Changelog" + + Changelog: + type: object + nullable: true + description: Сведения об изменении версии документа + properties: + bundle_id: + type: string + format: uuid + example: b46d6b82-9e51-48e3-8dce-a38dfc6b2a26 + "no": + type: string + description: Порядковый номер изменения (на клиенте выводится как `№{no}`) + example: "2" + description: + type: string + nullable: true + description: >- + Описание изменения. Если длиннее 20 символов — клиент включает + сворачивание текста; при отсутствии показывает «Нет описания изменений» + example: Внесены изменения в структуру документа. Добавлена подпись руководителя проекта. + created_at: + type: string + format: date-time + example: "2023-05-23T20:01:55.496768Z" + updated_at: + type: string + format: date-time + example: "2023-05-23T20:01:55.496768Z" + created_by: + type: integer + example: 2203 + author: + $ref: "#/components/schemas/ChangelogAuthor" + + ChangelogAuthor: + type: object + description: Автор изменения + properties: + id: + type: integer + example: 2203 + username: + type: string + example: a.aleshkov + email: + type: string + format: email + example: a.aleshkov@sarex.io + first_name: + type: string + example: Андрей + last_name: + type: string + example: Алешков diff --git a/apps/subscriptions/.env.example b/apps/subscriptions/.env.example new file mode 100644 index 0000000..4edb925 --- /dev/null +++ b/apps/subscriptions/.env.example @@ -0,0 +1,64 @@ +# ============================================================================ +# sarex-subscriptions — пример переменных окружения (api + cron-задачи) +# +# Django-сервис уведомлений: HTTP-API (uwsgi) + две CronJob-задачи рассылки +# (run_notifications / run_immediately_notifications). Все компоненты +# используют один модуль настроек config.settings.production и общий набор +# переменных. +# +# Скопируйте нужные значения в config.env / .env соответствующего окружения. +# Переменные, помеченные (обяз.), обязательны для работы — без них процесс +# упадёт при старте либо при обращении к соответствующему сервису. +# bool в Django читается как строка: непустая строка = True (см. замечания). +# ============================================================================ + +# --- База данных PostgreSQL/PostGIS (api + cron) ---------------------------- +# Движок — core.db.backends.postgis (GeoDjango), нужен PostGIS. +DATABASE_HOST=127.0.0.1 # (обяз.) хост PostgreSQL +DATABASE_PORT=5432 # (обяз.) порт PostgreSQL +DATABASE_NAME=subscriptions # (обяз.) имя базы данных +DATABASE_USER=user # (обяз.) пользователь БД +DATABASE_PASSWORD=password # (обяз.) пароль пользователя БД + +# --- Хранилище S3 / MinIO (api: static + media) ----------------------------- +# STATICFILES_STORAGE и DEFAULT_FILE_STORAGE = S3Boto3Storage, поэтому креды +# фактически обязательны для корректной работы статики/медиа. +YC_S3_ACCESS_KEY_ID=access_key # (обяз.) access key +YC_S3_SECRET_ACCESS_KEY=secret_key # (обяз.) secret key +YC_S3_BUCKET_NAME=subscriptions # (обяз.) имя бакета +YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net # (обяз.) endpoint S3 + +# --- Внешние сервисы (в основном cron-рассылки) ----------------------------- +SYSTEM_LOG_HOST=http://localhost:8888 # (обяз.) URL сервиса system-log +USER_SERVICE_HOST=http://localhost:8000 # (обяз.) URL сервиса пользователей (Django/ЛК) +USER_SERVICE_LOGIN=superuser # логин для авторизации в user-service (по умолч. "") +USER_SERVICE_PASSWORD=password # пароль для авторизации в user-service (по умолч. "") +ALLOWED_HOST_EMAIL=https://lk.sarex.io # базовый URL для ссылок в письмах (по умолч. https://lk.sarex.io) + +# --- Email через Mailgun (cron) --------------------------------------------- +IS_MAILGUN_USE=0 # использовать Mailgun (по умолч. True; 0 — выключить) +MAILGUN_API_KEY= # API-ключ Mailgun (по умолч. "") +# MAILGUN_BASE_URL=https://api.mailgun.net/v3/mg.sarex.io # базовый URL Mailgun API +# MAILGUN_EMAIL_FROM=hello@sarex.io # адрес отправителя + +# --- Email через SMTP (cron; используется если задан хост) ------------------ +SMTP_EMAIL_HOST= # хост SMTP (по умолч. None — SMTP отключён) +SMTP_EMAIL_PORT= # порт SMTP (по умолч. None) +# SMTP_EMAIL_FROM=hello@sarex.io # адрес отправителя (по умолч. hello@sarex.io) + +# --- Telegram (cron) -------------------------------------------------------- +IS_USE_TELEGRAM=false # использовать Telegram (по умолч. True; см. замечание про bool) +TELEGRAM_BOT_TOKEN= # токен бота (по умолч. "") + +# --- Трейсинг OpenTelemetry (api; необязательно) ---------------------------- +# Блок включается, если USE_OTEL задана НЕПУСТОЙ строкой (даже "False" → вкл!). +# USE_OTEL=True # включить OTEL-трейсинг и otel-логгер +# SERVICE_NAME=subscriptions # имя сервиса в трейсах (по умолч. subscriptions) +# TRACER_ENDPOINT=localhost:4375 # адрес OTLP-коллектора (по умолч. localhost:4375) +# USE_INSECURE=True # подключение без TLS (та же логика bool, что и USE_OTEL) + +# --- Инфраструктурные / вспомогательные (кодом приложения НЕ читаются) ------- +# API_ADDRESS=8000 # задаётся в манифестах; порт uwsgi фиксирован в uwsgi.ini (0.0.0.0:8000) +# DJANGO_SETTINGS_MODULE=config.settings.production # задаётся в entrypoint.sh / wsgi.py +# NEXUS_USERNAME= # build-arg Dockerfile: доступ к приватному PyPI (nexus.infra.sarex.io) +# NEXUS_PASSWORD= # build-arg Dockerfile: пароль к приватному PyPI diff --git a/apps/subscriptions/CONFIGURATION.md b/apps/subscriptions/CONFIGURATION.md new file mode 100644 index 0000000..3b440cb --- /dev/null +++ b/apps/subscriptions/CONFIGURATION.md @@ -0,0 +1,200 @@ +# Конфигурация sarex-subscriptions (api + cron-задачи) + +Документ описывает все переменные окружения и способы конфигурирования сервиса `sarex-subscriptions` — Django-приложения рассылки уведомлений (email/Telegram) по подпискам. + +Компоненты сервиса используют **один и тот же** модуль настроек `config.settings.production` и общий набор переменных окружения: + +- **api** — HTTP-сервис (uwsgi, `config.wsgi:application`), REST API подписок/получателей/шаблонов; отдаёт статику и медиа через S3; +- **cron `run_notifications`** (`subscription-periodic`) — периодическая рассылка отложенных уведомлений; +- **cron `run_immediately_notifications`** (`subscription-immediately`) — рассылка немедленных уведомлений. + +Обе cron-задачи — это management-команды Django (`server/apps/notification/management/commands`), запускаемые как отдельные `CronJob` из того же образа. + +## Способы конфигурирования + +Сервис настраивается **через переменные окружения** (`os.getenv` в `config/settings/base.py` и `config/settings/production.py`) плюс жёстко заданные в коде настройки. Отдельной библиотеки разбора конфига (как cleanenv в Go-сервисах) здесь нет — используется штатный механизм Django settings. + +Часть настроек **захардкожена** в `base.py` и не выносится в окружение: `SECRET_KEY`, `DEBUG`, `ALLOWED_HOSTS`, `CORS_*`, REST Framework, локаль/таймзона (`ru-ru`, `Europe/Moscow`). В `production.py` `DEBUG=False` и заданы фиксированные `ALLOWED_HOSTS`/`CORS_ALLOWED_ORIGINS`. + +Обязательные переменные не помечены тегами (как в Go), но при их отсутствии процесс падает при старте (подключение к БД) либо при обращении к соответствующему сервису (клиенты system-log/user-service, хранилище S3). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | `docker-compose.yml` поднимает контейнер `db` (`postgis/postgis`) и сервисы `api`/`migrations`. Значения БД (`DATABASE_USER/PASSWORD/NAME`, `ALLOWED_HOST_EMAIL`) подставляются из окружения/файла `.env` рядом с compose. api собирается из `compose/server/Dockerfile` и стартует через `entrypoint.sh` | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml`: зависимость от `universal-chart`. Блоки `services.api.envs` (обычные значения, per-env `_default/stage/preprod/production`) и `services.api.secretEnvs` (из k8s-секретов). Плюс `cronjobs.periodic` / `cronjobs.immediately` — шаблоны `CronJob` в `.helm/templates/`, наследующие `envs`/`secretEnvs` api | +| Kubernetes — этот infra-репозиторий (`iac/apps/subscriptions`) | `base/` — kustomize-манифесты с инъекцией секретов через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + postgresql), `brusnika-stage` / `brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блоки `envs`/`secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); в `workflow.rules` задаются переменные пайплайна (`SERVICE_NAME`, `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME` и т.п.) | + +**Миграции БД (api).** Выполняются **в entrypoint контейнера api** перед стартом uwsgi: `python manage.py migrate --settings=config.settings.production` (`compose/server/entrypoint.sh`). Cron-задачи миграций не выполняют. Локально в `docker-compose.yml` отдельный сервис `migrations` вызывает `makemigrations` (генерация миграций, не применение). + +--- + +## api (`sarex-subscriptions`) + +Переменные читаются в `config/settings/base.py` (S3, OTEL) и `config/settings/production.py` (БД и внешние сервисы; `production.py` импортирует всё из `base.py`). + +### База данных (PostgreSQL / PostGIS) + +Движок — `core.db.backends.postgis` (GeoDjango, требуется PostGIS). Все пять переменных обязательны — без них подключение к БД не поднимется. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `DATABASE_HOST` | string | да | — | Хост PostgreSQL | +| `DATABASE_PORT` | string | да | — | Порт PostgreSQL | +| `DATABASE_NAME` | string | да | — | Имя базы данных | +| `DATABASE_USER` | string | да | — | Пользователь БД | +| `DATABASE_PASSWORD` | string | да | — | Пароль пользователя БД | + +### Хранилище S3 (static + media) + +`STATICFILES_STORAGE` и `DEFAULT_FILE_STORAGE` = `storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL="public-read"`. Значения по умолчанию отсутствуют (`os.getenv` без default → `None`), поэтому для корректной отдачи статики/медиа креды фактически обязательны. + +| Переменная | Тип | Обяз. | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | да | Access key (`AWS_ACCESS_KEY_ID`) | +| `YC_S3_SECRET_ACCESS_KEY` | string | да | Secret key (`AWS_SECRET_ACCESS_KEY`) | +| `YC_S3_BUCKET_NAME` | string | да | Имя бакета (`AWS_STORAGE_BUCKET_NAME`) | +| `YC_S3_ENDPOINT_URL` | string | да | Endpoint S3 (`AWS_S3_ENDPOINT_URL`) | + +### Трейсинг (OpenTelemetry) + +Блок OTEL в `base.py` (и обёртка WSGI в `wsgi.py`) включается по `os.getenv('USE_OTEL', False)`. **Важно:** проверяется истинность строки, а не её значение — любая непустая строка (в т.ч. `"False"`, `"0"`) включает трейсинг. Задействует пакет `django_otel_tools`. + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool-строка | не задана (выкл.) | Включает OTEL-трейсинг, otel-логгер и `OtelMiddleware` | +| `SERVICE_NAME` | string | `subscriptions` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool-строка | не задана (выкл.) | Небезопасное (без TLS) подключение к коллектору; та же логика истинности строки, что и `USE_OTEL` | + +> Захардкожено (не через окружение): `SECRET_KEY`, `DEBUG` (`False` в production), `ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CORS_ALLOW_ALL_ORIGINS=True`. + +--- + +## cron-задачи (`run_notifications`, `run_immediately_notifications`) + +Обе команды используют тот же модуль настроек и, помимо БД, обращаются к внешним сервисам для сбора данных и рассылки. Переменные читаются в `production.py` и потребляются в `server/apps/notification/management/commands/*.py`. + +### Внешние сервисы и рассылка + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SYSTEM_LOG_HOST` | string | да | — | Базовый URL сервиса system-log (логирование событий) | +| `USER_SERVICE_HOST` | string | да | — | Базовый URL сервиса пользователей (Django/ЛК) | +| `USER_SERVICE_LOGIN` | string | нет | `""` | Логин для авторизации в user-service | +| `USER_SERVICE_PASSWORD` | string | нет | `""` | Пароль для авторизации в user-service | +| `ALLOWED_HOST_EMAIL` | string | нет | `https://lk.sarex.io` | Базовый URL для формирования ссылок в письмах (`context_processing/document.py`) | + +### Email — Mailgun + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `IS_MAILGUN_USE` | bool-строка | нет | `True` | Использовать Mailgun для отправки писем | +| `MAILGUN_API_KEY` | string | нет | `""` | API-ключ Mailgun | +| `MAILGUN_BASE_URL` | string | нет | `https://api.mailgun.net/v3/mg.sarex.io` | Базовый URL Mailgun API | +| `MAILGUN_EMAIL_FROM` | string | нет | `hello@sarex.io` | Адрес отправителя | + +### Email — SMTP + +SMTP-ветка активируется, только если **заданы оба** `SMTP_EMAIL_HOST` и `SMTP_EMAIL_PORT` (по умолчанию `None` — SMTP отключён). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SMTP_EMAIL_HOST` | string | нет | `None` | Хост SMTP-сервера | +| `SMTP_EMAIL_PORT` | string | нет | `None` | Порт SMTP-сервера | +| `SMTP_EMAIL_FROM` | string | нет | `hello@sarex.io` | Адрес отправителя | + +### Telegram + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `IS_USE_TELEGRAM` | bool-строка | нет | `True` | Использовать Telegram-рассылку | +| `TELEGRAM_BOT_TOKEN` | string | нет | `""` | Токен Telegram-бота (`settings.TELEGRAM_BOT_TOKEN`) | + +> Значения `IS_*` читаются как строки Django-настроек: непустая строка истинна. Чтобы выключить канал, инфраструктурные манифесты задают `"false"`/`"0"` — но с точки зрения Python это тоже непустые строки, поэтому фактическое поведение зависит от того, как значение интерпретируется в коде команды (см. замечания). + +--- + +## Инфраструктурные и вспомогательные переменные + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_ADDRESS` | `base/*.yaml`, `.helm/values.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом не читается** — порт uwsgi фиксирован в `uwsgi.ini` (`http = 0.0.0.0:8000`) | +| `DJANGO_SETTINGS_MODULE` | `entrypoint.sh`, `wsgi.py`, `manage.py` | Модуль настроек Django (`config.settings.production` в проде) | +| `NEXUS_USERNAME`, `NEXUS_PASSWORD` | `compose/server/Dockerfile` (build-arg), `.gitlab-ci.yml` | Доступ к приватному PyPI (`nexus.infra.sarex.io`) при сборке образа | +| `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` (`variables`) | Параметры сборки/пайплайна | +| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME`, `ENABLE_BUILD_IMAGE` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `generic/common-ci` (деплой по окружениям stage/preprod/production) | + +> Обратите внимание: `SERVICE_NAME` встречается в двух ролях — как переменная приложения (имя сервиса в OTEL-трейсах) и как переменная CI (имя сервиса для сборки/чарта). Значения задаются в разных местах и не связаны между собой. + +--- + +## Деплой из этого репозитория (`iac/apps/subscriptions`) + +Здесь используется **kustomize** (не собственный Helm-чарт сервиса). Секреты БД и S3 инъектируются агентом **Vault** и подгружаются в окружение процесса до старта: + +``` +set -a +[ -f /vault/secrets/subscriptions-postgresql ] && . /vault/secrets/subscriptions-postgresql +[ -f /vault/secrets/subscriptions-minio ] && . /vault/secrets/subscriptions-minio +set +a +exec /server/entrypoint.sh +``` + +### `base/` + +- `backend-deployment.yaml` — обычные переменные инлайн в `env:` (`API_ADDRESS`, `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST`, `IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`, `SMTP_EMAIL_FROM/HOST/PORT`). Vault-шаблоны формируют `DATABASE_HOST/PORT/NAME/USER/PASSWORD` (из `secrets/data/postgresql/apps/subscriptions`, БД `subscriptions_db`) и `YC_S3_ACCESS_KEY_ID/SECRET_ACCESS_KEY/BUCKET_NAME/ENDPOINT_URL` (из `secrets/data/minio/apps/subscriptions`). +- `backend-service.yaml`, `namespace.yaml` (`istio-injection: enabled`), `serviceaccount.yaml` (`subscriptions-vault`). +- `kustomization.yaml` собирает namespace, serviceaccount, deployment и service. + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + `postgresql.yaml` (HelmRelease `postgresql-contour`: создаёт БД `subscriptions_db`, пользователя `subscriptions`, расширения `ltree`/`pg_stat_statements`/`postgis`/`timescaledb`, восстановление из дампа). +- **`brusnika-stage`** / **`brusnika-prod`** — Flux `HelmRelease` на `universal-chart` (v0.1.7). Переменные — в `envs` (`DATABASE_HOST/PORT/NAME`, `API_ADDRESS`, `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST`, `IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`, `SMTP_*`), секреты — в `secretEnvs` (`postgres-secret`: `username`/`password`; `yc-s3-secret`: `key_id`/`access_key`/`storage_bucket_name`/`endpoint_url`). Отличаются только `DATABASE_HOST` (`postgres-service` в prod vs `192.168.2.45` в stage). + +> В этих kustomize/brusnika-манифестах **не задаются** `USE_OTEL`, `TELEGRAM_BOT_TOKEN`, `MAILGUN_API_KEY`, `USER_SERVICE_LOGIN/PASSWORD`, `ALLOWED_HOST_EMAIL` — используются дефолты из кода. Telegram и Mailgun выключены (`IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`), рассылка идёт по SMTP. + +--- + +## Отличия от Helm-чарта репозитория (`.helm/values.yaml`) + +Собственный чарт сервиса (`.helm/`) — это **другой** путь деплоя, с более широким набором переменных, чем kustomize/brusnika здесь: + +- дополнительно задаёт `ALLOWED_HOST_EMAIL`, `USE_OTEL=True`, `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE=True` (per-env), а также секреты `MAILGUN_API_KEY` (`mailgun-cred`) и `TELEGRAM_BOT_TOKEN` (`telegram-bot-secret`); +- `IS_USE_TELEGRAM=true`, монтирует `uwsgi.ini` через ConfigMap; +- определяет `CronJob` `subscription-periodic` (`0,30 * * * *`, `run_notifications`) и `subscription-immediately` (`* * * * *`, `run_immediately_notifications`), наследующие `envs`/`secretEnvs` api. + +--- + +## Замечания и потенциальные проблемы + +- **`manage.py` указывает на несуществующий модуль настроек.** По умолчанию `manage.py` задаёт `DJANGO_SETTINGS_MODULE=config.settings.local`, но модуля `local.py` в репозитории нет (есть только `base.py` и `production.py`). Поэтому management-команды нужно запускать с явным `--settings=config.settings.production` (как это и делают entrypoint и cron-задачи). Локальный сервис `migrations` в `docker-compose.yml` вызывает `makemigrations` **без** `--settings` — команда упадёт из-за отсутствия `local`. +- **`USE_OTEL`/`USE_INSECURE` работают по истинности строки.** `os.getenv('USE_OTEL', False)` возвращает строку; любое непустое значение (включая `"False"`, `"0"`) включает трейсинг. Чтобы выключить — переменную нужно **не задавать вовсе**, а не ставить `False`. +- **Каналы рассылки `IS_MAILGUN_USE` / `IS_USE_TELEGRAM` — тоже строки.** Значения по умолчанию — `True` (Python-объект), но из окружения приходит строка; поведение зависит от того, как значение проверяется в коде команды. При настройке важно учитывать это (в манифестах используют `"false"`/`"0"`). +- **S3 обязателен для статики/медиа.** Хранилища заданы как S3 без файлового фолбэка; при отсутствии `YC_S3_*` operations со статикой/медиа будут падать, хотя сам процесс поднимется. +- **`SECRET_KEY` захардкожен** в `base.py` и не выносится в окружение — для продакшена это стоит вынести в секрет. +- **Расхождения имён БД между окружениями:** infra `base` и postgresql-чарт используют `subscriptions_db`, brusnika-оверлеи — `subscriptions`. При подключении важно использовать значение конкретного окружения. +- **Два деплой-пути расходятся по набору переменных** (kustomize/brusnika vs `.helm`): OTEL, Telegram-токен, Mailgun-ключ и `ALLOWED_HOST_EMAIL` присутствуют только в `.helm`. Это стоит учитывать при переносе окружения. + +--- + +## Минимальный набор для запуска + +**api** (uwsgi, миграции при старте): + +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `YC_S3_ACCESS_KEY_ID`, `YC_S3_SECRET_ACCESS_KEY`, `YC_S3_BUCKET_NAME`, `YC_S3_ENDPOINT_URL` +- при необходимости — `USE_OTEL` (+ `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE`) + +**cron `run_notifications` / `run_immediately_notifications`**: + +- блок БД (как у api) +- `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST` (+ `USER_SERVICE_LOGIN`, `USER_SERVICE_PASSWORD` при авторизации) +- канал рассылки: `IS_MAILGUN_USE` + `MAILGUN_API_KEY` **или** `SMTP_EMAIL_HOST` + `SMTP_EMAIL_PORT`; при Telegram — `IS_USE_TELEGRAM` + `TELEGRAM_BOT_TOKEN` +- при необходимости — `ALLOWED_HOST_EMAIL` (ссылки в письмах) + +См. пример значений в `.env.example` рядом с этим файлом. diff --git a/apps/subscriptions/openapi.yaml b/apps/subscriptions/openapi.yaml new file mode 100644 index 0000000..4c3fc11 --- /dev/null +++ b/apps/subscriptions/openapi.yaml @@ -0,0 +1,719 @@ +openapi: 3.0.3 + +info: + title: sarex-subscriptions API + version: "1.0.0" + description: | + REST API сервиса **sarex-subscriptions** — управление подписками на уведомления, + получателями, шаблонами и просмотр истории рассылок (события, транзакции, сообщения). + + Сервис на Django REST Framework. Роутинг — `rest_framework.routers.DefaultRouter`, + все ресурсы смонтированы под префиксом `/api/v1/` (`config/urls.py`). + + ### Аутентификация + Настроены `BasicAuthentication` и `SessionAuthentication` + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`). `DEFAULT_PERMISSION_CLASSES` + не заданы, поэтому по умолчанию действует `AllowAny` — эндпоинты доступны без + авторизации, а Basic-креды используются опционально. + + ### Пагинация + `LimitOffsetPagination`, `PAGE_SIZE = 1000`. Списки возвращают объект + `{ count, next, previous, results }`. Управление — query-параметрами `limit` и `offset`. + + ### Фильтрация + Бэкенд фильтров — `DjangoFilterBackend`. Ряд полей принимают список значений + через запятую (кастомный `CustomFilterList`, lookup `in`). + + ### Замечания (расхождения кода) + - `GET /transaction/{id}/` (retrieve) не имеет сериализатора в + `serializer_class_by_action` (есть ключи `list` и `get`, но не `retrieve`) — + поведение отдельного объекта может отличаться от списка. + - `MessageRetrieveSerializer` объявляет поле `created_at`, которого нет в модели + `Message` — поле показано в схеме как в коде сериализатора, но фактически может + приводить к ошибке. + - `RecipientWriteSerializer.extra_kwargs` ссылается на `telegram_user_name`, тогда как + в модели поле называется `telegram_username`. + +servers: + - url: https://api.sarex.io/api/v1 + description: Production (через ЛК) + - url: https://stage-api.sarex.io/api/v1 + description: Stage + - url: http://sarex-subscriptions-service.subscriptions-prod/api/v1 + description: Внутрикластерный адрес (ClusterIP) + +tags: + - name: subscription + description: Подписки на уведомления + - name: recipient + description: Получатели уведомлений + - name: event + description: События уведомлений + - name: template + description: Шаблоны уведомлений + - name: transaction + description: Транзакции рассылки + - name: message + description: Отправленные сообщения + +security: + - basicAuth: [] + - {} + +paths: + # ========================================================================== + # Subscription + # ========================================================================== + /subscription/: + get: + tags: [subscription] + summary: Список подписок + operationId: listSubscriptions + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: user_id + in: query + description: ID пользователя получателя (фильтр по recipient.user_id) + schema: { type: integer } + - name: company_id + in: query + description: ID компании; список значений через запятую + schema: { type: string } + - name: service_name + in: query + description: Название сервиса; список значений через запятую + schema: { type: string } + - name: instance_id + in: query + description: ID сущности; список значений через запятую + schema: { type: string } + - name: instance_uid + in: query + description: UUID сущности; список значений через запятую + schema: { type: string } + - name: model_name + in: query + description: Название модели (точное совпадение) + schema: { type: string } + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Subscription' } + post: + tags: [subscription] + summary: Создать подписку + description: | + Требуется хотя бы одно из полей `instance_id` / `instance_uid` / + `public_instance_id`. Если подписка с таким `instance_uid`/`instance_id` + (+ `model_name`, `recipient`) уже существует — возвращается существующая. + Если `period` не передан — устанавливается `TwoTimesDay` c `first_time=9:00`, + `second_time=17:00`. + operationId: createSubscription + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SubscriptionWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/Subscription' } + '400': + $ref: '#/components/responses/ValidationError' + + /subscription/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [subscription] + summary: Получить подписку + operationId: retrieveSubscription + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Subscription' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [subscription] + summary: Удалить подписку + operationId: destroySubscription + responses: + '204': { description: No Content } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # Recipient + # ========================================================================== + /recipient/: + get: + tags: [recipient] + summary: Список получателей + operationId: listRecipients + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: user_id + in: query + description: ID пользователя; список значений через запятую + schema: { type: string } + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Recipient' } + post: + tags: [recipient] + summary: Создать получателя + operationId: createRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': + $ref: '#/components/responses/ValidationError' + + /recipient/{user_id}/: + description: | + Поиск объекта выполняется по полю `user_id` (`lookup_field = "user_id"`), + а не по первичному ключу `id`. + parameters: + - name: user_id + in: path + required: true + description: ID пользователя (lookup_field) + schema: { type: integer } + get: + tags: [recipient] + summary: Получить получателя + operationId: retrieveRecipient + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [recipient] + summary: Обновить получателя + description: Поля `email`, `user_id`, `id` доступны только на чтение и не изменяются. + operationId: updateRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [recipient] + summary: Частично обновить получателя + operationId: partialUpdateRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [recipient] + summary: Удалить получателя + operationId: destroyRecipient + responses: + '204': { description: No Content } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationEvent + # ========================================================================== + /event/: + get: + tags: [event] + summary: Список событий + operationId: listEvents + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationEvent' } + post: + tags: [event] + summary: Зарегистрировать событие + operationId: createEvent + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEventWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEvent' } + '400': { $ref: '#/components/responses/ValidationError' } + + /event/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [event] + summary: Получить событие + operationId: retrieveEvent + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEvent' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationTemplate + # ========================================================================== + /template/: + get: + tags: [template] + summary: Список шаблонов + operationId: listTemplates + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationTemplate' } + post: + tags: [template] + summary: Создать шаблон + operationId: createTemplate + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplateWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplate' } + '400': { $ref: '#/components/responses/ValidationError' } + + /template/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [template] + summary: Получить шаблон + operationId: retrieveTemplate + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplate' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationTransaction (read-only) + # ========================================================================== + /transaction/: + get: + tags: [transaction] + summary: Список транзакций рассылки + operationId: listTransactions + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationTransaction' } + + /transaction/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [transaction] + summary: Получить транзакцию + description: > + Внимание: для действия retrieve не задан сериализатор + (`serializer_class_by_action` содержит только `list`/`get`), поведение + одиночного объекта может отличаться. + operationId: retrieveTransaction + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTransaction' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # Message (read-only) + # ========================================================================== + /message/: + get: + tags: [message] + summary: Список сообщений + operationId: listMessages + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Message' } + + /message/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [message] + summary: Получить сообщение + operationId: retrieveMessage + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Message' } + '404': { $ref: '#/components/responses/NotFound' } + +# ============================================================================ +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + + parameters: + Limit: + name: limit + in: query + description: Кол-во объектов на странице (LimitOffsetPagination) + schema: { type: integer, default: 1000 } + Offset: + name: offset + in: query + description: Смещение от начала выборки + schema: { type: integer, minimum: 0 } + PathId: + name: id + in: path + required: true + description: Первичный ключ объекта + schema: { type: integer } + + responses: + NotFound: + description: Объект не найден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + ValidationError: + description: Ошибка валидации + content: + application/json: + schema: + type: object + additionalProperties: true + example: + field_name: ["Обязательное поле."] + + schemas: + # ---- Общие ---- + PaginatedList: + type: object + properties: + count: { type: integer, example: 42 } + next: + type: string + format: uri + nullable: true + example: "http://host/api/v1/subscription/?limit=1000&offset=1000" + previous: + type: string + format: uri + nullable: true + results: + type: array + items: {} + required: [count, results] + + Detail: + type: object + properties: + detail: { type: string, example: "Не найдено." } + + # ---- Перечисления ---- + PeriodType: + type: integer + description: | + Периодичность рассылки: + * 0 — Два раза в день + * 1 — Каждый день (default) + * 2 — Один раз в неделю + * 3 — Сразу при изменении + enum: [0, 1, 2, 3] + default: 1 + + WeekdayType: + type: integer + description: | + День недели: 1 — Пн, 2 — Вт, 3 — Ср, 4 — Чт, 5 — Пт, 6 — Сб, 7 — Вс + enum: [1, 2, 3, 4, 5, 6, 7] + nullable: true + + EventType: + type: integer + description: | + Тип события: 0 — Edit (default), 1 — Create, 2 — Delete, 3 — Copy, 4 — Read + enum: [0, 1, 2, 3, 4] + default: 0 + + MessageServiceType: + type: integer + description: "Способ отправки: 0 — Email (default), 1 — Telegram" + enum: [0, 1] + default: 0 + + TransactionStatus: + type: integer + description: | + Статус транзакции: 0 — Новая, 1 — В обработке, 2 — Успешно, 3 — Ошибка + enum: [0, 1, 2, 3] + + # ---- Subscription ---- + Subscription: + type: object + properties: + id: { type: integer, readOnly: true } + model_name: { type: string, maxLength: 255 } + instance_id: { type: integer, nullable: true } + instance_uid: { type: string, format: uuid, nullable: true } + public_instance_id: { type: string, format: uuid, nullable: true } + company_id: { type: integer } + service_name: { type: string, maxLength: 255, nullable: true } + recipient: + type: integer + description: ID получателя (FK recipient.Recipient) + period: { $ref: '#/components/schemas/PeriodType' } + weekday: { $ref: '#/components/schemas/WeekdayType' } + first_time: { type: string, maxLength: 6, nullable: true, example: "9:00" } + second_time: { type: string, maxLength: 6, nullable: true, example: "17:00" } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [id, model_name, company_id, recipient] + + SubscriptionWrite: + type: object + description: > + Одно из instance_id / instance_uid / public_instance_id обязательно. + properties: + model_name: { type: string, maxLength: 255 } + instance_id: { type: integer, nullable: true } + instance_uid: { type: string, format: uuid, nullable: true } + public_instance_id: { type: string, format: uuid, nullable: true } + company_id: { type: integer } + service_name: { type: string, maxLength: 255, nullable: true } + recipient: { type: integer, description: ID получателя } + period: + allOf: [{ $ref: '#/components/schemas/PeriodType' }] + nullable: true + weekday: { $ref: '#/components/schemas/WeekdayType' } + first_time: { type: string, maxLength: 6, nullable: true } + second_time: { type: string, maxLength: 6, nullable: true } + required: [model_name, company_id, recipient] + + # ---- Recipient ---- + Recipient: + type: object + properties: + id: { type: integer, readOnly: true } + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128, default: "" } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + email: { type: string, format: email } + user_id: { type: integer, nullable: true } + required: [id] + + RecipientWrite: + type: object + properties: + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128 } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + email: { type: string, format: email } + user_id: { type: integer, nullable: true } + + RecipientUpdate: + type: object + description: "email, user_id, id — только для чтения." + properties: + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128 } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + + # ---- NotificationEvent ---- + NotificationEvent: + type: object + properties: + id: { type: integer, readOnly: true } + registered_at: { type: string, format: date-time } + routing_key: { type: string, maxLength: 256, description: "Slug-тег" } + attributes: { type: object, additionalProperties: true, default: {} } + short_description: { type: string, maxLength: 512, nullable: true } + required: [id, registered_at, routing_key] + + NotificationEventWrite: + type: object + properties: + registered_at: { type: string, format: date-time } + routing_key: { type: string, maxLength: 256 } + attributes: { type: object, additionalProperties: true } + short_description: { type: string, maxLength: 512, nullable: true } + required: [registered_at, routing_key] + + # ---- NotificationTemplate ---- + NotificationTemplate: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 1024 } + title: { type: string, maxLength: 1024 } + template_text: { type: string } + attributes: { type: object, additionalProperties: true, default: {} } + event_type: { $ref: '#/components/schemas/EventType' } + model_name: { type: string, maxLength: 255, nullable: true } + required: [id, name, title, template_text] + + NotificationTemplateWrite: + type: object + properties: + name: { type: string, maxLength: 1024 } + title: { type: string, maxLength: 1024 } + template_text: { type: string } + attributes: { type: object, additionalProperties: true } + event_type: { $ref: '#/components/schemas/EventType' } + model_name: { type: string, maxLength: 255, nullable: true } + required: [name, title, template_text] + + # ---- NotificationTransaction ---- + NotificationTransaction: + type: object + properties: + id: { type: integer, readOnly: true } + status: { $ref: '#/components/schemas/TransactionStatus' } + started_at: { type: string, format: date-time } + finished_at: { type: string, format: date-time, nullable: true } + error_message: { type: string, nullable: true } + event: { type: integer, nullable: true, description: "ID связанного события" } + required: [id, status, started_at] + + # ---- Message ---- + Message: + type: object + properties: + id: { type: integer, readOnly: true } + created_at: + type: string + format: date-time + description: > + Объявлено в сериализаторе; в модели Message поле отсутствует (см. замечания). + service_type: { $ref: '#/components/schemas/MessageServiceType' } + transaction: { type: integer, description: "ID транзакции (FK)" } + subject: { type: string, default: "" } + text: { type: string } + recipients: + type: array + items: { type: integer } + description: "ID получателей (M2M recipient.Recipient)" + required: [id, transaction, text] diff --git a/apps/system-log/.env.example b/apps/system-log/.env.example new file mode 100644 index 0000000..86e8001 --- /dev/null +++ b/apps/system-log/.env.example @@ -0,0 +1,58 @@ +# ============================================================================ +# system-log — пример переменных окружения (api + worker) +# +# Скопируйте нужный блок в config.env / .env соответствующего сервиса. +# Переменные, помеченные (обяз.), обязательны — без них процесс не стартует. +# bool принимает 1/0, true/false. +# ============================================================================ + +# --- Общие: приложение и логирование (api + worker) ------------------------- +APP_NAME=SYSTEM_LOG # (обяз.) имя приложения +APP_VERSION=0.0.1 # (обяз.) версия приложения +LOG_LEVEL=info # (обяз.) уровень логирования: info/debug/... + +# --- HTTP-сервер (только api; worker не читает) ----------------------------- +HTTP_HOST=127.0.0.1 # (обяз. для api) хост HTTP-сервера +HTTP_PORT=8888 # (обяз. для api) порт HTTP-сервера (/ping) + +# --- PostgreSQL (api + worker) ---------------------------------------------- +POSTGRES_ADDRESS=127.0.0.1 # (обяз.) хост PostgreSQL +POSTGRES_PORT=6432 # (обяз.) порт PostgreSQL +POSTGRES_DB=systemlog # (обяз.) имя базы данных +POSTGRES_USER=user # (обяз.) пользователь БД +POSTGRES_PASSWORD=password # (обяз.) пароль пользователя БД +ENABLE_SQL_QUERY=1 # логировать SQL-запросы (по умолчанию 0) +ENABLE_SSL=0 # TLS к PostgreSQL с проверкой по YC-PG-CERTIFICATE (по умолчанию 0) +# YC-PG-CERTIFICATE= # содержимое (PEM) CA-сертификата PostgreSQL; нужно при ENABLE_SSL=1 + +# --- Kafka (только api) ----------------------------------------------------- +# KAFKA_ENABLE обязательна. Если 0 — остальные KAFKA_* можно не задавать. +KAFKA_ENABLE=0 +# KAFKA_BROKERS=localhost:9091,localhost:9092 # список брокеров через запятую +# KAFKA_GROUP=system-log-local # имя consumer-группы +# KAFKA_CLIENT_ID=system-log-local # client id (в логах брокера) +# KAFKA_USERNAME=username # пользователь Kafka +# KAFKA_PASSWORD=password # пароль пользователя Kafka +# KAFKA_USE_SSL=0 # TLS для Kafka +# KAFKA_ENABLE_LOGGING=0 # отладочное логирование клиента +# KAFKA_PEM_PATH= # СОДЕРЖИМОЕ (PEM) сертификата (не путь!), при KAFKA_USE_SSL=1 +# KAFKA_TOPIC=system-log-local # топик для продюсера + +# --- Трейсинг OpenTelemetry (только api; необязательно) --------------------- +# TRACER_USE=true # включить трейсинг (по умолчанию true) +# TRACER_HOST=localhost:4317 # адрес OTLP-коллектора +# TRACER_USE_INSECURE=true # подключение без TLS +# SERVICE_NAME=system-log # имя сервиса в трейсах +# TRACER_LOGGER_NAME=tracer_logger # имя otel-логгера + +# --- Внешние сервисы (только worker) ---------------------------------------- +DOCUMENTATIONS_URL=http://localhost:6666 # (обяз. для worker) URL сервиса documentations +DJANGO_HOST=http://localhost:8000 # (обяз. для worker) URL Django/ЛК +SUPER_USERNAME=superuser # (обяз. для worker) логин суперпользователя Django +SUPER_PASSWORD=password # (обяз. для worker) пароль суперпользователя Django + +# --- Вспомогательные (не читаются кодом приложений) ------------------------- +PG_URL=postgres://user:password@localhost:6432/systemlog # строка подключения для CLI migrate (Makefile) +EXTERNAL_POSTGRES_PORT=6432 # внешний порт проброса Postgres в docker-compose +# NAMESPACE=system-log # задаётся в манифестах, кодом не читается +# POSTGRES_POOL_SIZE=3 # задаётся в манифестах, кодом не читается diff --git a/apps/system-log/CONFIGURATION.md b/apps/system-log/CONFIGURATION.md new file mode 100644 index 0000000..1892aa5 --- /dev/null +++ b/apps/system-log/CONFIGURATION.md @@ -0,0 +1,163 @@ +# Конфигурация system-log (api + worker) + +Документ описывает все переменные окружения и способы конфигурирования сервисов репозитория `system-log`: + +- **api** — HTTP-сервис (`platform/system-log`), пишет события в PostgreSQL и (опционально) в Kafka; +- **worker** — фоновый воркер (`platform/system-log-worker`), обогащает события данными из сервисов documentations и Django (ЛК). + +## Способы конфигурирования + +Оба сервиса настраиваются **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`cleanenv`](https://github.com/ilyakaznacheev/cleanenv) (функция `config.NewConfig()` → `cleanenv.ReadEnv`). Отдельного конфиг-файла (yaml/toml) у приложений нет. + +Обязательные переменные помечены тегом `env-required:"true"` — при их отсутствии `NewConfig()` вернёт ошибку и процесс завершится (`log.Fatalf`). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose + бинарник) | Файл `config.env` в корне репозитория. `docker-compose.yml` (`env_file: ./config.env`) поднимает только контейнер Postgres (`timescale/timescaledb-ha`); сам сервис запускается бинарником. `Makefile` через `include config.env` использует `PG_URL` для миграций | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values-.yaml`: блок `envs` (обычные значения) и `secrets` (значения из k8s-секретов). Деплой запускается пайплайнами `generic/common-ci` | +| Kubernetes — этот infra-репозиторий (`iac/apps/system-log`) | `base/` — kustomize-манифесты с инъекцией секретов через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + postgresql), `brusnika-stage` (Flux `HelmRelease` на `universal-chart`, блоки `envs`/`secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci`; в `workflow.rules` задаются переменные пайплайна (`RELEASE_NAME`, `*_NAMESPACE`, `CHART_NAME`, `CHART_VERSION`, `STAND`, `IMAGE_PATH`, `HELM_SET_ARGS` и т.п.) | + +**Миграции БД (api).** Отдельного шага миграций в entrypoint нет: миграции выполняются **в процессе при старте api** — сборка идёт с тегом `-tags migrate`, и `init()` в `internal/app/http/migrate.go` применяет `migrate up` из каталога `/migrations` перед запуском HTTP-сервера. Локально миграции можно прогнать через `make migrate-up-local` (использует `PG_URL` из `config.env`). Воркер миграций не выполняет. + +--- + +## api (`system-log`) + +Переменные читаются структурой `config.Config` (`config/config.go`): `App`, `Log`, `HTTP`, `POSTGRES`, `KAFKA`, `TRACER`. + +### Приложение и логирование + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `APP_NAME` | string | да | — | Имя приложения | +| `APP_VERSION` | string | да | — | Версия приложения | +| `LOG_LEVEL` | string | да | — | Уровень логирования (`info`/`INFO`, `debug` и т.п.) | +| `HTTP_HOST` | string | да | — | Хост прослушивания HTTP-сервера | +| `HTTP_PORT` | uint | да | — | Порт HTTP-сервера (эндпоинт `/ping` — liveness/readiness) | + +### PostgreSQL + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | да | — | Хост PostgreSQL | +| `POSTGRES_PORT` | string | да | — | Порт PostgreSQL | +| `POSTGRES_DB` | string | да | — | Имя базы данных | +| `POSTGRES_USER` | string | да | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | да | — | Пароль пользователя БД | +| `ENABLE_SQL_QUERY` | bool | нет | `false` | Логировать SQL-запросы | +| `ENABLE_SSL` | bool | нет | `false` | Подключаться к PostgreSQL по TLS с проверкой по `YC-PG-CERTIFICATE`; иначе к строке подключения добавляется `?sslmode=disable` | +| `YC-PG-CERTIFICATE` | string | нет | — | Содержимое (PEM) CA-сертификата PostgreSQL; используется при `ENABLE_SSL=1` | + +### Kafka + +`KAFKA_ENABLE` обязателен всегда. Если `KAFKA_ENABLE=0`, продюсер не создаётся и остальные `KAFKA_*` можно не задавать. Если `KAFKA_ENABLE=1`, для корректной работы нужны брокеры/креды/топик (в самом коде они помечены как необязательные, но без них подключение к Kafka не поднимется). + +| Переменная | Тип | Обяз. | Назначение | +| --- | --- | --- | --- | +| `KAFKA_ENABLE` | bool | да | Включает отправку сообщений в Kafka | +| `KAFKA_BROKERS` | string | нет | Список адресов брокеров через запятую (напр. `host:9091,host:9092`) | +| `KAFKA_GROUP` | string | нет | Имя consumer-группы (отображается в логах брокера) | +| `KAFKA_CLIENT_ID` | string | нет | Client ID (отображается в логах брокера) | +| `KAFKA_USERNAME` | string | нет | Пользователь Kafka | +| `KAFKA_PASSWORD` | string | нет | Пароль пользователя Kafka | +| `KAFKA_USE_SSL` | bool | нет | Включить TLS для подключения к Kafka | +| `KAFKA_ENABLE_LOGGING` | bool | нет | Включить отладочное логирование клиента Kafka | +| `KAFKA_PEM_PATH` | string | нет | **Содержимое** (PEM) сертификата для Kafka при `KAFKA_USE_SSL=1`. Несмотря на имя `..._PATH`, значение трактуется как сам сертификат, а не путь к файлу (`pkg/sarex-kafka-connector/kafkasrx.go`) | +| `KAFKA_TOPIC` | string | нет | Топик, в который продюсер шлёт события | + +### Трейсинг (OpenTelemetry) + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACER_USE` | bool | `true` | Включает OpenTelemetry-трейсинг и otel-логгер | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `SERVICE_NAME` | string | `system-log` | Имя сервиса в трейсах | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя otel-логгера | + +> Тип `bool` в cleanenv принимает `1`/`0`, `true`/`false` и т.п. + +--- + +## worker (`system-log-worker`) + +Переменные читаются структурой `config.Config` (`config/config.go`): `App`, `Log`, `POSTGRES`, `DOCUMENTATIONS`, `DJANGO`. **Kafka и трейсинг воркер не использует.** HTTP-сервера у воркера нет (структура `HTTP` в конфиге отсутствует), поэтому `HTTP_HOST`/`HTTP_PORT` кодом не читаются, хотя и задаются в манифестах. + +### Приложение, логирование, PostgreSQL + +Совпадают с api: `APP_NAME`, `APP_VERSION`, `LOG_LEVEL` (все обязательны) и блок PostgreSQL — `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` (обязательны), `ENABLE_SQL_QUERY`, `ENABLE_SSL`, `YC-PG-CERTIFICATE` (необязательны). См. таблицы выше. + +### Внешние сервисы (обогащение событий) + +| Переменная | Тип | Обяз. | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATIONS_URL` | string | да | Базовый URL сервиса documentations (клиент `documentations.New`) | +| `DJANGO_HOST` | string | да | Базовый URL Django/ЛК (клиент `projecttask.New`) | +| `SUPER_USERNAME` | string | да | Логин суперпользователя для авторизации в Django | +| `SUPER_PASSWORD` | string | да | Пароль суперпользователя для авторизации в Django | + +> Воркер работает циклически (внутренний интервал опроса — фиксированные `20s` в коде `internal/app/worker/worker.go`, не настраивается переменной окружения). + +--- + +## Инфраструктурные и вспомогательные переменные + +Не читаются кодом приложений, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `PG_URL` | `config.env`, `Makefile` (`make migrate-*`) | Строка подключения для CLI `migrate` при локальных миграциях | +| `EXTERNAL_POSTGRES_PORT` | `config.env`, `docker-compose.yml` | Внешний порт проброса контейнера Postgres | +| `NAMESPACE` | Helm-чарты, `base/*.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом приложений не читается** | +| `POSTGRES_POOL_SIZE` | Helm-чарты, `base/*.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом приложений не читается** (в `config.Config` поля пула соединений нет) | +| `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `STAND`, `*_NAMESPACE`, `IMAGE_PATH`, `HELM_SET_ARGS`, `DOCKERFILE_PATH`, `ENABLE_*` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `generic/common-ci` (сборка чарта/образа, деплой) | + +--- + +## Деплой из этого репозитория (`iac/apps/system-log`) + +Здесь используется **kustomize** (не собственный Helm-чарт сервиса). Секреты БД, Kafka и Django инъектируются агентом **Vault** и подгружаются в окружение процесса до старта (`set -a; . /vault/secrets/...; exec /app`). + +### `base/` + +- `backend-deployment.yaml` (api) — обычные переменные заданы инлайн в `env:`; Vault-шаблоны формируют `POSTGRES_ADDRESS/PORT/DB/USER/PASSWORD` (из `secrets/data/postgresql/apps/system-log`) и `KAFKA_USERNAME/PASSWORD/BROKERS/TOPIC` (из `secrets/data/kafka/apps/system-log`). Прочие Kafka-параметры и `KAFKA_PEM_PATH=/tmp` — инлайн. +- `worker-deployment.yaml` (worker) — Vault формирует `POSTGRES_*` и `SUPER_USERNAME/SUPER_PASSWORD` (из `secrets/data/vault/common/django_auth`); `DOCUMENTATIONS_URL`, `DJANGO_HOST` и прочее — инлайн. +- `kustomization.yaml` собирает `namespace`, `serviceaccount`, api/worker deployments и service. + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + `postgresql.yaml` (HelmRelease `postgresql-contour`, создаёт БД `system_log_db`, пользователя `system_log`, расширения `ltree`/`pg_stat_statements`/`timescaledb`, восстановление из дампа). +- **`brusnika-stage`** — Flux `HelmRelease` на `universal-chart` (per-env значения `_default`/`stage`/`preprod`/`production`). Переменные — в `envs`, секреты — в `secretEnvs` (`postgres-secret`, `ya-kafka-secret`, `yc-kafka-certificate`, `superuser`). + +--- + +## Замечания и потенциальные проблемы + +- **`config.env` для локального запуска неполон.** Для api не заданы `KAFKA_ENABLE` (обязательна) и `APP_NAME`/`APP_VERSION`/`LOG_LEVEL`/`HTTP_*` присутствуют, а вот блок `TRACER_*` возьмётся из дефолтов. Для worker в `config.env` **нет обязательных** `DJANGO_HOST`, `SUPER_USERNAME`, `SUPER_PASSWORD` (и `DOCUMENTATIONS_URL` есть) — без них воркер не стартует. Также `config.env` содержит `HTTP_HOST`/`HTTP_PORT`, которые воркер не использует. +- **Имя `KAFKA_PEM_PATH` вводит в заблуждение:** код кладёт значение переменной как содержимое PEM-сертификата, а не путь к файлу. При этом в `README.md` фигурирует `KAFKA_PEM_CERT`, а brusnika-оверлей задаёт **обе** переменные (`KAFKA_PEM_CERT` и `KAFKA_PEM_PATH`) с одним значением — кодом читается только `KAFKA_PEM_PATH`. +- **`NAMESPACE` и `POSTGRES_POOL_SIZE`** задаются во всех манифестах, но кодом не читаются (размер пула соединений в конфиге не предусмотрен). +- Имя переменной `YC-PG-CERTIFICATE` содержит дефисы — cleanenv сопоставляет её по точному совпадению тега `env`. +- Значения `POSTGRES_DB`/`APP_NAME` расходятся между окружениями (`system_log` vs `system-log`, `system_log_db` в Vault) — при подключении важно использовать значение конкретного окружения. + +--- + +## Минимальный набор для локального запуска + +**api** (Postgres — через docker-compose, api — бинарником с миграциями при старте): + +- `APP_NAME`, `APP_VERSION`, `LOG_LEVEL` +- `HTTP_HOST`, `HTTP_PORT` +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` +- `KAFKA_ENABLE=0` (иначе — весь блок `KAFKA_*`) +- при необходимости — `ENABLE_SQL_QUERY`, `ENABLE_SSL` (+ `YC-PG-CERTIFICATE`), `TRACER_*` + +**worker**: + +- `APP_NAME`, `APP_VERSION`, `LOG_LEVEL` +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` +- `DOCUMENTATIONS_URL`, `DJANGO_HOST`, `SUPER_USERNAME`, `SUPER_PASSWORD` + +См. пример значений в `config.env` каждого репозитория. diff --git a/apps/system-log/openapi.yaml b/apps/system-log/openapi.yaml new file mode 100644 index 0000000..3143872 --- /dev/null +++ b/apps/system-log/openapi.yaml @@ -0,0 +1,427 @@ +openapi: 3.0.3 + +info: + title: system-log API + version: "1.0.0" + description: | + REST API сервиса **system-log** (`platform/system-log`) — приём и выборка + записей журнала системных событий (кто, над чем и какое действие совершил). + + Сервис написан на Go (фреймворк **Fiber v2**). Роутинг собирается в + `internal/controller/http/v0`: базовая группа `/api` → подгруппа `v0` → + ресурс `/system_log`. Плюс служебный эндпоинт `/ping`. + + ### Аутентификация + Аутентификация на уровне приложения не настроена — эндпоинты доступны без + авторизации. Ограничение доступа обеспечивается сетевым слоем (ClusterIP / + Istio), а не самим сервисом. + + ### Пагинация + Списочные ответы возвращают объект `{ count, limit, offset, next, prev, result }`. + Управление — параметрами `limit` и `offset`. Значения по умолчанию: + `limit = 100`, `offset = 0` (константы `_defaultLimit` / `_defaultOffset` + в `internal/controller/http/v0/systemlog/controller.go`). Ссылки `next`/`prev` + формируются в `pkg/pagination`. У эндпоинта поиска (`POST /search`) полей + `next`/`prev` нет. + + ### Фильтрация + Массивные фильтры (`actor_ids`, `event_names`, `model_names`, `instance_ids`, + `target_ids`, `company_ids`, `statuses`, `message`) в GET-запросе передаются + списком значений через запятую и парсятся Fiber `QueryParser`. Поля + `instance_uuids` и `resource_uuids` разбираются вручную (`strings.Split` + + `uuid.Parse`) — при невалидном UUID возвращается `400`. + + ### Замечания (расхождения кода) + - `metadata` в фильтре объявлено как `map[string]interface{}` с query-тегом, + но при передаче в query-строке Fiber не восстановит вложенный объект — + фильтрация по `metadata` практически применима через тело `POST /search`. + - Поле `is_processed_by_worker` присутствует в фильтре, но отдаётся в выборку + наравне с остальными; используется воркером обогащения. + - Ответ `POST /system_log/` при успехе — простая строка `"OK"` (не объект). + +servers: + - url: https://api.sarex.io + description: Production (ingress) + - url: https://stage-api.sarex.io + description: Stage (ingress) + - url: http://backend-svc.system-log.svc.cluster.local + description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000) + - url: http://localhost:8888 + description: Локальный запуск (config.env) + +tags: + - name: system_log + description: Записи журнала системных событий + - name: service + description: Служебные эндпоинты + +paths: + # ========================================================================== + # Service + # ========================================================================== + /ping: + get: + tags: [service] + summary: Проверка живости + description: Liveness/readiness-проба (используется в probes деплоймента). Всегда `200 OK` без тела. + operationId: ping + security: [] + responses: + '200': + description: OK + + # ========================================================================== + # System log + # ========================================================================== + /api/v0/system_log/: + get: + tags: [system_log] + summary: Список записей журнала (с фильтрами) + description: | + Возвращает отфильтрованные записи журнала с пагинацией. + Массивные параметры принимают список значений через запятую. + operationId: getSystemLogRecords + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: actor_ids + in: query + description: ID авторов событий; список значений через запятую + example: "1,2,3" + schema: { type: string } + - name: event_names + in: query + description: Названия событий (напр. create, edit, delete, transfer); через запятую + example: "create,edit,delete" + schema: { type: string } + - name: model_names + in: query + description: Названия моделей (напр. document, bundle, project); через запятую + example: "document,bundle" + schema: { type: string } + - name: instance_ids + in: query + description: ID сущностей; список значений через запятую + example: "1,2,3" + schema: { type: string } + - name: instance_uuids + in: query + description: UUID сущностей; список значений через запятую (валидируются как UUID) + example: "3fa85f64-5717-4562-b3fc-2c963f66afa6" + schema: { type: string } + - name: resource_uuids + in: query + description: UUID ресурсов; список значений через запятую (валидируются как UUID) + schema: { type: string } + - name: registered_at_gte + in: query + description: Нижняя граница времени регистрации (>=) + example: "2025-02-16T15:04:05Z" + schema: { type: string, format: date-time } + - name: registered_at_lte + in: query + description: Верхняя граница времени регистрации (<=) + example: "2025-02-16T15:04:05Z" + schema: { type: string, format: date-time } + - name: instance_path + in: query + description: Путь сущности (ltree), точное совпадение + example: "1.79899.80131" + schema: { type: string } + - name: target_ids + in: query + description: ID целей; список значений через запятую + example: "1,2,3" + schema: { type: string } + - name: target_path + in: query + description: Путь цели (ltree), точное совпадение + example: "4.3.2.1" + schema: { type: string } + - name: company_ids + in: query + description: ID компаний; список значений через запятую + example: "1,2,3" + schema: { type: string } + - name: statuses + in: query + description: Подстатусы события; список значений через запятую + example: "add_bundle,delete_bundle,rename_document" + schema: { type: string } + - name: message + in: query + description: Сообщения; список значений через запятую + schema: { type: string } + - name: is_processed_by_worker + in: query + description: Признак обработки записи воркером обогащения + schema: { type: boolean } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/SystemLogListResponse' } + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + post: + tags: [system_log] + summary: Создать записи журнала + description: | + Принимает пакет записей в поле `system_logs`. Для каждой записи должно + быть задано **ровно одно** из `instance_id` / `instance_uuid` + (не оба и не ни одного — иначе `400`). Обязательны `event_name` и + `model_name`. При успехе возвращается строка `"OK"`. + operationId: createSystemLogRecords + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SystemLogCreateRequest' } + responses: + '200': + description: Записи созданы + content: + application/json: + schema: + type: string + example: "OK" + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v0/system_log/search: + post: + tags: [system_log] + summary: Поиск записей журнала (фильтры в теле) + description: | + Аналог GET-выборки, но фильтры передаются в теле запроса + (`filters`). Позволяет использовать сложные фильтры (в т.ч. `metadata`), + которые неудобно кодировать в query-строке. В ответе нет `next`/`prev`. + operationId: searchSystemLogs + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SystemLogSearchRequest' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/SystemLogSearchResponse' } + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + +# ============================================================================ +components: + parameters: + Limit: + name: limit + in: query + description: Кол-во записей на странице + schema: { type: integer, format: uint64, default: 100 } + Offset: + name: offset + in: query + description: Смещение от начала выборки + schema: { type: integer, format: uint64, default: 0, minimum: 0 } + + responses: + BadRequest: + description: Ошибка разбора/валидации запроса + content: + application/json: + schema: { $ref: '#/components/schemas/ControllerError' } + InternalError: + description: Внутренняя ошибка сервиса + content: + application/json: + schema: { $ref: '#/components/schemas/ControllerError' } + + schemas: + # ---- Общие ---- + ControllerError: + type: object + description: Формат ошибки (`internal/controller/errors.go`, `ErrorToResponse`). + properties: + message: { type: string, example: "error parse body" } + status_code: { type: integer, example: 400 } + + # ---- Доменная запись ---- + SystemLog: + type: object + description: Запись журнала системных событий (`internal/entity/system_log.go`). + properties: + actor_id: + type: integer + format: uint64 + description: ID автора события + example: 1023 + event_name: + type: string + description: Название события (напр. edit, create, copy) + example: "edit" + model_name: + type: string + description: Над какой сущностью произошло событие (напр. document, inspection) + example: "document" + instance_id: + type: integer + format: uint64 + nullable: true + description: ID сущности (для моделей с числовым ключом) + example: 80131 + instance_uuid: + type: string + format: uuid + nullable: true + description: UUID сущности (для моделей с UUID-ключом) + registered_at: + type: string + format: date-time + nullable: true + description: Время регистрации события + example: "2023-05-04T08:09:53.852916Z" + instance_path: + type: string + nullable: true + description: Путь сущности (ltree) + example: "1.79899.80131" + target_id: + type: integer + format: uint64 + nullable: true + description: ID цели события + company_id: + type: integer + format: uint64 + nullable: true + description: ID компании (может обогащаться воркером) + example: 1 + metadata: + type: object + additionalProperties: true + description: Произвольные дополнительные данные о событии + example: { "document_name": "КСГ.pdf", "count_bundle": 3 } + message: + type: string + nullable: true + description: Произвольное сообщение + example: "bundle was deleted from document" + status: + type: string + nullable: true + description: Подстатус события + example: "delete_bundle" + resource_uuid: + type: string + format: uuid + nullable: true + description: UUID связанного ресурса + target_path: + type: string + nullable: true + description: Путь цели (ltree) + required: [event_name, model_name] + + # ---- Запросы ---- + SystemLogCreateRequest: + type: object + description: > + Пакет записей для создания. Для каждой записи обязательно ровно одно из + instance_id / instance_uuid. + properties: + system_logs: + type: array + minItems: 1 + items: { $ref: '#/components/schemas/SystemLog' } + required: [system_logs] + + SystemLogFilter: + type: object + description: Набор фильтров выборки (`entity.SystemLogFilter`). + properties: + limit: { type: integer, format: uint64, default: 100 } + offset: { type: integer, format: uint64, default: 0 } + actor_ids: + type: array + items: { type: integer, format: uint64 } + event_names: + type: array + items: { type: string } + model_names: + type: array + items: { type: string } + instance_ids: + type: array + items: { type: integer, format: uint64 } + instance_uuids: + type: array + items: { type: string, format: uuid } + registered_at_gte: { type: string, format: date-time, nullable: true } + registered_at_lte: { type: string, format: date-time, nullable: true } + instance_path: { type: string, nullable: true } + target_ids: + type: array + items: { type: integer, format: uint64 } + target_path: { type: string, nullable: true } + company_ids: + type: array + items: { type: integer, format: uint64 } + statuses: + type: array + items: { type: string } + message: { type: string, nullable: true } + metadata: + type: object + additionalProperties: true + resource_uuids: + type: array + items: { type: string, format: uuid } + is_processed_by_worker: { type: boolean, nullable: true } + + SystemLogSearchRequest: + type: object + description: Тело запроса поиска (`SearchParams` в `dto.go`). + properties: + limit: { type: integer, format: uint64, default: 100 } + offset: { type: integer, format: uint64, default: 0 } + filters: { $ref: '#/components/schemas/SystemLogFilter' } + + # ---- Ответы ---- + SystemLogListResponse: + type: object + description: Ответ списочной выборки (`entity.SystemLogRecordsRequest`). + properties: + count: { type: integer, format: uint64, example: 1226 } + limit: { type: integer, format: uint64, nullable: true, example: 100 } + offset: { type: integer, format: uint64, nullable: true, example: 0 } + next: + type: string + nullable: true + example: "http://localhost:8888/api/v0/system_log?limit=100&offset=100" + prev: + type: string + nullable: true + result: + type: array + items: { $ref: '#/components/schemas/SystemLog' } + required: [count, result] + + SystemLogSearchResponse: + type: object + description: Ответ поиска (`entity.SystemLogRecordsSearch`) — без next/prev. + properties: + count: { type: integer, format: uint64, example: 1226 } + limit: { type: integer, format: uint64, nullable: true, example: 100 } + offset: { type: integer, format: uint64, nullable: true, example: 0 } + result: + type: array + items: { $ref: '#/components/schemas/SystemLog' } + required: [count, result] diff --git a/apps/transmittal/.env.example b/apps/transmittal/.env.example new file mode 100644 index 0000000..1fa7fdd --- /dev/null +++ b/apps/transmittal/.env.example @@ -0,0 +1,121 @@ +# App +TRANSMITTAL_SERVICE_APP__NAME='Transmittal Service' +TRANSMITTAL_SERVICE_APP__LOG_LEVEL=INFO +TRANSMITTAL_SERVICE_APP__HOST=https://stage.sarex.io/transmittal +TRANSMITTAL_SERVICE_APP__ENVIRONMENT=stage + +# Auth +# Replace newlines with \n +TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY='' + +# CORS +TRANSMITTAL_SERVICE_CORS__ALLOW_ORIGINS='["*"]' +TRANSMITTAL_SERVICE_CORS__ALLOW_METHODS='["*"]' +TRANSMITTAL_SERVICE_CORS__ALLOW_HEADERS='["*"]' +TRANSMITTAL_SERVICE_CORS__ALLOW_CREDENTIALS=True + +# Uvicorn +TRANSMITTAL_SERVICE_UVICORN__HOST=127.0.0.1 +TRANSMITTAL_SERVICE_UVICORN__PORT=8001 +TRANSMITTAL_SERVICE_UVICORN__ENABLE_AUTO_RELOAD=False +TRANSMITTAL_SERVICE_UVICORN__LOG_LEVEL=info +TRANSMITTAL_SERVICE_UVICORN__NUM_WORKERS=1 +TRANSMITTAL_SERVICE_UVICORN__ROOT_PATH='' + +# Database +TRANSMITTAL_SERVICE_DATABASE__USER=postgres +TRANSMITTAL_SERVICE_DATABASE__PASSWORD=password +TRANSMITTAL_SERVICE_DATABASE__HOST=127.0.0.1 +TRANSMITTAL_SERVICE_DATABASE__PORT=5432 +TRANSMITTAL_SERVICE_DATABASE__NAME=postgres +# Ssl settings +TRANSMITTAL_SERVICE_DATABASE__ENABLE_SSL=false +TRANSMITTAL_SERVICE_DATABASE__SSL_MODE=verify-full # or verify-ca +TRANSMITTAL_SERVICE_DATABASE__SSL_ROOT_CERT_PATH=root.crt + +# For local database +# TRANSMITTAL_SERVICE_PGDATA=/var/lib/postgresql/data/pgdata +# TRANSMITTAL_SERVICE_PGHOST=127.0.0.1 +# TRANSMITTAL_SERVICE_PGPORT=5432 + +# RabbitMQ +TRANSMITTAL_SERVICE_RABBITMQ__USER=guest +TRANSMITTAL_SERVICE_RABBITMQ__PASSWORD=guest +TRANSMITTAL_SERVICE_RABBITMQ__VHOST=/ +TRANSMITTAL_SERVICE_RABBITMQ__HOST=localhost +TRANSMITTAL_SERVICE_RABBITMQ__PORT=5672 + +# Sarex backend repository +TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__BASE_URL=https://stage.sarex.io +TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__TIMEOUT=30 +TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__BASIC_AUTH_ENCODED= + +# Resource repository +TRANSMITTAL_SERVICE_RESOURCE_REPOSITORY__BASE_URL=http://sarex-resources-service.resources-stage +TRANSMITTAL_SERVICE_RESOURCE_REPOSITORY__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_RESOURCE_REPOSITORY__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_RESOURCE_REPOSITORY__TIMEOUT=30 + +# Flows repository +TRANSMITTAL_SERVICE_FLOWS_REPOSITORY__BASE_URL= +TRANSMITTAL_SERVICE_FLOWS_REPOSITORY__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_FLOWS_REPOSITORY__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_FLOWS_REPOSITORY__TIMEOUT=30 + +# Documentations repository +TRANSMITTAL_SERVICE_DOCUMENTATIONS_REPOSITORY__BASE_URL=http://api-service.documentations-stage +TRANSMITTAL_SERVICE_DOCUMENTATIONS_REPOSITORY__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_DOCUMENTATIONS_REPOSITORY__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_DOCUMENTATIONS_REPOSITORY__TIMEOUT=30 + +# S3 +TRANSMITTAL_SERVICE_S3_CLIENT__MAX_POOL_CONNECTIONS=10 +TRANSMITTAL_SERVICE_S3_CLIENT__CONNECT_TIMEOUT=10 +TRANSMITTAL_SERVICE_S3_CLIENT__READ_TIMEOUT=30 +TRANSMITTAL_SERVICE_S3_CLIENT__REGION_NAME=ru-central1 +TRANSMITTAL_SERVICE_S3_CLIENT__VERIFY=True +TRANSMITTAL_SERVICE_S3_CLIENT__DEFAULT_BUCKET=transmittal-storage-stage +TRANSMITTAL_SERVICE_S3_CLIENT__ENDPOINT=storage.yandexcloud.net +TRANSMITTAL_SERVICE_S3_CLIENT__ACCESS_KEY= +TRANSMITTAL_SERVICE_S3_CLIENT__SECRET_KEY= +TRANSMITTAL_SERVICE_S3_CLIENT__USE_SSL=True +TRANSMITTAL_SERVICE_S3_CLIENT__USE_PATH_STYLE=True +TRANSMITTAL_SERVICE_S3_CLIENT__REQUEST_CHECKSUM_CALCULATION=when_required +TRANSMITTAL_SERVICE_S3_CLIENT__RESPONSE_CHECKSUM_VALIDATION=when_required + +# Html to pdf converter +TRANSMITTAL_SERVICE_HTML_TO_PDF_CONVERTER__BASE_URL=http://export-project-service.sarex-stage +TRANSMITTAL_SERVICE_HTML_TO_PDF_CONVERTER__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_HTML_TO_PDF_CONVERTER__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_HTML_TO_PDF_CONVERTER__TIMEOUT=30 + +# Markings +TRANSMITTAL_SERVICE_MARKINGS__BASE_URL=http://pdf-markings-service.processing-stage +TRANSMITTAL_SERVICE_MARKINGS__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_MARKINGS__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_MARKINGS__TIMEOUT=30 + +# You must use exactly one of email configuration (mailgun or smtp), mailgun considered to be default +# Mailgun +TRANSMITTAL_SERVICE_MAILGUN__BASE_URL=https://api.mailgun.net/v3/ +TRANSMITTAL_SERVICE_MAILGUN__MAX_CONNECTIONS=10 +TRANSMITTAL_SERVICE_MAILGUN__MAX_KEEPALIVE_CONNECTIONS=5 +TRANSMITTAL_SERVICE_MAILGUN__TIMEOUT=30 +TRANSMITTAL_SERVICE_MAILGUN__EMAIL=some@example.com +TRANSMITTAL_SERVICE_MAILGUN__API_KEY="" + +# Smtp +# TRANSMITTAL_SERVICE_SMTP__HOST= +# TRANSMITTAL_SERVICE_SMTP__PORT= +# TRANSMITTAL_SERVICE_SMTP__EMAIL= +# TRANSMITTAL_SERVICE_SMTP__ENABLE_TLS= +# TRANSMITTAL_SERVICE_SMTP__LOGIN= +# TRANSMITTAL_SERVICE_SMTP__PASSWORD= + +# Otel +TRANSMITTAL_SERVICE_OTEL__ENABLE=True +TRANSMITTAL_SERVICE_OTEL__HOST=http://localhost:4317 +TRANSMITTAL_SERVICE_OTEL__SERVICE_NAME=backend.transmittals +TRANSMITTAL_SERVICE_OTEL__INSECURE=True \ No newline at end of file diff --git a/apps/transmittal/CONFIGURATION.md b/apps/transmittal/CONFIGURATION.md new file mode 100644 index 0000000..44ae379 --- /dev/null +++ b/apps/transmittal/CONFIGURATION.md @@ -0,0 +1,263 @@ +# Конфигурация проекта transmittal-api + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/transmittal_service/infra/settings.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `AppSettings`). + +Особенности разбора (`SettingsConfigDict`): + +- `env_prefix="TRANSMITTAL_SERVICE_"` — все переменные приложения начинаются с этого префикса; +- `env_nested_delimiter="__"` — вложенные секции задаются двойным подчёркиванием, напр. `TRANSMITTAL_SERVICE_DATABASE__HOST` → `database.host`; +- `env_ignore_empty=False` — пустая строка считается заданным значением (не игнорируется), поэтому пустое обязательное поле-строка проходит валидацию, а вот отсутствие обязательного поля приводит к ошибке старта. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Настройки Uvicorn читаются отдельным классом `UvicornSettings` (тот же префикс/делимитер). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `make config` копирует `.example.env` → `.env` как шаблон, но приложение **не загружает `.env` автоматически** (в коде нет `env_file`/`dotenv`) — файл нужно экспортировать самому, напр. `set -a && . ./.env && set +a` | +| Локально (контейнеры) | `makefile`: цели `container-run`, `container-run-database`, `container-run-rabbitmq` пробрасывают переменные хоста через `--env` | +| Kubernetes (Helm) | `.helm/values-.yaml`: блок `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблоны `deployment.yaml`, `worker.yaml`, `crontab_periodic.yaml` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и переменные job-а `test-unit` для запуска тестов | + +Способы запуска процессов (`[project.scripts]` в `pyproject.toml`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `start_api` (`make run-api`) | `cmd/api.py` | HTTP API (uvicorn/gunicorn) | +| `taskiq worker …` (`make run-worker`) | `tasks.broker:broker` | Обработчик фоновых задач | +| `process_expired_transmittals` | `cmd/process_expired_transmittals.py` | Разовая задача обработки просроченных трансмитталов (в k8s — CronJob `0 3 * * *`) | +| `create_system_values` | `cmd/create_system_values.py` | Создание системных значений | +| `generate-act` | `cmd/generate_act.py` | Генерация акта | + +Порядок запуска в контейнере (`scripts/entrypoint.sh`): сначала выполняются миграции (`alembic upgrade heads`), затем стартует gunicorn с воркерами `ConfigurableWorker`. + +## Переменные приложения + +Все перечисленные ниже переменные имеют префикс `TRANSMITTAL_SERVICE_`. В столбце «Переменная» указано полное имя. Дефолт `—` означает, что значение обязательно (иначе ошибка старта). + +### App (`app.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_APP__NAME` | string | `Transmittal Service` | Имя приложения | +| `TRANSMITTAL_SERVICE_APP__LOG_LEVEL` | enum | `INFO` | Уровень логирования: `CRITICAL`/`FATAL`/`ERROR`/`WARNING`/`WARN`/`INFO`/`DEBUG`/`NOTSET` | +| `TRANSMITTAL_SERVICE_APP__ENVIRONMENT` | enum | — | Окружение развёртывания: `stage`/`preprod`/`prod` | +| `TRANSMITTAL_SERVICE_APP__HOST` | string | — | Внешний базовый URL сервиса (используется в письмах/ссылках) | + +### Auth (`auth.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY` | string | — | Публичный RSA-ключ для проверки JWT. Экранированные `\n` автоматически заменяются на реальные переводы строк | + +### CORS (`cors.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_CORS__ALLOW_ORIGINS` | list[str] (JSON) | — | Разрешённые Origin, напр. `["*"]` | +| `TRANSMITTAL_SERVICE_CORS__ALLOW_METHODS` | list[str] (JSON) | — | Разрешённые HTTP-методы | +| `TRANSMITTAL_SERVICE_CORS__ALLOW_HEADERS` | list[str] (JSON) | — | Разрешённые заголовки | +| `TRANSMITTAL_SERVICE_CORS__ALLOW_CREDENTIALS` | bool | — | Разрешить передачу учётных данных | + +### Uvicorn (`uvicorn.*`) + +Читаются отдельным классом `UvicornSettings`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_UVICORN__HOST` | string | `127.0.0.1` | Адрес прослушивания | +| `TRANSMITTAL_SERVICE_UVICORN__PORT` | int | `8001` | Порт | +| `TRANSMITTAL_SERVICE_UVICORN__ENABLE_AUTO_RELOAD` | bool | `False` | Live/hot-reload (для разработки) | +| `TRANSMITTAL_SERVICE_UVICORN__LOG_LEVEL` | enum | `info` | `critical`/`error`/`warning`/`info`/`debug`/`trace` | +| `TRANSMITTAL_SERVICE_UVICORN__NUM_WORKERS` | int | `1` | Число воркеров gunicorn | +| `TRANSMITTAL_SERVICE_UVICORN__ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси) | + +### Database (`database.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_DATABASE__HOST` | string | — | Хост PostgreSQL | +| `TRANSMITTAL_SERVICE_DATABASE__PORT` | int | — | Порт PostgreSQL | +| `TRANSMITTAL_SERVICE_DATABASE__USER` | string | — | Пользователь БД | +| `TRANSMITTAL_SERVICE_DATABASE__PASSWORD` | string | — | Пароль пользователя БД | +| `TRANSMITTAL_SERVICE_DATABASE__NAME` | string | — | Имя базы данных | +| `TRANSMITTAL_SERVICE_DATABASE__POOL_SIZE` | int | `5` | Размер пула соединений | +| `TRANSMITTAL_SERVICE_DATABASE__MAX_POOL_OVERFLOW` | int | `5` | Доп. соединения сверх пула | +| `TRANSMITTAL_SERVICE_DATABASE__ENABLE_SSL` | bool | — | Подключение к БД по TLS | +| `TRANSMITTAL_SERVICE_DATABASE__SSL_MODE` | enum | — | `verify-full`/`verify-ca`/`""`. Обязателен при `ENABLE_SSL=true` | +| `TRANSMITTAL_SERVICE_DATABASE__SSL_ROOT_CERT_PATH` | string | — | Путь к CA-сертификату. Обязателен при `ENABLE_SSL=true` | + +> При `ENABLE_SSL=true` валидатор требует непустые `SSL_MODE` и `SSL_ROOT_CERT_PATH`, иначе — ошибка старта. Итоговый DSN собирается в `database.uri`. + +### RabbitMQ (`rabbitmq.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_RABBITMQ__USER` | string | — | Пользователь | +| `TRANSMITTAL_SERVICE_RABBITMQ__PASSWORD` | string | — | Пароль | +| `TRANSMITTAL_SERVICE_RABBITMQ__VHOST` | string | — | Виртуальный хост | +| `TRANSMITTAL_SERVICE_RABBITMQ__HOST` | string | — | Хост | +| `TRANSMITTAL_SERVICE_RABBITMQ__PORT` | int | — | Порт | + +### HTTP-клиенты внешних сервисов + +Все клиенты наследуют общий набор полей (`HttpClient`): `BASE_URL`, `MAX_CONNECTIONS`, `MAX_KEEPALIVE_CONNECTIONS`, `TIMEOUT`. Все четыре поля обязательны (дефолтов нет). + +| Секция / префикс | Назначение | Доп. поля | +| --- | --- | --- | +| `TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__*` | Основной backend Sarex | `BASIC_AUTH_ENCODED` — base64 от `login:password` (обязателен), декодируется в пару login/password | +| `TRANSMITTAL_SERVICE_RESOURCE_REPOSITORY__*` | Сервис ресурсов (IAM/resources) | — | +| `TRANSMITTAL_SERVICE_DOCUMENTATIONS_REPOSITORY__*` | Сервис документаций | — | +| `TRANSMITTAL_SERVICE_FLOWS_REPOSITORY__*` | Сервис flows | — | +| `TRANSMITTAL_SERVICE_HTML_TO_PDF_CONVERTER__*` | Конвертер HTML→PDF (export-project) | — | +| `TRANSMITTAL_SERVICE_MARKINGS__*` | Сервис PDF-маркировок | — | + +Для каждого — четыре переменные, напр. для markings: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `TRANSMITTAL_SERVICE_MARKINGS__BASE_URL` | string | Базовый URL сервиса | +| `TRANSMITTAL_SERVICE_MARKINGS__MAX_CONNECTIONS` | int | Макс. число соединений | +| `TRANSMITTAL_SERVICE_MARKINGS__MAX_KEEPALIVE_CONNECTIONS` | int | Макс. keep-alive соединений | +| `TRANSMITTAL_SERVICE_MARKINGS__TIMEOUT` | int | Таймаут запроса (сек) | + +Для `SAREX_BACKEND_REPOSITORY` дополнительно: + +| Переменная | Тип | Назначение | +| --- | --- | --- | +| `TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__BASIC_AUTH_ENCODED` | string (base64) | Basic-auth в виде base64(`login:password`) | + +### S3 (`s3_client.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_S3_CLIENT__ENDPOINT` | string | — | Эндпоинт S3. Если без `http(s)://` — префикс добавляется автоматически по `USE_SSL` | +| `TRANSMITTAL_SERVICE_S3_CLIENT__REGION_NAME` | string | — | Регион | +| `TRANSMITTAL_SERVICE_S3_CLIENT__DEFAULT_BUCKET` | string | — | Бакет по умолчанию | +| `TRANSMITTAL_SERVICE_S3_CLIENT__ACCESS_KEY` | string | — | Access key | +| `TRANSMITTAL_SERVICE_S3_CLIENT__SECRET_KEY` | string | — | Secret key | +| `TRANSMITTAL_SERVICE_S3_CLIENT__USE_SSL` | bool | — | Использовать SSL | +| `TRANSMITTAL_SERVICE_S3_CLIENT__VERIFY` | bool | — | Проверять TLS-сертификат | +| `TRANSMITTAL_SERVICE_S3_CLIENT__MAX_POOL_CONNECTIONS` | int | `10` | Размер пула соединений | +| `TRANSMITTAL_SERVICE_S3_CLIENT__CONNECT_TIMEOUT` | int | `10` | Таймаут подключения (сек) | +| `TRANSMITTAL_SERVICE_S3_CLIENT__READ_TIMEOUT` | int | `30` | Таймаут чтения (сек) | +| `TRANSMITTAL_SERVICE_S3_CLIENT__USE_PATH_STYLE` | bool | `True` | Path-style адресация | +| `TRANSMITTAL_SERVICE_S3_CLIENT__REQUEST_CHECKSUM_CALCULATION` | enum | `when_required` | `when_supported`/`when_required` | +| `TRANSMITTAL_SERVICE_S3_CLIENT__RESPONSE_CHECKSUM_VALIDATION` | enum | `when_required` | `when_supported`/`when_required` | + +### Почта: Mailgun или SMTP + +Должен быть настроен **ровно один** из двух сервисов (валидатор `check_mailing_service`), иначе — ошибка старта. По умолчанию используется Mailgun. + +Mailgun (`mailgun.*`) — наследует поля `HttpClient` плюс: + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_MAILGUN__BASE_URL` | string | — | URL API, напр. `https://api.mailgun.net/v3/` | +| `TRANSMITTAL_SERVICE_MAILGUN__MAX_CONNECTIONS` | int | — | Макс. соединений | +| `TRANSMITTAL_SERVICE_MAILGUN__MAX_KEEPALIVE_CONNECTIONS` | int | — | Макс. keep-alive | +| `TRANSMITTAL_SERVICE_MAILGUN__TIMEOUT` | int | — | Таймаут (сек) | +| `TRANSMITTAL_SERVICE_MAILGUN__EMAIL` | string | — | Адрес отправителя | +| `TRANSMITTAL_SERVICE_MAILGUN__API_KEY` | string | — | API-ключ Mailgun | + +SMTP (`smtp.*`) — альтернатива Mailgun: + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_SMTP__HOST` | string | — | SMTP-хост | +| `TRANSMITTAL_SERVICE_SMTP__PORT` | int | — | SMTP-порт | +| `TRANSMITTAL_SERVICE_SMTP__EMAIL` | string | — | Адрес отправителя | +| `TRANSMITTAL_SERVICE_SMTP__ENABLE_TLS` | bool | `False` | Включить TLS | +| `TRANSMITTAL_SERVICE_SMTP__LOGIN` | string \| null | `None` | Логин | +| `TRANSMITTAL_SERVICE_SMTP__PASSWORD` | string \| null | `None` | Пароль | + +### OpenTelemetry (`otel.*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRANSMITTAL_SERVICE_OTEL__ENABLE` | bool | `False` | Включить трейсинг | +| `TRANSMITTAL_SERVICE_OTEL__HOST` | string | — | Адрес OTLP-коллектора | +| `TRANSMITTAL_SERVICE_OTEL__SERVICE_NAME` | string | — | Имя сервиса в трейсах | +| `TRANSMITTAL_SERVICE_OTEL__INSECURE` | bool | `False` | Небезопасное (без TLS) подключение к коллектору | + +## Переменные инфраструктуры, сборки и вспомогательных утилит + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `TRANSMITTAL_SERVICE_PGDATA` | `.example.env` (закомментировано) | Каталог данных локального PostgreSQL | +| `TRANSMITTAL_SERVICE_PGHOST` | `.example.env` (закомментировано) | Хост локального PostgreSQL | +| `TRANSMITTAL_SERVICE_PGPORT` | `makefile` (`container-run-database`) | Внутренний порт контейнера Postgres при пробросе | +| `OCI` | `makefile` | Инструмент контейнеризации (`docker`/`podman`), по умолчанию `docker` | +| `TRANSMITTAL_SERVICE_IMAGE_NAME` / `TRANSMITTAL_SERVICE_IMAGE_VERSION` | `makefile` | Имя/тег собираемого образа | +| `TRANSMITTAL_SERVICE_CONTAINER__NETWORK_NAME` | `makefile` | Имя docker-сети | +| `TRANSMITTAL_SERVICE_DATABASE_CONTAINER__*` | `makefile` | Параметры контейнера Postgres (образ, имя, volume) | +| `TRANSMITTAL_SERVICE_RABBITMQ_CONATINER__*` | `makefile` | Параметры контейнера RabbitMQ | +| `PIP_INDEX_URL`, `PIP_TRUSTED_HOST` | `Dockerfile` (build-arg) | Приватный индекс пакетов при сборке | +| `TRANSMITTAL_SERVICE_PYTHON_IMAGE_NAME`, `TRANSMITTAL_SERVICE_PYTHON_IMAGE_TAG` | `Dockerfile` (build-arg) | Базовый образ Python (по умолчанию `python:3.12-slim`) | +| `UID`, `GID`, `USERNAME` | `Dockerfile` (build-arg) | Пользователь внутри образа (по умолчанию `10000`/`10001`/`transmittal_service`) | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `envs` для каждого окружения (`stage`/`preprod`/`production`) и содержат те же переменные приложения `TRANSMITTAL_SERVICE_*`, что описаны выше (различаются адресами БД/сервисов, портами, `LOG_LEVEL`, `ROOT_PATH`, бакетом, доменом Mailgun и т.п.). + +Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `TRANSMITTAL_SERVICE_DATABASE__USER` | `ya-pg-secret` | `username` | +| `TRANSMITTAL_SERVICE_DATABASE__PASSWORD` | `ya-pg-secret` | `password` | +| `YC-PG-CERTIFICATE` | `ya-pg-secret` | `certificate` | +| `TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY` | `public-key` | `key` | +| `TRANSMITTAL_SERVICE_SAREX_BACKEND_REPOSITORY__BASIC_AUTH_ENCODED` | `django-auth` | `key` | +| `TRANSMITTAL_SERVICE_S3_CLIENT__ACCESS_KEY` | `s3-secret` | `access_key` | +| `TRANSMITTAL_SERVICE_S3_CLIENT__SECRET_KEY` | `s3-secret` | `secret_key` | +| `TRANSMITTAL_SERVICE_RABBITMQ__USER` | `rabbitmq-cred` | `username` | +| `TRANSMITTAL_SERVICE_RABBITMQ__PASSWORD` | `rabbitmq-cred` | `password` | +| `TRANSMITTAL_SERVICE_MAILGUN__API_KEY` | `mailgun-cred` | `api_key` | + +Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `ya-pg-secret` (ключ `certificate`) как файл `/opt/.postgresql/root.crt` — именно на него указывает `TRANSMITTAL_SERVICE_DATABASE__SSL_ROOT_CERT_PATH` в prod-конфигурации. + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы), `api.*` (host/prefix/path ingress), `worker.*`, `job.name_periodic`, `imagePullSecrets`. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | +| --- | --- | --- | +| ветка `master` | `preprod` | `transmittal-api-preprod` | +| ветка `stage` | `stage` | `transmittal-api-stage` | +| тег (`CI_COMMIT_TAG`) | `prod` | `transmittal-api-prod` | + +Ключевые переменные пайплайна: `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `DOCKERFILE_PATH`, `HELM_SET_ARGS` (`--set deployment.image=…`), флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_DEPLOY`. Job `test-unit` задаёт полный набор `TRANSMITTAL_SERVICE_*` переменных для прогона юнит-тестов. + +## Замечания и потенциальные проблемы + +- Переменные БД в `.example.env` должны иметь префикс `TRANSMITTAL_SERVICE_DATABASE__*` — код читает их именно так. Ранее в файле они были записаны без префикса (`DATABASE__USER` и т.д.) и не подхватывались; сейчас исправлено. В `makefile`, `.gitlab-ci.yml` и Helm имена с префиксом корректны. +- Приложение **не загружает `.env` автоматически** (в `settings.py` не задан `env_file`, зависимости `python-dotenv` нет). `make config` лишь создаёт файл-шаблон; переменные нужно экспортировать в окружение вручную либо задавать через `--env` (см. `make container-run`). +- `env_ignore_empty=False`: пустая строка воспринимается как заданное значение. Например `TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY=''` — валидное (пустой ключ), а вот полностью отсутствующая обязательная переменная вызовет ошибку старта. +- Переменная `YC-PG-CERTIFICATE` прокидывается из секрета в окружение, но **кодом приложения не читается** — сертификат используется как смонтированный файл (`root.crt`). Имя с дефисами не соответствует схеме `TRANSMITTAL_SERVICE_*`. +- Почтовый сервис: должен быть задан ровно один из `mailgun`/`smtp`. Если заданы оба или ни одного — сервис не стартует. +- В `.example.env` секция `FLOWS_REPOSITORY__BASE_URL` пустая, но поле обязательно — для реального запуска его нужно заполнить. + +## Минимальный набор для локального запуска + +Postgres и RabbitMQ поднимаются через `make container-deps`, приложение — через `make run-api` / `make run-worker`. Минимально необходимо задать (с префиксом `TRANSMITTAL_SERVICE_`): + +- `APP__ENVIRONMENT`, `APP__HOST` +- `AUTH__PUBLIC_KEY` (можно пустой для локальной разработки) +- `CORS__ALLOW_ORIGINS`, `CORS__ALLOW_METHODS`, `CORS__ALLOW_HEADERS`, `CORS__ALLOW_CREDENTIALS` +- `DATABASE__HOST`, `DATABASE__PORT`, `DATABASE__USER`, `DATABASE__PASSWORD`, `DATABASE__NAME`, `DATABASE__ENABLE_SSL` (`false` локально), `DATABASE__SSL_MODE`, `DATABASE__SSL_ROOT_CERT_PATH` +- `RABBITMQ__USER`, `RABBITMQ__PASSWORD`, `RABBITMQ__VHOST`, `RABBITMQ__HOST`, `RABBITMQ__PORT` +- `BASE_URL`/`MAX_CONNECTIONS`/`MAX_KEEPALIVE_CONNECTIONS`/`TIMEOUT` для всех шести HTTP-клиентов (`SAREX_BACKEND_REPOSITORY` + `BASIC_AUTH_ENCODED`, `RESOURCE_REPOSITORY`, `DOCUMENTATIONS_REPOSITORY`, `FLOWS_REPOSITORY`, `HTML_TO_PDF_CONVERTER`, `MARKINGS`) +- `S3_CLIENT__*` (эндпоинт, регион, бакет, ключи, `USE_SSL`, `VERIFY`) +- один из почтовых сервисов: `MAILGUN__*` **или** `SMTP__*` +- `OTEL__ENABLE` (`False` локально), при `True` — `OTEL__HOST`, `OTEL__SERVICE_NAME` + +Готовые значения-примеры для всех переменных приведены в `.example.env` (с учётом замечаний выше по префиксу БД). \ No newline at end of file diff --git a/apps/transmittal/ENDPOINTS.md b/apps/transmittal/ENDPOINTS.md new file mode 100644 index 0000000..d607615 --- /dev/null +++ b/apps/transmittal/ENDPOINTS.md @@ -0,0 +1,185 @@ +# Эндпоинты, с которыми взаимодействует transmittal-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `transmittal-frontend`). + +## Как устроено взаимодействие + +Все запросы описаны декларативно в реестре `module/api/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`); +- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query); +- `body(args)` — опционально, формирование тела запроса; +- `cache`, `queryOptions`, `responseType`, `axiosConfig` — опции кеширования, повторов и типа ответа. + +Запрос выполняется единой функцией `fetch(endpoint, params, controller)`, которая через `httpService` (`module/api/http-service.ts`, поверх `@sarex-team/sdk-js` + `axios`) отправляет запрос на базовый хост сервиса. Базовый хост подставляется `resolveHost(service)` из `module/api/hosts.ts` в зависимости от `BUILD_ENV`. Ошибки маппируются в человекочитаемые сообщения в `module/api/errors.ts`. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `transmittals` | Сервис передачи документации (трансмитталы, шаблоны) | `https://stage-api.sarex.io/transmittals` | `https://api.sarex.io/transmittals` | +| `documentations` | Сервис документации (документы, бандлы, файлы) | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `sarexApi` | Gateway/API Sarex (`/gateway`, `/eav`, `/cde`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` | +| `processes` | Сервис рабочих процессов (flows, reviews) | `https://stage-api.sarex.io/flows` | `https://api.sarex.io/flows` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `files` | Сервис файлов | `https://stage-api.sarex.io/files` | `https://api.sarex.io/files` | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `bim` | BIM-API | `https://stage-bim-api.sarex.io` | `https://bim-api.sarex.io` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> Также определены окружения `local`, `preprod` и `contour` (относительные пути для изолированного контура). В `local` сервис `sarex` проксируется на `/sarex-backend`. Подключаемый удалённый модуль documentations описан отдельно в `module/api/module-hosts.ts`. + +## Эндпоинты по сервисам + +### `transmittals` — Сервис передачи документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getTransmittals` | POST | `api/v1/transmittals` | Список трансмитталов (пагинация по `bookmark`, фильтр по `resource_id`) | +| `getTransmittalStatuses` | POST | `api/v1/transmittals/global_statuses` | Глобальные статусы по набору `resource_ids` | +| `getTransmittalById` | GET | `api/v1/transmittals/{transmittalId}` | Трансмиттал по id | +| `getAct` | GET | `api/v1/transmittals/{transmittalId}/act_available` | Доступность акта для трансмиттала | +| `downloadAct` | GET | `api/v1/transmittals/{transmittalId}/download_act` | Скачивание акта | +| `createTransmittal` | POST | `api/v1/transmittals/create` | Создание трансмиттала | +| `approveTransmittal` | PUT | `api/v1/transmittals/{transmittalId}/approve` | Принять трансмиттал (с комментарием) | +| `declineTransmittal` | PUT | `api/v1/transmittals/{transmittalId}/decline` | Отклонить трансмиттал (с комментарием) | +| `deleteTransmittal` | DELETE | `api/v1/transmittals/{transmittalId}` | Удалить трансмиттал | +| `linkReviewToTransmittal` | POST | `api/v1/transmittals/{transmittalId}/link_review` | Привязать review к трансмитталу | +| `getStatusTypes` | GET | `api/v1/transmittals/status` | Справочник типов статусов | +| `getSearchProject` | POST | `api/v1/transmittals/search` | Поиск/фильтрация трансмитталов в проекте | +| `getSearchProjects` | POST | `api/v1/transmittals/search/resources` | Поиск/фильтрация по нескольким ресурсам | +| `getSteps` | GET | `api/v1/steps` | Список шагов | +| `getStep` | GET | `api/v1/steps/{id}` | Шаг по id | +| `getSearchTemplates` | POST | `/api/v1/transmittal_templates` | Поиск шаблонов трансмитталов | +| `createTemplate` | POST | `/api/v1/transmittal_templates/create` | Создать шаблон | +| `updateTemplate` | PATCH | `/api/v1/transmittal_templates/{templateId}` | Обновить шаблон | +| `deleteTemplate` | DELETE | `/api/v1/transmittal_templates/{templateId}` | Удалить шаблон | +| `getTemplateListForSelect` | GET | `/api/v1/transmittal_templates/select?resource={resourceId}` | Список шаблонов для выбора | +| `getSingleTemplate` | GET | `/api/v1/transmittal_templates/{templateId}?resource={resourceId}` | Шаблон по id | + +### `documentations` — Сервис документации + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getDisks` | GET | `api/v1/disks` | Список дисков | +| `getAllPermmission` | GET | `api/v1/permissions` | Все права доступа | +| `getDocPermission` | GET | `api/v1/documents/{id}/permissions` | Права доступа документа | +| `postPermission` | POST | `api/v1/documents/{id}/permissions` | Выдать права сервисному аккаунту | +| `postBundle` | POST | `api/v1/bundles` | Создать бандл | +| `postFile` | POST | `api/v1/bundles/{bundleId}/{fileKey}?single_upload=1` | Загрузить файл (single upload) | +| `uploadFolderStart` | POST | `api/v1/bundles/{bundleId}/{key}/upload_multipart?upload_path={folderPath}` | Начать multipart-загрузку | +| `uploadPart` | PUT | `api/v1/bundles/{bundleId}/{bundleKey}?part_number={partNumber}` | Загрузить часть файла | +| `bundleComplite` | POST | `api/v1/bundles/{bundleId}/upload_finish` | Завершить загрузку бандла | +| `getFile` | GET | `api/v1/bundles/{bundleId}/{bundleKey}` | Получить файл бандла | +| `getBundles` | GET | `api/v1/documents/{id}/bundles` | Бандлы документа | +| `addBundle` | POST | `api/v1/documents/{documentId}/add_bundle` | Привязать бандл к документу | +| `moveBundles` | PATCH | `api/v1/documents/{documentId}/move_bundles` | Переместить бандлы | +| `removeBundle` | DELETE | `api/v1/bundles/{id}` | Удалить бандл | +| `postWorkspace` | POST | `api/v1/workspaces` | Создать рабочую область | +| `getDocumentById` | GET | `api/v1/documents/{id}` | Документ по id | +| `getDocumentWithBundles` | GET | `api/v1/documents/{id}?extend=bundles` | Документ с бандлами | +| `fetchBatchDocuments` | POST | `/api/v1/documents/batch` | Пакетное получение документов | +| `changeDocument` | PATCH | `api/v1/documents/{id}` | Переименовать документ | +| `updatePath` | PATCH | `api/v1/documents/{id}/update-path` | Сменить родителя документа | +| `updateDocumentsPaths` | PATCH | `api/v1/documents/update-path` | Массовая смена родителя | +| `deleteDocument` | DELETE | `api/v1/documents/{id}` | Удалить документ | +| `deleteDocuments` | DELETE | `api/v1/documents?document_ids={ids}` | Удалить несколько документов | +| `downloadFile` | GET | `api/v1/bundles/{lastBundleId}/{key}/download` | Скачать файл | +| `downloadAllFiles` | GET | `api/v1/bundles/{lastBundleId}/download` | Скачать все файлы бандла | +| `downloadFolder` | GET | `api/v1/documents/{docId}/download?depth={depth}` | Скачать папку | +| `getFolderChildrenWithActiveProcesses` | POST | `/api/v1/documents/flows` | Дети папки с активными процессами | +| `conversionFile` | POST | `api/v1/conversion` | Конвертация документа (в IFC) | +| `addMarks` | PUT | `api/v1/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи | +| `sign` | POST | `api/v1/bundles/{bundleId}/sign` | Подписать бандл | +| `cancelQrCode` | PATCH | `api/v1/bundles/{bundleId}/cancel_qr` | Отменить QR-код | +| `restartWorkflow` | POST | `api/v1/bundles/{bundleId}/restart` | Перезапустить workflow бандла | +| `getPublicLink` | GET | `api/v1/public/documents/public_link/{public_link_id}` | Получить публичную ссылку | +| `deletePublicLink` | DELETE | `api/v1/documents/public_link/{public_link_id}` | Удалить публичную ссылку | +| `removeDoc` | DELETE | `api/v1/documents/bin?parent_id={id}` | Переместить в корзину | +| `recoveryDocument` | PATCH | `api/v1/documents/bin/restore?parent_id={id}` | Восстановить из корзины | +| `copyFolderStructure` | POST | `api/v1/documents/copy_structure` | Копировать структуру папок | + +### `sarexApi` — Gateway/API Sarex + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getUsersWithTransmittalProjectPermissions` | GET | `/gateway/api/v2/users/?...&resource_id={projectId}&permissions=...` | Пользователи с правами в проекте | +| `getDocuments` | GET | `/gateway/api/v1/disks/{id}/documents?...` | Документы диска (с фильтрами/поиском) | +| `filterByAttributes` | GET | `/gateway/api/v1/disks/{diskId}/documents?parent_id={rootDocumentId}&{params}` | Фильтрация документов по атрибутам | +| `getResources` | GET | `/gateway/api/v1/resources` | Список ресурсов | +| `createDocument` | POST | `/gateway/api/v1/documents` | Создать документ/папку/проект | +| `fetchDocumentAncestors` | POST | `/gateway/api/v1/documents/ancestors` | Предки документов | +| `fetchDocumentsBundleVersions` | POST | `/gateway/api/v1/documents/bundle_versions` | Версии бандлов документов | +| `getAttributesByDocumet` | GET | `/gateway/api/v1/documents/{id}/attributes` | Атрибуты документа | +| `updateAttributes` | PUT | `/gateway/api/v1/documents/{id}/attributes` | Обновить атрибуты документа | +| `addAttributes` | POST | `/gateway/eav/api/v0/entity/` | Создать сущность атрибутов (EAV) | +| `getDefaultAttributes` | GET | `/eav/api/v0/schema/?model=document&company_id=...&type_identifier=...` | Схема атрибутов по типу | +| `getAttributes` | GET | `/eav/api/v0/attribute/?company_id={params}` | Атрибуты компании | +| `getAttributesWithParams` | GET | `/eav/api/v0/schema/?model=document&{params}` | Схема атрибутов с параметрами | +| `updateSubscription` | POST | `/gateway/api/v1/subscription/` | Создать/обновить подписку | +| `deleteSubscription` | DELETE | `/gateway/api/v1/documents/{documentId}/subscription/` | Удалить подписку | +| `getActivityLog` | GET | `/gateway/api/v1/system_log/?...` | Журнал активности документа | +| `fetchDocumentByResourceId` | GET | `/gateway/api/v1/resources-rpc/parent-document-by-resource-id/{resourceId}/` | Родительский документ по resource id | +| `getRemovedDocuments` | GET | `/gateway/api/v1/documents/bin?parent_id={id}{params}` | Удалённые документы в папке | +| `getRemovedFilteredDocuments` | GET | `/gateway/api/v1/documents/bin{params}` | Удалённые документы (фильтр) | +| `getBindings` | GET | `/cde/app/v1/bundles/{bundleId}/bindings` | Привязки бандла | +| `completeUpload` | POST | `{uploadUrl}/complete` | Завершение загрузки (по переданному URL) | + +### `sarex` — Локальный сервис данных + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getSettings` | GET | `/api/client/settings/` | Клиентские настройки (кешируется) | +| `getUser` | GET | `/api/core/users/{userId}/` | Пользователь по id | +| `getUsersByCompanyIds` | GET | `/api/core/users/?company={ids}&limit=&offset=&show_inactive=true` | Пользователи компаний | +| `getTargets` | GET | `/api/core/targets/` | Список таргетов | +| `getCompanies` | GET | `/api/core/companies/` | Список компаний | +| `getDepartmentById` | GET | `/api/core/admin/departments?company={companyId}` | Отделы компании | +| `getUsersPositionById` | GET | `/api/core/admin/positions/?company={companyId}` | Должности компании | +| `getByFullUrl` | GET | `{url}` | Запрос по произвольному URL | + +### `processes` — Сервис рабочих процессов (flows) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getProcesses` | GET | `api/v1/flows/?{query}` | Список процессов (flows) | +| `createReview` | POST | `api/v1/reviews/` | Создать review | +| `deleteReview` | DELETE | `api/v1/reviews/{id}/` | Удалить review | +| `activateReview` | PATCH | `api/v1/reviews/{id}/approve/` | Активировать/утвердить review | +| `createReviewDocuments` | POST | `api/v1/documents/` | Добавить документы в review | + +### `workflows` — Сервис обработки документов + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getWorkflow` | GET | `api/v1/workflows/{workflowId}` | Workflow по id | +| `getWorkflows` | POST | `api/v1/workflows/batch` | Пакетное получение workflow | + +### `workspaces` — Сервис рабочих областей + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getWorkspaces` | GET | `api/v1/workspaces/{uuid}` | Рабочая область по uuid | + +### `remarks` — Сервис замечаний + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getRemarksTotalCount` | GET | `api/v1/total_count` | Общее число замечаний | + +### `files` — Сервис файлов + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `downloadFiles` | GET | `/api/v1/documents/{documentIds}` | Скачать документы по id | +| `downloadFileBundles` | POST | `/api/v1/documents/` | Скачать бандлы (ответ `blob`) | + +## Обработка ошибок + +Коды ответов маппируются в сообщения (`module/api/errors.ts`): `400` — некорректный запрос, `404` — ресурс не найден, `500` (и прочие) — ошибка сервера. Для каждого сервиса задано человекочитаемое имя (напр. `transmittals` → «Сервис передачи документации»), которое подставляется в текст ошибки. По умолчанию у запросов включён показ уведомления об ошибке (`showErrorNotification: true`). diff --git a/apps/transmittal/openapi.yaml b/apps/transmittal/openapi.yaml new file mode 100644 index 0000000..58f175e --- /dev/null +++ b/apps/transmittal/openapi.yaml @@ -0,0 +1,1292 @@ +openapi: 3.0.3 + +info: + title: Transmittal Service API + version: "1.0.0" + description: | + REST API сервиса **transmittal-api** (`pdm/transmittal-api`) — управление + трансмитталами (передачей документации на согласование), их шаблонами, + шагами/действиями маршрута согласования и генерацией актов. + + Сервис написан на Python (**FastAPI**). Приложение собирается фабрикой + `get_application` в `src/transmittal_service/app/http_server.py`. Роутинг + состоит из двух групп: + + - публичный API — префикс `/api/v1` (`controller/http/api/v1/api.py`); + - внутренний API — префикс `/internal/v1` (`controller/http/internal/v1/api.py`), + предназначен для вызовов внутри кластера (через ingress не публикуется). + + Интерактивная документация Swagger доступна по `/api/docs`, схема — + по `/api/openapi.json` (с учётом `root_path`). + + ### Аутентификация + Публичные эндпоинты (кроме `/api/v1/healthcheck`) требуют аутентификации. + Токен передаётся заголовком `Authorization: Bearer ` (FastAPI + `HTTPBearer`). Поддерживаются два режима (`controller/http/security.py`): + + 1. **Zitadel** — если передан дополнительный заголовок `identity` + (`Identity `), полезная нагрузка берётся из этого токена + (`urn:zitadel:iam:user:metadata`). Валидность проверяется на уровне + Istio, подпись сервисом не проверяется. + 2. **sarex-backend** — если заголовка `identity` нет, подпись основного + токена проверяется публичным RSA-ключом + (`TRANSMITTAL_SERVICE_AUTH__PUBLIC_KEY`, алгоритм `RS512`). + + Внутренние эндпоинты (`/internal/v1/*`) аутентификации на уровне приложения + не требуют — ограничение доступа обеспечивается сетевым слоем. + + ### Пагинация + Списочные ответы используют «bookmark»-пагинацию. Ответ оборачивается в + `Page` — `{ result, next, prev }`, где `next`/`prev` — курсоры (bookmark). + Размер страницы задаётся параметром `limit` (в теле или query, зависит от + эндпоинта), продолжение — параметром `bookmark`. Часть ответов оборачивается + в более простой `Result` — `{ result }` (без курсоров). + + ### Обработка ошибок + Ошибки возвращаются в формате **RFC 7807** (`application/problem+json`), + схема `ApiError` — `{ type, title, status, detail, instance }` + (`controller/http/middleware.py`, класс `ExceptionHandler`). Идентификатор + запроса возвращается в заголовке `X-Request-Id`, длительность обработки — + в `Server-Timing`. + + ### Замечания (расхождения кода) + - Ошибка «ресурс не найден» (`NotFound`) маппится на статус **`410 Gone`**, + а не на привычный `404 Not Found`. + - При отсутствии/некорректности `Authorization` FastAPI `HTTPBearer` + возвращает `403`, тогда как ошибки разбора токена в middleware дают `401`. + - Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в + стандартном формате `422` (не `application/problem+json`). + - Конфликт имени шаблона (`TemplateNameIsNotUnique`) возвращает `409`. + + contact: + name: transmittal-api + url: https://gitlab/pdm/transmittal-api + +servers: + - url: https://api.sarex.io/transmittals + description: Production (ingress, root_path=/transmittals) + - url: https://stage-api.sarex.io/transmittals + description: Stage (ingress, root_path=/transmittals) + - url: http://transmittal-service.transmittal-api-stage.svc.cluster.local + description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000). Единственный способ достучаться до /internal/v1 + - url: http://localhost:8001 + description: Локальный запуск (Uvicorn, порт по умолчанию 8001) + +tags: + - name: infra + description: Служебные эндпоинты (проверка доступности) + - name: transmittals + description: Трансмитталы — создание, просмотр, согласование, поиск, акты + - name: transmittal_templates + description: Шаблоны трансмитталов + - name: steps + description: Шаги маршрута согласования + - name: internal + description: Внутренние эндпоинты (только внутри кластера) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Infra + # ========================================================================== + /api/v1/healthcheck: + get: + tags: [infra] + summary: Проверка доступности зависимостей + description: | + Параллельно проверяет доступность всех внешних зависимостей (БД, S3, + и HTTP-сервисов) и возвращает агрегированный статус `healthy` или + `partially_healthy`. Аутентификация не требуется. + operationId: healthcheck + security: [] + responses: + '200': + description: Статус доступности зависимостей + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResponse' + + # ========================================================================== + # Steps + # ========================================================================== + /api/v1/steps: + get: + tags: [steps] + summary: Список шагов + operationId: getSteps + parameters: + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 32767 + default: 100 + - name: bookmark + in: query + required: false + schema: + type: string + nullable: true + responses: + '200': + description: Страница шагов + content: + application/json: + schema: + $ref: '#/components/schemas/Page_StepReadResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/steps/{id}: + get: + tags: [steps] + summary: Шаг по id + operationId: getStep + parameters: + - name: id + in: path + required: true + schema: + type: integer + minimum: 0 + maximum: 32767 + responses: + '200': + description: Шаг + content: + application/json: + schema: + $ref: '#/components/schemas/StepReadResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Transmittals + # ========================================================================== + /api/v1/transmittals: + post: + tags: [transmittals] + summary: Список трансмитталов ресурса + description: Пагинированный список трансмитталов по `resource_id` со сводной статистикой по статусам. + operationId: listTransmittals + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalListRequestDto' + responses: + '200': + description: Страница трансмитталов и статистика по статусам + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalListResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/create: + post: + tags: [transmittals] + summary: Создать трансмиттал + description: | + Создаёт трансмиттал. Требуется хотя бы один документ (`documents_to_approve`) + и хотя бы один получатель (`receivers`). + operationId: createTransmittal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalCreateRequestDto' + responses: + '200': + description: Идентификатор созданного трансмиттала + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalCreateResponseDto' + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/global_statuses: + post: + tags: [transmittals] + summary: Глобальные статусы по набору ресурсов + description: Возвращает статистику статусов трансмитталов по списку `resource_ids`. + operationId: getGlobalStatuses + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalGetGlobalStatusesRequestDto' + responses: + '200': + description: Статистика статусов по ресурсам + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalGetGlobalStatusesResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/status: + get: + tags: [transmittals] + summary: Справочник статусов + operationId: listStatuses + responses: + '200': + description: Список типов статусов + content: + application/json: + schema: + $ref: '#/components/schemas/StatusesListResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + + /api/v1/transmittals/count: + get: + tags: [transmittals] + summary: Число открытых трансмитталов пользователя + description: Количество незакрытых трансмитталов, требующих действия текущего пользователя. + operationId: myOpenTransmittalsCount + responses: + '200': + description: Количество открытых трансмитталов + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalOpenCountResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + + /api/v1/transmittals/search: + post: + tags: [transmittals] + summary: Поиск трансмитталов в ресурсе + description: Поиск и фильтрация трансмитталов в рамках одного ресурса (`resource_id`). + operationId: searchTransmittals + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalSearchFilterRequestDto' + responses: + '200': + description: Страница трансмитталов и статистика по статусам + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalListResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/search/resources: + post: + tags: [transmittals] + summary: Поиск по нескольким ресурсам + description: Фильтрация трансмитталов по списку ресурсов (`resource_ids`); возвращает статистику статусов по ресурсам. + operationId: searchTransmittalsResources + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalSearchFilterDto' + responses: + '200': + description: Статистика статусов по ресурсам + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalGetGlobalStatusesResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}: + parameters: + - $ref: '#/components/parameters/TransmittalId' + get: + tags: [transmittals] + summary: Трансмиттал по id + operationId: getTransmittal + responses: + '200': + description: Трансмиттал с историей действий и документами + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalReadResponseDto' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [transmittals] + summary: Удалить трансмиттал + operationId: deleteTransmittal + responses: + '204': { description: Удалено } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}/approve: + parameters: + - $ref: '#/components/parameters/TransmittalId' + put: + tags: [transmittals] + summary: Принять трансмиттал + description: Фиксирует действие «принять» текущего пользователя на текущем шаге (с опциональным комментарием). По завершении может инициировать генерацию акта. + operationId: approveTransmittal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserActionCreateRequest' + responses: + '200': + description: Созданное действие пользователя + content: + application/json: + schema: + $ref: '#/components/schemas/UserActionCreateResponseDto' + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}/decline: + parameters: + - $ref: '#/components/parameters/TransmittalId' + put: + tags: [transmittals] + summary: Отклонить трансмиттал + description: Фиксирует действие «отклонить» текущего пользователя на текущем шаге (с опциональным комментарием). + operationId: declineTransmittal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserActionCreateRequest' + responses: + '200': + description: Созданное действие пользователя + content: + application/json: + schema: + $ref: '#/components/schemas/UserActionCreateResponseDto' + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}/act_available: + parameters: + - $ref: '#/components/parameters/TransmittalId' + get: + tags: [transmittals] + summary: Доступность акта + description: Признак готовности акта для скачивания по данному трансмитталу. + operationId: actAvailable + responses: + '200': + description: Готовность акта + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalGetActReadinessResponse' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}/download_act: + parameters: + - $ref: '#/components/parameters/TransmittalId' + get: + tags: [transmittals] + summary: Скачать акт + description: Возвращает presigned-URL для скачивания акта из S3. + operationId: downloadAct + responses: + '200': + description: Presigned-URL акта + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalDownloadActResponse' + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittals/{transmittal_id}/link_review: + parameters: + - $ref: '#/components/parameters/TransmittalId' + post: + tags: [transmittals] + summary: Привязать review к трансмитталу + operationId: linkReview + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalLinkReviewRequest' + responses: + '204': { description: Review привязан } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Transmittal templates + # ========================================================================== + /api/v1/transmittal_templates: + post: + tags: [transmittal_templates] + summary: Список шаблонов трансмитталов + description: Пагинированный список шаблонов с фильтрами. Тело запроса опционально (при отсутствии применяются значения по умолчанию). + operationId: listTransmittalTemplates + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalTemplateListFiltersRequest' + responses: + '200': + description: Страница шаблонов + content: + application/json: + schema: + $ref: '#/components/schemas/Page_TransmittalTemplateListResponse' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittal_templates/create: + post: + tags: [transmittal_templates] + summary: Создать шаблон + operationId: createTransmittalTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalTemplateCreateRequestDto' + responses: + '200': + description: Идентификатор созданного шаблона + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalTemplateCreateResponseDto' + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '409': { $ref: '#/components/responses/Conflict' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittal_templates/select: + get: + tags: [transmittal_templates] + summary: Шаблоны для выбора + description: Плоский список шаблонов (id/имя/автор), доступных для выбора в рамках ресурса. + operationId: listTransmittalTemplatesForSelect + parameters: + - name: resource + in: query + required: true + schema: + type: string + format: uuid + - name: template_id + in: query + required: false + description: Один или несколько id шаблонов для фильтрации + schema: + type: array + items: + type: string + format: uuid + nullable: true + responses: + '200': + description: Список шаблонов для выбора + content: + application/json: + schema: + $ref: '#/components/schemas/Result_TransmittalTemplateSelectListResponse' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/transmittal_templates/{transmittal_template_id}: + parameters: + - $ref: '#/components/parameters/TransmittalTemplateId' + get: + tags: [transmittal_templates] + summary: Шаблон по id + operationId: getTransmittalTemplate + parameters: + - name: resource + in: query + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Шаблон трансмиттала + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalTemplateGetSingleResponse' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [transmittal_templates] + summary: Обновить шаблон + operationId: updateTransmittalTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalTemplateUpdateRequest' + responses: + '204': { description: Обновлено } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '409': { $ref: '#/components/responses/Conflict' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [transmittal_templates] + summary: Удалить шаблон + operationId: deleteTransmittalTemplate + responses: + '204': { description: Удалено } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '410': { $ref: '#/components/responses/Gone' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Internal (cluster-only) + # ========================================================================== + /internal/v1/transmittals: + post: + tags: [internal] + summary: Список трансмитталов (внутренний) + description: Внутренний список трансмитталов по фильтрам. Без аутентификации на уровне приложения; доступен только внутри кластера. + operationId: internalGetTransmittals + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalListInternalRequest' + responses: + '200': + description: Список трансмитталов + content: + application/json: + schema: + $ref: '#/components/schemas/Result_TransmittalReadListResponseDto' + '422': { $ref: '#/components/responses/ValidationError' } + + /internal/v1/transmittals/by_bundle_ids: + post: + tags: [internal] + summary: Трансмитталы по bundle id (внутренний) + description: Возвращает трансмитталы, связанные с переданными `bundle_ids`. Без аутентификации на уровне приложения; доступен только внутри кластера. + operationId: internalGetTransmittalsByBundleIds + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransmittalGetByBundleIdsRequest' + responses: + '200': + description: Трансмитталы по bundle id + content: + application/json: + schema: + $ref: '#/components/schemas/Result_TransmittalGetByBundleIdsResponse' + '422': { $ref: '#/components/responses/ValidationError' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. Проверяется публичным + ключом (`RS512`) в режиме sarex-backend либо принимается как есть в + режиме Zitadel (см. заголовок `identity`). + identityToken: + type: apiKey + in: header + name: identity + description: | + Опциональный заголовок `identity` (`Identity `) для режима Zitadel. + При его наличии полезная нагрузка берётся из этого токена, а подпись + основного токена сервисом не проверяется. + + parameters: + TransmittalId: + name: transmittal_id + in: path + required: true + schema: + type: string + format: uuid + TransmittalTemplateId: + name: transmittal_template_id + in: path + required: true + schema: + type: string + format: uuid + + responses: + BadRequest: + description: Некорректный запрос / бизнес-правило нарушено + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ApiError' + Unauthorized: + description: Токен не предоставлен или невалиден + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ApiError' + Forbidden: + description: Недостаточно прав + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ApiError' + Conflict: + description: Конфликт (например, имя шаблона уже занято) + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ApiError' + Gone: + description: Запрошенный ресурс не существует (маппинг `NotFound` → `410`) + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ApiError' + ValidationError: + description: Ошибка валидации тела/параметров запроса (FastAPI/Pydantic) + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + + schemas: + # --- Общие ------------------------------------------------------------ + ApiError: + type: object + description: Ошибка в формате RFC 7807 (application/problem+json) + properties: + type: { type: string } + title: { type: string } + status: { type: integer } + detail: { type: string } + instance: + type: string + description: URL запроса, вызвавшего ошибку + required: [type, title, status, detail, instance] + + HTTPValidationError: + type: object + properties: + detail: + type: array + items: + $ref: '#/components/schemas/ValidationErrorItem' + + ValidationErrorItem: + type: object + properties: + loc: + type: array + items: + anyOf: + - type: string + - type: integer + msg: { type: string } + type: { type: string } + required: [loc, msg, type] + + ReceiversRequestDto: + type: object + description: Получатели трансмиттала (запрос). Хотя бы один из наборов должен быть непуст. + properties: + users: + type: array + items: { type: integer, minimum: 0, maximum: 9223372036854775807 } + uniqueItems: true + departments: + type: array + items: { type: integer } + uniqueItems: true + roles: + type: array + items: { type: integer } + uniqueItems: true + + ReceiversReadResponseDto: + type: object + properties: + users: { type: array, items: { type: integer } } + departments: { type: array, items: { type: integer } } + roles: { type: array, items: { type: integer } } + required: [users, departments, roles] + + ReceiversFilterDto: + type: object + properties: + users: { type: array, items: { type: integer } } + departments: { type: array, items: { type: integer } } + roles: { type: array, items: { type: integer } } + + DocumentVersionRequestDto: + type: object + properties: + document_id: { type: integer, minimum: 0, maximum: 2147483647 } + bundle_id: { type: string, format: uuid } + required: [document_id, bundle_id] + + DocumentVersionReadResponse: + type: object + properties: + document_id: { type: integer } + bundle_id: { type: string, format: uuid } + required: [document_id, bundle_id] + + StatusReadResponseDto: + type: object + properties: + id: { type: integer } + name: { type: string } + slug: { type: string } + required: [id, name, slug] + + StatusSimpleDto: + type: object + properties: + slug: { type: string } + name: { type: string } + required: [slug, name] + + StatusesListResponseDto: + type: object + properties: + statuses: + type: array + items: { $ref: '#/components/schemas/StatusSimpleDto' } + required: [statuses] + + StepReadResponseDto: + type: object + properties: + id: { type: integer } + name: { type: string } + slug: { type: string } + required: [id, name, slug] + + ActionReadResponse: + type: object + properties: + id: { type: integer } + step_id: { type: integer } + name: { type: string } + slug: { type: string } + required: [id, step_id, name, slug] + + ReviewLightResponse: + type: object + properties: + id: { type: integer } + name: { type: string } + required: [id, name] + + UserActionCreateRequest: + type: object + properties: + comment: { type: string, nullable: true } + + UserActionCreateResponseDto: + type: object + properties: + id: { type: string, format: uuid } + transmittal_id: { type: string, format: uuid } + action_id: { type: integer } + comment: { type: string, nullable: true } + required: [id, transmittal_id, action_id, comment] + + UserActionReadResponse: + type: object + properties: + id: { type: string, format: uuid } + user_id: { type: integer } + transmittal_id: { type: string, format: uuid } + action_id: { type: integer } + comment: { type: string, nullable: true } + action: { $ref: '#/components/schemas/ActionReadResponse' } + created_at: { type: string, format: date-time } + required: [id, user_id, transmittal_id, action_id, comment, action, created_at] + + # --- Wrappers --------------------------------------------------------- + Page_StepReadResponseDto: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/StepReadResponseDto' } + next: { type: string } + prev: { type: string } + required: [result, next, prev] + + Page_TransmittalReadListResponseDto: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalReadListResponseDto' } + next: { type: string } + prev: { type: string } + required: [result, next, prev] + + Page_TransmittalTemplateListResponse: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalTemplateListResponse' } + next: { type: string } + prev: { type: string } + required: [result, next, prev] + + Result_TransmittalTemplateSelectListResponse: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalTemplateSelectListResponse' } + required: [result] + + Result_TransmittalReadListResponseDto: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalReadListResponseDto' } + required: [result] + + Result_TransmittalGetByBundleIdsResponse: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalGetByBundleIdsResponse' } + required: [result] + + # --- Transmittals ----------------------------------------------------- + TransmittalCreateRequestDto: + type: object + properties: + name: { type: string, minLength: 1, maxLength: 255 } + company_id: { type: integer, minimum: 0, maximum: 18446744073709551615 } + additional_information: { type: string, nullable: true } + resource_id: { type: string, format: uuid } + auto_send: { type: boolean } + documents_to_approve: + type: array + description: Не менее одного документа + minItems: 1 + uniqueItems: true + items: { $ref: '#/components/schemas/DocumentVersionRequestDto' } + receivers: { $ref: '#/components/schemas/ReceiversRequestDto' } + deadline_at: + type: string + format: date-time + nullable: true + description: Дедлайн в будущем + required: [name, company_id, additional_information, resource_id, auto_send, documents_to_approve, receivers, deadline_at] + + TransmittalCreateResponseDto: + type: object + properties: + id: { type: string, format: uuid } + required: [id] + + TransmittalListRequestDto: + type: object + properties: + limit: { type: integer, default: 1000 } + bookmark: { type: string, nullable: true } + resource_id: { type: string, format: uuid } + required: [resource_id] + + TransmittalReadListResponseDto: + type: object + properties: + id: { type: string, format: uuid } + resource_id: { type: string, format: uuid } + created_by: { type: integer } + current_step_id: { type: integer } + "no": { type: integer } + name: { type: string } + additional_information: { type: string, nullable: true } + steps: { type: array, items: { type: integer } } + auto_send: { type: boolean } + receivers: { $ref: '#/components/schemas/ReceiversReadResponseDto' } + deadline_at: { type: string, format: date-time, nullable: true } + status: { $ref: '#/components/schemas/StatusReadResponseDto' } + step: { $ref: '#/components/schemas/StepReadResponseDto' } + sent_at: { type: string, format: date-time, nullable: true } + received_at: { type: string, format: date-time, nullable: true } + act_available: { type: boolean } + reviews: + type: array + nullable: true + items: { $ref: '#/components/schemas/ReviewLightResponse' } + required: [id, resource_id, created_by, current_step_id, "no", name, additional_information, steps, auto_send, receivers, deadline_at, status, step, sent_at, received_at, act_available] + + TransmittalReadResponseDto: + type: object + properties: + id: { type: string, format: uuid } + resource_id: { type: string, format: uuid } + created_by: { type: integer } + current_step_id: { type: integer } + "no": { type: integer } + name: { type: string } + additional_information: { type: string, nullable: true } + steps: { type: array, items: { type: integer } } + auto_send: { type: boolean } + receivers: { $ref: '#/components/schemas/ReceiversReadResponseDto' } + deadline_at: { type: string, format: date-time, nullable: true } + sent_at: { type: string, format: date-time, nullable: true } + received_at: { type: string, format: date-time, nullable: true } + act_available: { type: boolean } + status: { $ref: '#/components/schemas/StatusReadResponseDto' } + step: { $ref: '#/components/schemas/StepReadResponseDto' } + documents_to_approve: + type: array + items: { $ref: '#/components/schemas/DocumentVersionReadResponse' } + history: + type: array + items: { $ref: '#/components/schemas/UserActionReadResponse' } + required: [id, resource_id, created_by, current_step_id, "no", name, additional_information, steps, auto_send, receivers, deadline_at, sent_at, received_at, act_available, status, step, documents_to_approve, history] + + TransmittalStatusesStatResponseDto: + type: object + properties: + status: { $ref: '#/components/schemas/StatusReadResponseDto' } + count: { type: integer } + required: [status, count] + + TransmittalListResponseDto: + type: object + properties: + page: { $ref: '#/components/schemas/Page_TransmittalReadListResponseDto' } + statuses_stat: + type: array + items: { $ref: '#/components/schemas/TransmittalStatusesStatResponseDto' } + required: [page, statuses_stat] + + TransmittalGlobalStatusesStatResponseDto: + type: object + properties: + resource_id: { type: string, format: uuid } + statuses: + type: array + items: { $ref: '#/components/schemas/TransmittalStatusesStatResponseDto' } + required: [resource_id, statuses] + + TransmittalGetGlobalStatusesRequestDto: + type: object + properties: + resource_ids: + type: array + items: { type: string, format: uuid } + required: [resource_ids] + + TransmittalGetGlobalStatusesResponseDto: + type: object + properties: + result: + type: array + items: { $ref: '#/components/schemas/TransmittalGlobalStatusesStatResponseDto' } + required: [result] + + TransmittalDownloadActResponse: + type: object + properties: + presigned_url: { type: string } + required: [presigned_url] + + TransmittalGetActReadinessResponse: + type: object + properties: + act_available: { type: boolean } + required: [act_available] + + TransmittalOpenCountResponseDto: + type: object + properties: + count: { type: integer } + required: [count] + + TransmittalLinkReviewRequest: + type: object + properties: + review_id: { type: integer } + required: [review_id] + + TransmittalSearchFilterRequestDto: + type: object + description: Фильтр поиска в рамках одного ресурса + properties: + q: { type: string, nullable: true } + resource_id: { type: string, format: uuid } + status_slug: { type: array, items: { type: string } } + created_by: { type: array, items: { type: integer } } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversFilterDto' }] + created_from: { type: string, format: date-time, nullable: true } + created_to: { type: string, format: date-time, nullable: true } + received_from: { type: string, format: date-time, nullable: true } + received_to: { type: string, format: date-time, nullable: true } + deadline_at: { type: string, format: date-time, nullable: true } + unlimited: { type: boolean, nullable: true, default: false } + auto_send: { type: array, items: { type: boolean } } + limit: { type: integer, nullable: true, default: 100 } + bookmark: { type: string, nullable: true } + required: [resource_id] + + TransmittalSearchFilterDto: + type: object + description: Фильтр поиска по нескольким ресурсам + properties: + q: { type: string, nullable: true } + resource_ids: { type: array, items: { type: string, format: uuid } } + status_slug: { type: array, items: { type: string } } + created_by: { type: array, items: { type: integer } } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversFilterDto' }] + created_from: { type: string, format: date-time, nullable: true } + created_to: { type: string, format: date-time, nullable: true } + received_from: { type: string, format: date-time, nullable: true } + received_to: { type: string, format: date-time, nullable: true } + deadline_at: { type: string, format: date-time, nullable: true } + unlimited: { type: boolean, nullable: true, default: false } + auto_send: { type: array, items: { type: boolean } } + limit: { type: integer, nullable: true, default: 100 } + bookmark: { type: string, nullable: true } + + # --- Internal --------------------------------------------------------- + TransmittalListInternalRequest: + type: object + properties: + limit: { type: integer, default: 100 } + transmittal_ids: { type: array, items: { type: string, format: uuid } } + company_id: { type: integer, nullable: true } + resource_id: { type: string, format: uuid, nullable: true } + + TransmittalGetByBundleIdsRequest: + type: object + properties: + bundle_ids: { type: array, items: { type: string, format: uuid } } + required: [bundle_ids] + + TransmittalGetByBundleIdsReadDto: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + status: { $ref: '#/components/schemas/StatusReadResponseDto' } + created_at: { type: string, format: date-time } + required: [id, name, status, created_at] + + TransmittalGetByBundleIdsResponse: + type: object + properties: + bundle_id: { type: string, format: uuid } + transmittals: + type: array + items: { $ref: '#/components/schemas/TransmittalGetByBundleIdsReadDto' } + required: [bundle_id, transmittals] + + # --- Templates -------------------------------------------------------- + TransmittalTemplateListFiltersRequest: + type: object + properties: + include_wo_resources: { type: boolean, default: true } + resources: { type: array, nullable: true, items: { type: string, format: uuid } } + companies: { type: array, nullable: true, items: { type: integer } } + q: { type: string, nullable: true } + bookmark: { type: string, nullable: true } + limit: { type: integer, default: 20 } + + TransmittalTemplateListResponse: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + is_active: { type: boolean } + resource_id: { type: string, format: uuid, nullable: true } + allowed_initiators: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }] + created_by: { type: integer } + created_at: { type: string, format: date-time } + has_deadline_interval: { type: boolean } + deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях } + auto_send: { type: boolean, nullable: true } + has_additional_information: { type: boolean } + additional_information: { type: string, nullable: true } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }] + invalid_receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }] + required: [id, name, is_active, resource_id, allowed_initiators, created_by, created_at, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers, invalid_receivers] + + TransmittalTemplateCreateRequestDto: + type: object + properties: + name: { type: string, minLength: 1, maxLength: 255 } + resource_id: { type: string, format: uuid, nullable: true } + allowed_initiators: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }] + company_id: { type: integer } + has_deadline_interval: { type: boolean } + deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях } + auto_send: { type: boolean, nullable: true } + has_additional_information: { type: boolean } + additional_information: { type: string, nullable: true } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }] + required: [name, resource_id, allowed_initiators, company_id, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers] + + TransmittalTemplateCreateResponseDto: + type: object + properties: + id: { type: string, format: uuid } + required: [id] + + TransmittalTemplateSelectListResponse: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + created_by: { type: integer } + required: [id, name, created_by] + + TransmittalTemplateGetSingleResponse: + type: object + properties: + name: { type: string } + created_by: { type: integer } + has_deadline_interval: { type: boolean } + deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях } + auto_send: { type: boolean, nullable: true } + has_additional_information: { type: boolean } + additional_information: { type: string, nullable: true } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }] + invalid_receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversReadResponseDto' }] + required: [name, created_by, has_deadline_interval, deadline_interval, auto_send, has_additional_information, additional_information, receivers, invalid_receivers] + + TransmittalTemplateUpdateRequest: + type: object + description: Частичное обновление. Все поля опциональны. + properties: + name: { type: string, minLength: 1, maxLength: 255, nullable: true } + is_active: { type: boolean, nullable: true } + resource_id: { type: string, format: uuid, nullable: true } + allowed_initiators: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }] + has_deadline_interval: { type: boolean, nullable: true } + deadline_interval: { type: integer, minimum: 1, nullable: true, description: Интервал дедлайна в днях } + auto_send: { type: boolean, nullable: true } + has_additional_information: { type: boolean, nullable: true } + additional_information: { type: string, nullable: true } + receivers: + nullable: true + allOf: [{ $ref: '#/components/schemas/ReceiversRequestDto' }] + + # --- Infra ------------------------------------------------------------ + ResourceStatus: + type: object + properties: + name: { type: string } + available: { type: boolean } + required: [name, available] + + HealthCheckResponse: + type: object + properties: + availability: + type: array + items: { $ref: '#/components/schemas/ResourceStatus' } + status: + type: string + enum: [healthy, partially_healthy] + description: Вычисляемое поле — `healthy`, если доступны все зависимости + readOnly: true + required: [availability, status] diff --git a/apps/workspaces/.env.example b/apps/workspaces/.env.example new file mode 100644 index 0000000..0adecee --- /dev/null +++ b/apps/workspaces/.env.example @@ -0,0 +1,55 @@ +# ============================================================================ +# workspaces-api — пример переменных окружения +# +# Скопируйте нужные строки в .env в корне репозитория workspaces-api. +# Разбор выполняется библиотекой envconfig (config/config.go, config.FromEnv). +# envconfig НЕ помечает переменные как required — процесс стартует даже без них, +# но без корректных значений БД/documentation сервис работать не будет. +# Пометка (нужна) ниже означает практическую обязательность, а не env-required. +# bool принимает 1/0, true/false, t/f. +# ============================================================================ + +# --- HTTP-сервер ------------------------------------------------------------ +API_ADDRESS=0.0.0.0:6666 # (нужна) адрес прослушивания HTTP-сервера host:port (эндпоинт /ping) + +# --- PostgreSQL ------------------------------------------------------------- +POSTGRES_ADDRESS=127.0.0.1 # (нужна) хост PostgreSQL +POSTGRES_PORT=5432 # (нужна) порт PostgreSQL +POSTGRES_DB=workspaces # (нужна) имя базы данных +POSTGRES_USER=user # (нужна) пользователь БД +POSTGRES_PASSWORD=password # (нужна) пароль пользователя БД +POSTGRES_POOL_SIZE=10 # размер пула соединений (по умолчанию 0) +ENABLE_SQL_QUERY=1 # логировать SQL-запросы (по умолчанию 0) +ENABLE_SSL=0 # TLS к PostgreSQL с проверкой по YC-PG-CERTIFICATE (по умолчанию 0) +# YC-PG-CERTIFICATE= # содержимое (PEM) CA-сертификата PostgreSQL; нужно при ENABLE_SSL=1 + +# --- Сервис documentation --------------------------------------------------- +DOCUMENTATION_HOST=https://stage-api.sarex.io/documentation # (нужна) базовый URL сервиса documentation +DOCUMENTATION_ORIGINATOR=local_ws # идентификатор источника, передаваемый в documentation +DOCUMENTATION_LOGGER_FEATURE=1 # фича логирования обращений к documentation (по умолчанию 0) + +# --- Bundles ---------------------------------------------------------------- +# BUNDLES_RETRY_COUNT=5 # число ретраев клиента bundles (по умолчанию/при <=0 берётся 3) +# BUNDLES_NJOBS=5 # число параллельных задач при работе с bundles (по умолчанию/при <=0 берётся 3) + +# --- Sentry ----------------------------------------------------------------- +SENTRY_DSN=https://62dbe0a9ee5745eead25f2a705cb35ce@o279218.ingest.sentry.io/6229949 # DSN Sentry +SENTRY_DEBUG=0 # debug-режим Sentry (по умолчанию 0) +ENVIRONMENT=local # имя окружения (передаётся в Sentry как environment) + +# --- Трейсинг OpenTelemetry (необязательно) --------------------------------- +# TRACER_USE=false # включить трейсинг и otel-логгер (по умолчанию false) +# TRACER_HOST=localhost:4317 # адрес OTLP-коллектора +# TRACER_USE_INSECURE=true # подключение без TLS (по умолчанию true) +# SERVICE_NAME=workspaces # имя сервиса в трейсах (по умолчанию workspaces) +# TRACER_LOGGER_NAME=tracer_logger # имя otel-логгера (по умолчанию tracer_logger) + +# --- Вспомогательные (не читаются config.Config) ---------------------------- +POSTGRES_EXTERNAL_PORT=5432 # внешний порт проброса контейнера Postgres в docker-compose +API_PORT=6666 # порт API в docker-compose (сервис api там закомментирован) +# FAKE_API_ADDRESS=0.0.0.0:7777 # адрес фейкового bundle-API (fake_bundle_api/main.go; локальная разработка/тесты) +# NAMESPACE=workspaces # задаётся в манифестах/чарте, кодом приложения не читается +# INTERNAL_PATH=/internal/ # задаётся в чарте, кодом приложения не читается +# DJANGO_HOST=... # задаётся в манифестах iac, но config.Config его НЕ читает +# DJANGO_ORIGINATOR=... # задаётся в манифестах iac, но config.Config его НЕ читает +# DJANGO_BASIC_AUTH=... # инъектируется из секрета в манифестах iac, но config.Config его НЕ читает diff --git a/apps/workspaces/CONFIGURATION.md b/apps/workspaces/CONFIGURATION.md new file mode 100644 index 0000000..810d00c --- /dev/null +++ b/apps/workspaces/CONFIGURATION.md @@ -0,0 +1,152 @@ +# Конфигурация workspaces-api + +Документ описывает все переменные окружения и способы конфигурирования сервиса `workspaces-api` (репозиторий `pdm/workspaces-api`) и его развёртывания из этого infra-репозитория (`iac/apps/workspaces`). + +`workspaces-api` — HTTP-сервис (`cmd/api`), хранит рабочие пространства (workspaces) и приложения (apps) в PostgreSQL, обращается к сервису documentation и к bundle-сервису. Вместе с ним из одного образа собираются утилиты миграций (`cmd/migrations`) и CLI (`cmd/workspaces-cli`). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.FromEnv()` → `envconfig.Process`). Отдельного конфиг-файла (yaml/toml) у приложения нет. + +В отличие от `env-required`-подхода, **envconfig здесь не помечает переменные обязательными** — при отсутствии значения `FromEnv()` не завершает процесс, поле остаётся нулевым. Поэтому «обязательность» переменных БД/documentation фактическая, а не форсированная кодом: без них сервис стартует, но работать не будет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose + бинарник) | Файл `.env` в корне репозитория. `docker-compose.yml` (`env_file: .env`) поднимает только контейнер Postgres (`postgres:13`); сам API-сервис в compose закомментирован и запускается бинарником. `Makefile` (`make docker`) прокидывает `.env` в docker-compose | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml` (`universal-chart`): блоки `envs` (обычные значения по окружениям `_default`/`stage`/`preprod`/`production`) и `secretEnvs` (значения из k8s-секретов). Деплой запускается пайплайнами `generic/common-ci` | +| Kubernetes — этот infra-репозиторий (`iac/apps/workspaces`) | `base/` — kustomize-манифесты с инъекцией секретов через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + postgresql через Flux), `brusnika-stage` и `brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блоки `envs`/`secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`); в `workflow.rules` задаются переменные пайплайна (`STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `HELM_SET_ARGS` и т.п.), а также build-args образа | + +**Миграции БД.** Отдельного env-флага для миграций нет: порядок запуска задаёт `entrypoint.sh` — сначала выполняется `/migrations migrate`, затем стартует `/api`. Утилита миграций читает тот же конфиг (`config/config.go`) и использует `ENABLE_SSL`/`YC-PG-CERTIFICATE` для TLS-подключения к БД (`cmd/migrations/main.go`). В kustomize-/Helm-манифестах миграции запускаются той же командой в `args`/`command` контейнера (`set -e; /migrations migrate; exec /api`). + +--- + +## Переменные приложения (`config.Config`) + +Читаются структурой `config.Config` (`config/config.go`). Тип `bool` в envconfig принимает `1`/`0`, `true`/`false`, `t`/`f`. + +### HTTP-сервер + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера (`host:port`), напр. `0.0.0.0:8000`. Эндпоинт `/ping` — liveness/readiness | + +### PostgreSQL + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | +| `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_POOL_SIZE` | int | `0` | Размер пула соединений к БД | +| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы (query-hook в go-pg) | +| `ENABLE_SSL` | bool | `false` | Подключаться к PostgreSQL по TLS с проверкой по `YC-PG-CERTIFICATE`; читается и в api, и в миграциях | +| `YC-PG-CERTIFICATE` | string | — | Содержимое (PEM) CA-сертификата PostgreSQL; используется при `ENABLE_SSL=1` (`cmd/api/main.go`, `cmd/migrations/main.go`) | + +### Сервис documentation + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATION_HOST` | string | — | Базовый URL сервиса documentation | +| `DOCUMENTATION_ORIGINATOR` | string | — | Идентификатор источника, передаваемый в сервис documentation | +| `DOCUMENTATION_LOGGER_FEATURE` | bool | `false` | Включает фичу логирования обращений к сервису documentation | + +### Bundles + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `BUNDLES_RETRY_COUNT` | int | `3` | Кол-во ретраев клиента bundles; при значении `<= 0` в `FromEnv()` принудительно берётся `3` | +| `BUNDLES_NJOBS` | int | `3` | Кол-во параллельных задач при работе с bundles; при значении `<= 0` берётся `3` | + +### Sentry + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `SENTRY_DSN` | string | — | DSN для отправки ошибок в Sentry | +| `SENTRY_DEBUG` | bool | `false` | Debug-режим Sentry | +| `ENVIRONMENT` | string | — | Имя окружения (передаётся в Sentry как environment) | + +### Трейсинг (OpenTelemetry) + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `TRACER_USE` | bool | `false` | Включает OpenTelemetry-трейсинг и otel-логгер | +| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `SERVICE_NAME` | string | `workspaces` | Имя сервиса в трейсах | +| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя otel-логгера | + +> Дефолты `TRACER_*` и `SERVICE_NAME` заданы прямо в тегах `default:"..."` структуры `config.Config`; остальные поля дефолтов не имеют (нулевое значение типа). + +--- + +## Инфраструктурные, сборочные и вспомогательные переменные + +Не читаются основным кодом приложения (`config.Config`), но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `POSTGRES_EXTERNAL_PORT` | `.env`, `docker-compose.yml` | Внешний порт проброса контейнера Postgres | +| `API_PORT` | `.env`, `docker-compose.yml` (закомментированный сервис api) | Порт API при локальном запуске в контейнере | +| `FAKE_API_ADDRESS` | `fake_bundle_api/main.go` (`envconfig`) | Адрес фейкового bundle-API для локальной разработки/тестов; читается отдельной утилитой, не основным сервисом | +| `GITLAB_CREDENTIALS` | `api.Dockerfile` (build-arg) | Креды `https://:@gitlab.sarex.io` для доступа к приватным Go-модулям при сборке | +| `CI_COMMIT_SHORT_SHA` | `api.Dockerfile` (build-arg) → `APP_VERSION` | Хэш коммита, прокидываемый в сборку `make api` | +| `APP_VERSION` | `Makefile` (`make api`) | Версия приложения при сборке (значение — из `CI_COMMIT_SHORT_SHA`) | +| `NAMESPACE` | `.helm/values.yaml`, `base/*.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом приложения не читается** | +| `INTERNAL_PATH` | `.helm/values.yaml` | Задаётся в чарте, но **кодом приложения не читается** | +| `DJANGO_HOST`, `DJANGO_ORIGINATOR` | `base/backend-deployment.yaml`, brusnika-оверлеи | Заданы в манифестах, но **`config.Config` их не читает** (в текущем коде полей Django нет) | +| `DJANGO_BASIC_AUTH` | `base/backend-deployment.yaml` (Vault), brusnika `secretEnvs` | Инъектируется из секрета, но **`config.Config` его не читает** | + +--- + +## Деплой из этого репозитория (`iac/apps/workspaces`) + +Здесь используется **kustomize** (`base/` + оверлеи), а не собственный Helm-чарт сервиса. Секреты БД и Django инъектируются агентом **Vault** и подгружаются в окружение процесса до старта (`set -a; . /vault/secrets/...; exec /api`). + +### `base/` + +- `backend-deployment.yaml` (api, namespace `workspaces`) — обычные переменные заданы инлайн в `env:` (`POSTGRES_POOL_SIZE`, `BUNDLES_*`, `API_ADDRESS`, `NAMESPACE`, `ENABLE_SQL_QUERY`, `ENABLE_SSL`, `DOCUMENTATION_*`, `ENVIRONMENT`, `DJANGO_HOST`, `DJANGO_ORIGINATOR`). Vault-шаблоны формируют файл `/vault/secrets/workspaces-db` с `POSTGRES_ADDRESS/PORT/DB/USER/PASSWORD` (из `secrets/data/postgresql/apps/workspaces`) и `/vault/secrets/workspaces-django-auth` с `DJANGO_BASIC_AUTH` (из `secrets/data/vault/common/django_auth`). Контейнер стартует через `command: /bin/sh -ec` + `args`, который подгружает эти файлы (`set -a; . /vault/secrets/...`) и `exec /api`. Vault-роль — `workspaces`, ServiceAccount — `workspaces-vault`. Namespace размечен `istio-injection: enabled`. +- `frontend-deployment.yaml` (`frontend`, образ `workspaces-v2-frontend`) и `frontend-service.yaml` — статический фронтенд, переменных окружения не имеет. +- `backend-service.yaml` (`backend-svc`, `:80 → 8000`), `namespace.yaml`, `serviceaccount.yaml`. +- `kustomization.yaml` собирает namespace, serviceaccount, backend/frontend deployments и services (namespace `workspaces`). + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + `postgresql.yaml` (Flux `HelmRelease` `postgresql-contour`: БД `workspaces_db`, пользователь `workspaces`, расширение `uuid-ossp`, восстановление из дампа, интеграция с Vault). Патч `replicas.yaml` (`replicas: 1` для `workspaces-api`). +- **`brusnika-stage`** и **`brusnika-prod`** — Flux `HelmRelease` на `universal-chart` (`0.1.7`) для api и фронтенда. Переменные — в `envs`, секреты — в `secretEnvs` (`postgres-secret` → `POSTGRES_USER`/`POSTGRES_PASSWORD`, `django-auth` → `DJANGO_BASIC_AUTH`). Отличаются значениями `POSTGRES_ADDRESS` и `DOCUMENTATION_HOST` (stage: `192.168.2.45` / `https://test.sarex.brusnika.tech/documentations`; prod: `postgres-service` / `https://cde.brusnika.ru/documentations`). Api-под запускает `/migrations migrate` перед `/api` в `args`. + +--- + +## JWT-ключи + +RSA-ключи для JWT лежат в `.pub_keys/` (`prod.rsa.pub`, `stage.rsa.pub`, `test.rsa*`) и при сборке образа копируются в `/etc/sarex/keys` (см. `api.Dockerfile`). Пути к ключам передаются в код работы с токенами как аргументы; отдельной переменной окружения для пути к ключам в текущем коде нет. + +--- + +## Замечания и потенциальные проблемы + +- **Опечатка в `.env`:** переменная названа `POSTGRES_POLL_SIZE`, тогда как код читает `POSTGRES_POOL_SIZE`. При локальном запуске из `.env` размер пула не применится и останется `0`. +- **`env-required` не используется:** отсутствие обязательных значений (БД, documentation) не приводит к ошибке `FromEnv()` — процесс стартует с пустыми полями и падает позже при обращении к БД/сервисам. +- **Django-переменные не читаются кодом:** `DJANGO_HOST`, `DJANGO_ORIGINATOR`, `DJANGO_BASIC_AUTH` заданы в манифестах `iac` (base + brusnika-оверлеи), но в `config.Config` соответствующих полей нет — значения игнорируются приложением. +- **`NAMESPACE` и `INTERNAL_PATH`** задаются в манифестах/чарте, но кодом приложения не читаются. +- Имя переменной `YC-PG-CERTIFICATE` содержит дефисы — envconfig сопоставляет её по точному совпадению тега. +- **Расхождение секретов между источниками деплоя:** в `.helm/values.yaml` `secretEnvs.POSTGRES_PORT._default.secretName` указан как `a-pg-secret` (похоже на опечатку от `ya-pg-secret`); ключи секрета БД различаются между окружениями (`workspaces-postgresql-secret` с ключами `username`/`ca.crt` vs `ya-pg-secret`/`yc-pg-certificate`). В kustomize (`base/`) те же значения приходят из Vault, а не из k8s-секретов. +- **Порты различаются по окружениям:** локально/`_default` — `8000`, в preprod/production Helm-чарта — `8080` (см. `API_ADDRESS` и probes). + +--- + +## Минимальный набор для локального запуска + +Postgres — через docker-compose, api — бинарником (миграции применяются `entrypoint.sh`/вручную перед стартом): + +- `API_ADDRESS` (напр. `0.0.0.0:6666`) +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` +- `DOCUMENTATION_HOST`, `DOCUMENTATION_ORIGINATOR` +- `ENVIRONMENT` (напр. `local`) +- при необходимости — `ENABLE_SQL_QUERY`, `SENTRY_DSN`, `ENABLE_SSL` (+ `YC-PG-CERTIFICATE`), `TRACER_*` + +См. пример значений в `.env` / `.env.example`. diff --git a/apps/workspaces/ENDPOINTS-workspace-v2-frontend.md b/apps/workspaces/ENDPOINTS-workspace-v2-frontend.md new file mode 100644 index 0000000..0947846 --- /dev/null +++ b/apps/workspaces/ENDPOINTS-workspace-v2-frontend.md @@ -0,0 +1,206 @@ +# Эндпоинты, с которыми взаимодействует workspace-v2-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается микрофронтенд `workspace-v2-frontend`, а также способ конфигурирования базовых хостов на этапе сборки. + +## Как устроено взаимодействие + +Все запросы описаны декларативно в реестре `module/networking/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`: + +- `service` — логическое имя сервиса (enum `EServices` из `module/httpService/hosts.ts`); +- `method` — HTTP-метод (`EMethod`: `GET`/`POST`/`PUT`/`PATCH`/`DELETE`); +- `auth` — требуется ли авторизация; +- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query); +- `body(args)` — опционально, формирование тела запроса. + +Запрос выполняется единой функцией `fetch(endpoint, params)` (`module/networking/endpoints.ts`), которая вызывает `httpService.getRequest(...)` (`module/httpService/httpService.tsx`, поверх `@sarex-team/sdk-js`). Базовый хост подставляется `resolveHost(service)` из `module/httpService/hosts.ts` — обёрткой над `resolveHost` из SDK, которая по `buildEnv` выбирает набор хостов из `allHosts`. Подключаемые удалённые модули (module federation) описаны отдельно в `module/config/module-hosts.ts`. + +## Конфигурирование (базовые хосты, `BUILD_ENV`) + +Фронтенд конфигурируется **только на этапе сборки** — переменной окружения `BUILD_ENV`. Рантайм-переменных окружения у собранного бандла нет. + +Значение подставляется в бандл через `webpack.DefinePlugin` как константа `__BUILD_ENV__` (`webpack/config.build.js`, `config.serve.js`, `config.start.js`). Модуль `module/httpService/buildEnv.ts` читает её: `buildEnv = __BUILD_ENV__ || EBuildEnv.prod` — **по умолчанию `prod`**. Допустимые значения проверяются в `webpack/env.js` (`BUILD_ENV = process.env.BUILD_ENV || "prod"`). + +| Переменная | Где задаётся | Назначение | +| --- | --- | --- | +| `BUILD_ENV` | build-arg в `.gitlab-ci.yml` → `ARG BUILD_ENV` в `Dockerfile` → `npm run build` | Окружение сборки; определяет набор базовых хостов. Значение по умолчанию — `prod` | +| `NPM_NEXUS_TOKEN` | build-arg в `.gitlab-ci.yml` → `ARG NPM_NEXUS_TOKEN` в `Dockerfile` (`.npmrc`) | Токен доступа к приватному npm-реестру `nexus.infra.sarex.io` при установке зависимостей | + +Допустимые значения `BUILD_ENV` (`EBuildEnv` в `module/httpService/buildEnv.ts` / `webpack/env.js`): `local`, `stage`, `prod`, `preprod`, `contour`, `severstal`, `uralchem`. В CI (`.gitlab-ci.yml`) собираются стадии `preprod`, `stage`, `prod`. Локальные npm-скрипты: `start` → `local`, `serve` → `stage`, `storybook` → `local`. + +> Замечание: окружения `severstal` и `uralchem` в коде помечены комментариями «уточнить в будущем используется ли». + +## Базовые хосты по сервисам и окружениям + +Значения из `module/httpService/hosts.ts` (`allHosts`). Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. Ниже приведены основные окружения `stage` и `prod`; полный набор (`local`, `preprod`, `contour`, `severstal`, `uralchem`) — в `allHosts`. + +| Сервис (`EServices`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `documentations` | Сервис документации | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `sarexApi` | Gateway/API Sarex (`/gateway`, `/files`, `/issues`, `/notes`, `/mapper`) | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `gateway` | Gateway Sarex | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `sarex` | Локальный сервис данных (ядро/ЛК) | `/` | `/` | +| `bim` | BIM-API (v1) | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` | +| `bimv2` | BIM-API (v2) | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` | +| `comparisons` | Сервис сравнений | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists` | `https://api.sarex.io/checklists` | +| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | +| `sarexAgents` | Сервис агентов | `https://sarex-agents.dev.stage.sarex.io` | `https://agents.sarex.tech` | +| `eavV4` | EAV API v4 | `https://stage-api.sarex.io/eav/api/v4` | `https://api.sarex.io/eav/api/v4` | +| `premises` | Сервис помещений | `https://stage-api.sarex.io/premises/api/v1` | `https://api.sarex.io/premises/api/v1` | +| `absolute` | Пустой хост (относительные/полные URL) | `""` | `""` | + +> В `local` сервис `sarex` проксируется на `https://localhost:9000/sarex-backend`, остальные — на `https://localhost:9000/sarex-api-backend/...`. В `contour` используются относительные пути (`/workspaces`, `/documentations` и т.д.). Сервисы `checklists`, `remarks` (помечен в коде как «не используется»), `google`, `zitadel`, `sarexAgents`, `eavV4`, `premises` объявлены в хостах, но в текущем реестре `endpoints.ts` эндпоинтов к ним нет. + +## Подключаемые модули (module federation) + +Хосты remoteEntry для микрофронтендов (`module/config/module-hosts.ts`, выбор по `buildEnv`): + +| Модуль | `stage` | `prod` | +| --- | --- | --- | +| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | +| `assistant` | `https://stage-modules.sarex.io/assistant/static/module/remoteEntry.js` | `https://modules.sarex.io/assistant/static/module/remoteEntry.js` | + +## Эндпоинты по сервисам + +### `workspaces` — Сервис рабочих областей + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getWorkspaceStates` | GET | да | `/api/v1/workspaces/{uuid}/states` | Список состояний рабочей области | +| `getWorkspaceStatesActual` | GET | да | `/api/v1/workspaces/{uuid}?version={currentStateIdState}` | Актуальное состояние по версии | +| `saveWorkspaceState` | POST | да | `/api/v1/workspaces/{uuid}/states` | Сохранить состояние (тело — объект `state`) | +| `editWorkspaceState` | PATCH | да | `/api/v1/states/{uuid}` | Изменить состояние (`name`/`data`) | +| `putDefaultState` | PATCH | да | `/api/v1/states/{uuid}` | Пометить состояние как дефолтное (`is_default`) | +| `deleteWorkspaceState` | DELETE | да | `/api/v1/states/{uuid}` | Удалить состояние | +| `getCompanyApps` | GET | да | `/api/v1/company/{companyId}/apps` | Приложения компании | +| `createAppInstance` | POST | да | `/api/v1/workspaces/{workspaceId}/apps/{appId}/instances` | Создать инстанс приложения | +| `postDocument` | POST | да | `/api/v1/workspaces/{uuid}/documents` | Привязать документ к рабочей области (`document_id`) | +| `deleteDocument` | DELETE | да | `/api/v1/workspaces/{workspaceId}/documents/{documentId}` | Отвязать документ от рабочей области | + +### `documentations` — Сервис документации + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getDocumet` | GET | да | `/api/v1/documents/{docId}?extend=bundles` | Документ с бандлами | +| `deleteWorkspace` | DELETE | да | `/api/v1/documents/{id}` | Удалить документ | +| `getCompanyIdByDocId` | GET | да | `/api/v1/documents/{docId}/company` | Компания документа | +| `getDocumentTypes` | GET | да | `/api/v1/documents/types` | Типы документов | +| `getDocumentPermissions` | GET | да | `/api/v1/documents/{id}/permissions` | Права доступа документа | +| `getDisks` | GET | да | `/api/v1/disks` | Список дисков | +| `getDocumentsByDisk` | GET | да | `/api/v1/disks/{diskId}/documents` | Документы диска | +| `fetchReviewDocuments` | POST | да | `/api/v1/disks/{diskId}/documents` | Документы по набору id (`document_ids`) | +| `getFile` | GET | да | `/api/v1/bundles/{bundleId}/pdf/download` | Скачать PDF бандла | +| `downloadFile` | GET | да | `/api/v1/bundles/{bundleId}/{key}/download` | Скачать файл бандла | +| `downloadAllFiles` | GET | да | `/api/v1/bundles/{bundleId}/download` | Скачать все файлы бандла | +| `updateBundle` | PATCH | да | `/api/v1/bundles/{bundleId}` | Обновить бандл (`attributes`) | +| `restartWorkflow` | POST | да | `/api/v1/bundles/{bundleId}/restart` | Перезапустить workflow бандла | + +### `sarexApi` — Gateway/API Sarex + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getDocuments` | GET | да | `/gateway/api/v1/disks/{diskId}/documents?parent_id=&child_id=&search=` | Документы диска (фильтры) | +| `loadFile` | GET | да | `/files/api/v1/bundles/{bundleId}/{key}` | Файл бандла | +| `getPagePdf` | GET | да | `{url}` | PDF-страница по произвольному URL | +| `getRemarksByDocument` | POST | да | `/issues/api/issues/filter/?limit=&offset=` | Замечания документа (фильтр) | +| `getRemark` | GET | да | `/issues/api/issues/{uuid}/` | Замечание по uuid | +| `getCustomStatuses` | GET | да | `/issues/api/companies/{companyId}/status-model/v2/?issue_type_id={issueType}` | Модель статусов замечаний компании | +| `getIssueTypes` | GET | да | `/issues/api/issue-types/?company_id={companyId}` | Типы замечаний компании | +| `getNotes` | GET | да | `/mapper/api/v1/notes/workspace/workspace/{instanceId}/?{query}` | Заметки рабочей области | +| `createLinkNote` | POST | да | `/notes/api/v1/links/` | Привязать ссылку к заметке (`link_id`, `note_id`) | +| `updateLinkNote` | POST | да | `/notes/api/v1/links/` | Обновить привязку ссылки к заметке | + +### `sarex` — Локальный сервис данных (ядро/PM) + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getUsers` | GET | да | `/api/core/users` | Список пользователей | +| `getUser` | GET | да | `/api/core/users/{id}/` | Пользователь по id | +| `getUsersByIDs` | GET | да | `/api/core/users/?id={ids}` | Пользователи по набору id | +| `getTarget` | GET | да | `/api/core/targets/{targetId}/` | Таргет по id | +| `getCoordinates` | GET | да | `/api/commons/cs/` | Системы координат | +| `createLink` | POST | да | `/api/core/target-links/` | Создать ссылку таргета | +| `updateLink` | PUT | да | `/api/core/target-links/{id}/` | Обновить ссылку таргета | +| `deleteLink` | DELETE | да | `/api/core/target-links/{id}/` | Удалить ссылку таргета | +| `getProject` | GET | да | `/api/pm/msp/projects/?bundle_id={bundleID}&schema=tiny` | Проект по бандлу | +| `getProjectStates` | GET | да | `/api/pm/msp/projects/{projectID}/states/` | Состояния проекта | +| `getResources` | GET | да | `/api/pm/msp/resources/?parent=&type=&projects=&limit=&offset=` | Список ресурсов | +| `getResourceById` | GET | да | `/api/pm/msp/resources/{id}/` | Ресурс по id | +| `getInstancesResources` | GET | да | `/api/pm/msp/resources/?{query}&model={model}` | Ресурсы по инстансам/модели | +| `getResourcesTasks` | GET | да | `/api/pm/msp/resources-tasks/?{query}` | Задачи ресурсов | +| `getResourcesByElementAndDocumentId` | GET | да | `/api/pm/msp/resources/?limit=50000&offset=0&{instance}&model=&{path}&{projects}&documents={document}` | Ресурсы по элементу и документу | +| `createResource` | POST | да | `/api/pm/msp/resources/` | Создать ресурс | +| `updateResource` | PATCH | да | `/api/pm/msp/resources/{id}/` | Обновить ресурс | +| `deleteResource` | DELETE | да | `/api/pm/msp/resources/{id}/` | Удалить ресурс | +| `bulkCreate` | POST | да | `/api/pm/msp/resources/bulk_create/` | Массовое создание ресурсов | +| `createResourceConnection` | POST | да | `/api/pm/msp/resources/{resourceId}/bind_elements/` | Привязать элементы к ресурсу | +| `getResourceConnection` | GET | да | `/api/pm/msp/resources-elements/?resources=&instances=&path=` | Связи ресурс–элемент | +| `deleteResourceConnection` | DELETE | да | `/api/pm/msp/resources-elements/{connectionId}/` | Удалить связь ресурс–элемент | + +### `gateway` — Gateway Sarex + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getThumbnails` | POST | да | `/api/v1/disks/{diskId}/thumbnails` | Превью документов (`document_ids`) | +| `getWorkspaceState` | GET | да | `/api/v1/workspace/states/{uuid}` | Состояние рабочей области | +| `getScreenShot` | GET | да | `/api/v1/workspace/{wsId}` | Скриншот/данные рабочей области | +| `getRelatedDocuments` | GET | да | `/api/v1/documents/related_documents?scope_id={bimId}&entity_id={bimElementId}` | Связанные документы элемента | +| `bindRelatedDocuments` | POST | да | `/api/v1/documents/related_documents` | Привязать связанные документы | +| `unbindRelatedDocuments` | POST | да | `/api/v1/documents/related_documents/bulk_delete` | Отвязать связанные документы (по `ids`) | + +### `workflows` — Сервис обработки документов + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getWorkflow` | GET | да | `/api/v1/workflows/{id}` | Workflow по id | + +### `bim` — BIM-API (v1) + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getElementsByIds` | GET | да | `/api/v1/bims/{id}/sarexid/{elementSarexIds}` | Элементы по sarex-id | +| `getElements` | GET | да | `{url}` | Элементы по произвольному URL | +| `postElementsStatus` | POST | да | `/api/v1/changes` | Обновить статус элементов | + +### `bimv2` — BIM-API (v2) + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getBimElementById` | GET | да | `/api/v1/bims/{bimId}` | BIM-модель по id | +| `getElementsByIdsV2` | POST | да | `/api/v1/bims/{id}/sarexid?with_hierarchy={isHierarchy}` | Элементы по sarex-id (`sarex_ids`) | +| `getElementsV2` | GET | да | `{url}` | Элементы по произвольному URL | +| `getElementsAsCsv` | POST | да | `/api/v1/bims/{bundleBimId}/csv_properties` | Выгрузка свойств элементов в CSV | +| `getElementPropertiesById` | GET | да | `/api/v1/bims/{bimId}/elements/{elementSarexId}/properties` | Свойства элемента | +| `getElementStatusesById` | GET | да | `/api/v1/bims/{bimId}/status_models` | Модели статусов | +| `getElementsFilterFields` | GET | да | `/api/v1/bims/{bimId}/filter_fields` | Поля фильтрации элементов | +| `postFilterElements` | POST | да | `/api/v1/bims/{bimId}/elements` | Фильтрация элементов (`filters`) | +| `postElementsStatusV2` | POST | да | `/api/v1/changes` | Обновить статус элементов | +| `updateElementsStatuses` | POST | да | `/api/v1/bims/{bimId}/changes?with_hierarchy={isHierarchy}` | Обновить статусы (`new_status_type/value`) | +| `fetchChangeLogs` | GET | да | `/api/v1/bims/{bimId}/changes?sarex_ids=&status_type=&offset=&limit=` | Журнал изменений статусов | +| `fetchElementsStatuses` | GET | да | `/api/v1/bims/{bimId}/statuses` | Статусы элементов | +| `fetchElementsStatusColor` | GET | да | `/api/v1/bims/{bimId}/statuses_color` | Цвета статусов | + +### `comparisons` — Сервис сравнений + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getComparisonElements` | GET | да | `/api/v1/elements?{params}` | Элементы сравнения | +| `getFilterFields` | GET | да | `/api/v1/filter_fields?doc_id={id}` | Поля фильтрации | +| `getTolerance` | GET | да | `/api/v1/tolerance?bundle_id={id}` | Допуски по бандлу | +| `updateElementField` | PATCH | да | `/api/v1/elements/{elementId}` | Обновить поле элемента | + +### `absolute` — Пустой хост (произвольные URL) + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `cloudJS` | GET | нет | `{path}` | Загрузка ресурса по пути без авторизации | +| `cloudJSWithAuth` | GET | да | `{path}` | То же, с авторизацией | +| `getLoadElements` | GET | да | `{path}` | Загрузка элементов по произвольному пути | + +## Обработка ошибок + +Запросы проходят через `fetch` (`module/networking/endpoints.ts`): при ошибке она логируется в консоль (`console.error`) и возвращается объект ошибки вызывающему коду. Централизованного маппинга кодов ответов в человекочитаемые сообщения (как в v1-фронтенде) в реестре нет — обработка ошибок выполняется на уровне вызывающих модулей/`httpService`. diff --git a/apps/workspaces/ENDPOINTS-workspaces-frontend.md b/apps/workspaces/ENDPOINTS-workspaces-frontend.md new file mode 100644 index 0000000..9acfe51 --- /dev/null +++ b/apps/workspaces/ENDPOINTS-workspaces-frontend.md @@ -0,0 +1,81 @@ +# Эндпоинты, с которыми взаимодействует workspaces-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается микрофронтенд `workspaces-frontend`, а также способ конфигурирования базовых хостов на этапе сборки. + +## Как устроено взаимодействие + +Все запросы описаны декларативно в реестре `module/networking/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`: + +- `service` — логическое имя сервиса (тип `Services` из `module/networking/hosts.ts`); +- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`); +- `auth` — требуется ли авторизация; +- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров); +- `body(args)` — опционально, формирование тела запроса; +- `transport` — транспорт (`AxiosTransport` из `module/networking/axiosTransport.ts`). + +Запрос выполняется единой функцией `fetch(endpoint, params)` (`module/networking/endpoints.ts`): итоговый URL = `resolveHost(service)` + `path(params)`. Базовый хост подставляется `resolveHost(service)` из `module/networking/hosts.ts`. Ошибки маппируются в человекочитаемые сообщения в `module/networking/errors.ts`. + +## Конфигурирование (базовые хосты, `BUILD_ENV`) + +Фронтенд конфигурируется **только на этапе сборки** — переменной окружения `BUILD_ENV`. Рантайм-переменных окружения у собранного бандла нет. + +Значения хостов «зашиваются» в бандл через `webpack.DefinePlugin` (`webpack.config.js`): плагин получает объект `hosts` из `networking.config.js`, где функция `extractHosts` по `process.env.BUILD_ENV` выбирает набор хостов и превращает его в define-константы вида `___host`. Затем `module/networking/hosts.ts` читает эти константы (`__workspaces_host`, `__google_host`, `__bim_host`, `__sarex_host`, `__sarexS3_host`). + +| Переменная | Где задаётся | Назначение | +| --- | --- | --- | +| `BUILD_ENV` | build-arg в `.gitlab-ci.yml` → `ARG BUILD_ENV` в `Dockerfile` → `npm run build-module` | Окружение сборки; определяет набор базовых хостов (`local`/`stage`/`preprod`/`prod`) | +| `NPM_NEXUS_TOKEN` | build-arg в `.gitlab-ci.yml` → `ARG NPM_NEXUS_TOKEN` в `Dockerfile` (`.npmrc`) | Токен доступа к приватному npm-реестру `nexus.infra.sarex.io` при установке зависимостей | + +Значения `BUILD_ENV` по стадиям CI (`.gitlab-ci.yml`): `preprod`, `stage`, `prod`. + +> Замечание: в `build.config.js` определён режим сборки для `local`, но в `networking.config.js` набор хостов для `local` **не задан** — при `BUILD_ENV=local` `extractHosts` бросит `Cannot get hosts for BUILD_ENV=local`. Также в `networking.config.js` для `prod`/`preprod` дополнительно объявлены хосты `documentations` и `workflows`, но `module/networking/hosts.ts` их не читает и в эндпоинтах они не используются. + +## Базовые хосты по сервисам и окружениям + +Значения из `networking.config.js`. Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. Сервис `absolute` всегда имеет пустой хост (`""`) — путь используется как есть (относительный/абсолютный URL). + +| Сервис (`service`) | Назначение | `stage` | `preprod` | `prod` | +| --- | --- | --- | --- | --- | +| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces/` | `https://api.preprod.sarex.io/workspaces/` | `https://api.sarex.io/workspaces/` | +| `bim` | BIM-API | `https://stage-api.sarex.io/bim/` | `https://api.preprod.sarex.io/bim/` | `https://api.sarex.io/bim/` | +| `sarex` | Локальный сервис данных (ЛК) | `https://stage.sarex.io/` | `https://lk.preprod.sarex.io/` | `https://lk.sarex.io/` | +| `sarexS3` | S3-хранилище ЛК | `https://lk.sarex.io/s3/` | `https://lk.preprod.sarex.io/s3/` | `https://lk.sarex.io/s3/` | +| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp/` | `https://storage.googleapis.com/srx-tmp/` | `https://storage.googleapis.com/srx-tmp/` | +| `absolute` | Пустой хост (относительные/полные URL) | `""` | `""` | `""` | + +> Сервисы `google`, `sarex`, `sarexS3` определены в конфиге хостов, но в текущем реестре `endpoints.ts` эндпоинтов к ним нет — фактически используются `workspaces`, `bim` и `absolute`. + +## Эндпоинты по сервисам + +### `workspaces` — Сервис рабочих областей + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getWorkspaces` | GET | да | `api/v1/workspaces/{uuid}` | Рабочая область по uuid | +| `getWorkspaceStates` | GET | да | `api/v1/workspaces/{uuid}/states` | Список состояний рабочей области | +| `getWorkspaceState` | GET | да | `api/v1/states/{uuid}` | Состояние по uuid | +| `saveWorkspaceState` | POST | да | `api/v1/workspaces/{uuid}/states` | Сохранить состояние (тело — объект `state`) | +| `getCompanyApps` | GET | да | `api/v1/company/{companyId}/apps` | Приложения компании | +| `createAppInstance` | POST | да | `api/v1/workspaces/{workspaceId}/apps/{appId}/instances` | Создать инстанс приложения в рабочей области | + +### `bim` — BIM-API + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `getBimElements` | GET | да | `api/v1/bims/{id}/sarexid/{elementSarexIds}` | Элементы BIM по sarex-id | +| `getElements` | GET | да | `{url}` | Запрос по произвольному URL (пагинация/выборка элементов) | +| `postElementsByIds` | POST | да | `api/v1/bims/{id}/sarexid` | Элементы по набору sarex-id (тело `sarex_ids`) | +| `postElementsStatus` | POST | да | `api/v1/changes` | Обновить статус элементов (тело `element_ids`, `current_state.abap_status`) | + +### `absolute` — Пустой хост (произвольные URL) + +| Ключ | Метод | Auth | Путь | Назначение | +| --- | --- | --- | --- | --- | +| `cloudJS` | GET | нет | `{path}` | Загрузка ресурса по пути без авторизации (напр. remoteEntry/JS модулей) | +| `cloudJSWithAuth` | GET | да | `{path}` | То же, но с авторизацией | + +## Обработка ошибок + +Ошибки маппируются в `module/networking/errors.ts`. Для каждого сервиса задано человекочитаемое имя (`serviceToName`): `workspaces` → «Сервис рабочих областей», `bim` → «Сервис BIM», `google` → «Сервис хранения данных», `sarex`/`sarexS3` → «Локальный сервис данных», `absolute` → «Сервис». + +Коды ответов (`httpCodeToError`): `400` — «некорректный формат запроса», `404` — «ресурс не найден», `500` — «ошибка сервера». Для прочих кодов берётся ближайший: `4xx` → сообщение `400`, остальные → `500`. Итоговый текст формируется как «`<Имя сервиса>` вернул ошибку: `<сообщение>`». diff --git a/apps/workspaces/openapi.yaml b/apps/workspaces/openapi.yaml new file mode 100644 index 0000000..918d08b --- /dev/null +++ b/apps/workspaces/openapi.yaml @@ -0,0 +1,906 @@ +openapi: 3.0.3 +info: + title: workspaces-api + description: >- + HTTP API сервиса workspaces-api (репозиторий `pdm/workspaces-api`). + Хранит рабочие области (workspaces), документы, состояния (states), + динамические состояния и приложения (apps). Схема составлена по + маршрутам `cmd/api/bootstrap.go` и структурам пакетов `workspace` и `apps`. + + Группы маршрутов: + * публичные (`/ping`, `/metrics`) — без middleware; + * `/api/v1` — внешний API, требует JWT (middleware `auth.JWTToCtx`); + * `/internal/v1`, `/internal/v2` — внутренний API без JWT + (доступ ограничивается сетевой политикой/ingress). + version: "1.0.0" + +servers: + - url: https://api.sarex.io/workspaces + description: production + - url: https://api.preprod.sarex.io/workspaces + description: preprod + - url: https://stage-api.sarex.io/workspaces + description: stage + - url: http://localhost:6666 + description: local + +tags: + - name: health + description: Liveness/readiness и метрики + - name: workspaces + description: Рабочие области + - name: documents + description: Документы рабочих областей + - name: states + description: Состояния рабочих областей + - name: dynamic-states + description: Динамические состояния + - name: apps + description: Приложения и их инстансы + - name: internal + description: Внутренние эндпоинты (без JWT) + +security: + - bearerAuth: [] + +paths: + # ------------------------------------------------------------------ health + /ping: + get: + tags: [health] + summary: Liveness/readiness проба + security: [] + responses: + "200": + description: Сервис жив + + /metrics: + get: + tags: [health] + summary: Метрики Prometheus + security: [] + responses: + "200": + description: Метрики в формате Prometheus + content: + text/plain: + schema: + type: string + + # -------------------------------------------------------------- api/v1 · ws + /api/v1/workspaces: + post: + tags: [workspaces] + summary: Создать рабочую область (v2) + description: Создаёт рабочую область по списку id документов (`CreateV2Workspace`). + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateV2WorkspaceRequest" + responses: + "200": + description: Созданная рабочая область + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{ws_id}: + get: + tags: [workspaces] + summary: Получить рабочую область + parameters: + - $ref: "#/components/parameters/WsId" + - name: archived + in: query + description: Включать архивные сущности + schema: { type: integer, enum: [0, 1] } + - name: state + in: query + description: uuid состояния; при указании в ответ добавляется последнее состояние + schema: { type: string, format: uuid } + responses: + "200": + description: Рабочая область + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{id}/archive: + post: + tags: [workspaces] + summary: Архивировать рабочую область + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{id}/unarchive: + post: + tags: [workspaces] + summary: Разархивировать рабочую область + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{id}/cache: + delete: + tags: [workspaces] + summary: Очистить кеш бандлов рабочей области + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { description: Кеш очищен } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{ws_id}/states: + get: + tags: [states] + summary: Список состояний рабочей области + parameters: + - $ref: "#/components/parameters/WsId" + - name: expand + in: query + description: "`state_json` — вернуть полные данные состояний (иначе — краткий список)" + schema: { type: string, enum: [state_json] } + responses: + "200": + description: Состояния + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/State" } + "500": { $ref: "#/components/responses/InternalError" } + post: + tags: [states] + summary: Создать состояние рабочей области + parameters: [ { $ref: "#/components/parameters/WsId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateStateRequest" } + responses: + "200": + description: Созданное состояние + content: + application/json: + schema: { $ref: "#/components/schemas/State" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{ws_id}/documents: + post: + tags: [documents] + summary: Добавить документ в рабочую область + parameters: [ { $ref: "#/components/parameters/WsId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AddDocumentRequest" } + responses: + "200": { description: Документ добавлен } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{ws_id}/documents/{doc_id}: + delete: + tags: [documents] + summary: Удалить документ из рабочей области + parameters: + - $ref: "#/components/parameters/WsId" + - $ref: "#/components/parameters/DocId" + responses: + "200": { description: Документ удалён } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{ws_id}/dynamic_states: + get: + tags: [dynamic-states] + summary: Список динамических состояний рабочей области + parameters: [ { $ref: "#/components/parameters/WsId" } ] + responses: + "200": + description: Динамические состояния + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/DynamicState" } + "500": { $ref: "#/components/responses/InternalError" } + post: + tags: [dynamic-states] + summary: Создать динамическое состояние + parameters: [ { $ref: "#/components/parameters/WsId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateDynamicStateRequest" } + responses: + "200": + description: Созданное динамическое состояние + content: + application/json: + schema: { $ref: "#/components/schemas/DynamicState" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/workspaces/{workspace_id}/apps/{app_id}/instances: + post: + tags: [apps] + summary: Создать инстанс приложения в рабочей области + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: Созданный инстанс + content: + application/json: + schema: { $ref: "#/components/schemas/AppInstance" } + "400": + description: Некорректный запрос или дубликат инстанса + content: + application/json: + schema: { $ref: "#/components/schemas/ErrorResponse" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + # -------------------------------------------------------- api/v1 · documents + /api/v1/documents/{doc_id}: + patch: + tags: [documents] + summary: Обновить документ + description: "Не реализовано в текущем коде — всегда возвращает 400 `Implement me`." + parameters: [ { $ref: "#/components/parameters/DocId" } ] + responses: + "400": { $ref: "#/components/responses/BadRequest" } + + /api/v1/documents/{id}/archive: + post: + tags: [documents] + summary: Архивировать документ + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/documents/{id}/unarchive: + post: + tags: [documents] + summary: Разархивировать документ + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + # ----------------------------------------------------------- api/v1 · states + /api/v1/states/{id}: + get: + tags: [states] + summary: Получить состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": + description: Состояние + content: + application/json: + schema: { $ref: "#/components/schemas/State" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + patch: + tags: [states] + summary: Изменить состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/SetStateRequest" } + responses: + "200": { $ref: "#/components/responses/Ok" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + delete: + tags: [states] + summary: Удалить (архивировать) состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/states/{id}/archive: + post: + tags: [states] + summary: Архивировать состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/states/{id}/unarchive: + post: + tags: [states] + summary: Разархивировать состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { $ref: "#/components/responses/Ok" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + # -------------------------------------------------- api/v1 · dynamic states + /api/v1/dynamic_states/{id}: + get: + tags: [dynamic-states] + summary: Получить динамическое состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": + description: Динамическое состояние + content: + application/json: + schema: { $ref: "#/components/schemas/DynamicState" } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + patch: + tags: [dynamic-states] + summary: Изменить динамическое состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/SetDynamicStateRequest" } + responses: + "200": { $ref: "#/components/responses/Ok" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + delete: + tags: [dynamic-states] + summary: Удалить динамическое состояние + parameters: [ { $ref: "#/components/parameters/Id" } ] + responses: + "200": { description: Удалено } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + # ------------------------------------------------------------ api/v1 · apps + /api/v1/company/{company_id}/apps: + get: + tags: [apps] + summary: Приложения компании + parameters: [ { $ref: "#/components/parameters/CompanyId" } ] + responses: + "200": + description: Список приложений + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/App" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + post: + tags: [apps] + summary: Создать приложение + parameters: [ { $ref: "#/components/parameters/CompanyId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AppAttributes" } + responses: + "200": + description: Созданное приложение + content: + application/json: + schema: { $ref: "#/components/schemas/App" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/apps/{app_id}: + patch: + tags: [apps] + summary: Изменить приложение + parameters: [ { $ref: "#/components/parameters/AppId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ChangeAppRequest" } + responses: + "200": + description: Обновлённое приложение + content: + application/json: + schema: { $ref: "#/components/schemas/App" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/apps/{app_id}/archive: + post: + tags: [apps] + summary: Архивировать приложение + parameters: [ { $ref: "#/components/parameters/AppId" } ] + responses: + "200": { description: Приложение архивировано } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/apps/{app_id}/unarchive: + post: + tags: [apps] + summary: Разархивировать приложение + parameters: [ { $ref: "#/components/parameters/AppId" } ] + responses: + "200": { description: Приложение разархивировано } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/app-instances/{instance_id}/archive: + post: + tags: [apps] + summary: Архивировать инстанс приложения + parameters: [ { $ref: "#/components/parameters/InstanceId" } ] + responses: + "200": { description: Инстанс архивирован } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /api/v1/app-instances/{instance_id}/unarchive: + post: + tags: [apps] + summary: Разархивировать инстанс приложения + parameters: [ { $ref: "#/components/parameters/InstanceId" } ] + responses: + "200": { description: Инстанс разархивирован } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + # -------------------------------------------------------------- internal/v1 + /internal/v1/workspaces: + post: + tags: [internal] + summary: Создать рабочую область (v1, по ссылкам на бандлы) + description: Внутренний эндпоинт без JWT. Создаёт рабочую область по списку URL бандлов. + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" } + responses: + "200": + description: Созданная рабочая область + content: + application/json: + schema: { $ref: "#/components/schemas/Workspace" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /internal/v1/workspaces/{ws_id}/documents: + post: + tags: [internal] + summary: Добавить документ в рабочую область (без проверки прав) + security: [] + parameters: [ { $ref: "#/components/parameters/WsId" } ] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AddDocumentRequest" } + responses: + "200": { description: Документ добавлен } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /internal/v1/workspaces/{ws_id}/states: + get: + tags: [internal] + summary: Список состояний рабочей области (internal) + security: [] + parameters: [ { $ref: "#/components/parameters/WsId" } ] + responses: + "200": + description: Состояния + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/State" } + "500": { $ref: "#/components/responses/InternalError" } + + # -------------------------------------------------------------- internal/v2 + /internal/v2/workspaces: + post: + tags: [internal] + summary: Создать рабочую область (v2, internal) + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateV2WorkspaceRequest" } + responses: + "200": + description: Созданная рабочая область + content: + application/json: + schema: { $ref: "#/components/schemas/Workspace" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /internal/v2/workspaces/is_workspaces_v2: + post: + tags: [internal] + summary: Проверить, являются ли рабочие области v2 + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/IsWorkspaceV2Request" } + responses: + "200": + description: Карта id → признак v2 + content: + application/json: + schema: { $ref: "#/components/schemas/IsWorkspaceV2Response" } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + + /internal/v2/documents/{docs_id}: + delete: + tags: [internal] + summary: Удалить документ (internal) + security: [] + parameters: + - name: docs_id + in: path + required: true + schema: { type: string } + responses: + "200": { description: Документ удалён } + "404": { $ref: "#/components/responses/NotFound" } + "500": { $ref: "#/components/responses/InternalError" } + + /internal/v2/documents/restore: + patch: + tags: [internal] + summary: Восстановить документы и рабочие области (internal) + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RestoreDocumentsRequest" } + responses: + "200": { description: Восстановлено } + "400": { $ref: "#/components/responses/BadRequest" } + "500": { $ref: "#/components/responses/InternalError" } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + JWT в заголовке `Authorization: Bearer `. Токен также может + передаваться query-параметром `jwt` (удаляется middleware после разбора). + + parameters: + Id: + name: id + in: path + required: true + schema: { type: string, format: uuid } + WsId: + name: ws_id + in: path + required: true + schema: { type: string, format: uuid } + WorkspaceId: + name: workspace_id + in: path + required: true + schema: { type: string, format: uuid } + DocId: + name: doc_id + in: path + required: true + schema: { type: string } + AppId: + name: app_id + in: path + required: true + schema: { type: string, format: uuid } + InstanceId: + name: instance_id + in: path + required: true + schema: { type: string, format: uuid } + CompanyId: + name: company_id + in: path + required: true + schema: { type: integer, format: int64 } + + responses: + Ok: + description: Успех + content: + application/json: + schema: { $ref: "#/components/schemas/OkResponse" } + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: { $ref: "#/components/schemas/ErrorResponse" } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: { $ref: "#/components/schemas/ErrorResponse" } + InternalError: + description: Внутренняя ошибка сервера + content: + application/json: + schema: { $ref: "#/components/schemas/ErrorResponse" } + + schemas: + ErrorResponse: + type: object + properties: + error: { type: string } + required: [error] + + OkResponse: + type: object + properties: + ok: { type: boolean } + required: [ok] + + # ---- requests ---- + CreateWorkspaceRequest: + type: object + description: "`workspace/api/workspace/create.go` (v1)" + properties: + title: { type: string, maxLength: 254 } + subtitle: { type: string, maxLength: 254 } + target: { type: integer, nullable: true } + files: + type: array + description: Ссылки на бандлы документов + items: { type: string } + features: + type: array + items: { type: string } + required: [title, files] + + CreateV2WorkspaceRequest: + type: object + description: "`workspace/api/workspace/createV2.go` (v2)" + properties: + title: { type: string, maxLength: 254 } + subtitle: { type: string, maxLength: 254 } + target: { type: integer, nullable: true } + documents: + type: array + description: id документов сервиса documentation + items: { type: integer, format: int64 } + features: + type: array + items: { type: string } + auto_created: { type: boolean } + required: [documents] + + IsWorkspaceV2Request: + type: object + properties: + ids: + type: array + items: { type: string } + required: [ids] + + IsWorkspaceV2Response: + type: object + properties: + result: + type: object + additionalProperties: { type: boolean } + description: Карта uuid рабочей области → является ли она v2 + + AddDocumentRequest: + type: object + properties: + document_id: { type: integer, format: int64 } + required: [document_id] + + RestoreDocumentsRequest: + type: object + properties: + document_ids: + type: array + minItems: 1 + items: { type: integer, format: int64 } + required: [document_ids] + + CreateStateRequest: + type: object + properties: + name: { type: string, maxLength: 254 } + data: { type: string } + dynamic_state_id: { type: string, format: uuid, nullable: true } + required: [name, data] + + SetStateRequest: + type: object + description: Частичное обновление; все поля опциональны + properties: + name: { type: string, nullable: true } + is_default: { type: boolean, nullable: true } + data: { type: string, nullable: true } + + CreateDynamicStateRequest: + type: object + properties: + name: { type: string, maxLength: 254 } + autoload: { type: boolean } + required: [name] + + SetDynamicStateRequest: + type: object + properties: + name: { type: string, nullable: true } + autoload: { type: boolean, nullable: true } + + AppAttributes: + type: object + description: "`apps/models.go`" + properties: + name: { type: string, minLength: 1, maxLength: 100 } + entrypoint: { type: string, minLength: 1, maxLength: 1024, format: uri } + devices: + type: array + items: { type: string, enum: [mobile, tablet, desktop] } + module_name: { type: string, minLength: 1, maxLength: 100 } + required: [name, entrypoint, devices, module_name] + + ChangeAppRequest: + type: object + description: >- + Частичное обновление приложения; должно быть задано хотя бы одно поле + (`apps/api/changeapp.go`). + properties: + name: { type: string, maxLength: 100 } + entrypoint: { type: string, maxLength: 1024, format: uri } + devices: + type: array + items: { type: string, enum: [mobile, tablet, desktop] } + module_name: { type: string, maxLength: 100 } + + # ---- models ---- + Workspace: + type: object + description: "Агрегат рабочей области (`workspace.Model`)" + properties: + id: { type: string, format: uuid } + title: { type: string } + subtitle: { type: string } + features: + type: array + items: { type: string } + target: { type: integer, nullable: true } + auto_created: { type: boolean, nullable: true } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + documents: + type: array + items: { $ref: "#/components/schemas/Document" } + documents_v2: + type: array + items: { $ref: "#/components/schemas/DocumentV2" } + state: + allOf: [ { $ref: "#/components/schemas/State" } ] + nullable: true + app_instances: + type: array + items: { $ref: "#/components/schemas/AppInstance" } + + Document: + type: object + description: "`workspace.ModelDocument`" + properties: + id: { type: string, format: uuid } + name: { type: string } + workspace_id: { type: string, format: uuid } + is_archived: { type: boolean } + kind: { type: string } + url: { type: string } + bundle: { type: string } + createdAt: { type: string, format: date-time } + updatedAt: { type: string, format: date-time } + + DocumentV2: + type: object + description: "`workspace.ModelDocumentV2`" + properties: + id: { type: string, format: uuid } + document_id: { type: integer, format: int64 } + created_at: { type: string, format: date-time } + + State: + type: object + description: "`workspace.ModelState`" + properties: + id: { type: string, format: uuid } + name: { type: string } + workspace_id: { type: string, format: uuid } + created_at: { type: string, format: date-time } + data: { type: string } + author_id: { type: integer, format: int64, nullable: true } + is_default: { type: boolean } + indestructible: { type: boolean } + dynamic_state_id: { type: string, format: uuid, nullable: true } + + DynamicState: + type: object + description: "`workspace.ModelDynamicState`" + properties: + id: { type: string, format: uuid } + name: { type: string } + author_id: { type: integer, format: int64, nullable: true } + autoload: { type: boolean } + workspace_id: { type: string, format: uuid } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + states: + type: array + items: { $ref: "#/components/schemas/State" } + + App: + type: object + description: "`apps.App`" + properties: + id: { type: string, format: uuid } + company_id: { type: integer, format: int64 } + name: { type: string } + entrypoint: { type: string } + devices: + type: array + items: { type: string, enum: [mobile, tablet, desktop] } + module_name: { type: string } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + + AppInstance: + type: object + description: "`apps.AppInstance`" + properties: + id: { type: string, format: uuid } + app_id: { type: string, format: uuid } + workspace_id: { type: string, format: uuid } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + app: + allOf: [ { $ref: "#/components/schemas/App" } ] + nullable: true