Add example .env files and detailed configuration documentation for checklists, contracts, drawings, and eav services.

This commit is contained in:
emelinda 2026-07-14 00:13:03 +03:00
parent 9cff5d6e39
commit cce76e48a9
15 changed files with 4118 additions and 0 deletions

View File

@ -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

View File

@ -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`.

View File

@ -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 <jwt>`. Поддерживаются два режима:
1. **Zitadel** — если передан дополнительный заголовок `identity`
(`Identity <jwt>`), полезная нагрузка (`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 <token>`. Алгоритм `RS512`,
ключ `JWT_AUTH_PUBLIC_KEY`. Разбор выполняется без проверки подписи
(`verify_signature=False`).
identityToken:
type: apiKey
in: header
name: identity
description: |
Опциональный заголовок `identity` (`Identity <jwt>`) для режима 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]

View File

@ -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

View File

@ -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-<env>.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-<env>.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-<env>.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`.

View File

@ -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`.

404
apps/contracts/openapi.yaml Normal file
View File

@ -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 <jwt>`,
пользователь берётся из полезной нагрузки этого токена
(`urn:zitadel:iam:user:metadata`). Подпись на уровне приложения
не проверяется (валидность обеспечивается сетевым слоем/Istio).
2. **sarex-backend** — если заголовка `Identity` нет, подпись основного
токена `Authorization: Bearer <jwt>` проверяется публичным 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 <jwt>` (проверяется по RSA-ключу).
Опционально может передаваться заголовок `Identity: Bearer <jwt>`
(режим 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"

View File

@ -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`), например «Произошла ошибка при запросе пользователей» / «мест работы» / «ролей» / «функциональных групп».

View File

@ -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` — весь ответ).

View File

@ -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

View File

@ -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.<env>=…`, `--set universal-chart.global.env=<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`.

426
apps/drawings/openapi.yaml Normal file
View File

@ -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

54
apps/eav/.env.example Normal file
View File

@ -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

186
apps/eav/CONFIGURATION.md Normal file
View File

@ -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-<env>.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-<env>.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`.

1303
apps/eav/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff