diff --git a/apps/checklists/.env.example b/apps/checklists/.env.example new file mode 100644 index 0000000..ae0042a --- /dev/null +++ b/apps/checklists/.env.example @@ -0,0 +1,31 @@ +# Piccolo (обязательно для запуска) +PYTHONPATH=src +PICCOLO_CONF=db.config + +# App +DEBUG=true + +# HTTP app +HTTP_APP_HOST=0.0.0.0 +HTTP_APP_PORT=8000 +HTTP_APP_ROOT_PATH="" +HTTP_APP_WORKERS=1 +HTTP_APP_ADMIN_ENABLE=true + +# Database +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres + +# OpenTelemetry +OTEL_ENABLE=false +OTEL_URL=http://signoz-otel-collector-external.signoz.svc.cluster.local:4317 +OTEL_SERVICE_NAME=checklists-backend.checklists-stage +OTEL_INSECURE=true + +# Auth (JWT) +# При JWT_AUTH_ENABLE=false middleware отключён и используется дефолтный пользователь +JWT_AUTH_ENABLE=false +JWT_AUTH_PUBLIC_KEY=key diff --git a/apps/checklists/CONFIGURATION.md b/apps/checklists/CONFIGURATION.md new file mode 100644 index 0000000..1f60db9 --- /dev/null +++ b/apps/checklists/CONFIGURATION.md @@ -0,0 +1,171 @@ +# Конфигурация проекта checklists-backend + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). + +В отличие от единого класса настроек, конфигурация разбита на несколько независимых классов `BaseSettings`, у каждого — **свой** `env_prefix` (плоские имена, без вложенного разделителя): + +| Класс | `env_prefix` | Раздел | +| --- | --- | --- | +| `Config` | *(нет префикса)* | `debug` | +| `HTTPAppConfig` | `HTTP_APP_` | Параметры HTTP-приложения/uvicorn | +| `DatabaseConfig` | `DATABASE_` | Подключение к PostgreSQL | +| `OTELConfig` | `OTEL_` | Трейсинг/логи OpenTelemetry | +| `JWTAuthConfig` | `JWT_AUTH_` | Аутентификация по JWT | + +Подклассы подключаются к корневому `Config` как поля со значениями по умолчанию (`http_app`, `database`, `otel`, `jwt_auth`) и читают окружение в момент импорта. Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Особенности: + +- **`.env` не загружается автоматически** — в `config.py` не задан `env_file`, зависимости `python-dotenv` нет. Файл `.env.template` — это шаблон; переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s. +- Помимо переменных приложения, для запуска нужны две инфраструктурные переменные Piccolo: `PYTHONPATH=src` и `PICCOLO_CONF=db.config` (заданы в `.env.template` и в `Dockerfile`). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (`make run`) | Переменные окружения процесса. `.env.template` — шаблон, приложение его **не подхватывает** автоматически | +| Контейнер | `docker/http/Dockerfile` задаёт `PYTHONPATH`/`PICCOLO_CONF`; прочие переменные пробрасываются при запуске | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и `HELM_SET_ARGS` | + +Способы запуска (`Makefile`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `make run` | `src/cmd/http/main.py` → `uvicorn` (factory `app.http:create_app`) | HTTP API | +| `make migrate` | `piccolo migrations forwards all` | Применение миграций БД | +| `make migrations` | `piccolo migrations new checklists --auto` | Генерация новой миграции | +| `make format` / `make format-check` | `ruff` | Форматирование/линт | + +Порядок запуска в контейнере (`docker/http/entrypoint.sh`): сначала выполняются миграции (`piccolo migrations forwards all`), затем стартует приложение (`src/cmd/http/main.py`). Uvicorn запускается в режиме фабрики; `reload` включается при `DEBUG=true`, число воркеров — из `HTTP_APP_WORKERS`. + +## Переменные приложения + +В столбце «Переменная» указано полное имя (префикс + поле). Дефолт `—` означает, что значение обязательно. + +### App (`Config`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DEBUG` | bool | `True` | Режим отладки. Влияет на `reload` uvicorn, логирование SQL-запросов Piccolo (`log_queries`/`log_responses`), а также на `production`-флаг и `debug` piccolo-admin | + +### HTTP-приложение (`HTTP_APP_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `HTTP_APP_HOST` | string | `0.0.0.0` | Адрес прослушивания | +| `HTTP_APP_PORT` | int | `8000` | Порт | +| `HTTP_APP_ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси; в k8s — `/checklists`) | +| `HTTP_APP_WORKERS` | int | `1` | Число воркеров uvicorn | +| `HTTP_APP_ADMIN_ENABLE` | bool | `True` | Монтировать ли piccolo-admin по пути `/admin/` | + +### Database (`DATABASE_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DATABASE_HOST` | string | `postgres` | Хост PostgreSQL | +| `DATABASE_PORT` | int | `5432` | Порт PostgreSQL | +| `DATABASE_NAME` | string | `postgres` | Имя базы данных | +| `DATABASE_USER` | string | `postgres` | Пользователь БД | +| `DATABASE_PASSWORD` | string | `postgres` | Пароль пользователя БД | + +Подключение собирается в `src/db/config.py` (`PostgresEngine`). SSL-параметров в настройках нет; в prod TLS обеспечивается на уровне подключения/CA-сертификата (см. `docker/http/ca.crt`). + +### OpenTelemetry (`OTEL_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `OTEL_ENABLE` | bool | `False` | Включить трейсинг/логи OTEL. При `True` подключаются `fastapi-otel-tools` и инструментирование FastAPI | +| `OTEL_URL` | string | `http://signoz-otel-collector-external.signoz.svc.cluster.local:4317` | Адрес OTLP-коллектора | +| `OTEL_SERVICE_NAME` | string | `checklists-backend.checklists-stage` | Имя сервиса в трейсах | +| `OTEL_INSECURE` | bool | `True` | Небезопасное (без TLS) подключение к коллектору | + +> При `OTEL_ENABLE=true` `access_log` uvicorn отключается (логи идут через OTEL-обработчик). + +### Auth (`JWT_AUTH_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `JWT_AUTH_ENABLE` | bool | `False` | Включить `JWTAuthMiddleware`. При `False` middleware не подключается, и в контекст подставляется дефолтный пользователь (для локальной разработки) | +| `JWT_AUTH_PUBLIC_KEY` | string | `key` | Публичный RSA-ключ для JWT (алгоритм `RS512`) | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в запуске/сборке. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `PYTHONPATH` | `.env.template`, `Dockerfile` | Путь к исходникам (`src`) | +| `PICCOLO_CONF` | `.env.template`, `Dockerfile` | Путь к конфигу Piccolo (`db.config`) | +| `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`checklists-backend`) | +| `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile (`./docker/http/Dockerfile`) | +| `IMAGE_NAME` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Имя собираемого образа | +| `CHART_NAME` / `CHART_VERSION` / `RELEASE_NAME` | `.gitlab-ci.yml` | Параметры релиза Helm | + +Базовый образ — `python:3.13-slim-bookworm`; менеджер зависимостей — `uv` (`uv sync --locked`). В образ добавляется CA-сертификат Yandex (`docker/http/ca.crt`). + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Сервис деплоится подключаемым чартом `universal-chart` (OCI-зависимость). Окружение выбирается ключом `universal-chart.global.env` (`stage`/`preprod`/`production`); для каждой переменной значение берётся из блока с ключом текущего окружения либо из `_default`. + +Обычные значения (блок `envs`) переопределяют дефолты кода, в частности: + +| Переменная | Значение в чарте | +| --- | --- | +| `HTTP_APP_ROOT_PATH` | `/checklists` | +| `HTTP_APP_WORKERS` | `3` | +| `HTTP_APP_ADMIN_ENABLE` | `true` | +| `DATABASE_PORT` | `6432` (PgBouncer) | +| `DATABASE_NAME` | `checklists_db` (stage), `checklists` (preprod/production) | +| `OTEL_ENABLE` | `true` | +| `OTEL_URL` | `http://otel-collector.opentelemetry-collector.svc.cluster.local:4317` | +| `OTEL_SERVICE_NAME` | `checklists-backend.proc` (stage), `…checklists-preprod`, `…checklists-prod` | +| `JWT_AUTH_ENABLE` | `true` | +| `DEBUG` | `false` | + +Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) | +| --- | --- | --- | +| `DATABASE_USER` | `checklists-postgresql-secret` (stage) / `ya-pg-secret` (preprod, production) | `user` | +| `DATABASE_PASSWORD` | `checklists-postgresql-secret` / `ya-pg-secret` | `password` | +| `DATABASE_HOST` | `checklists-postgresql-secret` / `ya-pg-secret` | `host` | +| `JWT_AUTH_PUBLIC_KEY` | `jwt-secret` | `public-key` | + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики — 1 по умолчанию, 2 в production, ресурсы), `image.*`, `service.*` (ClusterIP, порт `80` → `8000`). Проверки `liveness`/`readiness` отключены. + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline.yaml`, `common-security-scan.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | +| --- | --- | --- | +| ветка `stage` | `stage` | `proc` | +| ветка `master` | `preprod` | `checklists-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `checklists-prod` | + +Ключевые переменные пайплайна: `SERVICE_NAME`, `DOCKERFILE_PATH`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (проброс `image.name`, `global.env`, метаданных коммита). Job `lint` прогоняет `ruff check`/`ruff format --check` на образе `uv`. + +## Замечания и потенциальные проблемы + +- Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение вручную либо задавать в манифестах. +- Аутентификация JWT в коде выполняет разбор токена с `options={"verify_signature": False}` в обоих режимах (sarex-backend и Zitadel) — подпись фактически не проверяется на уровне приложения, доверие обеспечивается сетевым слоем (Istio). При `JWT_AUTH_ENABLE=false` middleware не подключается и используется дефолтный пользователь из `entity/context.py`. +- Внутренние эндпоинты (`/internal/*`) аутентификации на уровне приложения не требуют. +- SSL-настроек подключения к БД в коде нет; в prod используется PgBouncer (`DATABASE_PORT=6432`) и CA-сертификат, вшитый в образ. +- Piccolo-admin доступен по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`; в режиме `DEBUG=false` он поднимается в `production`-режиме. + +## Минимальный набор для локального запуска + +Нужен доступный PostgreSQL. Помимо `PYTHONPATH=src` и `PICCOLO_CONF=db.config`, для запуска достаточно значений по умолчанию — обязательных переменных без дефолта нет. Практически стоит задать: + +- `DEBUG` (`true` локально) +- `HTTP_APP_HOST`, `HTTP_APP_PORT` +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `JWT_AUTH_ENABLE` (`false` для локальной разработки — тогда используется дефолтный пользователь) +- `OTEL_ENABLE` (`false` локально) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/checklists/openapi.yaml b/apps/checklists/openapi.yaml new file mode 100644 index 0000000..8761b1a --- /dev/null +++ b/apps/checklists/openapi.yaml @@ -0,0 +1,963 @@ +openapi: 3.0.3 + +info: + title: Checklists + version: "0.1.0" + description: | + REST API сервиса **checklists-backend** — управление чек-листами + (`Checklist`) и их результатами (`ChecklistResult`). + + Сервис написан на Python (**FastAPI** + ORM **Piccolo**). Приложение + собирается фабрикой `create_app` в `src/app/http.py`. Роутинг состоит из + двух групп: + + - публичный API — префикс `/api/v1` (`controller/http/api`); + - внутренний API — префикс `/internal/v1` (`controller/http/internal_api`), + предназначен для вызовов внутри кластера (через ingress не публикуется). + + Интерактивная документация (ReDoc) доступна по `/docs/`, схема — + по `/openapi.json/` (с учётом `root_path`). Админ-панель Piccolo монтируется + по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`. + + ### Аутентификация + Аутентификация включается флагом `JWT_AUTH_ENABLE`. При включённом + `JWTAuthMiddleware` (`controller/http/middlewares.py`) публичные эндпоинты + требуют заголовок `Authorization: Bearer `. Поддерживаются два режима: + + 1. **Zitadel** — если передан дополнительный заголовок `identity` + (`Identity `), полезная нагрузка (`user_id`, `company_ids`) берётся + из этого токена (`urn:zitadel:iam:user:metadata`). + 2. **sarex-backend** — если заголовка `identity` нет, разбирается основной + токен (алгоритм `RS512`, ключ `JWT_AUTH_PUBLIC_KEY`). + + В обоих режимах разбор выполняется с `verify_signature=False` — подпись на + уровне приложения не проверяется, доверие обеспечивается сетевым слоем. + Внутренние эндпоинты (`/internal/*`) и пути `/docs/`, `/openapi.json/`, + `/admin/*` аутентификацию пропускают. При `JWT_AUTH_ENABLE=false` middleware + не подключается и используется дефолтный пользователь. + + ### Пагинация + Списочные ответы используют пагинацию limit/offset и оборачиваются в + `PaginatedResponse` — `{ count, result }`, где `count` — число объектов в + текущем ответе, `result` — сами объекты. Параметры: `limit` (по умолчанию + `100`), `offset` (по умолчанию `0`), сортировка — `order_by`/`ascending`. + + ### Обработка ошибок + Доменные ошибки (`controller/http/errors.py`) возвращаются как + `application/json` с телом `{ "detail": "<текст>" }`. Маппинг: + + - `ResourceNotPermittedError` → **403**; + - `ResourceNotFoundError` → **404**; + - `StateConflictError` → **409**; + - `ChecklistResultValidationError` → **422**; + - прочее → **500**. + + Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в + стандартном формате `422` (`HTTPValidationError`). Все публичные эндпоинты + (`/api/*`) при невалидном/отсутствующем токене возвращают `401`. + + contact: + name: checklists-backend + url: https://gitlab/proc/checklists-backend + +servers: + - url: https://api.sarex.io/checklists + description: Production (ingress, root_path=/checklists) + - url: https://stage-api.sarex.io/checklists + description: Stage (ingress, root_path=/checklists) + - url: http://checklists-backend-service.proc.svc.cluster.local + description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000). Единственный способ достучаться до /internal/v1 + - url: http://localhost:8000 + description: Локальный запуск (Uvicorn, порт по умолчанию 8000) + +tags: + - name: Checklists + description: Чек-листы — создание, просмотр, поиск, удаление + - name: Checklist results + description: Результаты чек-листов — создание, просмотр, обновление, удаление + - name: internal + description: Внутренние эндпоинты (только внутри кластера) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Checklists + # ========================================================================== + /api/v1/checklists/: + post: + tags: [Checklists] + summary: Создать чек-лист + operationId: createChecklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistCreate' + responses: + '201': + description: Созданный чек-лист + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistReadFull' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + get: + tags: [Checklists] + summary: Список чек-листов + operationId: listChecklists + parameters: + - { $ref: '#/components/parameters/OrderByChecklist' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - name: company_id + in: query + required: false + description: Фильтр по ID компаний + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + responses: + '200': + description: Страница чек-листов (компактное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/checklists/filter/: + post: + tags: [Checklists] + summary: Список чек-листов (фильтры в теле) + description: Аналог `GET /api/v1/checklists/`, но фильтры и пагинация передаются в теле запроса. + operationId: filterChecklists + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistFiltersWithPagination' + responses: + '200': + description: Страница чек-листов (компактное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/checklists/{instance_id}/: + get: + tags: [Checklists] + summary: Чек-лист по id + operationId: getChecklist + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '200': + description: Чек-лист (полное представление) + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistReadFull' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Checklists] + summary: Удалить чек-лист + operationId: deleteChecklist + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '204': + description: Чек-лист удалён + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Checklist results + # ========================================================================== + /api/v1/results/: + post: + tags: [Checklist results] + summary: Создать результат чек-листа + description: | + Создаёт результат по `checklist_id`. Данные чек-листа переносятся бэком + автоматически; в теле передаются метаданные и список значений инпутов + (`input_values`). + operationId: createChecklistResult + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultCreate' + responses: + '201': + description: Созданный результат + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '422': { $ref: '#/components/responses/ValidationError' } + get: + tags: [Checklist results] + summary: Список результатов чек-листов + operationId: listChecklistResults + parameters: + - { $ref: '#/components/parameters/OrderByChecklistResult' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { $ref: '#/components/parameters/FilterResultId' } + - { $ref: '#/components/parameters/FilterChecklistId' } + - { $ref: '#/components/parameters/FilterCreatorId' } + - { $ref: '#/components/parameters/FilterIsDraft' } + - { $ref: '#/components/parameters/FilterIsLocked' } + responses: + '200': + description: Страница результатов + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '422': { $ref: '#/components/responses/ValidationError' } + + /api/v1/results/{instance_id}/: + get: + tags: [Checklist results] + summary: Результат чек-листа по id + operationId: getChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '200': + description: Результат чек-листа + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + patch: + tags: [Checklist results] + summary: Обновить результат чек-листа + description: Частичное обновление значений инпутов и флага черновика. + operationId: updateChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultPartialUpdate' + responses: + '200': + description: Обновлённый результат + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultRead' + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + delete: + tags: [Checklist results] + summary: Удалить результат чек-листа + operationId: deleteChecklistResult + parameters: + - { $ref: '#/components/parameters/InstanceId' } + responses: + '204': + description: Результат удалён + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/NotFound' } + '422': { $ref: '#/components/responses/ValidationError' } + + # ========================================================================== + # Internal + # ========================================================================== + /internal/v1/results/: + get: + tags: [internal] + summary: Список результатов (внутренний) + description: Внутрикластерный эндпоинт. Аутентификация на уровне приложения не выполняется. + operationId: internalListChecklistResults + security: [] + parameters: + - { $ref: '#/components/parameters/OrderByChecklistResult' } + - { $ref: '#/components/parameters/Ascending' } + - { $ref: '#/components/parameters/Limit' } + - { $ref: '#/components/parameters/Offset' } + - { $ref: '#/components/parameters/FilterResultId' } + - { $ref: '#/components/parameters/FilterChecklistId' } + - { $ref: '#/components/parameters/FilterCreatorId' } + - { $ref: '#/components/parameters/FilterIsDraft' } + - { $ref: '#/components/parameters/FilterIsLocked' } + responses: + '200': + description: Страница результатов + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead' + '422': { $ref: '#/components/responses/ValidationError' } + + /internal/v1/results/lock: + patch: + tags: [internal] + summary: Массовое обновление блокировки результатов + description: Устанавливает флаг `is_locked` для списка результатов по их id. Внутрикластерный эндпоинт. + operationId: internalBulkUpdateLock + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChecklistResultBulkLockingPartialUpdate' + responses: + '204': + description: Флаги блокировки обновлены + '422': { $ref: '#/components/responses/ValidationError' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. Алгоритм `RS512`, + ключ `JWT_AUTH_PUBLIC_KEY`. Разбор выполняется без проверки подписи + (`verify_signature=False`). + identityToken: + type: apiKey + in: header + name: identity + description: | + Опциональный заголовок `identity` (`Identity `) для режима Zitadel. + При его наличии полезная нагрузка (`user_id`, `company_ids`) берётся из + этого токена. + + parameters: + InstanceId: + name: instance_id + in: path + required: true + schema: + type: integer + Limit: + name: limit + in: query + required: false + description: Максимальное количество объектов + schema: + type: integer + default: 100 + Offset: + name: offset + in: query + required: false + description: Количество пропущенных объектов + schema: + type: integer + default: 0 + Ascending: + name: ascending + in: query + required: false + description: Сортировка по возрастанию + schema: + type: boolean + default: true + OrderByChecklist: + name: order_by + in: query + required: false + description: Поле для сортировки + schema: + type: string + enum: [id] + default: id + OrderByChecklistResult: + name: order_by + in: query + required: false + description: Поле для сортировки + schema: + type: string + enum: [id] + default: id + FilterResultId: + name: id + in: query + required: false + description: Фильтр по ID результата + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterChecklistId: + name: checklist_id + in: query + required: false + description: Фильтр по ID чек-листа + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterCreatorId: + name: creator_id + in: query + required: false + description: Фильтр по ID создателя результата + schema: + type: array + nullable: true + items: + type: integer + minimum: 1 + FilterIsDraft: + name: is_draft + in: query + required: false + description: Признак «чернового» результата + schema: + type: boolean + nullable: true + FilterIsLocked: + name: is_locked + in: query + required: false + description: Признак блокировки результата + schema: + type: boolean + nullable: true + + responses: + Unauthorized: + description: Токен не предоставлен или невалиден + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + NotFound: + description: Запрошенный ресурс не найден + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + Conflict: + description: Конфликт состояния + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + ValidationError: + description: | + Ошибка валидации тела/параметров запроса (FastAPI/Pydantic) либо + доменная ошибка валидации результата (`{ detail }`). + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + + schemas: + # ---- Общие ---- + HTTPError: + type: object + properties: + detail: + type: string + description: Описание ошибки + required: [detail] + + HTTPValidationError: + type: object + properties: + detail: + type: array + items: + $ref: '#/components/schemas/ValidationError' + + ValidationError: + type: object + properties: + loc: + type: array + items: + anyOf: + - type: string + - type: integer + msg: + type: string + type: + type: string + required: [loc, msg, type] + + # ---- Ограничения инпутов ---- + InputChoiceOption: + type: object + properties: + id: + type: string + format: uuid + description: Уникальный идентификатор опции + name: + type: string + description: Название опции + example: "Да" + order: + type: integer + description: Порядковый номер опции + example: 1 + color: + type: string + nullable: true + description: Цвет опции (hex или именованный html-цвет) + example: "#00aa00" + selected_tip: + type: string + nullable: true + description: Заметка при выборе опции + example: 'Рекомендуется добавить комментарий при ответе "Нет"' + alt_name: + type: string + nullable: true + description: Название опции для записи в историю + example: "Одобрено" + required: [id, name, order, selected_tip] + + InputChoiceConstraints: + type: object + properties: + type: + type: string + enum: [choice] + options: + type: array + nullable: true + description: Список опций для выбора + items: + $ref: '#/components/schemas/InputChoiceOption' + required: [type, options] + + InputStringConstraints: + type: object + properties: + type: + type: string + enum: [string] + min_length: + type: integer + minimum: 0 + default: 0 + description: Минимально допустимое количество символов + max_length: + type: integer + minimum: 1 + default: 1000 + description: Максимально допустимое количество символов + required: [type] + + InputConstraints: + oneOf: + - $ref: '#/components/schemas/InputChoiceConstraints' + - $ref: '#/components/schemas/InputStringConstraints' + discriminator: + propertyName: type + mapping: + choice: '#/components/schemas/InputChoiceConstraints' + string: '#/components/schemas/InputStringConstraints' + + # ---- Чек-лист (создание) ---- + ChecklistInputCreate: + type: object + properties: + name: + type: string + maxLength: 1024 + description: Название + example: "Комментарий" + order: + type: integer + description: Порядковый номер + is_required: + type: boolean + description: Обязательное ли поле для заполнения + constraints: + $ref: '#/components/schemas/InputConstraints' + required: [name, order, is_required, constraints] + + ChecklistItemCreate: + type: object + properties: + description: + type: string + maxLength: 8192 + description: Описание шага + order: + type: integer + description: Порядковый номер + inputs: + type: array + description: Список элементов ввода + items: + $ref: '#/components/schemas/ChecklistInputCreate' + required: [description, order, inputs] + + ChecklistCreate: + type: object + properties: + name: + type: string + maxLength: 250 + description: Название + example: "Чек-лист проверки документов" + description: + type: string + maxLength: 8192 + description: Описание чек-листа + company_id: + type: integer + minimum: 1 + description: ID компании + example: 1 + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistItemCreate' + required: [name, description, company_id, items] + + # ---- Чек-лист (чтение) ---- + ChecklistInputRead: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + name: + type: string + maxLength: 1024 + order: + type: integer + is_required: + type: boolean + constraints: + $ref: '#/components/schemas/InputConstraints' + required: [id, created_at, updated_at, name, order, is_required, constraints] + + ChecklistItemRead: + type: object + properties: + id: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + description: + type: string + maxLength: 8192 + order: + type: integer + inputs: + type: array + description: Список элементов ввода + items: + $ref: '#/components/schemas/ChecklistInputRead' + required: [id, created_at, updated_at, description, order, inputs] + + ChecklistReadCompact: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + name: + type: string + description: + type: string + company_id: + type: integer + minimum: 1 + required: [id, created_at, updated_at, name, description, company_id] + + ChecklistReadFull: + allOf: + - $ref: '#/components/schemas/ChecklistReadCompact' + - type: object + properties: + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistItemRead' + required: [items] + + ChecklistFiltersWithPagination: + type: object + properties: + order_by: + type: string + enum: [id] + default: id + ascending: + type: boolean + default: true + limit: + type: integer + default: 100 + offset: + type: integer + default: 0 + company_id: + type: array + nullable: true + description: Фильтр по ID компаний + items: + type: integer + minimum: 1 + + # ---- Результаты чек-листов ---- + ChecklistResultInputCreate: + type: object + properties: + input_id: + type: integer + description: ID инпута, для которого устанавливается значение + example: 1 + value: + description: 'Значение (тип зависит от инпута: id опции для choice, строка для string)' + nullable: true + example: "Да" + required: [input_id, value] + + ChecklistResultCreate: + type: object + properties: + entity_type: + type: string + description: Сущность, для которой создан результат + example: "review" + entity_id: + type: string + description: ID сущности, для которой создан результат + example: "1" + checklist_id: + type: integer + description: ID чек-листа + example: 1 + accessible_by: + type: array + description: SA ID роли/места/пользователя, которым доступен результат + items: + type: string + format: uuid + is_draft: + type: boolean + description: Является ли результат черновым + input_values: + type: array + description: Список устанавливаемых значений + items: + $ref: '#/components/schemas/ChecklistResultInputCreate' + required: [entity_type, entity_id, checklist_id, accessible_by, is_draft, input_values] + + ChecklistResultPartialUpdate: + type: object + properties: + input_values: + type: array + description: Список устанавливаемых значений + items: + $ref: '#/components/schemas/ChecklistResultInputCreate' + is_draft: + type: boolean + description: Является ли результат черновым + required: [input_values, is_draft] + + ChecklistResultInput: + type: object + properties: + input_id: + type: integer + description: ID инпута, для которого создан результат + name: + type: string + description: Название + order: + type: integer + description: Порядковый номер + value: + type: string + nullable: true + description: Введённое значение (строка или UUID выбранной опции) + value_text: + type: string + nullable: true + description: Текстовое представление введённого значения + color: + type: string + nullable: true + description: Цвет значения + required: [input_id, name, order, value, value_text, color] + + ChecklistResultItem: + type: object + properties: + description: + type: string + description: Описание шага + order: + type: integer + description: Порядковый номер + inputs: + type: array + description: Список введённых значений + items: + $ref: '#/components/schemas/ChecklistResultInput' + required: [description, order, inputs] + + ChecklistResultRead: + type: object + properties: + id: + type: integer + example: 1 + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + entity_type: + type: string + example: "review" + entity_id: + type: string + example: "1" + checklist_id: + type: integer + accessible_by: + type: array + items: + type: string + format: uuid + is_draft: + type: boolean + company_id: + type: integer + description: ID компании + is_locked: + type: boolean + description: Заблокирован ли результат для изменений + items: + type: array + description: Список шагов чек-листа + items: + $ref: '#/components/schemas/ChecklistResultItem' + required: + - id + - created_at + - updated_at + - entity_type + - entity_id + - checklist_id + - accessible_by + - is_draft + - company_id + - is_locked + - items + + ChecklistResultBulkLockingPartialUpdate: + type: object + properties: + ids: + type: array + description: Список id результатов, которым нужно обновить флаг + items: + type: integer + example: [1, 2, 3] + is_locked: + type: boolean + description: Заблокирован ли результат для изменений + required: [ids, is_locked] + + # ---- Пагинация ---- + PaginatedResponse_ChecklistReadCompact: + type: object + properties: + count: + type: integer + description: Количество объектов + example: 1 + result: + type: array + description: Объекты + items: + $ref: '#/components/schemas/ChecklistReadCompact' + required: [count, result] + + PaginatedResponse_ChecklistResultRead: + type: object + properties: + count: + type: integer + description: Количество объектов + example: 1 + result: + type: array + description: Объекты + items: + $ref: '#/components/schemas/ChecklistResultRead' + required: [count, result] diff --git a/apps/contracts/.env.example b/apps/contracts/.env.example new file mode 100644 index 0000000..7657626 --- /dev/null +++ b/apps/contracts/.env.example @@ -0,0 +1,19 @@ +# App +LOG_LEVEL=debug +ADDRESS=:8080 + +# Auth +# Публичный RSA-ключ (PEM) для проверки JWT. +# Читается напрямую через os.Getenv("PUBLIC_KEY") в cmd/http/main.go. +PUBLIC_KEY= + +# Database +# DSN подключения к PostgreSQL (pgx), напр. postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable +DB_URL=postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable +DB_POOL_SIZE=10 + +# CLI (миграции) — cmd/cli +# Путь к каталогу с миграциями (по умолчанию migrations) +DB_MIGRATIONS_PATH=migrations +# Необязательно: путь к .env-файлу для cli (эквивалент флага -env-file) +# ENV_FILE=.env diff --git a/apps/contracts/CONFIGURATION.md b/apps/contracts/CONFIGURATION.md new file mode 100644 index 0000000..a812483 --- /dev/null +++ b/apps/contracts/CONFIGURATION.md @@ -0,0 +1,135 @@ +# Конфигурация проекта contracts + +Документ описывает все переменные окружения и способы конфигурирования сервиса `contracts` (Go, HTTP API + CLI миграций). + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig) по структуре `Config` в `internal/app/http/config.go`. Дополнительно `.env`-файл автоматически подгружается через [`github.com/joho/godotenv`](https://github.com/joho/godotenv): + +- HTTP-процесс (`cmd/http/main.go`) вызывает `godotenv.Load(".env")` перед разбором конфигурации — если файл `.env` есть в рабочем каталоге, его переменные попадают в окружение; +- CLI-процесс (`cmd/cli/main.go`) загружает файл из `ENV_FILE` (или `.env` по умолчанию), путь можно задать флагом `-env-file`. + +Особенности разбора (`go-envconfig`): + +- глобального префикса нет — верхнеуровневые поля читаются по своим именам (`LOG_LEVEL`, `ADDRESS`); +- вложенные секции задаются префиксом на уровне структуры: `Database` → `env:", prefix=DB_"`, `Auth` → `env:", prefix=AUTH_"`; +- значения по умолчанию заданы в тегах через `default=…`; поля без `default` при отсутствии переменной остаются пустыми (нулевым значением типа), а не приводят к панике на этапе разбора — ошибки всплывают позже (например, невалидный `DB_URL` или пустой `PUBLIC_KEY`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | `.env` в рабочем каталоге (авто-загрузка `godotenv`) + переменные окружения процесса | +| Локально (docker-compose) | `docker-compose.yml`: сервис `contracts` берёт переменные из `env_file: .env`; поднимается вместе с `postgres` | +| Kubernetes (Helm) | `.helm/values-.yaml`: блоки `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблон `.helm/templates/deployment.yaml` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` и переменные `workflow.rules` (namespace, release, chart) | + +Способы запуска процессов: + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `http` | `cmd/http/main.go` | HTTP API (Fiber v3), слушает `ADDRESS` | +| `cli migrate` | `cmd/cli/main.go` | Применение миграций БД (`golang-migrate`), каталог `DB_MIGRATIONS_PATH` | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала `./cli migrate`, затем `./http`. + +## Переменные приложения + +В столбце «Переменная» указано полное имя (с учётом префикса секции). Дефолт `—` означает, что значения по умолчанию нет. + +### App + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `LOG_LEVEL` | string | `debug` | Уровень логирования (zap): `debug`/`info`/`warn`/`error` и т.п. | +| `ADDRESS` | string | `:8080` | Адрес и порт прослушивания HTTP-сервера (Fiber) | + +### Database (`DB_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_URL` | string | — | DSN подключения к PostgreSQL (`pgxpool.ParseConfig`), напр. `postgres://user:pass@host:5432/db?sslmode=verify-full` | +| `DB_POOL_SIZE` | int32 | `10` | Максимальный размер пула соединений (`pgxpool.Config.MaxConns`) | + +### Auth (`AUTH_*`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `AUTH_PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM) для проверки JWT. См. замечание ниже — фактически используется `PUBLIC_KEY` | +| `PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM). Читается напрямую в `cmd/http/main.go` через `os.Getenv("PUBLIC_KEY")` и записывается в `config.Auth.PublicKey`, перекрывая `AUTH_PUBLIC_KEY` | + +> При старте `AuthProvider` парсит ключ (`pem.Decode` + `x509.ParsePKIXPublicKey`). Если `PUBLIC_KEY` пустой или невалидный — приложение падает с `panic` ещё до старта HTTP-сервера. + +## Переменные CLI (миграции) + +Читаются в `cmd/cli/main.go` (структура `cliConfig`, префикс `DB_`). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DB_URL` | string | — | DSN подключения к PostgreSQL для применения миграций (обязателен, иначе ошибка `DB_URL is required`) | +| `DB_MIGRATIONS_PATH` | string | `migrations` | Путь к каталогу с SQL-миграциями (`golang-migrate`) | +| `ENV_FILE` | string | `.env` | Путь к `.env`-файлу, из которого CLI загружает переменные (можно задать флагом `-env-file=PATH`) | + +## Переменные инфраструктуры и сборки + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная / параметр | Где используется | Назначение | +| --- | --- | --- | +| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | `docker-compose.yml` | Параметры локального контейнера PostgreSQL (`postgres`/`admin`/`postgres`) | +| build-stage `golang:1.24` | `Dockerfile` | Базовый образ для сборки бинарников `http` и `cli` | +| runtime `alpine:latest` | `Dockerfile` | Финальный образ; копируются `http`, `cli`, `migrations/`, `entrypoint.sh`; открыт порт `8080` | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `envs` (в текущих values он пуст: `envs: []`). Значения из секретов (блок `secrets`) монтируются как env через `secretKeyRef` в `.helm/templates/deployment.yaml`: + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `DB_URL` | `ya-pg-secret` | `db_url` | +| `PUBLIC_KEY` | `public-key` | `key` | + +Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы, `service_name`/`service_port`), `api.*` (host/prefix/path ingress), `imagePullSecrets`. + +Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `ya-pg-secret` (ключ `certificate`) как файл `/opt/.postgresql/root.crt` (см. `deployment.yaml`). Секрет `ya-pg-secret` при отсутствии создаётся шаблоном `ya-pg-secret.yaml` со случайными значениями и политикой `helm.sh/resource-policy: keep`. + +Параметры окружений (`.helm/values-.yaml`): + +| Окружение | `api.host` | `deployment.service_port` | +| --- | --- | --- | +| stage | `stage-api.sarex.io` | `8080` | +| preprod | `api.preprod.sarex.io` | `80` | +| production | `api.sarex.io` | `8080` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | Namespace | Chart version | +| --- | --- | --- | --- | +| ветка `master` | `preprod` | `contracts-preprod` | `0.0.1-preprod` | +| ветка `stage` | `stage` | `contracts-stage` | `0.0.1-stage` | +| тег (`CI_COMMIT_TAG`) | `prod` | `contracts-prod` | `0.0.1-prod` | + +Общие переменные пайплайна: `RELEASE_NAME=contracts`, `CHART_NAME=contracts`, `IMAGE_PATH=deployment.image`, `HELM_SET_ARGS="--set deployment.image=${IMAGE_NAME}"`, `DOCKERFILE_PATH=Dockerfile`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY` (`true`), `ENABLE_LINTER` (`false`). Для merge request-ов пайплайн запускается без деплоя. + +## Замечания и потенциальные проблемы + +- **Дублирование ключа авторизации.** В `Config` объявлено поле `Auth.PublicKey` с тегом `AUTH_PUBLIC_KEY`, но `cmd/http/main.go` дополнительно читает `os.Getenv("PUBLIC_KEY")` и перезаписывает им значение. В Helm секрет прокидывается как `PUBLIC_KEY`. Практически используется именно `PUBLIC_KEY`; `AUTH_PUBLIC_KEY` в текущем деплое не задаётся. +- **`.env` загружается автоматически** (в отличие от Python-сервисов): `godotenv.Load(".env")` в HTTP-процессе и `godotenv.Load(ENV_FILE|.env)` в CLI. Файл `.env` при этом попадает под `.gitignore` (`*.env`) и в репозиторий не коммитится. +- **Пустой `PUBLIC_KEY` — фатально.** `auth.New` делает `panic`, если ключ не удаётся распарсить как PEM/PKIX. Для локального запуска нужен валидный публичный ключ. +- **`DB_URL` обязателен и для http, и для cli.** Невалидный DSN приводит к ошибке `pgxpool.ParseConfig`/подключения; в CLI пустой `DB_URL` даёт явную ошибку `DB_URL is required`. +- **`envs: []` в values.** Все прикладные переменные в k8s сейчас приходят только из секретов (`DB_URL`, `PUBLIC_KEY`); `LOG_LEVEL`/`ADDRESS` используют дефолты (`debug`, `:8080`). + +## Минимальный набор для локального запуска + +PostgreSQL поднимается через `docker-compose up postgres`, приложение — сборкой `cmd/http` (или целиком через docker-compose). Минимально необходимо задать: + +- `DB_URL` — DSN до PostgreSQL (для локали обычно `?sslmode=disable`) +- `PUBLIC_KEY` — валидный публичный RSA-ключ (PEM) для проверки JWT +- при необходимости: `LOG_LEVEL`, `ADDRESS`, `DB_POOL_SIZE` (иначе применяются дефолты) +- для миграций (`cli migrate`): `DB_URL` и, при нестандартном расположении, `DB_MIGRATIONS_PATH` + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/contracts/ENDPOINTS.md b/apps/contracts/ENDPOINTS.md new file mode 100644 index 0000000..1c5a35d --- /dev/null +++ b/apps/contracts/ENDPOINTS.md @@ -0,0 +1,64 @@ +# Эндпоинты, с которыми взаимодействует contracts-frontend + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `contracts-frontend`). + +## Как устроено взаимодействие + +Запросы сгруппированы по доменам в каталоге `src/shared/api/fetch/*.api.ts`. Каждая функция вызывает соответствующий метод `httpService` (`src/shared/api/http-service.ts`), который создаётся фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). Для запроса указываются: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (добавляется к базовому хосту сервиса); +- `data` — тело запроса (для `post`/`put`); +- `axiosConfig.params` — query-параметры; +- `cache`, `queryKey` — опции кеширования (react-query-подобные ключи из `src/shared/api/keys/*`); +- `isCSRF` — включение CSRF-обработки (для `departments`). + +Базовый хост подставляется по значению `service` и текущему окружению `__BUILD_ENV__` (`local`/`stage`/`preprod`/`prod`, по умолчанию `prod`; см. `http-service.ts`). В режиме `local` для http-сервиса устанавливается тип `zitadel` (`setTypeOfHttpService("zitadel")`). Итоговый URL = `<базовый хост сервиса>` + `url`. + +## Базовые хосты по сервисам и окружениям + +Значения из `src/shared/api/hosts.ts`. Ниже перечислены сервисы, **фактически используемые** запросами модуля; в реестре хостов определены и другие сервисы (`bim`, `bimv2`, `workflows`, `workspaces`, `documentations`, `comparisons`, `remarks`, `projects`, `eavV1`, `notifications`, `google`, `sarexApi`, `zitadel`), но обращений к ним в `fetch/*` нет. + +| Сервис (`service`) | Назначение | `stage` | `prod` | +| --- | --- | --- | --- | +| `contracts` | Сервис договоров (contracts-backend) | `https://stage-api.sarex.io/contracts` | `https://api.sarex.io/contracts` | +| `sarex` | Локальный backend Sarex (core/admin) | `""` (относительные пути) | `""` | +| `gateway` | Gateway/API Sarex (ресурсы/проекты) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | + +> Также определены окружения `local` и `preprod`. В `local` сервисы проксируются на относительные пути (`contracts` → `/sarex-contracts`, `sarex` → `/sarex-backend`, `gateway` → `/sarex-gateway`, `zitadel` → `/zitadel`). Значения `preprod` используют домен `api.preprod.sarex.io`. + +## Эндпоинты по сервисам + +### `contracts` — Сервис договоров + +Определены в `src/shared/api/fetch/contract.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchContractsByResourceId` | GET | `/api/v0/contracts` | Список договоров (query: `limit`, `offset`, `resource_id`, `tenant_id`) | +| `fetchCreateContractByResourceId` | POST | `/api/v0/contracts` | Создать договор | +| `fetchUpdateContract` | PUT | `/api/v0/contracts/{contract.id}` | Обновить договор по id | + +> Функция удаления `fetchDeleteContract` (`DELETE /api/v0/contracts/{contractId}`) присутствует в коде, но закомментирована. + +### `sarex` — Локальный backend Sarex (core/admin) + +Определены в `company.api.ts`, `contractor.api.ts`, `department.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCompanies` | GET | `/api/core/admin/companies/` | Список компаний (кешируется, ключ `companies`) | +| `fetchContractors` | GET | `/api/core/admin/contractors/?company_id={companyId}` | Контрагенты компании (кешируется, ключ `contractors`) | +| `fetchDepartments` | GET | `/api/core/admin/departments/` | Отделы (кешируется, ключ `departments`, `isCSRF: true`) | + +### `gateway` — Gateway/API Sarex + +Определён в `project.api.ts`. + +| Функция | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchProjects` | GET | `/api/v1/resources/?company_id={companyId}` | Список ресурсов/проектов компании (кешируется, ключ `projects`) | + +## Обработка запросов и кеширование + +Кеширование включается флагом `cache: true` с ключом `queryKey` (значения ключей — в `src/shared/api/keys/*.ts`: `companies`, `projects`, `contractors`, `departments`). Обработка ошибок и авторизация (в т.ч. режим `zitadel` для `local`) выполняются внутри `httpService` из `@sarex-team/sdk-js`. diff --git a/apps/contracts/openapi.yaml b/apps/contracts/openapi.yaml new file mode 100644 index 0000000..81bca62 --- /dev/null +++ b/apps/contracts/openapi.yaml @@ -0,0 +1,404 @@ +openapi: 3.0.3 + +info: + title: Contracts Service API + version: "1.0.0" + description: | + REST API сервиса **contracts** (`platform/contracts`) — управление + договорами (контрактами): создание (в т.ч. массовое), получение по id, + списочный вывод с фильтрами и пагинацией, обновление. + + Сервис написан на Go (**Fiber v3**). Приложение собирается в + `internal/app/http/app.go` (`New`). Роутинг вложен под общий префикс + `/api/v0` (`app.server.Group("/api/v0")`), внутри — группа + `/contracts` (`internal/controller/http/v0`). + + ### Аутентификация + Все эндпоинты группы `/api/v0/contracts` защищены middleware + (`internal/adapter/auth/middleware.go`). Поддерживаются два режима: + + 1. **Zitadel** — если передан заголовок `Identity: Bearer `, + пользователь берётся из полезной нагрузки этого токена + (`urn:zitadel:iam:user:metadata`). Подпись на уровне приложения + не проверяется (валидность обеспечивается сетевым слоем/Istio). + 2. **sarex-backend** — если заголовка `Identity` нет, подпись основного + токена `Authorization: Bearer ` проверяется публичным RSA-ключом + (`PUBLIC_KEY`). + + Корневой эндпоинт `GET /api/v0/` (health/ping) аутентификации не требует. + + ### Идентификаторы + Идентификатор договора — **ULID** (строка), парсится через + `ulid.Parse`. `resource_id` — **UUID**. + + ### Пагинация + Списочный вывод использует `limit`/`offset` (query-параметры, по умолчанию + `limit=100`, `offset=0`). + + ### Обработка ошибок + Ошибки возвращаются с соответствующим HTTP-статусом; тело — либо строка + с описанием, либо `{"error": "..."}` (при внутренней панике). Коды: + `400` — некорректный запрос/невалидный id, `401` — проблемы аутентификации, + `403` — пользователь не состоит в компании (`tenant_id`), `404` — договор + не найден, `500` — внутренняя ошибка, `501` — метод не реализован. + + ### Замечания (расхождения кода) + - `PATCH` и `DELETE` (как по коллекции, так и по id) возвращают + **`501 Not Implemented`** — обработчики-заглушки. + - `POST /api/v0/contracts` принимает **как одиночный объект, так и массив**: + тип создания выбирается по форме тела (объект → создание одного договора, + массив → пакетное создание). + - `GET /api/v0/contracts` (список) требует query-параметр `tenant_id` + (middleware `UserInCompanyMiddleware` проверяет, что пользователь состоит + в этой компании; иначе `400`/`403`). + +servers: + - url: https://api.sarex.io/contracts + description: production + - url: https://stage-api.sarex.io/contracts + description: stage + - url: https://api.preprod.sarex.io/contracts + description: preprod + +security: + - bearerAuth: [] + +tags: + - name: contracts + description: Договоры + - name: service + description: Служебные эндпоинты + +paths: + /api/v0/: + get: + tags: [service] + summary: Health / ping + description: Возвращает `200 OK` без тела. Аутентификация не требуется. + security: [] + responses: + "200": + description: OK + + /api/v0/contracts: + post: + tags: [contracts] + summary: Создать договор или несколько договоров + description: | + Принимает либо одиночный объект `CreateContractRequest`, либо массив + таких объектов. Форма тела определяет режим создания. + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/CreateContractRequest" + - type: array + items: + $ref: "#/components/schemas/CreateContractRequest" + responses: + "201": + description: Договор(ы) создан(ы) + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ContractResponse" + - type: array + items: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Некорректное тело запроса / ошибка валидации + "401": + description: Ошибка аутентификации + "500": + description: Внутренняя ошибка + get: + tags: [contracts] + summary: Список договоров + description: | + Возвращает договоры с фильтрацией и пагинацией. Требует `tenant_id` + (проверяется принадлежность пользователя к компании). + parameters: + - name: tenant_id + in: query + required: true + schema: + type: integer + format: int64 + description: Идентификатор компании/арендатора + - name: limit + in: query + required: false + schema: + type: integer + format: int64 + default: 100 + - name: offset + in: query + required: false + schema: + type: integer + format: int64 + default: 0 + - name: resource_id + in: query + required: false + schema: + type: string + format: uuid + - name: contractor_id + in: query + required: false + schema: + type: integer + format: int64 + responses: + "200": + description: Список договоров + content: + application/json: + schema: + $ref: "#/components/schemas/ContractPaginatedResponse" + "400": + description: Некорректные параметры запроса / отсутствует tenant_id + "401": + description: Ошибка аутентификации + "403": + description: Пользователь не состоит в указанной компании + "500": + description: Внутренняя ошибка + put: + tags: [contracts] + summary: Пакетное обновление договоров + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UpdateContractRequest" + responses: + "200": + description: Договоры обновлены + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Некорректное тело запроса + "401": + description: Ошибка аутентификации + "500": + description: Внутренняя ошибка + patch: + tags: [contracts] + summary: Пакетное частичное обновление (не реализовано) + responses: + "501": + description: Not Implemented + delete: + tags: [contracts] + summary: Пакетное удаление (не реализовано) + responses: + "501": + description: Not Implemented + + /api/v0/contracts/{id}: + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Идентификатор договора (ULID) + get: + tags: [contracts] + summary: Получить договор по id + responses: + "200": + description: Договор + content: + application/json: + schema: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Невалидный id + "401": + description: Ошибка аутентификации + "404": + description: Договор не найден + "500": + description: Внутренняя ошибка + put: + tags: [contracts] + summary: Обновить договор + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateContractRequest" + responses: + "200": + description: Договор обновлён + content: + application/json: + schema: + $ref: "#/components/schemas/ContractResponse" + "400": + description: Невалидный id / некорректное тело + "401": + description: Ошибка аутентификации + "404": + description: Договор не найден + "500": + description: Внутренняя ошибка + patch: + tags: [contracts] + summary: Частичное обновление (не реализовано) + responses: + "501": + description: Not Implemented + delete: + tags: [contracts] + summary: Удалить договор (не реализовано) + responses: + "501": + description: Not Implemented + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + Основной токен `Authorization: Bearer ` (проверяется по RSA-ключу). + Опционально может передаваться заголовок `Identity: Bearer ` + (режим Zitadel), который имеет приоритет. + + schemas: + Contractor: + type: object + description: Произвольный JSON-объект с данными контрагента (в БД — JSONB). + additionalProperties: true + + CreateContractRequest: + type: object + required: [number, tenant_id, started_at, deadline_at] + properties: + number: + type: string + minLength: 1 + description: Номер договора + tenant_id: + type: integer + format: int64 + description: Идентификатор компании/арендатора + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + minimum: 0 + description: + type: string + + UpdateContractRequest: + type: object + required: [number, tenant_id, started_at, deadline_at] + properties: + id: + type: string + description: ULID договора + number: + type: string + minLength: 1 + tenant_id: + type: integer + format: int64 + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + minimum: 0 + description: + type: string + + ContractResponse: + type: object + properties: + id: + type: string + description: ULID договора + number: + type: string + tenant_id: + type: integer + format: int64 + resource_id: + type: string + format: uuid + nullable: true + contractor: + $ref: "#/components/schemas/Contractor" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + started_at: + type: string + format: date-time + deadline_at: + type: string + format: date-time + cost: + type: number + format: double + description: + type: string + + ContractPaginatedResponse: + type: object + properties: + count: + type: integer + format: int64 + limit: + type: integer + format: int64 + offset: + type: integer + format: int64 + results: + type: array + items: + $ref: "#/components/schemas/ContractResponse" diff --git a/apps/control-interface/ENDPOINTS.md b/apps/control-interface/ENDPOINTS.md new file mode 100644 index 0000000..cd653e1 --- /dev/null +++ b/apps/control-interface/ENDPOINTS.md @@ -0,0 +1,162 @@ +# Эндпоинты, с которыми взаимодействует srx-admin + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается приложение `srx-admin` (панель администрирования, деплой `control-interface`). + +## Как устроено взаимодействие + +`srx-admin` — это монорепозиторий (`admin-monorepo`) c двумя фронтенд-сервисами и общим пакетом: + +- `services/admin` — хост-приложение (основной админ-интерфейс); +- `services/assets` — федеративный модуль (Module Federation), встраиваемый в хост; +- `packages/app-kit` — общий пакет с реестром API-функций и таблицей хостов. + +Запросы описаны не единым реестром, а по доменам — в файлах `shared/api/fetch/*.api.ts`. Каждый домен экспортирует фабрику (например `UserApi`, `AssetApi`, `ProjectApi`), которая принимает `httpService` и возвращает набор методов. Внутри метода вызывается `httpService.getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest` со структурой: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (с подстановкой параметров прямо в строку или через `axiosConfig.params`); +- `data` — тело запроса (для POST/PUT/PATCH); +- `axiosConfig`, `cache`, `queryKey`, `controller`, `isCSRF` — опции axios, кеширования, ключа запроса, отмены и CSRF-токена. + +`httpService` создаётся в `shared/api/http-service.ts` через `createHttpService` из `@sarex-team/sdk-js`. Базовый хост подставляется по логическому имени `service` из `packages/app-kit/src/shared/api/hosts.ts` в зависимости от окружения сборки `__ENDPOINT__` (`BUILD_ENV`, по умолчанию `prod`). Итоговый URL = `<базовый хост сервиса>` + `url`. + +## Базовые хосты по сервисам и окружениям + +Значения из `packages/app-kit/src/shared/api/hosts.ts`. Ниже приведены `stage` и `prod`; дополнительно определены окружения `local`, `contour` и `preprod` (см. примечание). Сервисы, к которым `srx-admin` реально обращается, отмечены значком «●» в колонке «Используется». + +| Сервис (`service`) | Назначение | Используется | `stage` | `prod` | +| --- | --- | --- | --- | --- | +| `iam` | IAM: пользователи, отделы, должности, группы, права | ● | `https://stage-api.sarex.io/iam` | `https://api.sarex.io/iam` | +| `eavV1` | EAV: ассеты, атрибуты, права на ассеты, модули | ● | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` | +| `gateway` | Gateway: ресурсы (проекты) и права на ресурсы | ● | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` | +| `sarex` | Локальный сервис данных (`/api/core`, `/api/pm`, `/api/commons`) | ● | `""` (относительные пути) | `""` | +| `bimv2` | BIM v2: модели статусов | ● | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` | +| `premises` | Сервис помещений | ● | `https://stage-api.sarex.io/premises` | `https://api.sarex.io/premises` | +| `notifications` | Лямбда уведомлений (email) | ● | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` | +| `documentations` | Сервис документации | | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` | +| `workspaces` | Сервис рабочих областей | | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | +| `workflows` | Сервис обработки документов | | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | +| `comparisons` | Сервис сравнений | | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` | +| `remarks` | Сервис замечаний | | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` | +| `projects` | Сервис проектов | | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` | +| `bim` | BIM-API | | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` | +| `sarexApi` | Gateway/API Sarex (корень) | | `https://stage-api.sarex.io` | `https://api.sarex.io` | +| `google` | Временное хранилище (GCS) | | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | +| `zitadel` | IdP (аутентификация) | | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | + +> В окружении `local` сервисы проксируются на относительные пути (`sarex` → `/sarex-backend`, `gateway` → `/sarex-gateway`, `eavV1` → `/sarex-eav-v1`, `notifications` → `/sarex-notifications`, `iam` → `/iam`, `premises` → `/premises` и т. д.). Окружение `contour` использует относительные пути для изолированного контура. Подключаемые удалённые модули (Module Federation) описаны отдельно в `services/*/config/endpoints.ts` (см. раздел «Удалённые модули»). + +## Эндпоинты по сервисам + +### `iam` — IAM (пользователи, отделы, должности, группы, права) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchUsers` | GET | `/api/admin/v0/users/?{query}` | Список пользователей компании (пагинация, поиск, фильтры) | +| `fetchCreateUser` | POST | `/api/admin/v0/users/` | Создать пользователя | +| `fetchUpdateUser` | PATCH | `/api/admin/v0/users/{id}/` | Обновить пользователя | +| `fetchBulkUpdateUsers` | PATCH | `/api/admin/v0/users/` | Массовое обновление пользователей | +| `fetchBulkUpdateUsersActivation` | POST | `/api/admin/v0/users/activation/` | Массовая активация/деактивация пользователей | +| `fetchDepartments` | GET | `/api/admin/v0/departments/` | Список отделов (пагинация, поиск, фильтр по компании) | +| `fetchCreateDepartment` | POST | `/api/admin/v0/departments/` | Создать отдел (CSRF) | +| `fetchUpdateDepartment` | PUT | `/api/admin/v0/departments/{id}/` | Обновить отдел (CSRF) | +| `fetchDeleteDepartment` | DELETE | `/api/admin/v0/departments/{id}/` | Удалить отдел (CSRF) | +| `fetchPositions` | GET | `/api/admin/v0/positions` | Список должностей (пагинация, поиск, фильтр по компании) | +| `fetchCreatePosition` | POST | `/api/admin/v0/positions/` | Создать должность (CSRF) | +| `fetchUpdatePosition` | PUT | `/api/admin/v0/positions/{id}/` | Обновить должность (CSRF) | +| `fetchDeletePosition` | DELETE | `/api/admin/v0/positions/{id}/` | Удалить должность (CSRF) | +| `fetchGroups` | POST | `/api/admin/v0/groups/search/` | Поиск функциональных групп (фильтры, пагинация, сортировка) | +| `createGroup` | POST | `/api/admin/v0/groups` | Создать группу | +| `updateGroup` | PATCH | `/api/admin/v0/groups/{id}` | Обновить группу | +| `deleteGroup` | DELETE | `/api/admin/v0/groups/{id}` | Удалить группу | +| `fetchPermissions` | POST | `/api/admin/v0/permissions/search/` | Поиск прав (фильтры, пагинация, сортировка) | +| `createPermission` | POST | `/api/admin/v0/permissions` | Создать право | +| `deletePermission` | DELETE | `/api/admin/v0/permissions/{id}` | Удалить право | + +### `eavV1` — EAV (ассеты, атрибуты, права на ассеты, модули) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchGetAssetsV4` | GET | `/api/v4/assets/` | Список ассетов (v4, параметры фильтрации) | +| `fetchGetAssetsV5` | GET | `/api/v2/assets/` | Список ассетов (v5) | +| `fetchCreateBulkAssetsV4` | POST | `/api/v4/assets/` | Массовое создание ассетов (v4) | +| `fetchCreateBulkAssetsV5` | POST | `/api/v2/assets/` | Массовое создание ассетов (v5) | +| `fetchUpdateBulkAssetsV4` | PATCH | `/api/v4/assets/` | Массовое обновление ассетов (v4) | +| `fetchUpdateBulkAssetsV5` | PATCH | `/api/v2/assets/` | Массовое обновление ассетов (v5) | +| `fetchDeleteAssetV4` | DELETE | `/api/v4/assets/{assetId}/` | Удалить ассет (v4) | +| `fetchDeleteAssetV5` | DELETE | `/api/v2/assets/{assetId}/` | Удалить ассет (v5) | +| `fetchCopyRootAsset` | POST | `/api/v4/assets/{asset_id}/copy/` | Копировать корневой ассет | +| `fetchCopyAssets` | POST | `/api/v2/assets/copy-to-destination-bulk/` | Массовое копирование ассетов в назначения | +| `fecthGetAssetPermissions` | GET | `/api/v4/permissions/?asset_id={id}&service_account_id={id}` | Права доступа ассета | +| `fetchPostCreateAssetPermissions` | POST | `/api/v4/permissions/` | Создать права на ассет | +| `fetchPostUpdateAssetPermissions` | PATCH | `/api/v4/permissions/` | Обновить права на ассет | +| `fetchDeleteAssetPermissions` | DELETE | `/api/v4/permissions/{permissionId}/` | Удалить права на ассет | +| `fetchGetAssetPermissionsTree` | GET | `/api/v4/permissions/relative/?asset_id={id}` | Дерево наследуемых прав ассета | +| `fetchAttributes` | GET | `/api/v1/attribute/` | Список атрибутов компании (пагинация, поиск, фильтр по id) | +| `createAttribute` | POST | `/api/v1/attribute/` | Создать атрибут | +| `updateAttribute` | PUT | `/api/v1/attribute/{id}/` | Обновить атрибут | +| `deleteAttribute` | DELETE | `/api/v1/attribute/{attributeId}/` | Удалить атрибут | +| `fetchModules` | GET | `/api/v1/modules/` | Список модулей (CSRF) | + +### `gateway` — Gateway (ресурсы/проекты, права на ресурсы) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchProjects` | GET | `/api/v2/resources?show_all=true&limit=10000&company_id={id}` | Список проектов компании (кешируется) | +| `fetchGetProjectByProjectId` | GET | `/api/v2/resources/{projectId}` | Проект по id (кешируется) | +| `fetchCreateProject` | POST | `/api/v2/resources` | Создать проект/ресурс | +| `fetchUpdateProjectByProjectId` | PATCH | `/api/v2/resources/{projectId}` | Обновить проект | +| `fetchDeleteProjectByProjectId` | DELETE | `/api/v2/resources/{projectId}` | Удалить проект | +| `fetchParentDocumentByResourceId` | GET | `/api/v1/resources-rpc/parent-document-by-resource-id/{resourceId}` | Родительский документ по resource id | +| `fetchResources` | GET | `/api/v1/resources/?company_id={id}` | Список ресурсов компании (кешируется) | +| `fetchCreatePermission` | POST | `/api/v1/resource-permissions/` | Выдать права на ресурсы сервисному аккаунту | +| `fetchResourcesByUsersId` | POST | `/api/v1/resources/users-with-resources/` | Ресурсы по набору пользователей | +| `fetchBulkUpdateUsersPermissions` | PATCH | `/api/v1/resources/permissions-bulk/` | Массовое обновление прав на ресурсы | +| `fetchBulkUpdateUsersCompanyResourcesPermission` | POST | `/api/v1/company-resource-permissions/bulk/` | Массовая выдача прав на ресурсы компании | + +### `sarex` — Локальный сервис данных + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCoordinates` | GET | `/api/commons/cs/` | Справочник систем координат (кешируется) | +| `fetchLinksToPlanningByProjectId` | GET | `/api/pm/msp/projects/?resource_id={projectId}&strict=true` | Связи проекта с планированием (кешируется) | +| `fetchBulkUpdateUserNotifications` | PATCH | `/api/core/users/bulk/notifications/` | Массовое переключение уведомлений пользователей | +| `getMrpas` | POST | `/api/core/mrpa/list/` | Список МРПА (пагинация, фильтры, агрегации) | +| `createMrpa` | POST | `/api/core/mrpa/` | Загрузить МРПА (multipart/form-data) | +| `deleteMrpa` | DELETE | `/api/core/mrpa/{id}/` | Удалить МРПА | + +### `bimv2` — BIM v2 (модели статусов) + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchCreateCompanyStatusModel` | POST | `/api/v1/companies/{companyId}/status_model` | Создать модель статусов компании | +| `fetchGetCompanyStatusModels` | GET | `/api/v1/companies/{companyId}/status_model` | Модели статусов компании | +| `fetchGetBIMStatusModels` | GET | `/api/v1/bims/{bimId}/status_models` | Модели статусов BIM | +| `fetchUpdateBIMStatusModel` | POST | `/api/v1/bims/{bimId}/status_model` | Обновить модель статусов BIM | +| `fetchGetBIMStatuses` | POST | `/api/v1/bims/{bimId}/statuses?{search}` | Статусы BIM (с фильтром) | + +### `premises` — Сервис помещений + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getPremises` | POST | `/api/v1/premises/filter/` | Помещения по локациям и ресурсу | + +### `notifications` — Лямбда уведомлений + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `fetchSendEmail` | POST | `/` | Отправить email-уведомление (from `hello@sarex.io`) | + +## Удалённые модули (Module Federation) + +Помимо HTTP-API, `srx-admin` подгружает удалённые микрофронтенды через `remoteEntry.js`. Адреса заданы в `services/admin/config/endpoints.ts` и `services/assets/config/endpoints.ts` (объект `moduleEndpoints`). + +| Модуль | `stage` / `local` | `prod` | `contour` | +| --- | --- | --- | --- | +| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | `/documentations/static/module/remoteEntry.js` | +| `assets` | `https://stage-modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `https://modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `/control-interface/modules/assets/remoteEntry.js` | + +> В `preprod` используются хосты вида `https://modules.preprod.sarex.io/...`. + +## Обработка ошибок и авторизация + +Запросы выполняются через `httpService` (`@sarex-team/sdk-js` поверх `axios`). Для части эндпоинтов (`iam`: отделы, должности, создание пользователей/групп; `eavV1`: модули) передаётся флаг `isCSRF: true` — добавляется CSRF-токен. Ошибки обрабатываются на уровне SDK и сторов приложения; человекочитаемые сообщения задаются в сторах (`errorMessage`), например «Произошла ошибка при запросе пользователей» / «мест работы» / «ролей» / «функциональных групп». diff --git a/apps/cross-section/ENDPOINTS.md b/apps/cross-section/ENDPOINTS.md new file mode 100644 index 0000000..f82be13 --- /dev/null +++ b/apps/cross-section/ENDPOINTS.md @@ -0,0 +1,50 @@ +# Эндпоинты, с которыми взаимодействует cross-section + +Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `cross-section`). + +## Как устроено взаимодействие + +Запросы описаны в двух API-объектах в `module/api/endpoints.ts`: `crossSectionApi` (поперечные сечения) и `exportsApi` (экспорт и скачивание вложений). Каждый метод вызывает соответствующий хелпер `httpService` (`getRequest`/`postRequest`/`deleteRequest`) со структурой: + +- `service` — логическое имя сервиса (см. таблицу хостов ниже); +- `url` — путь запроса (с подстановкой параметров/query); +- `data` — опционально, тело запроса (для `POST`/`PUT`). + +`httpService` создаётся функцией `createHttpService` из `@sarex-team/sdk-js` в `module/api/http-service.ts`. Базовый хост подставляется по ключу `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV` (по умолчанию `prod`). В окружении `local` тип сервиса переключается на `original` (`setTypeOfHttpService("original")`). Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта. + +## Базовые хосты по сервисам и окружениям + +Значения из `module/api/hosts.ts`. Определены окружения `local`, `stage`, `prod`, `preprod`. + +| Сервис (`service`) | Назначение | `local` | `stage` | `prod` | `preprod` | +| --- | --- | --- | --- | --- | --- | +| `gateway` | Gateway/API Sarex (используется всеми эндпоинтами модуля) | `https://stage-api.sarex.io/gateway/` | `https://stage-api.sarex.io/gateway/` | `https://api.sarex.io/gateway/` | `https://api.preprod.sarex.io/gateway/` | +| `drawings` | Сервис чертежей | `https://stage-api.sarex.io/drawings/` | `https://stage-api.sarex.io/drawings/` | `https://api.sarex.io/drawings/` | `https://api.preprod.sarex.io/drawings/` | +| `sarex` | Локальный сервис данных (относительные пути) | `https://stage.sarex.io/` | `""` | `""` | `""` | +| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | `https://login.preprod.sarex.io` | + +> Фактически все эндпоинты модуля обращаются к сервису `gateway`. Сервисы `drawings`, `sarex` и `zitadel` объявлены в реестре хостов, но напрямую в `endpoints.ts` не используются. + +## Эндпоинты по сервисам + +### `gateway` — Gateway/API Sarex + +#### `crossSectionApi` — поперечные сечения + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `getCrossSections` | GET | `api/v1/drawings/cross-sections?instance_id={uuid}` | Список поперечных сечений по инстансу | +| `getCrossSectionData` | GET | `api/v1/drawings/cross-sections/{uuid}/data` | Данные поперечного сечения по uuid | +| `createCrossSections` | POST | `api/v1/drawings/cross-sections` | Создать поперечное сечение (тело — `model`) | +| `removeCrossSections` | DELETE | `api/v1/drawings/cross-sections/{uuid}/` | Удалить поперечное сечение | + +#### `exportsApi` — экспорт + +| Ключ | Метод | Путь | Назначение | +| --- | --- | --- | --- | +| `createExport` | POST | `api/v1/drawings/exports` | Создать экспорт (тело: `company_id`, `cross_section_id`, `file_type`; по умолчанию `file_type = "dwg"`) | +| `downloadExport` | GET | `api/v1/attachments/{attachment_id}` | Скачать вложение экспорта по id | + +## Обработка ошибок + +В модуле нет отдельного слоя маппинга ошибок (аналога `module/api/errors.ts`): обработка HTTP-ошибок выполняется на уровне `httpService` из `@sarex-team/sdk-js`. Каждый метод возвращает `response.data` (для `createExport` — весь ответ). diff --git a/apps/drawings/.env.example b/apps/drawings/.env.example new file mode 100644 index 0000000..3d8d2d7 --- /dev/null +++ b/apps/drawings/.env.example @@ -0,0 +1,30 @@ +# drawings-api — пример переменных окружения. +# Конфигурация читается через github.com/kelseyhightower/envconfig +# (config/config.go). Приложение НЕ загружает .env автоматически — +# переменные нужно экспортировать в окружение процесса самому, +# напр.: set -a && . ./.env && set +a + +# API +API_ADDRESS=localhost:6666 + +# Postgres +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_DB=drawings +POSTGRES_ADDRESS=localhost:6432 +POSTGRES_POOL_SIZE=10 +# TLS-подключение к БД. При ENABLE_SSL=true используется сертификат +# из YC-PG-CERTIFICATE (PEM-содержимое, не путь к файлу) +ENABLE_SSL=false +YC-PG-CERTIFICATE= + +# Workflow (интеграция с workflows-api через sdk-go) +WORKFLOW_HOST=http://workflows-api-service.proc/ +CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q +IMAGE_NAME_EXPORT_TO_DWG=cross-sections-to-dwg +IMAGE_TAG=develop +TASK_VERSION=1 +# Внутренний URL самого drawings-api — на него workflow вызывает webhook +DRAWING_INTERNAL_URL=http://drawings-api-service.aero/ +# URL сервиса attachments (передаётся в задачу экспорта) +ATTACHMENT_URL=http://attachments-service.documentations.svc.cluster.local:80 diff --git a/apps/drawings/CONFIGURATION.md b/apps/drawings/CONFIGURATION.md new file mode 100644 index 0000000..a8d0c34 --- /dev/null +++ b/apps/drawings/CONFIGURATION.md @@ -0,0 +1,120 @@ +# Конфигурация проекта drawings-api + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис написан на **Go** (`gitlab.com/sarex-team/rnd/drawings-api`) и настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (`envconfig.Process("", &Config)`). + +Особенности разбора: + +- **префикса нет** — переменные читаются по именам из тега `envconfig:"..."` (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`); +- вложенных секций через разделитель нет: `Config` — плоская композиция трёх структур (`Postgres`, `API`, `Workflow`), у каждого поля своё явное имя переменной; +- часть полей имеет дефолт через тег `default:"..."` (напр. `CONTAINER_REGISTRY`, `TASK_VERSION`); поля без дефолта при отсутствии переменной получают нулевое значение типа (пустая строка / `0` / `false`), ошибки старта из-за «обязательности» нет; +- при ошибке разбора (`envconfig.Process`) приложение завершается с `logger.Fatalf` (`config.MustParse`). + +Отдельного конфиг-файла (yaml/toml) у приложения нет. Файл `.env` в репозитории — только шаблон; приложение его **не загружает автоматически** (в коде нет чтения `.env`/dotenv), переменные нужно экспортировать в окружение самому. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (бинарник) | Переменные окружения процесса. `.env` — шаблон, экспортируется вручную, напр. `set -a && . ./.env && set +a`. Сборка — `make drawings-api` / `make migrations` | +| Локально (контейнер) | `Dockerfile` (multi-stage, `golang:1.22`) + `entrypoint.sh`. Переменные пробрасываются через `--env`/`--env-file` при запуске контейнера | +| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`) и выбор окружения по ветке/тегу через `workflow.rules` | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала применяются миграции (`migrations migrate`), затем стартует основной бинарник (`drawings-api`). + +Точки входа (`cmd/`): + +| Команда | Точка входа | Назначение | +| --- | --- | --- | +| `drawings-api` | `cmd/drawings-api` | HTTP API-сервер (gorilla/mux) | +| `migrations migrate` | `cmd/migrations` | Применение миграций БД (`robinjoseph08/go-pg-migrations`) | + +## Переменные приложения + +Все переменные ниже читаются кодом приложения (`config/config.go`). В столбце «Значение по умолчанию» указан дефолт из тега `default:"..."`; `—` означает, что дефолта нет (при отсутствии переменной поле получает нулевое значение типа). + +### API (`API`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера, напр. `0.0.0.0:8080` | + +### Postgres (`Postgres`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `POSTGRES_USER` | string | — | Пользователь БД | +| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | +| `POSTGRES_DB` | string | — | Имя базы данных | +| `POSTGRES_ADDRESS` | string | — | Адрес PostgreSQL в формате `host:port` | +| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (`pg.Options.PoolSize`) | +| `ENABLE_SSL` | bool | — | Подключение к БД по TLS. При `true` строится `tls.Config` из `YC-PG-CERTIFICATE` | +| `YC-PG-CERTIFICATE` | string | — | PEM-содержимое CA-сертификата PostgreSQL (не путь к файлу). Используется только при `ENABLE_SSL=true` | + +> При `ENABLE_SSL=true` из содержимого `YC-PG-CERTIFICATE` собирается пул корневых сертификатов; `ServerName` берётся из хостовой части `POSTGRES_ADDRESS`, при этом в коде выставлен `InsecureSkipVerify: true`. Имя переменной `YC-PG-CERTIFICATE` содержит дефисы (нестандартно для env), но именно так указано в теге `envconfig`. + +### Workflow (`Workflow`) + +Интеграция с сервисом workflows через `gitlab.com/sarex-team/sdk-go/pkg/workflows`: создание workflow экспорта cross-section в DWG (задача парсинга + задача webhook-уведомления). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `WORKFLOW_HOST` | string | — | Базовый URL сервиса workflows | +| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов для задач workflow | +| `IMAGE_NAME_EXPORT_TO_DWG` | string | — | Имя образа задачи экспорта cross-section в DWG | +| `IMAGE_TAG` | string | — | Тег образов задач workflow | +| `TASK_VERSION` | string | `1` | Версия задачи экспорта (параметр `version`) | +| `DRAWING_INTERNAL_URL` | string | — | Внутренний URL самого drawings-api; на него workflow вызывает webhook `POST {DRAWING_INTERNAL_URL}internal/v1/exports/{export_id}/webhook` | +| `ATTACHMENT_URL` | string | — | URL сервиса attachments (передаётся в задачу экспорта как `attachment_url`) | + +## Переменные из Helm-чарта (`.helm/values.yaml`) + +Деплой выполняется через `universal-chart` (`Chart.yaml`, зависимость `universal-chart`). Сервис `drawings-api` слушает порт `8080`; probes настроены на `/ping` (в чарте выключены). Обычные значения задаются в блоке `envs`, значения из секретов — в `secretEnvs`. Значения различаются по окружениям через ключи `_default` / `stage` / `preprod` / `production`. + +Обычные значения (`envs`) — те же переменные приложения, что описаны выше (`API_ADDRESS`, `ENABLE_SSL`, `WORKFLOW_HOST`, `CONTAINER_REGISTRY`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `TASK_VERSION`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL`), различаются адресами сервисов, тегами образов и флагом `ENABLE_SSL` по окружениям. + +Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`_default`) | Секрет (`stage`) | Ключ | +| --- | --- | --- | --- | +| `POSTGRES_USER` | `ya-pg-secret` | `drawings-postgresql-secret` | `username` | +| `POSTGRES_PASSWORD` | `ya-pg-secret` | `drawings-postgresql-secret` | `password` | +| `POSTGRES_DB` | `ya-pg-secret` | `drawings-postgresql-secret` | `database` | +| `POSTGRES_POOL_SIZE` | `ya-pg-secret` | `drawings-postgresql-secret` | `pool-size` | +| `POSTGRES_ADDRESS` | `ya-pg-secret` | `drawings-postgresql-secret` | `address` | +| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `drawings-postgresql-secret` | `ca.crt` | + +## Переменные в CI (`.gitlab-ci.yml`) + +Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`: + +| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | +| --- | --- | --- | --- | --- | +| ветка `stage` | `stage` | `aero` | `0.0.1-stage` | `universal-chart-stage` | +| ветка `master` | `preprod` | `drawings-preprod` | `0.0.1-preprod` | `universal-chart-preprod` | +| тег (`CI_COMMIT_TAG`) | `production` | `drawings-prod` | `0.0.1-prod` | `universal-chart-production` | +| merge request | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) | + +Ключевые переменные пайплайна: `SERVICE_NAME=drawings-api`, `DOCKERFILE_PATH=./Dockerfile`, `RELEASE_NAME=drawings-api`, `CHART_NAME=${SERVICE_NAME}`, `BUILD_ARGS` (`--build-arg CI_COMMIT_SHORT_SHA=…`), `HELM_SET_ARGS` (`--set universal-chart.services.drawings-api.image.name.=…`, `--set universal-chart.global.env=`, а также `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner`). + +## Замечания и потенциальные проблемы + +- **Нет обязательности полей.** В отличие от pydantic-конфигов других сервисов, `envconfig` не помечает поля обязательными — при отсутствии переменной поле молча получает нулевое значение. Например, пустой `POSTGRES_ADDRESS` не вызовет ошибку старта конфига, но приведёт к ошибке при подключении к БД. +- **Имя `YC-PG-CERTIFICATE` с дефисами** нестандартно для переменных окружения, но именно так задано в теге `envconfig` и в Helm-секрете. В отличие от других сервисов, здесь это **содержимое** сертификата (PEM), а не путь к файлу. +- **TLS к БД с `InsecureSkipVerify: true`.** При `ENABLE_SSL=true` корневой сертификат подхватывается, но проверка имени/цепочки фактически ослаблена флагом `InsecureSkipVerify`. +- **Webhook-петля.** `DRAWING_INTERNAL_URL` должен указывать на сам drawings-api внутри кластера — по нему workflow дергает `POST internal/v1/exports/{export_id}/webhook` для перевода экспорта в статус `done`. Неверный URL оставит экспорты в статусе `running`. +- **`.env` не загружается автоматически** — переменные нужно экспортировать вручную либо задавать через окружение контейнера. + +## Минимальный набор для локального запуска + +Минимально необходимо задать: + +- `API_ADDRESS` (напр. `localhost:6666`) +- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_ADDRESS`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL` (`false` локально; тогда `YC-PG-CERTIFICATE` не нужен) +- `WORKFLOW_HOST`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL` (для сценариев экспорта; `CONTAINER_REGISTRY` и `TASK_VERSION` имеют дефолты) + +Готовые значения-примеры приведены в `.env.example`. diff --git a/apps/drawings/openapi.yaml b/apps/drawings/openapi.yaml new file mode 100644 index 0000000..9d504c7 --- /dev/null +++ b/apps/drawings/openapi.yaml @@ -0,0 +1,426 @@ +openapi: 3.0.3 + +info: + title: Drawings API + version: "0.0.1" + description: | + REST API сервиса **drawings-api** (`gitlab.com/sarex-team/rnd/drawings-api`) — + управление разрезами (cross-sections) чертежей, их данными и экспортом + в DWG через сервис workflows. + + Сервис написан на **Go** (gorilla/mux, go-pg). Роутер собирается в + `cmd/drawings-api/bootstrap.go`. Помимо служебных эндпоинтов + (`/ping`, `/metrics`) есть две группы бизнес-маршрутов с одинаковым + набором операций: + + - `/api/v1/*` — публичный роутинг; + - `/internal/v1/*` — внутренний роутинг (набор тот же плюс webhook + экспорта, вызываемый воркером workflow). + + На все бизнес-маршруты навешены middleware: JSON-ответ (`rest.JSONResponse`), + request-id (`reqid.Middleware`) и логирование. Явной аутентификации в коде + сервиса нет — доступ ограничивается на уровне ingress/сети кластера. + + ### Экспорт в DWG + `POST /exports` создаёт запись экспорта и запускает workflow из двух задач + (парсинг cross-section в DWG + webhook-уведомление). По завершении workflow + вызывает `POST /internal/v1/exports/{export_id}/webhook`, который переводит + экспорт в статус `done`. + +servers: + - url: /api/v1 + description: Публичный префикс + - url: /internal/v1 + description: Внутренний префикс + +tags: + - name: service + description: Служебные эндпоинты + - name: cross-sections + description: Разрезы чертежей + - name: exports + description: Экспорт разрезов в DWG + +paths: + /ping: + get: + tags: [service] + summary: Healthcheck + description: Возвращает статус готовности. Доступен в корне (без префикса). + responses: + "200": + description: Сервис готов + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ready + + /metrics: + get: + tags: [service] + summary: Prometheus-метрики + description: Метрики в формате Prometheus. Доступен в корне (без префикса). + responses: + "200": + description: Метрики + content: + text/plain: + schema: + type: string + + # ----- Публичные маршруты (/api/v1) и внутренние (/internal/v1) идентичны, + # кроме webhook, который есть только на /internal/v1. Пути ниже указаны + # относительно префикса из блока servers. ----- + + /cross-sections: + post: + tags: [cross-sections] + summary: Создать разрез + description: Создаёт cross-section вместе с его данными (`data.raw_data`). + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCrossSectionRequest" + responses: + "200": + description: Созданный разрез + content: + application/json: + schema: + $ref: "#/components/schemas/CrossSection" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + get: + tags: [cross-sections] + summary: Список разрезов по instance_id + parameters: + - name: instance_id + in: query + required: true + description: UUID инстанса (чертежа) + schema: + type: string + format: uuid + responses: + "200": + description: Массив разрезов (пустой, если ничего не найдено) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CrossSection" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /cross-sections/{cs_id}: + delete: + tags: [cross-sections] + summary: Удалить разрез (soft-delete) + parameters: + - $ref: "#/components/parameters/CrossSectionId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "404": + description: Разрез не найден (возможно, уже удалён) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/StorageError" + + /cross-sections/{cs_id}/data: + get: + tags: [cross-sections] + summary: Данные разреза + parameters: + - $ref: "#/components/parameters/CrossSectionId" + responses: + "200": + description: Данные разреза + content: + application/json: + schema: + $ref: "#/components/schemas/Data" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports: + post: + tags: [exports] + summary: Создать экспорт разреза в DWG + description: | + Создаёт запись экспорта и запускает workflow экспорта в DWG. + В ответе `workflow_status` = `running`. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateExportRequest" + responses: + "200": + description: Созданный экспорт + content: + application/json: + schema: + $ref: "#/components/schemas/Export" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + get: + tags: [exports] + summary: Список экспортов по фильтрам + description: | + Хотя бы один из фильтров должен быть задан, иначе `400`. + Каждый параметр — список UUID/чисел через запятую. + parameters: + - name: cross_section_ids + in: query + required: false + description: UUID разрезов через запятую + schema: + type: string + - name: export_ids + in: query + required: false + description: UUID экспортов через запятую + schema: + type: string + - name: workflow_ids + in: query + required: false + description: UUID workflow через запятую + schema: + type: string + - name: attachment_ids + in: query + required: false + description: ID вложений (целые) через запятую + schema: + type: string + responses: + "200": + description: Массив экспортов (пустой, если ничего не найдено) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Export" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports/{export_id}: + delete: + tags: [exports] + summary: Удалить экспорт + parameters: + - $ref: "#/components/parameters/ExportId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + + /exports/{export_id}/webhook: + post: + tags: [exports] + summary: Webhook завершения экспорта (только /internal/v1) + description: | + Вызывается воркером workflow по завершении экспорта. Переводит + экспорт в статус `done`. Доступен только по внутреннему префиксу + `/internal/v1`. + parameters: + - $ref: "#/components/parameters/ExportId" + responses: + "200": + $ref: "#/components/responses/OK" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/StorageError" + +components: + parameters: + CrossSectionId: + name: cs_id + in: path + required: true + description: UUID разреза + schema: + type: string + format: uuid + ExportId: + name: export_id + in: path + required: true + description: UUID экспорта + schema: + type: string + format: uuid + + responses: + OK: + description: Успешно (тело — строка `"OK"`) + content: + application/json: + schema: + type: string + example: OK + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + StorageError: + description: Внутренняя ошибка (ошибка хранилища) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + schemas: + CrossSection: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + created_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + instance_id: + type: string + format: uuid + documents: + type: object + additionalProperties: + $ref: "#/components/schemas/ConnectedDocument" + exports: + type: array + items: + $ref: "#/components/schemas/Export" + + ConnectedDocument: + type: object + properties: + color: + type: string + name: + type: string + + Data: + type: object + properties: + cross_section_id: + type: string + format: uuid + raw_data: + type: string + + CreateCrossSectionRequest: + type: object + description: Разрез плюс его данные. Наследует поля CrossSection. + allOf: + - $ref: "#/components/schemas/CrossSection" + - type: object + properties: + data: + $ref: "#/components/schemas/Data" + + Author: + type: object + properties: + id: + type: integer + format: int64 + first_name: + type: string + last_name: + type: string + + Export: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + author: + $ref: "#/components/schemas/Author" + created_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + cross_section_id: + type: string + format: uuid + workflow_id: + type: string + format: uuid + nullable: true + workflow_status: + type: string + nullable: true + enum: [done, running, error] + file_type: + type: string + enum: [dwg] + attachment_id: + type: integer + nullable: true + + CreateExportRequest: + type: object + required: [cross_section_id, author, file_type, company_id] + properties: + cross_section_id: + type: string + format: uuid + author: + $ref: "#/components/schemas/Author" + file_type: + type: string + enum: [dwg] + company_id: + type: integer + format: int64 + + Error: + type: object + description: Ответ об ошибке (gotools/httperror). + properties: + error: + type: string diff --git a/apps/eav/.env.example b/apps/eav/.env.example new file mode 100644 index 0000000..7244416 --- /dev/null +++ b/apps/eav/.env.example @@ -0,0 +1,54 @@ +# Django +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_DEBUG=False +DJANGO_SECRET_KEY='v628rpgi^!!57jq9y7y3^by04c1bc@#6%0_a(ekxfmyat8gxew' + +# App +SERVICE_NAME=eav +VERSION=1.0.0 + +# Database (PostgreSQL) — читается только в config.settings.production +DJANGO_POSTGRES_HOST=127.0.0.1 +DJANGO_POSTGRES_PORT=6432 +DJANGO_POSTGRES_DATABASE=eav_db +DJANGO_POSTGRES_USER=sarex +DJANGO_POSTGRES_PASSWORD=password + +# Auth / JWT (RS512) — обязательны в config.settings.production +SIMPLE_JWT_ISSUER=django +# Replace newlines with \n +JWT_PRIVATE_KEY='' +JWT_PUBLIC_KEY='' + +# S3 (Yandex Object Storage, бото3) +YC_S3_ACCESS_KEY_ID= +YC_S3_SECRET_ACCESS_KEY= +YC_S3_BUCKET_NAME=eav +YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net + +# Kafka +KAFKA_ENABLED=True +KAFKA_HOST= +KAFKA_USERNAME=platform +KAFKA_PASSWORD= +KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt +SASL_MECHANISM=SCRAM-SHA-512 +SECURITY_PROTOCOL=SASL_SSL +ASSETS_TOPIC=assets_broadcast_test + +# Kafka topics (события EAV) +KAFKA_TOPIC_ATTRIBUTE_CREATED=eav.attribute.created.v1 +KAFKA_TOPIC_ATTRIBUTE_UPDATED=eav.attribute.updated.v1 +KAFKA_TOPIC_ATTRIBUTE_DELETED=eav.attribute.deleted.v1 +KAFKA_TOPIC_VALUE_OPTION_CREATED=eav.value_option.created.v1 +KAFKA_TOPIC_VALUE_OPTION_DELETED=eav.value_option.deleted.v1 + +# OpenTelemetry (трейсинг включается только если USE_OTEL задана) +USE_OTEL=False +SERVICE_NAME=eav.eav-backend +TRACER_ENDPOINT=localhost:4375 +USE_INSECURE=False +ENVIRONMENT=prod +MODULE=eav +TEAM=platform_team +COMPONENT=backend diff --git a/apps/eav/CONFIGURATION.md b/apps/eav/CONFIGURATION.md new file mode 100644 index 0000000..804ee9b --- /dev/null +++ b/apps/eav/CONFIGURATION.md @@ -0,0 +1,186 @@ +# Конфигурация проекта eav-python + +Документ описывает все переменные окружения и способы конфигурирования сервиса. + +## Способы конфигурирования + +Сервис — это Django-приложение (**Django 4.1 + Django REST Framework**), запускаемое как WSGI (`config.wsgi`) через **uWSGI** (порт `8000`, см. `compose/eav-backend/uwsgi.ini`). Настройки читаются из переменных окружения в `src/config/settings/base.py` и `src/config/settings/production.py`. Разбор выполняется частично через библиотеку [`django-environ`](https://django-environ.readthedocs.io/) (объект `env = environ.Env()`), частично напрямую через `os.getenv`. + +Особенности разбора: + +- **префикса/делимитера у секций нет** — каждая настройка задаётся плоской переменной окружения (напр. `DJANGO_POSTGRES_HOST`, `KAFKA_HOST`, `YC_S3_BUCKET_NAME`); +- **`.env` не загружается автоматически** — в коде нет вызова `environ.Env.read_env()` / `load_dotenv`, хотя `python-dotenv` присутствует в зависимостях. Переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s. Файл `.env` при этом в `.gitignore`; +- **выбор набора настроек** задаётся `DJANGO_SETTINGS_MODULE`: `config.settings.production` (боевой набор с БД, CORS, JWT), `config.settings.test` (только `base`), либо `config.settings.local` (по умолчанию в `manage.py`, в репозитории отсутствует, `.gitignore`); +- **часть переменных читается только в `production.py`** — БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`); в `base.py`/`test.py` их нет. + +Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (manage.py / uWSGI) | Переменные окружения процесса (`.env` нужно экспортировать вручную) | +| Локально (docker-compose) | `docker-compose.yml`: блок `environment` сервиса `backend` + образ `postgres` (timescaledb-postgis) | +| Kubernetes (Helm, репозиторий приложения) | `.helm/values-.yaml`: блоки `backend.deployment.envs` (обычные значения) и `backend.deployment.secrets` (из k8s-секретов через `secretKeyRef`); шаблон `templates/server.yaml`, роутинг — `templates/mesh-config.yaml` (Istio VirtualService) | +| Kubernetes (infra, Flux/Kustomize) | `infra/iac/apps/eav/base/backend-deployment.yaml`: секреты инъектируются Vault-агентом (`vault.hashicorp.com/agent-inject-*`) и экспортируются в окружение в `args`; настройки `production.py` монтируются из `django-configmap` | +| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci`, переменные пайплайна в `workflow.rules` | + +Способы запуска процессов: + +| Процесс | Точка входа | Назначение | +| --- | --- | --- | +| HTTP API | `compose/eav-backend/entrypoint.sh` → `uwsgi --ini uwsgi.ini` (`config.wsgi`, порт 8000) | REST API | +| Миграции | `entrypoint.sh` → `python3 manage.py migrate` (выполняется перед стартом uWSGI) | Миграции БД | +| Kafka-продюсер | `config/kafka.py` (инициализируется при импорте, если `KAFKA_ENABLED`) | Публикация событий EAV в топики | + +Порядок запуска в контейнере (`entrypoint.sh`): сначала `manage.py migrate`, затем `uwsgi` (оба под `opentelemetry-instrument`). + +## Переменные приложения + +Дефолт `—` означает, что значения по умолчанию в коде нет. + +### Django / приложение + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_SETTINGS_MODULE` | string | `config.settings.local` (в `manage.py`); в контейнере — `config.settings.production` | Какой набор настроек Django загружать | +| `DJANGO_DEBUG` | bool | `False` | Режим отладки Django (в `production.py` жёстко `False`) | +| `DJANGO_SECRET_KEY` | string | (захардкоженный дефолт) | Секретный ключ Django. В проде обязателен свой | +| `SERVICE_NAME` | string | `eav` | Имя сервиса (в `base.py`); в OTEL-секции дефолт `eav.eav-backend` | +| `VERSION` | string | `1.0.0` | Версия приложения | + +### Database — PostgreSQL (только `config.settings.production`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `DJANGO_POSTGRES_HOST` | string | — | Хост PostgreSQL | +| `DJANGO_POSTGRES_PORT` | int | `6432` | Порт PostgreSQL (в infra-манифесте — `5432`) | +| `DJANGO_POSTGRES_DATABASE` | string | — | Имя базы данных | +| `DJANGO_POSTGRES_USER` | string | — | Пользователь БД | +| `DJANGO_POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | + +> Engine — `django.db.backends.postgresql`. В `docker-compose.yml` поднимается `timescale/timescaledb-postgis` (проекту нужны расширения PostGIS/ltree). + +### Auth / JWT (только `config.settings.production`) + +Используются два механизма аутентификации (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): `ZitadelJWTAuthentication` (заголовок `Identity`) и `rest_framework_simplejwt` (RS512). + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `SIMPLE_JWT_ISSUER` | string | `django` | Значение claim `iss` (проверяется при верификации токена) | +| `JWT_PRIVATE_KEY` | string (PEM) | — | Приватный RSA-ключ (подпись). Экранированные `\n` заменяются на переводы строк. Обязателен | +| `JWT_PUBLIC_KEY` | string (PEM) | — | Публичный RSA-ключ (проверка). Экранированные `\n` заменяются на переводы строк. Обязателен | + +> `SIMPLE_JWT`: алгоритм `RS512`, `ACCESS_TOKEN_LIFETIME` 5 мин, `REFRESH_TOKEN_LIFETIME` 1 день, тип заголовка `Bearer`, claim пользователя — `user_id`. + +### S3 — Yandex Object Storage (`base.py`, boto3/django-storages) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | `None` | Access key | +| `YC_S3_SECRET_ACCESS_KEY` | string | `None` | Secret key | +| `YC_S3_BUCKET_NAME` | string | `None` | Бакет по умолчанию | +| `YC_S3_ENDPOINT_URL` | string | `None` | Эндпоинт S3 | + +> `DEFAULT_FILE_STORAGE`/`STATICFILES_STORAGE` — `storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL=public-read`. + +### Kafka (`base.py`, `config/kafka.py`) + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `KAFKA_ENABLED` | bool | `True` | Включить реального продюсера (иначе `MockProducer` — события не отправляются) | +| `KAFKA_HOST` | string | `""` | Адрес брокера (`bootstrap_servers`) | +| `KAFKA_USERNAME` | string | `platform` | Пользователь SASL | +| `KAFKA_PASSWORD` | string | `""` | Пароль SASL | +| `KAFKA_SSL_CAFILE` | string | `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt` | CA-сертификат для TLS | +| `SASL_MECHANISM` | string | `SCRAM-SHA-512` | Механизм SASL | +| `SECURITY_PROTOCOL` | string | `SASL_SSL` | Протокол безопасности Kafka | +| `ASSETS_TOPIC` | string | `assets_broadcast_test` | Топик рассылки по ассетам | + +Топики событий EAV: + +| Переменная | Значение по умолчанию | +| --- | --- | +| `KAFKA_TOPIC_ATTRIBUTE_CREATED` | `eav.attribute.created.v1` | +| `KAFKA_TOPIC_ATTRIBUTE_UPDATED` | `eav.attribute.updated.v1` | +| `KAFKA_TOPIC_ATTRIBUTE_DELETED` | `eav.attribute.deleted.v1` | +| `KAFKA_TOPIC_VALUE_OPTION_CREATED` | `eav.value_option.created.v1` | +| `KAFKA_TOPIC_VALUE_OPTION_DELETED` | `eav.value_option.deleted.v1` | + +### OpenTelemetry (`base.py`) + +Блок трейсинга активируется, только если задана переменная `USE_OTEL` (проверяется через `os.getenv('USE_OTEL', False)` — истинно при любом непустом значении). Используется `django-otel-tools`; при включении в начало `MIDDLEWARE` добавляется `OtelMiddleware`. + +| Переменная | Тип | Значение по умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool/string | `False` | Включить трейсинг и OTEL-логгер | +| `SERVICE_NAME` | string | `eav.eav-backend` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool/string | `False` | Небезопасное (без TLS) подключение к коллектору | +| `ENVIRONMENT` | string | `prod` | Атрибут ресурса `environment` | +| `MODULE` | string | `eav` | Атрибут ресурса `module` | +| `TEAM` | string | `platform_team` | Атрибут ресурса `team` | +| `COMPONENT` | string | `backend` | Атрибут ресурса `component` | + +## Переменные из Helm-чарта (`.helm/values-.yaml`) + +Обычные значения задаются в блоке `backend.deployment.envs` для каждого окружения (`stage`/`preprod`/`production`): `DJANGO_SETTINGS_MODULE`, `USE_OTEL`, `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE`, `ENVIRONMENT`, `KAFKA_HOST`, `ASSETS_TOPIC` (различаются адресами коллектора/брокера и именами топиков). + +Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`): + +| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) | +| --- | --- | --- | +| `DJANGO_POSTGRES_HOST` | `yc-pg-secret` | `host` | +| `DJANGO_POSTGRES_DATABASE` | `yc-pg-secret` | `database` | +| `DJANGO_POSTGRES_PORT` | `yc-pg-secret` | `port` (только preprod) | +| `DJANGO_POSTGRES_USER` | `yc-pg-secret` | `user` | +| `DJANGO_POSTGRES_PASSWORD` | `yc-pg-secret` | `password` | +| `DJANGO_CLICKHOUSE_HOST` | `yc-ch-secret` | `host` | +| `DJANGO_CLICKHOUSE_DATABASE` | `yc-ch-secret` | `database` | +| `DJANGO_CLICKHOUSE_USER` | `yc-ch-secret` | `user` | +| `DJANGO_CLICKHOUSE_PASSWORD` | `yc-ch-secret` | `password` | +| `YC_S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` | +| `YC_S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` | +| `YC_S3_BUCKET_NAME` | `yc-s3-secret` | `storage_bucket_name` | +| `YC_S3_ENDPOINT_URL` | `yc-s3-secret` | `endpoint_url` | +| `JWT_PRIVATE_KEY` | `jwt-secret` | `private_key` | +| `JWT_PUBLIC_KEY` | `jwt-secret` | `public_key` | +| `KAFKA_USERNAME` | `kafka-secret` / `yc-kafka-secret` | `username` | +| `KAFKA_PASSWORD` | `kafka-secret` / `yc-kafka-secret` | `password` | +| `KAFKA_HOST` | `yc-kafka-secret` | `host` (prod/stage) | + +Помимо env, чарт монтирует CA-сертификаты: PostgreSQL (`yc-pg-certificate` → `~/.postgresql/root.crt`) и Yandex Internal Root CA (`yc-ch-certificate` → `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt`, тот же путь, что в `KAFKA_SSL_CAFILE`), а также конфиг clickhouse-client. + +## Переменные в infra-манифесте (Flux/Kustomize, `infra/iac/apps/eav`) + +В отличие от Helm-чарта приложения, боевой деплой Sarex использует Vault-инъекцию (`base/backend-deployment.yaml`). Секреты рендерятся Vault-агентом в файлы `/vault/secrets/*` и экспортируются в окружение в `args` контейнера перед запуском `entrypoint.sh`: + +| Переменная(ые) | Источник (Vault path) | +| --- | --- | +| `DJANGO_POSTGRES_HOST/PORT/DATABASE/USER/PASSWORD` | `secrets/data/postgresql/apps/eav` | +| `YC_S3_ENDPOINT_URL/BUCKET_NAME/ACCESS_KEY_ID/SECRET_ACCESS_KEY` | `secrets/data/minio/apps/eav` | +| `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` | `secrets/data/vault/common/rsa_keys` | + +Прямо в `env` деплоймента задаются `KAFKA_ENABLED=False`, `ASSETS_TOPIC=sarex`, `DJANGO_SETTINGS_MODULE=config.settings.production`. Файл `production.py` монтируется из `django-configmap` (переопределяет `production.py` из образа; в нём `DEBUG=True`, `ALLOWED_HOSTS=['*']`, свои CORS/CSRF-домены и имена cookie `eav-sessionid`/`eav-csrftoken`). + +## Замечания и потенциальные проблемы + +- Приложение **не загружает `.env` автоматически** (нет `read_env`/`load_dotenv`). `python-dotenv` установлен, но не используется в настройках — переменные нужно экспортировать в окружение самому. +- Переменные БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`) читаются **только** в `config.settings.production`. При `test`/`base` их отсутствие не мешает старту, но БД по умолчанию не сконфигурирована. +- `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY` в `production.py` читаются через `env.str(...)` **без дефолта** — их отсутствие приводит к ошибке старта. В infra-варианте (`django-configmap`) используется `get_env_variable` с тем же требованием. +- `DJANGO_CLICKHOUSE_*` присутствуют в Helm-секретах, но **кодом приложения не читаются** (в текущих настройках ClickHouse не используется) — это подготовка/наследие инфраструктуры. +- `KAFKA_ENABLED`: при ложном значении используется `MockProducer` — события EAV в Kafka не публикуются (так сделано в infra-деплое: `KAFKA_ENABLED=False`). Значение разбирается `django-environ` как bool. +- Флаги OTEL (`USE_OTEL`, `USE_INSECURE`) читаются через `os.getenv(..., False)` и трактуются как истинные при **любой непустой строке**, включая `"False"`. Чтобы отключить — переменную нужно не задавать вовсе. +- `SERVICE_NAME` определяется дважды: как имя приложения (`base.py`, дефолт `eav`) и как имя сервиса в OTEL (дефолт `eav.eav-backend`) — фактически одна и та же переменная окружения. +- В `docker-compose.yml` захардкожен пароль БД (`zealot096`) — только для локального окружения. + +## Минимальный набор для локального запуска (`config.settings.production`) + +- `DJANGO_SETTINGS_MODULE=config.settings.production` +- `DJANGO_POSTGRES_HOST`, `DJANGO_POSTGRES_PORT`, `DJANGO_POSTGRES_DATABASE`, `DJANGO_POSTGRES_USER`, `DJANGO_POSTGRES_PASSWORD` +- `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` (обязательны; можно тестовую RSA-пару) +- `KAFKA_ENABLED=False` (чтобы не поднимать брокер) либо `KAFKA_HOST`/`KAFKA_USERNAME`/`KAFKA_PASSWORD` +- при работе с файлами: `YC_S3_ACCESS_KEY_ID`, `YC_S3_SECRET_ACCESS_KEY`, `YC_S3_BUCKET_NAME`, `YC_S3_ENDPOINT_URL` +- `USE_OTEL` — не задавать (иначе включится трейсинг) + +Готовые значения-примеры для всех переменных приведены в `.env.example`. diff --git a/apps/eav/openapi.yaml b/apps/eav/openapi.yaml new file mode 100644 index 0000000..236f17d --- /dev/null +++ b/apps/eav/openapi.yaml @@ -0,0 +1,1303 @@ +openapi: 3.0.3 + +info: + title: EAV Service API + version: "1.0.0" + description: | + REST API сервиса **eav-python** (`platform/eav-python`) — реализация паттерна + **EAV (Entity-Attribute-Value)** для платформы Sarex: управление атрибутами, + группами атрибутов, единицами измерения, опциями значений, схемами (доменами) + и ассетами. Компонент используется всеми модулями платформы (инспекции, + документы, задачи КСГ, замечания, BIM и т.д.) для гибкой атрибуции моделей. + + Сервис написан на Python (**Django 4.1 + Django REST Framework**) и запускается + как WSGI-приложение (`config.wsgi`) через uWSGI (порт `8000`). Роутинг задан в + `config/urls.py`. Внутри приложения существует несколько версий API, которые + снаружи публикуются под собственными префиксами через Istio VirtualService + (`.helm/templates/mesh-config.yaml`): + + | Внешний префикс (ingress) | Внутренний путь (приложение) | Назначение | + | --- | --- | --- | + | `/eav/api/v0` | `/api/v4` | Публичный API (защищённый дубликат v0) | + | `/eav/api/v1` | `/api/v6` | Публичный API (защищённый дубликат v1) | + | `/eav/api/v2` | `/api/v5` | Публичный API (защищённый дубликат v2) | + | `/eav/api/v3` | `/api/v3` | Публичный API v3 | + | `/eav/api/v4` | `/api/v4` | Публичный API v4 | + | `/eav/admin/` | `/eav/admin/` | Django-admin | + + Внутренние (не опубликованные через ingress) версии `/api/v0`, `/api/v1`, + `/api/v2` — незащищённые (исторические) варианты тех же ресурсов; + `/api/v4`–`/api/v6` — их защищённые дубликаты. Ниже документированы ресурсы + на примере пути `/api/v0/*` (форма запросов/ответов у соответствующих + защищённых версий совпадает). + + ### Аутентификация + Проверка выполняется по цепочке DRF + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): + + 1. **Zitadel** (`ZitadelJWTAuthentication`) — требуются одновременно заголовки + `Authorization: Bearer ` и `Identity: Identity `. Полезная нагрузка + (в т.ч. `tenant_identifier`) берётся из токена `Identity`. При отсутствии + заголовка `Identity` — переход к следующему механизму. + 2. **sarex-backend** (`rest_framework_simplejwt.JWTAuthentication`) — подпись + токена `Authorization: Bearer ` проверяется публичным RSA-ключом + (`JWT_PUBLIC_KEY`, алгоритм `RS512`). + 3. Дополнительно поддерживаются `SessionAuthentication` и `BasicAuthentication` + (для Django-admin / служебного доступа). + + Глобальные права — `AllowAny` (`DEFAULT_PERMISSION_CLASSES`); ограничение + доступа к отдельным ресурсам обеспечивается на уровне view/Istio. + + ### Пагинация + Списочные ответы используют DRF `LimitOffsetPagination` + (`PAGE_SIZE = 10000`). Управление — query-параметрами `limit` и `offset`. + + ### Мультиарендность + Многие эндпоинты принимают `company_id` и/или `tenant_identifier` (query) для + выборки атрибутов/схем в контексте конкретной компании. Специфичные для + компании атрибуты «замещают» общие (системные). + + ### Типы атрибутов (`TypeEnum`) + `0` — целочисленный, `1` — с плавающей запятой, `2` — строка, + `3` — одно из списка, `4` — многие из списка, `5` — да/нет, + `6` — дата со временем, `7` — дата. + +servers: + - url: https://api.sarex.io/eav/api + description: Production (external, через ingress) + - url: https://stage-api.sarex.io/eav/api + description: Stage (external, через ingress) + - url: http://eav-service.eav-prod/api + description: Внутренний адрес в кластере + +security: + - bearerAuth: [] + - bearerAuth: [] + identityAuth: [] + +paths: + /api/v0/attribute/: + get: + operationId: attribute_list + parameters: + - in: query + name: company_id + description: ID компании + schema: + type: string + nullable: true + title: ID компании + - in: query + name: tenant_identifier + description: Идентификатор компании + schema: + type: string + nullable: true + title: Идентификатор компании + tags: + - Attribute + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeRetrieve' + description: '' + post: + operationId: attribute_create + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + /api/v0/attribute/{id}/: + get: + operationId: attribute_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeRetrieve' + description: '' + put: + operationId: attribute_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + patch: + operationId: attribute_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + delete: + operationId: attribute_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID атрибута + required: true + tags: + - Attribute + responses: + '204': + description: '' + /api/v0/attribute-group/: + get: + operationId: attribute_group_list + tags: + - Attribute Group + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeGroupRetrieve' + description: '' + post: + operationId: attribute_group_create + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + /api/v0/attribute-group/{id}/: + get: + operationId: attribute_group_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeGroupRetrieve' + description: '' + put: + operationId: attribute_group_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + patch: + operationId: attribute_group_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeCreate' + description: '' + delete: + operationId: attribute_group_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID группы атрибутов + required: true + tags: + - Attribute Group + responses: + '204': + description: '' + /api/v0/schema/: + get: + operationId: attribute_schema_list + parameters: + - in: query + name: model_name + description: Наименование модели + schema: + type: string + - in: query + name: service_name + description: Наименование сервиса + schema: + type: string + - in: query + name: type_identifier + description: ID типа модели + schema: + type: string + tags: + - Attribute Schema + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaRetrieve' + description: '' + post: + operationId: attribute_schema_create + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + /api/v0/schema/{id}/: + get: + operationId: attribute_schema_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaRetrieve' + description: '' + put: + operationId: attribute_schema_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + patch: + operationId: attribute_schema_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/AttributeSchemaUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AttributeSchemaCreate' + description: '' + delete: + operationId: attribute_schema_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID схемы атрибутов + required: true + tags: + - Attribute Schema + responses: + '204': + description: '' + /api/v0/unit-option/: + get: + operationId: unit_option_list + tags: + - Unit Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionRetrieve' + description: '' + post: + operationId: unit_option_create + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + /api/v0/unit-option/{id}/: + get: + operationId: unit_option_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionRetrieve' + description: '' + put: + operationId: unit_option_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + patch: + operationId: unit_option_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/UnitOptionUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UnitOptionCreate' + description: '' + delete: + operationId: unit_option_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID единицы измерения + required: true + tags: + - Unit Option + responses: + '204': + description: '' + /api/v0/value-option/: + get: + operationId: value_option_list + tags: + - Value Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionRetrieve' + description: '' + post: + operationId: value_option_create + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + /api/v0/value-option/{id}/: + get: + operationId: value_option_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionRetrieve' + description: '' + put: + operationId: value_option_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + patch: + operationId: value_option_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/ValueOptionUpdate' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ValueOptionCreate' + description: '' + delete: + operationId: value_option_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: ID опции значения атрибута + required: true + tags: + - Value Option + responses: + '204': + description: '' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + JWT в заголовке `Authorization: Bearer `. Подпись проверяется + публичным RSA-ключом (RS512) для механизма sarex-backend. + identityAuth: + type: apiKey + in: header + name: Identity + description: >- + Токен Zitadel в заголовке `Identity: Identity ` (используется вместе + с `Authorization` для механизма ZitadelJWTAuthentication). + schemas: + AttributeRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + readOnly: true + title: Наименование атрибута + type: + type: string + readOnly: true + title: Тип атрибута + group: + type: integer + readOnly: true + nullable: true + title: Группа атрибута + author: + type: string + readOnly: true + title: Автор атрибута + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания атрибута + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления атрибута + options: + type: array + items: + $ref: '#/components/schemas/ValueOptionRetrieve' + title: Опции значения атрибута + required: + - author + - created_at + - group + - id + - name + - options + - tenant_identifier + - type + - updated_at + AttributeCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + title: Наименование атрибута + maxLength: 512 + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Тип атрибута + minimum: 0 + maximum: 32767 + group: + type: integer + nullable: true + title: Группа атрибута + author: + type: string + title: Автор атрибута + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + required: + - author + - id + - name + AttributeUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID атрибута + name: + type: string + title: Наименование атрибута + maxLength: 512 + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + title: Тип атрибута + minimum: 0 + maximum: 32767 + group: + type: integer + nullable: true + title: Группа атрибута + author: + type: string + title: Автор атрибута + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + AttributeGroupRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID группы атрибутов + name: + type: string + readOnly: true + title: Наименование группы атрибутов + parent: + type: integer + readOnly: true + title: Родительская группа атрибутов + author: + type: string + readOnly: true + title: Автор группы атрибутов + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания группы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления группы атрибутов + required: + - author + - created_at + - id + - name + - parent + - tenant_identifier + - updated_at + AttributeSchemaRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + readOnly: true + title: Наименование схемы атрибутов + group: + type: integer + readOnly: true + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + readOnly: true + title: ID модели + type_identifier: + type: string + readOnly: true + title: ID типа модели + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeRetrieve' + title: Атрибуты + readOnly: true + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + is_common: + type: boolean + readOnly: true + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + required: + - attributes + - created_at + - group + - id + - is_common + - model_name + - name + - tenant_identifier + - type_identifier + - updated_at + AttributeSchemaCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + title: Наименование схемы атрибутов + maxLength: 512 + group: + type: integer + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + title: ID модели + maxLength: 512 + type_identifier: + type: string + title: ID типа модели + maxLength: 512 + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeCreate' + readOnly: true + title: Атрибуты + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + required: + - attributes + - created_at + - id + - model_name + - name + - tenant_identifier + - type_identifier + - updated_at + AttributeSchemaUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID схемы атрибутов + name: + type: string + title: Наименование схемы атрибутов + maxLength: 512 + group: + type: integer + nullable: true + title: Группа схемы атрибутов + model_name: + type: string + title: ID модели + maxLength: 512 + type_identifier: + type: string + title: ID типа модели + maxLength: 512 + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeCreate' + readOnly: true + title: Атрибуты + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + default: false + title: Общий для компаний + created_at: + type: string + format: date-time + readOnly: true + title: Время создания схемы атрибутов + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления схемы атрибутов + UnitOptionRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + readOnly: true + title: Наименование единицы измерения + author: + type: string + readOnly: true + title: Автор единицы измерения + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания единицы измерения + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления единицы измерения + required: + - author + - created_at + - id + - name + - tenant_identifier + - updated_at + UnitOptionCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + title: Наименование единицы измерения + maxLength: 512 + author: + type: string + title: Автор единицы измерения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + required: + - author + - id + - name + UnitOptionUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID единицы измерения + name: + type: string + title: Наименование единицы измерения + maxLength: 512 + author: + type: string + title: Автор единицы измерения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + ValueOptionRetrieve: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + attribute: + type: integer + readOnly: true + title: ID атрибута + value: + type: string + title: Значение + author: + type: string + readOnly: true + title: Автор опции значения + tenant_identifier: + type: string + readOnly: true + nullable: true + title: Идентификатор компании + created_at: + type: string + format: date-time + readOnly: true + title: Время создания опции значения + updated_at: + type: string + format: date-time + readOnly: true + title: Время последнего обновления опции значения + name: + type: string + readOnly: true + title: Наименование опции значения + required: + - attribute + - author + - created_at + - id + - name + - tenant_identifier + - updated_at + - value + ValueOptionCreate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + value: + type: string + title: Значение + author: + type: string + title: Автор опции значения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + name: + type: string + title: Наименование опции значения + maxLength: 1024 + required: + - author + - id + - value + ValueOptionUpdate: + type: object + properties: + id: + type: integer + readOnly: true + title: ID опции значения + value: + type: string + title: Значение + author: + type: string + title: Автор опции значения + maxLength: 512 + tenant_identifier: + type: string + nullable: true + title: Идентификатор компании + maxLength: 512 + is_common: + type: boolean + title: Общий для компаний + name: + type: string + title: Наименование опции значения + maxLength: 1024 + TypeEnum: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + type: integer + description: |- + * `0` - Целочисленное + * `1` - С плавающей запятой + * `2` - Строковое + * `3` - Одно из списка + * `4` - Многие из списка + * `5` - Да/Нет + * `6` - Дата со временем + * `7` - Дата