From 28478d9c8600364323c6866fa667bc4cd4eb3f9b Mon Sep 17 00:00:00 2001 From: emelinda Date: Tue, 14 Jul 2026 19:23:02 +0300 Subject: [PATCH] Add example `.env` files and configuration documentation for `ams-sync`, `auth-flow`, `bim`, `cde`, `comparisons`, `django`, `document-link`, `flows`, `iam`, `inspections` services. --- apps/ams-sync/.env.example | 85 + apps/ams-sync/CONFIGURATION.md | 218 ++ apps/ams-sync/ENDPOINTS.md | 99 + apps/auth-flow/.env.example | 15 + apps/auth-flow/CONFIGURATION.md | 162 + apps/auth-flow/ENDPOINTS.md | 80 + apps/bim/.env.example | 64 + apps/bim/CONFIGURATION.md | 189 + apps/bim/ENDPOINTS.md | 55 + apps/bim/openapi.yaml | 1158 ++++++ apps/cde/.env.example | 80 + apps/cde/CONFIGURATION.md | 215 ++ apps/cde/ENDPOINTS.md | 130 + apps/cde/openapi.yaml | 298 ++ apps/comparisons/.env.example | 54 + apps/comparisons/CONFIGURATION.md | 148 + apps/comparisons/ENDPOINTS.md | 77 + apps/comparisons/openapi.yaml | 793 ++++ apps/django/.env.example | 259 ++ apps/django/CONFIGURATION.md | 357 ++ apps/django/ENDPOINTS.md | 127 + apps/django/FRONTEND_REQUESTS.md | 417 +++ apps/django/openapi.yaml | 589 +++ apps/document-link/.env.example | 16 + apps/document-link/CONFIGURATION.md | 95 + apps/document-link/ENDPOINTS.md | 66 + apps/documentations/api-v2.CONFIGURATION.md | 229 ++ apps/documentations/api-v2.ENDPOINTS.md | 88 + apps/documentations/api-v2.env.example | 87 + apps/documentations/api-v2.openapi.yaml | 1258 +++++++ apps/documentations/api.CONFIGURATION.md | 345 ++ apps/documentations/api.ENDPOINTS.md | 114 + apps/documentations/api.env.example | 131 + apps/documentations/api.openapi.yaml | 785 ++++ .../dps-message-hub.CONFIGURATION.md | 156 + .../dps-message-hub.ENDPOINTS.md | 45 + .../dps-message-hub.env.example | 42 + apps/documentations/frontend.CONFIGURATION.md | 66 + apps/documentations/frontend.ENDPOINTS.md | 224 ++ apps/documentations/pdm.CONFIGURATION.md | 241 ++ apps/documentations/pdm.env.example | 116 + apps/documentations/pdm.openapi.yaml | 918 +++++ apps/flows/.env.example | 187 + apps/flows/CONFIGURATION.md | 293 ++ apps/flows/ENDPOINTS.md | 142 + apps/flows/openapi.yaml | 1736 +++++++++ apps/iam/.env.example | 75 + apps/iam/CONFIGURATION.md | 174 + apps/iam/ENDPOINTS.md | 207 ++ apps/iam/openapi.yaml | 1170 ++++++ apps/inspections/.env.example | 65 + apps/inspections/CONFIGURATION.md | 216 ++ apps/inspections/ENDPOINTS.md | 115 + apps/inspections/openapi.yaml | 1243 +++++++ apps/issues/.env.example | 101 + apps/issues/CONFIGURATION.md | 245 ++ apps/issues/ENDPOINTS.md | 138 + apps/issues/openapi.yaml | 3278 +++++++++++++++++ 58 files changed, 20076 insertions(+) create mode 100644 apps/ams-sync/.env.example create mode 100644 apps/ams-sync/CONFIGURATION.md create mode 100644 apps/ams-sync/ENDPOINTS.md create mode 100644 apps/auth-flow/.env.example create mode 100644 apps/auth-flow/CONFIGURATION.md create mode 100644 apps/auth-flow/ENDPOINTS.md create mode 100644 apps/bim/.env.example create mode 100644 apps/bim/CONFIGURATION.md create mode 100644 apps/bim/ENDPOINTS.md create mode 100644 apps/bim/openapi.yaml create mode 100644 apps/cde/.env.example create mode 100644 apps/cde/CONFIGURATION.md create mode 100644 apps/cde/ENDPOINTS.md create mode 100644 apps/cde/openapi.yaml create mode 100644 apps/comparisons/.env.example create mode 100644 apps/comparisons/CONFIGURATION.md create mode 100644 apps/comparisons/ENDPOINTS.md create mode 100644 apps/comparisons/openapi.yaml create mode 100644 apps/django/.env.example create mode 100644 apps/django/CONFIGURATION.md create mode 100644 apps/django/ENDPOINTS.md create mode 100644 apps/django/FRONTEND_REQUESTS.md create mode 100644 apps/django/openapi.yaml create mode 100644 apps/document-link/.env.example create mode 100644 apps/document-link/CONFIGURATION.md create mode 100644 apps/document-link/ENDPOINTS.md create mode 100644 apps/documentations/api-v2.CONFIGURATION.md create mode 100644 apps/documentations/api-v2.ENDPOINTS.md create mode 100644 apps/documentations/api-v2.env.example create mode 100644 apps/documentations/api-v2.openapi.yaml create mode 100644 apps/documentations/api.CONFIGURATION.md create mode 100644 apps/documentations/api.ENDPOINTS.md create mode 100644 apps/documentations/api.env.example create mode 100644 apps/documentations/api.openapi.yaml create mode 100644 apps/documentations/dps-message-hub.CONFIGURATION.md create mode 100644 apps/documentations/dps-message-hub.ENDPOINTS.md create mode 100644 apps/documentations/dps-message-hub.env.example create mode 100644 apps/documentations/frontend.CONFIGURATION.md create mode 100644 apps/documentations/frontend.ENDPOINTS.md create mode 100644 apps/documentations/pdm.CONFIGURATION.md create mode 100644 apps/documentations/pdm.env.example create mode 100644 apps/documentations/pdm.openapi.yaml create mode 100644 apps/flows/.env.example create mode 100644 apps/flows/CONFIGURATION.md create mode 100644 apps/flows/ENDPOINTS.md create mode 100644 apps/flows/openapi.yaml create mode 100644 apps/iam/.env.example create mode 100644 apps/iam/CONFIGURATION.md create mode 100644 apps/iam/ENDPOINTS.md create mode 100644 apps/iam/openapi.yaml create mode 100644 apps/inspections/.env.example create mode 100644 apps/inspections/CONFIGURATION.md create mode 100644 apps/inspections/ENDPOINTS.md create mode 100644 apps/inspections/openapi.yaml create mode 100644 apps/issues/.env.example create mode 100644 apps/issues/CONFIGURATION.md create mode 100644 apps/issues/ENDPOINTS.md create mode 100644 apps/issues/openapi.yaml 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/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/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/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/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