Add example .env file, configuration documentation, and endpoint documentation for resources, remarks, and reviews services.

This commit is contained in:
emelinda 2026-07-13 22:38:53 +03:00
parent d7ec0cc9aa
commit b18e31d5d6
5 changed files with 1442 additions and 0 deletions

114
apps/remarks/ENDPOINTS.md Normal file
View File

@ -0,0 +1,114 @@
# Эндпоинты, с которыми взаимодействует remarks-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `remarks-frontend`).
## Как устроено взаимодействие
В отличие от `transmittal-frontend`, в `remarks-frontend` **нет декларативного реестра эндпоинтов** (`endpoints.ts`). Запросы формируются по месту — в API-объектах (`module/api/*.ts`) и в MobX-сторах (`module/store/*.ts`) — прямыми вызовами методов `httpService`:
- `httpService.getRequest(options)`
- `httpService.postRequest(options)`
- `httpService.putRequest(options)` (объявлен, в коде не используется)
- `httpService.patchRequest(options)`
- `httpService.deleteRequest(options)`
Каждый вызов задаётся объектом-параметром:
- `service` — логическое имя сервиса (см. таблицу хостов ниже), определяет базовый хост;
- `url` — путь запроса (часто шаблонная строка с подстановкой id/query);
- `data` — тело запроса (для `POST`/`PATCH`/`PUT`);
- `axiosConfig` — доп. настройки axios (`params` для query-параметров, `responseType: "blob"` для файлов и т.п.);
- `showErrorNotification` — включает показ уведомления об ошибке средствами SDK.
`httpService` (`module/api/http-client.ts`) — это `Proxy` поверх базового `baseHttpService` (`module/api/http-service.ts`, создаётся через `createHttpService` из `@sarex-team/sdk-js`). Прокси добавляет единую обработку ответа `403`: показывает toast с текстом `response.data.detail` либо сообщением «У вас недостаточно прав для выполнения данного действия.». Базовый хост подставляется SDK по значению `service` и текущему окружению `BUILD_ENV` (значения — из `module/api/hosts.ts`, по умолчанию `prod`). В окружении `local` тип HTTP-сервиса переключается на `zitadel` (`setTypeOfHttpService("zitadel")`).
Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта.
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`.
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` |
| `sarexApi` | Gateway/API Sarex (`/issues`, `/flows`, `/eav`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `gateway` | Gateway Sarex | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
| `documentations` | Сервис документации | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` |
| `eavV4` | Сервис атрибутов/ассетов (EAV v4) | `https://stage-api.sarex.io/eav/api/v4` | `https://api.sarex.io/eav/api/v4` |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определены окружения `local` и `preprod`. В `preprod` хосты указывают на `https://api.preprod.sarex.io/*` (для `zitadel``https://login.preprod.sarex.io`). В `local` сервис `sarex` проксируется на `/sarex-backend`, а остальные сервисы указывают на `stage`-хосты (`https://stage-api.sarex.io/*`, `zitadel``https://idp.dev.stage.sarex.io`). Сервис `zitadel` явно в коде не вызывается — используется SDK для аутентификации.
## Эндпоинты по сервисам
### `sarexApi` — Gateway/API Sarex (issues / flows / eav)
Базовый хост — корневой (`https://<env>-api.sarex.io`); маршрутизация задаётся префиксами пути (`/issues/api`, `/flows/api`, `/eav/api`).
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| POST | `/issues/api/issues/filter/` | Список/фильтрация замечаний (пагинация `limit`/`offset`) | `api/issues/issues.ts` |
| POST | `/issues/api/issues/` | Создать замечание | `store/collectionRemarks.ts` |
| GET | `/issues/api/issues/{uuid}/` | Замечание по uuid | `store/collectionRemarks.ts` |
| PATCH | `/issues/api/issues/{uuid}/` | Обновить замечание | `store/collectionRemarks.ts`, `store/remarks.ts` |
| POST | `/issues/api/issues/export/?{params}` | Экспорт замечаний (ответ `blob`) | `store/remarks.ts` |
| GET | `/issues/api/issues/daterange/` | Диапазон дат замечаний | `store/filters.ts` |
| POST | `/issues/api/issues/filter-options/?{params}` | Опции фильтра замечаний | `store/filters.ts` |
| GET | `/issues/api/issue-changes/?issue_id={uuid}` | История изменений замечания | `store/remark.ts` |
| POST | `/issues/api/comments/` | Создать комментарий | `store/collectionRemarks.ts` |
| DELETE | `/issues/api/comments/{id}/` | Удалить комментарий | `store/collectionRemarks.ts` |
| GET | `/issues/api/attachments/{fileId}/` | Получить вложение | `store/collectionRemarks.ts` |
| POST | `/issues/api/attachments/` | Загрузить вложение | `store/collectionRemarks.ts` |
| DELETE | `/issues/api/attachments/{id}/` | Удалить вложение | `store/collectionRemarks.ts` |
| GET | `/issues/api/companies/{companyId}/status-model/v2/` | Модель статусов компании | `store/remarks.ts` |
| GET | `/flows/api/v1/documents/` | Документы (flows) | `store/filters.ts` |
| GET | `/flows/api/v1/documents/?{query}&offset=0&limit=100000` | Документы (flows, полная выборка) | `store/remarks.ts` |
| POST | `/flows/api/v1/flows/filter/` | Фильтрация flows | `store/filters.ts` |
| GET | `/eav/api/v0/attribute/?company_id={params}` | Атрибуты компании (EAV v0) | `store/remarks.ts` |
### `remarks` — Сервис замечаний
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| GET | `/api/v1/remarks?target=true{params}` | Список замечаний по target | `store/remarks.ts` |
| DELETE | `/api/v1/remarks/{uuid}` | Удалить замечание | `store/collectionRemarks.ts` |
### `gateway` — Gateway Sarex
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| GET | `/api/v2/users/?{query}` | Пользователи (с фильтрами прав/таргета) | `store/remarks.ts` |
| GET | `/api/v2/users/` | Пользователи (без фильтров) | `store/remarks.ts` |
| GET | `/api/v1/documents/{docId}/attributes/` | Атрибуты документа | `store/remarks.ts` |
| GET | `/api/v1/resources/` | Список ресурсов | `store/resourcesStore.ts` |
### `documentations` — Сервис документации
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| GET | `/api/v1/documents/{docId}?extend=bundles` | Документ с бандлами | `store/remark.ts` |
### `eavV4` — Сервис ассетов (EAV v4)
Базовый хост уже включает префикс `/eav/api/v4`.
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| POST | `/assets/search/` | Поиск ассетов по набору id | `api/assets.ts` |
| GET | `/assets/` | Список ассетов (фильтры, пагинация, `tag`) | `api/assets.ts` |
### `sarex` — Локальный сервис данных
| Метод | Путь | Назначение | Где вызывается |
| --- | --- | --- | --- |
| GET | `/api/client/settings/` | Клиентские настройки | `store/remarks.ts` |
| GET | `/api/core/admin/departments/?company={companyId}` | Отделы компании (пагинация по `next`) | `store/remarks.ts` |
| GET | `/api/core/admin/positions/?company={companyId}` | Должности компании (пагинация по `next`) | `store/remarks.ts` |
## Обработка ошибок
Отдельного файла-маппера ошибок (аналога `module/api/errors.ts` в `transmittal-frontend`) в проекте нет. Обработка сосредоточена в двух местах:
- `module/api/http-client.ts` — прокси перехватывает ответ `403` и показывает toast (`react-toastify`) с текстом `response.data.detail` или сообщением по умолчанию «У вас недостаточно прав для выполнения данного действия.»;
- SDK `@sarex-team/sdk-js` — при `showErrorNotification: true` показывает стандартное уведомление об ошибке; в сторах ряд операций дополнительно оборачивается в `try/catch` с собственными toast-сообщениями (напр. «Замечание успешно создано!» / «Произошла ошибка при создании замечания»).

View File

@ -0,0 +1,58 @@
# Django settings module
# wsgi.py / manage.py по умолчанию используют config.settings.production
DJANGO_SETTINGS_MODULE=config.settings.production
# Django
# Читаются только при DEBUG (в base.py) и в production.py (ALLOWED_HOSTS зашиты в коде)
# SECRET_KEY и DEBUG заданы в самих settings-модулях, через окружение не переопределяются
# Environment
# Метка окружения (используется в атрибутах трейсинга): stage / preprod / prod
ENVIRONMENT=prod
# Database (PostGIS)
# Читаются в config/settings/production.py; ENGINE фиксирован: django.contrib.gis.db.backends.postgis
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=resources
DATABASE_USER=postgres
DATABASE_PASSWORD=password
# Database (тесты)
# Читаются только в config/settings/test.py (запуск pytest)
DJANGO_POSTGRES_DB=resources
DJANGO_POSTGRES_USER=postgres
DJANGO_POSTGRES_PASSWORD=12345678dev
DJANGO_POSTGRES_HOST=test_postgres
DJANGO_POSTGRES_PORT=5432
# S3 (django-storages / boto3, Yandex Object Storage)
# Маппятся на AWS_* в settings; DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage
YC_S3_ACCESS_KEY_ID=
YC_S3_SECRET_ACCESS_KEY=
YC_S3_BUCKET_NAME=resources
YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net
# Sarex backend (интеграция с ядром Sarex)
# SAREX_HOST -> SAREX_BASE_HOST; используется для получения токена и списка пользователей
SAREX_HOST=https://lk.sarex.io
SAREX_ADMIN_USERNAME=
SAREX_ADMIN_PASSWORD=
# Определяется в production.py, но кодом приложения напрямую не читается
SERVICE_ACCOUNTS_HOST=https://lk.sarex.io/api/core
# OpenTelemetry (django-otel-tools)
# ВНИМАНИЕ: трейсинг включается по любой непустой строке (os.getenv('USE_OTEL', False)),
# поэтому USE_OTEL=False строкой ТОЖЕ включит его. Для выключения переменную не задавать.
USE_OTEL=True
SERVICE_NAME=resources-backend.sarex-resources
TRACER_ENDPOINT=localhost:4375
# То же поведение "любая строка = True", что и у USE_OTEL
USE_INSECURE=True
MODULE=resources
TEAM=platform_team
COMPONENT=backend
# Uwsgi / инфраструктура
# Прокидывается в Helm/k8s, но кодом приложения не читается (порт задан в uwsgi.ini)
API_ADDRESS=8000

View File

@ -0,0 +1,203 @@
# Конфигурация проекта sarex-resources
Документ описывает все переменные окружения и способы конфигурирования сервиса ресурсов (`sarex-resources`, backend модуля «Ресурсы/Проекты»).
## Способы конфигурирования
Сервис — приложение на **Django 4.1 + Django REST Framework** (GeoDjango/PostGIS). В отличие от сервисов на `pydantic-settings`, конфигурация задаётся **классическими Django settings-модулями** в `server/config/settings/`, а не единым классом настроек:
- `base.py` — общие настройки (приложения, middleware, DRF, S3, интеграция с Sarex, OpenTelemetry);
- `production.py` — наследует `base.py` (`from .base import *`), задаёт `DEBUG=False`, `ALLOWED_HOSTS`, БД, CORS, S3, статику;
- `test.py` — наследует `base.py`, отдельная тестовая БД, снятие permission-классов DRF.
Активный модуль выбирается переменной **`DJANGO_SETTINGS_MODULE`**. По умолчанию (`server/config/wsgi.py`, `server/manage.py`) — `config.settings.production`.
Особенности разбора:
- **префикса у переменных нет** — имена плоские (`DATABASE_HOST`, `YC_S3_BUCKET_NAME` и т.п.);
- значения читаются напрямую через `os.getenv(...)`; вложенных секций/делимитеров (как `__` в pydantic-сервисах) нет;
- **типизация и валидация окружения отсутствуют** — всё приходит строками; булевы флаги (`USE_OTEL`, `USE_INSECURE`) проверяются на «truthy», поэтому **любая непустая строка, включая `"False"`, считается истиной** (см. «Замечания»);
- `.env`-файл приложением **не загружается автоматически** — переменные должны быть в окружении процесса (docker-compose `environment`, k8s `env`/`secretKeyRef`, либо `set -a && . ./.env && set +a`).
> **Важно для прод-развёртывания.** В кластере файл `server/config/settings/production.py` **подменяется** содержимым ConfigMap `django-configmap` (монтируется на `/server/config/settings/production.py`, см. `infra/iac/apps/resources/base/django-configmap.yaml` и `.helm/templates/api.yaml`). Именно версия из ConfigMap определяет фактические `ALLOWED_HOSTS`, `CORS_*`, `SERVICE_ACCOUNTS_HOST`, cookie-имена и часть значений Sarex. Репозиторный `production.py` — это шаблон/локальный вариант.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (docker-compose) | `docker-compose.yml` — сервисы `postgres`/`api`; переменные Postgres задаются в `environment`, приложение читает окружение контейнера |
| Локально (тесты) | `docker-compose.test.yml` + `pytest.ini` (`DJANGO_SETTINGS_MODULE=config.settings.test`), переменные `DJANGO_POSTGRES_*` |
| Kubernetes (Helm-чарт репозитория) | `.helm/values-<env>.yaml`: блоки `envs` (обычные значения) и `secrets` (из k8s-секретов); шаблон `.helm/templates/api.yaml` |
| Kubernetes (infra, GitOps) | `infra/iac/apps/resources/*` — kustomize `base` (Vault-инъекция env в аннотациях Deployment + ConfigMaps) и оверлеи `brusnika-stage`/`brusnika-prod` (`helmrelease.yaml`, universal-chart) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: подключает общие шаблоны `generic/common-ci`, переключает окружение по ветке/тегу через `workflow.rules` |
Способы запуска процессов:
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `uwsgi --ini /opt/server/uwsgi.ini` | `config.wsgi:application` | HTTP API (uWSGI, порт 8000) |
| `python manage.py migrate` | `manage.py` | Применение миграций (в `entrypoint.sh` перед стартом uWSGI) |
| `python manage.py <command>` | `manage.py` | Служебные Django-команды (`createsuperuser`, `shell`, миграции и т.п.) |
| `pytest` | `config.settings.test` | Юнит-тесты (`compose/test_server/entrypoint.sh`) |
Порядок запуска в контейнере (`compose/server/entrypoint.sh`): сначала `python manage.py migrate`, затем `opentelemetry-instrument uwsgi --plugin python3 --ini /opt/server/uwsgi.ini` с `DJANGO_SETTINGS_MODULE=config.settings.production`.
## Переменные приложения
Все переменные плоские (без префикса). Дефолт `—` означает, что значение при обращении вернёт `None`/пусто (Django/psycopg2 или интеграции могут упасть уже в рантайме, а не на старте).
### Выбор настроек
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_SETTINGS_MODULE` | string | `config.settings.production` | Модуль настроек Django. Значения: `config.settings.production` / `config.settings.test` |
### Окружение и трейсинг (`base.py`)
Блок OpenTelemetry активируется целиком по `USE_OTEL` (`django-otel-tools`): добавляется `OtelMiddleware`, настраивается трейсер и OTEL-логгер.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_OTEL` | bool-ish | `False` (выкл.) | Включение трейсинга. Truthy-проверка: любая непустая строка включает |
| `SERVICE_NAME` | string | `resources-backend.sarex-resources` | Имя сервиса в трейсах |
| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора |
| `USE_INSECURE` | bool-ish | `False` | Небезопасное (без TLS) подключение к коллектору; та же truthy-проверка |
| `ENVIRONMENT` | string | `prod` | Метка окружения в атрибутах трейсинга (`stage`/`preprod`/`prod`) |
| `MODULE` | string | `resources` | Атрибут `module` в трейсинге |
| `TEAM` | string | `platform_team` | Атрибут `team` в трейсинге |
| `COMPONENT` | string | `backend` | Атрибут `component` в трейсинге |
### База данных (`production.py`, `django.contrib.gis.db.backends.postgis`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DATABASE_HOST` | string | — | Хост PostgreSQL/PostGIS |
| `DATABASE_PORT` | int (string) | — | Порт PostgreSQL |
| `DATABASE_NAME` | string | — | Имя базы данных |
| `DATABASE_USER` | string | — | Пользователь БД |
| `DATABASE_PASSWORD` | string | — | Пароль пользователя БД |
> БД требует расширения PostGIS (образ `postgis/postgis`), т.к. используются гео-поля (`Location.geometry`/`geography`) и `django.contrib.gis`. TLS к БД настраивается на уровне libpq: в Helm-чарте секрет `yc-pg-certificate` монтируется как `/root/.postgresql/root.crt` (в settings отдельного `sslmode` нет).
### База данных для тестов (`test.py`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_POSTGRES_DB` | string | `resources` | Имя тестовой БД |
| `DJANGO_POSTGRES_USER` | string | `postgres` | Пользователь тестовой БД |
| `DJANGO_POSTGRES_PASSWORD` | string | `12345678dev` | Пароль тестовой БД |
| `DJANGO_POSTGRES_HOST` | string | `test_postgres` | Хост тестовой БД |
| `DJANGO_POSTGRES_PORT` | int (string) | `5432` | Порт тестовой БД |
Дополнительно `test.py` выставляет `DJANGO_ALLOW_ASYNC_UNSAFE=true` в коде.
### S3 / объектное хранилище (`base.py` и `production.py`, `django-storages` + `boto3`)
Файлы (`ResourcePhoto.image` и т.п.) хранятся в S3-совместимом хранилище; `DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL="public-read"`.
| Переменная | Тип | Значение по умолчанию | Назначение (Django-настройка) |
| --- | --- | --- | --- |
| `YC_S3_ACCESS_KEY_ID` | string | — | `AWS_ACCESS_KEY_ID` |
| `YC_S3_SECRET_ACCESS_KEY` | string | — | `AWS_SECRET_ACCESS_KEY` |
| `YC_S3_BUCKET_NAME` | string | — | `AWS_STORAGE_BUCKET_NAME` |
| `YC_S3_ENDPOINT_URL` | string | — | `AWS_S3_ENDPOINT_URL` |
### Интеграция с ядром Sarex (`base.py`)
Используется для получения токена (`/api/token/`) и списка пользователей (`/api/core/users/`) в эндпоинтах группировки пользователей/ресурсов (`users-grouped-by-resource`, `users-with-resources`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SAREX_HOST` | string | `https://lk.sarex.io` | Базовый хост Sarex (`SAREX_BASE_HOST`) |
| `SAREX_ADMIN_USERNAME` | string | — | Логин сервисной учётки Sarex |
| `SAREX_ADMIN_PASSWORD` | string | — | Пароль сервисной учётки Sarex |
| `SERVICE_ACCOUNTS_HOST` | string | `https://lk.sarex.io/api/core` | Определяется в `production.py`; кодом приложения напрямую не читается (см. «Замечания») |
## Переменные инфраструктуры, сборки и вспомогательных утилит
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `API_ADDRESS` | `.helm/values-*.yaml`, `infra/.../backend-deployment.yaml` | Задаётся в окружении (`8000`), кодом не читается — порт фактически берётся из `uwsgi.ini` (`http = 0.0.0.0:8000`) |
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `docker-compose.yml`, `docker-compose.test.yml` | Инициализация контейнера Postgres (не путать с `DATABASE_*`, которые читает Django) |
| `PYTHONUNBUFFERED` | `Dockerfile` | Небуферизованный stdout/stderr |
| build-настройки uWSGI | `compose/server/uwsgi.ini` | `processes=8`, `http=0.0.0.0:8000`, `harakiri`, `buffer-size`, `static-map` для `/static` и `/media` |
Приватный индекс пакетов для сборки (`django-otel-tools`) задан прямо в `requirements/base.txt` через `--extra-index-url` (nexus.infra.sarex.io).
## Переменные из Helm-чарта репозитория (`.helm/values-<env>.yaml`)
Обычные значения — блок `envs`, секреты — блок `secrets` (монтируются как env через `secretKeyRef`). Значения различаются по окружениям (`stage`/`preprod`/`production`): адрес БД (`DATABASE_HOST`), `SERVICE_NAME`, `TRACER_ENDPOINT`, `ENVIRONMENT`, число реплик.
Значения из секретов:
| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) |
| --- | --- | --- |
| `DATABASE_USER` | `ya-pg-secret` | `user` |
| `DATABASE_PASSWORD` | `ya-pg-secret` | `password` |
| `YC_S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` |
| `YC_S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` |
| `YC_S3_BUCKET_NAME` | `yc-s3-secret` | `storage_bucket_name` |
| `YC_S3_ENDPOINT_URL` | `yc-s3-secret` | `endpoint_url` |
| `SAREX_ADMIN_USERNAME` | `sarex-auth-secret` | `username` |
| `SAREX_ADMIN_PASSWORD` | `sarex-auth-secret` | `password` |
Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `yc-pg-certificate` (ключ `certificate`) как файл `/root/.postgresql/root.crt`, а также ConfigMap `uwsgi-configmap` (файл `uwsgi.ini`). Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы), `service_*`, `api_host` (istio `VirtualService`, prefix `/resource-management`).
## Переменные из infra (GitOps, kustomize `base`)
В `infra/iac/apps/resources/base/backend-deployment.yaml` секреты БД и S3 подаются через **HashiCorp Vault Agent** (аннотации `vault.hashicorp.com/*`), который рендерит файлы `/vault/secrets/resources-db` и `/vault/secrets/resources-s3`; они подгружаются в окружение в `args` контейнера (`set -a && . /vault/secrets/... && set +a`) перед запуском `entrypoint.sh`.
| Переменная | Источник (Vault path / значение) |
| --- | --- |
| `DATABASE_HOST` | `postgresql.resources.svc.cluster.local` (шаблон Vault) |
| `DATABASE_PORT` | `5432` |
| `DATABASE_NAME` | `resources_db` |
| `DATABASE_USER` | `secrets/data/postgresql/apps/resources``username` |
| `DATABASE_PASSWORD` | `secrets/data/postgresql/apps/resources``password` |
| `YC_S3_ENDPOINT_URL` | `secrets/data/minio/apps/resources``client.endpoint` |
| `YC_S3_BUCKET_NAME` | `resources` |
| `YC_S3_ACCESS_KEY_ID` | `secrets/data/minio/apps/resources``access_key` |
| `YC_S3_SECRET_ACCESS_KEY` | `secrets/data/minio/apps/resources``secret_key` |
| `DJANGO_SETTINGS_MODULE` | `config.settings.production` (env Deployment) |
| `API_ADDRESS` | `8000` (env Deployment) |
Оверлеи `brusnika-stage`/`brusnika-prod` используют `HelmRelease` (universal-chart): env `DJANGO_SETTINGS_MODULE`, `DATABASE_HOST/PORT/NAME`, `API_ADDRESS`, `YC_S3_ENDPOINT_URL`, `YC_S3_BUCKET_NAME`; секреты `DATABASE_USER/PASSWORD` (`postgres-secret`), `YC_S3_ACCESS_KEY_ID/SECRET_ACCESS_KEY` (`yc-s3-secret`, ключи `key-id`/`access-key`).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу:
| Условие | STAND | Namespace | RELEASE_NAME |
| --- | --- | --- | --- |
| ветка `master` | `preprod` | `resources-preprod` | `resources` |
| ветка `stage` | `stage` | `resources-stage` | `sarex-resources` |
| тег (`CI_COMMIT_TAG`) | `prod` | `resources-prod` | `sarex-resources` |
Ключевые переменные пайплайна: `CHART_NAME=sarex-resources`, `CHART_VERSION`, `DOCKERFILE_PATH=./compose/server/Dockerfile`, `IMAGE_PATH=deployment.image`, `HELM_SET_ARGS` (`--set deployment.image=…`), флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY`. Job `linter` запускает `flake8` (образ `python:3.8-buster`).
## Замечания и потенциальные проблемы
- **Truthy-флаги.** `USE_OTEL` и `USE_INSECURE` читаются как `os.getenv(name, False)` без приведения типа. Любая непустая строка — истина, включая `"False"`, `"0"`, `"no"`. Чтобы **выключить** трейсинг, переменную `USE_OTEL` нужно **не задавать вовсе** (а не ставить в `False`). В Helm-values она задана строкой `"True"`.
- **`.env` не загружается автоматически.** В settings нет `python-dotenv`/`env_file`; переменные должны попадать в окружение процесса (compose `environment`, k8s `env`/Vault, ручной `export`).
- **`production.py` подменяется в кластере.** Репозиторный `production.py` содержит хардкод `ALLOWED_HOSTS`, `CORS_*`, тестовый `SECRET_KEY` и др.; в проде используется версия из ConfigMap `django-configmap`. Различия: `ALLOWED_HOSTS=['*']`, другой `SERVICE_ACCOUNTS_HOST`, хардкод `SAREX_ADMIN_*`/`SAREX_BASE_HOST`, cookie-имена `resource-sessionid`/`resource-csrftoken`.
- **`SECRET_KEY` захардкожен** в `base.py`/`production.py` (в т.ч. пометка `# Delete after Test`) — секрет не берётся из окружения. Для реального прода его следует вынести в секрет.
- **`SERVICE_ACCOUNTS_HOST`** определяется, но напрямую в коде не используется (интеграции ходят по `SAREX_BASE_HOST`); переменная задаётся «на вырост».
- **`DATABASE_*` без дефолтов.** При отсутствии переменных Django получит `None` и подключение к БД упадёт в рантайме (не на импортстарте). Пять переменных БД обязательны для рабочего запуска.
- **Аутентификация DRF отключена.** `DEFAULT_AUTHENTICATION_CLASSES=[]`, permission-классы не заданы (эффективно `AllowAny`) — доступ ограничивается только сетевым слоем/istio. Учитывать при публикации.
- **Имя релиза различается между окружениями** (`resources` на preprod vs `sarex-resources` на stage/prod) — при работе с Helm/namespace это легко перепутать.
## Минимальный набор для локального запуска
Postgres (PostGIS) поднимается через `docker-compose up postgres`, приложение — через сервис `api` (`docker-compose.yml`) или локально `uwsgi`/`manage.py runserver`. Минимально необходимо задать:
- `DJANGO_SETTINGS_MODULE=config.settings.production` (или свой dev-модуль)
- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`
- `YC_S3_*` — если задействуется загрузка файлов/статики в S3 (иначе операции с файлами упадут)
- `SAREX_HOST`, `SAREX_ADMIN_USERNAME`, `SAREX_ADMIN_PASSWORD` — если нужны эндпоинты интеграции с пользователями Sarex
- OTEL — по умолчанию не задавать (`USE_OTEL` отсутствует); при включении — `USE_OTEL=1`, `SERVICE_NAME`, `TRACER_ENDPOINT`, при необходимости `USE_INSECURE`
Для тестов достаточно поднять `docker-compose.test.yml` — переменные `DJANGO_POSTGRES_*` имеют рабочие дефолты, `pytest.ini` уже указывает `config.settings.test`.
Готовые значения-примеры для всех переменных приведены в `.env.example`.

841
apps/resources/openapi.yaml Normal file
View File

@ -0,0 +1,841 @@
openapi: 3.0.3
info:
title: Sarex Resources API
version: "1.0.0"
description: |
REST API сервиса **sarex-resources** (`platform/sarex-resources`) — управление
ресурсами/проектами, их типами (иерархия), локациями, а также разрешениями
сервисных аккаунтов на ресурсы и на компании.
Сервис написан на Python (**Django 4.1 + Django REST Framework**, GeoDjango/PostGIS).
Роутинг задаётся в `server/config/urls.py`:
- `resource-management/` — Django-admin;
- `api/v1/` — основной API (`apps.resource.urls`);
- `api/v2/` — расширенный API ресурсов (`apps.resource.urls_v2`).
Списочные и CRUD-эндпоинты строятся `rest_framework.routers.DefaultRouter`
поверh `ModelViewSet`; часть операций — отдельные `APIView`.
### Аутентификация
На уровне приложения аутентификация и permission-классы DRF **отключены**
(`DEFAULT_AUTHENTICATION_CLASSES = []`, permission-классы не заданы —
эффективно `AllowAny`). Ограничение доступа обеспечивается сетевым слоем
(istio/ingress, внутрикластерный доступ к `sarex-resources-service`).
### Пагинация
Используется `rest_framework.pagination.LimitOffsetPagination` с очень большим
`PAGE_SIZE` (100000). Списочные ответы оборачиваются в
`{ count, next, previous, results }`; постранично управляется параметрами
`limit` и `offset`.
### Идентификаторы ресурсов
У ресурса есть внутренний `id` (int, в API почти не используется), публичный
`public_id`/`id` (UUID — основной идентификатор в URL detail-эндпоинтов v1/v2)
и устаревший `target_id` (`_target_id`, int) для обратной совместимости.
Удаление — «мягкое» (`deleted=True`), объекты с `deleted=True` из выборок
исключаются.
### Замечания (расхождения кода)
- Detail-роуты `resource` в v1/v2 ищут объект по `public_id` (UUID), а не по
первичному ключу; путь при этом выглядит как `/api/v1/resource/{public_id}/`.
- Ряд аналитических эндпоинтов (`users-grouped-by-resource`,
`users-with-resources`, `resources-grouped-by-sa`) возвращают
«сырые» структуры (`dict`/списки), не обёрнутые в пагинацию.
- `bulk_delete/resource_permission/` и `permissions-bulk/` выполняют
**жёсткое** удаление разрешений (`.delete()`), в отличие от «мягкого»
удаления сущностей.
- `RetrieveResourceByTargetIdAPIView` при отсутствии ресурса возвращает
`404` без тела.
contact:
name: sarex-resources
url: https://gitlab/platform/sarex-resources
servers:
- url: https://api.sarex.io/resource-management
description: Production admin (istio VirtualService, prefix /resource-management)
- url: http://sarex-resources-service.resources-prod
description: Внутрикластерный адрес (ClusterIP), production namespace
- url: http://sarex-resources-service.resources-stage
description: Внутрикластерный адрес (ClusterIP), stage namespace
- url: http://localhost:8888
description: Локальный запуск (docker-compose, 8888 → 8000)
tags:
- name: resources
description: Ресурсы (проекты)
- name: resources_v2
description: Расширенные ресурсы (api/v2)
- name: resource_types
description: Типы ресурсов (иерархия)
- name: locations
description: Локации (гео-данные)
- name: resource_permissions
description: Разрешения сервисных аккаунтов на ресурсы
- name: company_permissions
description: Разрешения сервисных аккаунтов на компании
- name: analytics
description: Служебные/аналитические эндпоинты (группировки, интеграция с Sarex)
paths:
/api/v1/resource/:
get:
tags: [resources]
summary: Список ресурсов
parameters:
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компании; поддерживает список через запятую" }
- { name: type, in: query, schema: { type: integer } }
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву (ltree)" }
- { name: show_in_overview, in: query, schema: { type: boolean } }
- { name: id, in: query, schema: { type: string }, description: "public_id (UUID); список через запятую" }
- { name: target_id, in: query, schema: { type: string }, description: "устаревший target_id; список через запятую" }
- { name: service_accounts, in: query, schema: { type: string }, description: "UUID сервисных аккаунтов через запятую (доступные ресурсы)" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedResourceList"
post:
tags: [resources]
summary: Создать ресурс
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceWrite" }
/api/v1/resource/{public_id}/:
parameters:
- { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
get:
tags: [resources]
summary: Получить ресурс по public_id
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceRetrieve" }
"404": { description: Не найден }
delete:
tags: [resources]
summary: Мягкое удаление ресурса (deleted=true)
responses:
"204": { description: No Content }
/api/v1/resource_type/:
get:
tags: [resource_types]
summary: Список типов ресурсов
parameters:
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourceTypeList" }
post:
tags: [resource_types]
summary: Создать тип ресурса
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeWrite" }
/api/v1/resource_type/filters/:
get:
tags: [resource_types]
summary: "Значения фильтра типов ресурсов (потомки родителя)"
parameters:
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; вернёт его потомков" }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
parent:
type: array
items:
type: object
properties:
id: { type: integer }
name: { type: string }
/api/v1/resource_type/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [resource_types]
summary: Получить тип ресурса
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeRetrieve" }
delete:
tags: [resource_types]
summary: Мягкое удаление типа ресурса
responses:
"204": { description: No Content }
/api/v1/location/:
get:
tags: [locations]
summary: Список локаций
parameters:
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedLocationList" }
post:
tags: [locations]
summary: Создать локацию
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/LocationWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/LocationWrite" }
/api/v1/location/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [locations]
summary: Получить локацию
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/LocationRetrieve" }
delete:
tags: [locations]
summary: Мягкое удаление локации
responses:
"204": { description: No Content }
/api/v1/resource_permission/:
get:
tags: [resource_permissions]
summary: Список разрешений на ресурсы
parameters:
- { name: public_resource_id, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: resource_id, in: query, schema: { type: string }, description: "внутренние id через запятую" }
- { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую (по resource.tenant_id)" }
- { name: company_id, in: query, schema: { type: string }, description: "синоним tenant_id" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourcePermissionList" }
post:
tags: [resource_permissions]
summary: "Создать разрешение(я) на ресурс"
description: "Принимает один объект или массив (bulk). Поле resource определяется по public_resource_id."
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/ResourcePermissionWrite"
- type: array
items: { $ref: "#/components/schemas/ResourcePermissionWrite" }
responses:
"201":
description: Created
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/ResourcePermissionWrite"
- type: array
items: { $ref: "#/components/schemas/ResourcePermissionWrite" }
/api/v1/resource_permission/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [resource_permissions]
summary: Получить разрешение на ресурс
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }
delete:
tags: [resource_permissions]
summary: Мягкое удаление разрешения
responses:
"204": { description: No Content }
/api/v1/company_resource_permission/:
get:
tags: [company_permissions]
summary: Список разрешений на компании
parameters:
- { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedCompanyPermissionList" }
post:
tags: [company_permissions]
summary: "Создать разрешение(я) на компанию"
description: "Принимает один объект или массив (bulk_create)."
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/CompanyPermission"
- type: array
items: { $ref: "#/components/schemas/CompanyPermission" }
responses:
"201":
description: Created
/api/v1/company_resource_permission/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [company_permissions]
summary: Получить разрешение на компанию
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/CompanyPermission" }
delete:
tags: [company_permissions]
summary: Мягкое удаление разрешения на компанию
responses:
"204": { description: No Content }
/api/v1/targets/{target_id}/resource/:
get:
tags: [resources]
summary: Получить ресурс по устаревшему target_id
parameters:
- { name: target_id, in: path, required: true, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceRetrieve" }
"404": { description: Не найден }
/api/v1/bulk_delete/resource_permission/:
post:
tags: [resource_permissions]
summary: Массовое (жёсткое) удаление разрешений на ресурсы
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: array
items: { type: integer }
required: [id]
responses:
"200": { description: OK }
"400": { description: "id не передан или содержит не-int" }
/api/v1/service-accounts/:
get:
tags: [analytics]
summary: Сервисные аккаунты, имеющие доступ к ресурсу
description: "Нужно указать ровно один из resource_id (UUID) или target_id (int)."
parameters:
- { name: resource_id, in: query, schema: { type: string, format: uuid } }
- { name: target_id, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
count: { type: integer }
results:
type: array
items: { type: string, format: uuid }
"400": { description: "не указан либо указаны оба параметра / ошибка парсинга" }
"404": { description: Ресурс не найден }
/api/v1/users-grouped-by-resource/:
get:
tags: [analytics]
summary: "Пользователи, сгруппированные по ресурсам"
description: "Обращается к ядру Sarex за списком пользователей. Возвращает map resource_id → [user_id]."
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items: { type: integer }
/api/v1/resources-grouped-by-sa/:
get:
tags: [analytics]
summary: "Ресурсы, сгруппированные по сервисным аккаунтам"
description: "Учитывает прямые (read) и компанейские разрешения. Возвращает map service_account → [public_resource_id]."
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items: { type: string, format: uuid }
/api/v1/users-with-resources/:
post:
tags: [analytics]
summary: "Доступные ресурсы для списка пользователей в рамках компании"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: array
items: { type: integer }
tenant_id: { type: integer }
required: [id, tenant_id]
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
type: object
properties:
id: { type: integer }
unrestricted_access: { type: boolean }
resources:
type: array
items:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
"400": { description: "не передан id или tenant_id / некорректный tenant_id" }
/api/v1/permissions-bulk/:
patch:
tags: [resource_permissions]
summary: "Синхронизация (bulk) разрешений на ресурсы и компании"
description: |
Приводит разрешения к переданному состоянию: создаёт недостающие,
удаляет лишние (жёстко). `permissions` — map service_account → [resource_id],
`unrestricted_permissions` — map service_account → bool (доступ ко всей компании).
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tenant_id: { type: integer }
permissions:
type: object
additionalProperties:
type: array
items: { type: string, format: uuid }
unrestricted_permissions:
type: object
additionalProperties: { type: boolean }
required: [tenant_id, permissions]
responses:
"200": { description: OK }
/api/v2/resource/:
get:
tags: [resources_v2]
summary: Список расширенных ресурсов
parameters:
- { name: tenant_id, in: query, schema: { type: string }, description: "список через запятую" }
- { name: type, in: query, schema: { type: integer } }
- { name: parent, in: query, schema: { type: integer } }
- { name: show_in_overview, in: query, schema: { type: boolean } }
- { name: id, in: query, schema: { type: string }, description: "public_id (UUID) через запятую" }
- { name: target_id, in: query, schema: { type: integer } }
- { name: service_accounts, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourceExpandedList" }
post:
tags: [resources_v2]
summary: Создать расширенный ресурс
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }
/api/v2/resource/{public_id}/:
parameters:
- { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: tenant_id, in: query, schema: { type: string }, description: "фильтр по компаниям (через запятую) при retrieve" }
get:
tags: [resources_v2]
summary: Получить расширенный ресурс по public_id
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedRetrieve" }
"404": { description: "there is no resource with provided id" }
patch:
tags: [resources_v2]
summary: Частичное обновление расширенного ресурса
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
delete:
tags: [resources_v2]
summary: Мягкое удаление расширенного ресурса
responses:
"204": { description: No Content }
components:
schemas:
PermissionType:
type: integer
description: "0=read, 1=write, 2=delete, 3=admin"
enum: [0, 1, 2, 3]
ResourceRetrieve:
type: object
properties:
id: { type: string, format: uuid, description: "public_id" }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
ResourceWrite:
type: object
properties:
id: { type: string, format: uuid, nullable: true, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name, tenant_id, target_id]
PaginatedResourceList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceRetrieve" }
ResourceTypeRetrieve:
type: object
properties:
id: { type: integer }
name: { type: string }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
ResourceTypeWrite:
type: object
properties:
id: { type: integer, readOnly: true }
name: { type: string }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name]
PaginatedResourceTypeList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceTypeRetrieve" }
LocationRetrieve:
type: object
properties:
id: { type: integer }
name: { type: string }
coordinate_system: { type: integer }
geometry: { type: object, description: "GeoJSON-геометрия" }
geography: { type: object, description: "GeoJSON-геометрия (geography)" }
latitude: { type: string, format: decimal }
longitude: { type: string, format: decimal }
created_by: { type: integer }
LocationWrite:
allOf:
- $ref: "#/components/schemas/LocationRetrieve"
- type: object
properties:
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name, coordinate_system, geometry, geography, latitude, longitude]
PaginatedLocationList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/LocationRetrieve" }
ResourcePermissionRetrieve:
type: object
properties:
id: { type: integer }
service_account: { type: string, format: uuid }
public_resource_id: { type: string, format: uuid }
type: { $ref: "#/components/schemas/PermissionType" }
created_by: { type: integer }
ResourcePermissionWrite:
type: object
properties:
id: { type: integer, readOnly: true }
service_account: { type: string, format: uuid }
public_resource_id: { type: string, format: uuid }
type: { $ref: "#/components/schemas/PermissionType" }
created_by: { type: integer }
resource: { type: integer, description: "заполняется сервером по public_resource_id" }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [service_account, public_resource_id]
PaginatedResourcePermissionList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }
CompanyPermission:
type: object
properties:
id: { type: integer, readOnly: true }
service_account: { type: string, format: uuid }
tenant_id: { type: integer }
created_by: { type: integer }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [service_account, tenant_id]
PaginatedCompanyPermissionList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/CompanyPermission" }
ResourcePhoto:
type: object
properties:
id: { type: integer }
author_id: { type: integer }
created_at: { type: string, format: date-time }
image: { type: string, format: uri }
ResourceExpandedRead:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
photos:
type: array
items: { $ref: "#/components/schemas/ResourcePhoto" }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
work_schedule_project_id: { type: integer, nullable: true }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }
ResourceExpandedRetrieve:
allOf:
- $ref: "#/components/schemas/ResourceExpandedRead"
PaginatedResourceExpandedList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceExpandedRead" }
ResourceExpandedWrite:
type: object
properties:
id: { type: string, format: uuid, nullable: true, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
work_schedule_project_id: { type: integer, nullable: true }
code: { type: string }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }
required: [name, tenant_id, target_id]
ResourceExpandedPartialUpdate:
type: object
properties:
id: { type: string, format: uuid, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer, readOnly: true }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer, readOnly: true }
target_id: { type: integer, readOnly: true }
code: { type: string }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }

226
apps/reviews/ENDPOINTS.md Normal file
View File

@ -0,0 +1,226 @@
# Эндпоинты, с которыми взаимодействует reviews-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `reviews-frontend` — страница обзора/согласования с проектами).
## Как устроено взаимодействие
В отличие от декларативного реестра `endpoints.ts`, запросы в `reviews-frontend` описаны императивно: в виде методов API-объектов и отдельных функций в каталоге `module/api/` (а также в нескольких сторах/страницах). Каждый вызов идёт через единый `httpService` (`module/api/http-service.ts`), созданный `createHttpService(...)` из `@sarex-team/sdk-js` (поверх `axios`).
Вызов задаётся объектом со следующими полями:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- метод определяется функцией `httpService` (`getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`);
- `url` — путь запроса **относительно базового хоста сервиса** (базовый хост уже включает версионный префикс, напр. `/api/v1`);
- `data` — тело запроса (для POST/PUT/PATCH);
- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer`, `responseType: "blob"`, `timeout` и т.п.);
- `controller``AbortController` для отмены запроса;
- `showErrorNotification` — показывать ли уведомление об ошибке (обрабатывается на стороне SDK).
Базовый хост подставляется SDK по имени `service` в зависимости от `BUILD_ENV` (см. `module/api/hosts.ts`). Итоговый URL = `<базовый хост сервиса>` + `url`.
Основные точки, где выполняются запросы:
| Файл | Экспорт | Назначение |
| --- | --- | --- |
| `module/api/index.ts` | `ReviewAPI`, `DocumentAPI`, `IssuesAPI` + отдельные функции (`getUsersByResourceId`, `getUsersByCompanyId`, `getDepartments*`, `getPositions*`, `getMrpaList`, `fetchParentDocumentByResourceId`, `fetchDocumentsBundleVersions`, `getDocumentAncestors`, `getDiskDocumentsPath`, `fetchExportReviews*`) | Ядро API: reviews, задачи, документы, справочники, экспорт |
| `module/api/agents.ts` | `AgentsAPI` | AI-агент подбора путей копирования (router-agent) |
| `module/api/checklists.ts` | `ChecklistsAPI` | Чек-листы и их результаты |
| `module/api/documentations.ts` | `DocumentationsAPI` | Дети папок с активными процессами |
| `module/api/marks.ts` | `MarksAPI` | Оркестрация штампов/подписей |
| `module/api/tranmittals.ts` | `TransmittalsAPI` | Создание трансмитталов и работа с шаблонами |
| `module/pages/Review/CheckList/AiCheck/api.ts` | `AiCheckAPI` | AI-проверка документов по чек-листу |
| `module/store/stores/resources.ts` | `ResourcesStore.fetchResources` | Список ресурсов компании |
| `module/store/stores/users.ts` | `Users.fetchSettings` | Клиентские настройки пользователя |
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Базовый хост уже включает версионный префикс сервиса, поэтому в таблицах эндпоинтов ниже указан только `url` (без него).
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `flows` | Сервис рабочих процессов (reviews, задачи, документы review) | `https://stage-api.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` |
| `documentations` | Сервис документации (бандлы, файлы, штампы, подписи) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` |
| `gateway_api_v1` | Gateway API v1 (ресурсы, документы, версии бандлов) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` |
| `gateway_api_v2` | Gateway API v2 (пользователи по ресурсу) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` |
| `eav_api_v0` | Сервис атрибутов (EAV) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` |
| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` |
| `transmittals` | Сервис передачи документации (трансмитталы, шаблоны) | `https://stage-api.sarex.io/transmittals/api/v1` | `https://api.sarex.io/transmittals/api/v1` |
| `issues` | Сервис замечаний | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` |
| `orchestrator` | Оркестратор процессов (штампы/подписи) | `https://stage-api.sarex.io/orchestrator` | `https://api.sarex.io/orchestrator/api` |
| `files` | Сервис файлов (скачивание бандлов) | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` |
| `lambdas` | Сервис экспорта (lambda-функции) | `https://stage-api.sarex.io/lambdas` | `https://api.sarex.io/lambdas` |
| `sarexAgents` | Сервис AI-агентов (проверка, подбор путей, загрузка файлов) | `https://sarex-agents.dev.stage.sarex.io/api/v1` | `https://agents.sarex.tech/api/v1` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определены окружения `local`, `preprod` и `contour`. В `contour` все хосты — относительные пути (изолированный контур), а `zitadel` пуст. В `local` сервис `sarex` указывает на `https://stage.sarex.io`, а `httpService` переключается в режим `zitadel` (`setTypeOfHttpService("zitadel")` в `http-service.ts`). Подключаемый удалённый модуль `documentations` (Module Federation) описан отдельно в `module/api/module-hosts.ts`.
## Эндпоинты по сервисам
### `flows` — Сервис рабочих процессов (reviews)
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `ReviewAPI.getReviews` | POST | `/reviews/filter/` | Список reviews с фильтрами (пагинация `limit`/`offset` в query) |
| `ReviewAPI.getReview` | GET | `/reviews/{id}/` | Review по id |
| `ReviewAPI.getReviewsByDocumentIds` | GET | `/documents/?document_ids={ids}&full=true&review_status=completed,canceled&limit=100000` | Документы review по id документов |
| `ReviewAPI.getReviewsByBundleCopiedIds` | GET | `/documents/?bundle_copied_ids={ids}&full=true&review_status=completed,canceled&limit=100000` | Документы review по id скопированных бандлов |
| `ReviewAPI.fetchCurrentTasks` | GET | `/tasks/` | Текущие задачи (фильтры `reviewer_id`, `is_active`, `resource_id`, пагинация) |
| `ReviewAPI.changePriorityTask` | PATCH | `/tasks/{id}/change-priority/` | Изменить приоритет задачи |
| `ReviewAPI.changeDurationTask` | PATCH | `/tasks/{id}/change-duration/` | Изменить длительность задачи |
| `ReviewAPI.getTasksEndDates` | GET | `/tasks/reviewers-max-end-dates/?{query}` | Макс. даты завершения по проверяющим |
| `ReviewAPI.getCountByResourceId` | POST | `/reviews/count_by_resource_id/` | Количество reviews по ресурсам |
| `ReviewAPI.getCountByReviewers` | POST | `/reviews/count_by_reviewer_id/` | Количество reviews по проверяющим |
| `ReviewAPI.getNextStepReviewers` | GET | `/steps/{stepId}/get_reviewers/?review_id={reviewId}` | Проверяющие следующего шага |
| `ReviewAPI.getReviewDocuments` | GET | `/reviews/{id}/documents/` | Документы review |
| `ReviewAPI.updateReviewDocument` | PUT | `/documents/{id}/` | Обновить документ review |
| `ReviewAPI.bulkUpdateReviewDocument` | PUT | `/reviews/{reviewId}/documents/` | Массовое обновление документов review |
| `ReviewAPI.setStatus` | PATCH | `/documents/set-status/?document_ids={ids}` | Проставить статус документам |
| `ReviewAPI.createReview` | POST | `/reviews/` | Создать review |
| `ReviewAPI.changeReviewers` | PATCH | `/reviews/{reviewId}/change_reviewers/` | Сменить проверяющих |
| `ReviewAPI.changeMinReviewers` | PATCH | `/reviews/{reviewId}/change-min-reviewers/` | Изменить мин. число проверяющих |
| `ReviewAPI.getTimeTrackerInfo` | GET | `/reviews/{reviewId}/time-tracking/` | Данные тайм-трекинга review |
| `ReviewAPI.startReview` | PATCH | `/reviews/{reviewId}/start/` | Запустить review |
| `ReviewAPI.patchReview` | PATCH | `/reviews/{id}/` | Обновить атрибуты review |
| `ReviewAPI.deleteReview` | DELETE | `/reviews/{id}/` | Удалить review |
| `ReviewAPI.activateReview` / `ReviewAPI.passReview` | PATCH | `/reviews/{id}/approve/` | Утвердить/пройти review (с комментарием и статусом) |
| `ReviewAPI.setReviewStep` | PATCH | `/reviews/{review_id}/set-step/{step_id}/` | Установить шаг review |
| `ReviewAPI.reviewUpdateBundles` | PATCH | `/reviews/{id}/update-bundles/` | Обновить бандлы review |
| `ReviewAPI.userAction` | POST | `/user-actions/` | Записать действие пользователя |
| `ReviewAPI.writeTransmittalCreated` | POST | `/reviews/{review_id}/transmittal-created/` | Отметить создание трансмиттала для review |
| `ReviewAPI.getProcesses` | GET | `/flows/light/?{query}` | Список процессов (облегчённый) |
| `ReviewAPI.getProcessById` | GET | `/flows/{id}/` | Процесс по id |
| `DocumentAPI.changeCopyPaths` | PATCH | `/documents/change-copy-paths/` | Изменить пути копирования документов |
| `ChecklistsAPI.createReviewsChecklistResult` | PATCH | `/reviews/{reviewId}/checklist-results/` | Результаты чек-листа для review |
### `documentations` — Сервис документации
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `DocumentAPI.getDocumentsBatch` | POST | `/documents/batch` | Пакетное получение документов |
| `DocumentAPI.mark` | PUT | `/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи |
| `DocumentAPI.sign` | POST | `/bundles/{bundleId}/sign` | Подписать бандл |
| `DocumentAPI.downloadFile` | GET | `/bundles/{bundleId}/{key}/download` | Скачать файл (ответ `blob`) |
| `DocumentAPI.getDisks` | GET | `/disks` | Список дисков |
| `DocumentationsAPI.getFolderChildrenWithActiveProcesses` | POST | `/documents/flows` | Дети папок с активными процессами |
| `AiCheckAPI.getBundlePresignedUrl` | GET | `/bundles/{bundleDocumentId}/presigned_url?key=pdf` | Presigned-URL PDF бандла |
### `gateway_api_v1` — Gateway API v1
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `fetchResources` (`ResourcesStore`) | GET | `/resources/?company_id={companyId}` | Список ресурсов компании |
| `fetchParentDocumentByResourceId` | GET | `/resources-rpc/parent-document-by-resource-id/{resourceId}/` | Родительский документ по resource id |
| `fetchDocumentsBundleVersions` | POST | `/documents/bundle_versions` | Версии бандлов документов |
| `getDocumentAncestors` | POST | `/documents/ancestors` | Предки документов |
| `getDiskDocumentsPath` | GET | `/disks/{diskId}/documents?child_id={childId}` | Путь документа на диске |
### `gateway_api_v2` — Gateway API v2
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `getUsersByResourceId` | GET | `/users/?{query}` | Пользователи по ресурсу (с правами) |
### `sarex` — Локальный сервис данных
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `Users.fetchSettings` | GET | `/api/client/settings/` | Клиентские настройки пользователя |
| `getUsersByCompanyId` | GET | `/api/core/users/?company={companyId}&{query}` | Пользователи компании |
| `getDepartments` | GET | `/api/core/admin/departments/` | Отделы |
| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={companyId}&{query}` | Отделы компании (пагинация) |
| `getPositions` | GET | `/api/core/admin/positions/` | Должности |
| `getPositionsV2` | GET | `/api/core/admin/positions/?company={companyId}&{query}` | Должности компании (пагинация) |
| `getMrpaList` | POST | `/api/core/mrpa/list/` | Список MRPA |
### `eav_api_v0` — Сервис атрибутов (EAV)
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `ReviewAPI.getAttributes` | GET | `/schema/?model_name=flow&company_id={companyId}` | Схема атрибутов модели `flow` |
### `checklists` — Сервис чек-листов
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `ChecklistsAPI.getChecklist` | GET | `/checklists/{id}/` | Чек-лист по id |
| `ChecklistsAPI.getChecklistResults` | GET | `/results/` | Результаты чек-листов (фильтры в query) |
| `ChecklistsAPI.createChecklistResult` | POST | `/results/` | Создать результат чек-листа |
| `ChecklistsAPI.updateChecklistResult` | PATCH | `/results/{id}/` | Обновить результат чек-листа |
### `transmittals` — Сервис передачи документации
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `TransmittalsAPI.createTransmittal` | POST | `/transmittals/create` | Создать трансмиттал |
| `TransmittalsAPI.getTransmittals` | POST | `/transmittals` | Список трансмитталов ресурса (пагинация по `bookmark`) |
| `TransmittalsAPI.getTemplate` | GET | `/transmittal_templates/{templateId}?resource={resourceId}` | Шаблон трансмиттала по id |
| `TransmittalsAPI.getSelectTemplates` | GET | `/transmittal_templates/select?resource={resourceId}` | Список шаблонов для выбора |
### `issues` — Сервис замечаний
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `IssuesAPI.getIssues` | POST | `/issues/filter/` | Список замечаний с фильтрами (пагинация в query) |
| `IssuesAPI.getIssuesTypesStatusModelsByCompanyId` | GET | `/status-models/?company_id={companyId}` | Модели статусов замечаний компании |
### `orchestrator` — Оркестратор процессов
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `MarksAPI.createMarkFlow` | POST | `/process` | Запустить процесс маркировки |
| `MarksAPI.getMarkFlow` | GET | `/process/{id}` | Процесс маркировки по id |
| `MarksAPI.startSign` | POST | `/sign` | Запустить подписание |
### `files` — Сервис файлов
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `DocumentAPI.downloadDocuments` | POST | `/documents/` | Скачать документы по `bundle_ids` (ответ `blob`) |
### `lambdas` — Сервис экспорта
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `fetchExportReviewsByResourceIDs` | GET | `/export-reviews/{params}` | Экспорт reviews в XLSX (ответ `blob`) |
| `fetchExportReview` | GET | `/export-reviews/{reviewId}/report/` | Экспорт отчёта по review в PDF (ответ `blob`) |
### `sarexAgents` — Сервис AI-агентов
| Ключ (метод API) | HTTP-метод | Путь (`url`) | Назначение |
| --- | --- | --- | --- |
| `AgentsAPI.suggestCopyPaths` | POST | `/runs/wait` | Запуск `router-agent` для подбора путей копирования |
| `AiCheckAPI.runValidator` | POST | `/runs/wait` | Запуск AI-проверки документов по чек-листу |
| `AiCheckAPI.getDocumentStorageStatus` | GET | `/files/documents/{documentId}/storage-status?tenant_id={tenantId}` | Статус загрузки документа в хранилище |
| `AiCheckAPI.uploadFileByUrl` | POST | `/files/upload/url` | Загрузить файл по URL в RAG-каталог |
| `AiCheckAPI.getEntitled` | GET | `/internal/tenant-agent-entitlements/{tenantId}/agents-status?user_id={userId}` | Доступность AI-агента для тенанта |
> Запросы `/runs/wait` выполняются с увеличенным таймаутом `RUN_WAIT_TIMEOUT_MS = 600000` мс (10 минут) и с `showErrorNotification: false`.
## Удалённый модуль (Module Federation)
Помимо HTTP-API, `reviews-frontend` подключает удалённый микрофронтенд `documentations` через Module Federation (`module/api/module-hosts.ts`, функция `getModuleHost`):
| Модуль | `stage` | `prod` |
| --- | --- | --- |
| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` |
В `contour` путь относительный (`/documentations/static/module/remoteEntry.js`), в `preprod``https://modules.preprod.sarex.io/...`.
## Аутентификация
Токен и режим аутентификации обеспечиваются `@sarex-team/sdk-js`. В окружении `local` `httpService` переводится в режim `zitadel` (`setTypeOfHttpService("zitadel")`), а хост IdP берётся из `hosts.zitadel` (`https://idp.dev.stage.sarex.io` для stage, `https://login.sarex.io` для prod). В остальных окружениях используется режим по умолчанию SDK.
## Обработка ошибок
Отдельного модуля маппинга ошибок (аналогичного `errors.ts`) в `reviews-frontend` нет. Обработка ошибок выполняется в двух местах:
- **SDK `@sarex-team/sdk-js`** — при `showErrorNotification: true` (значение по умолчанию для большинства запросов) показывает пользователю уведомление об ошибке. Для «тихих» запросов (AI-агенты, presigned-URL, часть фоновых вызовов) явно задаётся `showErrorNotification: false`.
- **Локальные `try/catch`** — в сторах (`resources.ts`, `users.ts`) и функциях экспорта (`fetchExportReviews*`) ошибки перехватываются и логируются через `console.error`, без проброса наверх.
## Замечания
- Пути (`url`) указываются **относительно** базового хоста сервиса, который уже содержит версионный префикс (`/api/v1`, `/api/v0` и т.п.). Это отличается от реестра `endpoints.ts` в некоторых других микрофронтендах, где префикс включается в путь эндпоинта.
- Файл `module/api/tranmittals.ts` назван с опечаткой (`tranmittals` вместо `transmittals`); экспорт при этом называется `TransmittalsAPI`.
- Сервис `sarex` в окружениях `stage`/`prod`/`preprod`/`contour` имеет пустой базовый хост (`""`) — запросы идут по относительным путям (через тот же origin/реверс-прокси); в `local` он указывает на `https://stage.sarex.io`.
- Хост сервиса `orchestrator` в `prod` содержит суффикс `/api` (`.../orchestrator/api`), тогда как в `stage`/`preprod` — без него (`.../orchestrator`); пути методов (`/process`, `/sign`) одинаковы.