Compare commits
No commits in common. "d7ec0cc9aa7ae5b4c0741379562bf8ef38b5ff77" and "b4568003a322500d8a4f28b7a585be428d12b5c0" have entirely different histories.
d7ec0cc9aa
...
b4568003a3
@ -1,57 +0,0 @@
|
||||
# PYTHONPATH (обязательно указывает на каталог с исходниками)
|
||||
PYTHONPATH=src
|
||||
|
||||
# Django
|
||||
DJANGO_SECRET_KEY="django-insecure-a2o70(=hkv486m(3l-(6d52y5-1&98qokc06%43_uj_gmr^v-a"
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=postgres
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# Auth
|
||||
# false — включён DefaultUserAuthentication (демо-пользователь для локальной разработки),
|
||||
# true — Zitadel + JWT (проверка подписи RS512)
|
||||
JWT_AUTH_ENABLE=false
|
||||
|
||||
# Sarex backend (core/users, users_by_sa) — Basic-auth
|
||||
SAREX_BACKEND_URL=https://stage.sarex.io
|
||||
SAREX_BACKEND_AUTH=base64(username:password)
|
||||
SAREX_BACKEND_TIMEOUT=30
|
||||
|
||||
# EAV (сервис атрибутов)
|
||||
EAV_URL=http://eav-service.eav-stage
|
||||
|
||||
# Gateway (resources)
|
||||
GATEWAY_URL=https://stage-api.sarex.io/gateway
|
||||
|
||||
# Resources API (IAM/resources) — используется в проде через Helm
|
||||
# RESOURCES_API_HOST=http://iams.platform.svc.cluster.local:8080
|
||||
|
||||
# S3 (Yandex Object Storage) — используется в prod-настройках (config.settings.prod)
|
||||
YC_S3_ACCESS_KEY_ID=""
|
||||
YC_S3_SECRET_ACCESS_KEY=""
|
||||
YC_S3_BUCKET_NAME=""
|
||||
YC_S3_ENDPOINT_URL=""
|
||||
|
||||
# Mailer service
|
||||
MAILER_URL="http://mailer-service.mailer:8000"
|
||||
MAILER_TIMEOUT=30
|
||||
|
||||
# Notifications
|
||||
NOTIFICATIONS_ENABLE=True
|
||||
NOTIFICATIONS_EMAIL_FROM=hello@sarex.io
|
||||
NOTIFICATIONS_SERVICE_URL="https://stage.sarex.io/rfi"
|
||||
|
||||
# RabbitMQ (брокер Celery) — дефолты заданы в settings/base.py
|
||||
# RABBITMQ_USERNAME=mcc
|
||||
# RABBITMQ_PASSWORD=mcc
|
||||
# RABBITMQ_HOST=rabbitmq-service
|
||||
# RABBITMQ_PORT=5672
|
||||
# RABBITMQ_VHOST=api
|
||||
|
||||
# Celery
|
||||
# True — уведомления отправляются асинхронно (apply_async), False — синхронно в процессе запроса
|
||||
USE_ASYNC_FUNCTIONS=True
|
||||
@ -1,211 +0,0 @@
|
||||
# Конфигурация проекта rfi-backend
|
||||
|
||||
Документ описывает все переменные окружения и способы конфигурирования сервиса.
|
||||
|
||||
## Способы конфигурирования
|
||||
|
||||
Сервис — это Django-приложение (Django 5.1, Django REST Framework). Настройки лежат в пакете `src/config/settings/`:
|
||||
|
||||
- `base.py` — общие настройки (`DEBUG = True`), читает переменные через `os.getenv(...)`;
|
||||
- `prod.py` — наследует `base.py` (`from .base import *`), выключает `DEBUG` и добавляет S3-хранилище (`django-storages` + `boto3`). Активируется через `DJANGO_SETTINGS_MODULE=config.settings.prod` (см. `docker/http/entrypoint.sh`).
|
||||
|
||||
Часть настроек уведомлений вынесена в отдельные классы `pydantic-settings` (`src/notifications/config.py`): `MailerConfig` (`env_prefix="MAILER_"`), `SarexBackendConfig` (`env_prefix="SAREX_BACKEND_"`), `NotificationsConfig` (`env_prefix="NOTIFICATIONS_"`). Эти классы имеют значения по умолчанию и переопределяются переменными окружения с соответствующим префиксом.
|
||||
|
||||
Отдельного конфиг-файла (yaml/toml) для приложения нет. Все настройки — переменные окружения процесса.
|
||||
|
||||
Источники переменных по способам запуска:
|
||||
|
||||
| Способ запуска | Откуда берутся переменные |
|
||||
| --- | --- |
|
||||
| Локально | `Makefile` через `SET_ENV`: `set -a; source .env; set +a`. Файл `.env` создаётся из `.env.template` вручную. `PYTHONPATH=src` обязателен |
|
||||
| Docker (prod) | `docker/http/Dockerfile` + `entrypoint.sh`: миграции и `uwsgi` под `DJANGO_SETTINGS_MODULE=config.settings.prod`. Переменные пробрасываются рантаймом (Helm) |
|
||||
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `services.api.envs` / `services.api.secretEnvs` (и аналогичные для `services.celery`) universal-chart. Значения различаются по окружению (`_default`/`stage`/`preprod`/`production`) |
|
||||
| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` переключают `STAND`/`NAMESPACE`, job `lint` гоняет ruff |
|
||||
|
||||
Точки входа и процессы:
|
||||
|
||||
| Процесс | Команда | Назначение |
|
||||
| --- | --- | --- |
|
||||
| HTTP API | `uv run src/manage.py runserver` (`make run`) / `uwsgi --ini uwsgi.ini` (prod) | Django REST API |
|
||||
| Celery worker | `uv run celery -A config worker -l info` | Асинхронные задачи уведомлений (`notifications.tasks`) |
|
||||
| Миграции | `uv run src/manage.py migrate` (`make migrate`) | Применение миграций (в контейнере — в `entrypoint.sh` перед стартом uwsgi) |
|
||||
|
||||
Приложение по умолчанию слушает порт **8000** (uwsgi `http = 0.0.0.0:8000`; Django `runserver` — тоже 8000). Внутрикластерный Service слушает порт 80 → targetPort 8000.
|
||||
|
||||
## Переменные приложения
|
||||
|
||||
Дефолт `—` означает, что значение обязательно (читается `os.getenv` без дефолта; при отсутствии будет `None`, что приведёт к ошибке подключения/старта соответствующей подсистемы).
|
||||
|
||||
### Django core
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `PYTHONPATH` | string | — | Должна быть `src` — корень пакета исходников |
|
||||
| `DJANGO_SECRET_KEY` | string | — | Секретный ключ Django (`SECRET_KEY`) |
|
||||
| `DJANGO_SETTINGS_MODULE` | string | `config.settings.base` | Модуль настроек. В контейнере — `config.settings.prod` |
|
||||
|
||||
Значения `DEBUG`, `ALLOWED_HOSTS = ["*"]`, `LANGUAGE_CODE = ru-ru`, `TIME_ZONE = UTC` заданы в коде и не читаются из окружения.
|
||||
|
||||
### Auth (`JWT_AUTH_ENABLE`)
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `JWT_AUTH_ENABLE` | bool (`True`/`true`) | `false` | Режим аутентификации. `true` — включаются `ZitadelJWTStatelessUserAuthentication` и `JWTStatelessUserAuthentication`; `false` — `DefaultUserAuthentication` (демо-пользователь `DefaultUser`, только для локальной разработки) |
|
||||
|
||||
Алгоритм JWT фиксирован в `SIMPLE_JWT` — `RS512`. Класс пользователя токена — `core.auth.User`.
|
||||
|
||||
### Database (`DB_*`)
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `DB_HOST` | string | — | Хост PostgreSQL |
|
||||
| `DB_PORT` | int | — | Порт PostgreSQL |
|
||||
| `DB_NAME` | string | — | Имя базы данных |
|
||||
| `DB_USER` | string | — | Пользователь БД |
|
||||
| `DB_PASSWORD` | string | — | Пароль пользователя БД |
|
||||
|
||||
Движок фиксирован: `django.db.backends.postgresql`.
|
||||
|
||||
### Sarex backend (`SAREX_BACKEND_*`)
|
||||
|
||||
Используется в `core/services.py` (получение пользователей `core/users/`, `users_by_sa/`) и в `notifications` (`SarexBackendConfig`). Авторизация — Basic (`Authorization: Basic <SAREX_BACKEND_AUTH>`).
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `SAREX_BACKEND_URL` | string | `https://stage.sarex.io` | Базовый URL sarex-backend |
|
||||
| `SAREX_BACKEND_AUTH` | string (base64) | — | Basic-auth в виде `base64(username:password)` |
|
||||
| `SAREX_BACKEND_TIMEOUT` | int | `30` | Таймаут запроса (используется `SarexBackendConfig`) |
|
||||
|
||||
### Внешние сервисы (EAV, Gateway, Resources)
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `EAV_URL` | string | — | Базовый URL EAV-сервиса атрибутов (`core/services.get_attributes`) |
|
||||
| `GATEWAY_URL` | string | — | Базовый URL gateway (`resources/`) для получения имени/списка ресурсов |
|
||||
| `RESOURCES_API_HOST` | string | — | Адрес IAM/resources API. Задаётся в Helm (в `.env.template` отсутствует) |
|
||||
|
||||
### S3 / Object Storage (`YC_S3_*`)
|
||||
|
||||
Читаются только в `config/settings/prod.py` (backend хранилища — `storages.backends.s3boto3.S3Boto3Storage`, ACL `public-read`).
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `YC_S3_ACCESS_KEY_ID` | string | — | Access key (`AWS_ACCESS_KEY_ID`) |
|
||||
| `YC_S3_SECRET_ACCESS_KEY` | string | — | Secret key (`AWS_SECRET_ACCESS_KEY`) |
|
||||
| `YC_S3_BUCKET_NAME` | string | — | Бакет (`AWS_STORAGE_BUCKET_NAME`) |
|
||||
| `YC_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 (`AWS_S3_ENDPOINT_URL`) |
|
||||
|
||||
### Mailer (`MAILER_*`)
|
||||
|
||||
Настройки клиента почтового сервиса (`MailerConfig`).
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `MAILER_URL` | string | `http://mailer-service.mailer:8000` | URL mailer-сервиса |
|
||||
| `MAILER_TIMEOUT` | int | `30` | Таймаут запроса (сек) |
|
||||
|
||||
### Notifications (`NOTIFICATIONS_*`)
|
||||
|
||||
Настройки подсистемы уведомлений (`NotificationsConfig`).
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `NOTIFICATIONS_ENABLE` | bool | `True` | Включить отправку уведомлений |
|
||||
| `NOTIFICATIONS_EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя |
|
||||
| `NOTIFICATIONS_SERVICE_URL` | string | `https://stage.sarex.io/rfi` | Внешний URL сервиса (для ссылок в письмах) |
|
||||
|
||||
### RabbitMQ (`RABBITMQ_*`)
|
||||
|
||||
Используются при сборке `CELERY_BROKER_URL` в `settings/base.py`. У всех заданы дефолты.
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `RABBITMQ_USERNAME` | string | `mcc` | Пользователь |
|
||||
| `RABBITMQ_PASSWORD` | string | `mcc` | Пароль |
|
||||
| `RABBITMQ_HOST` | string | `rabbitmq-service` | Хост |
|
||||
| `RABBITMQ_PORT` | int | `5672` | Порт (задаётся в Helm через `envs`) |
|
||||
| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост |
|
||||
|
||||
К итоговому URL добавляется `?heartbeat=30` (`BROKER_HEARTBEAT`). Прочие параметры брокера/Celery заданы в коде: `BROKER_POOL_LIMIT=10`, очередь `default`, `CELERY_TASK_SERIALIZER=json`, `CELERY_TIMEZONE=Europe/Moscow`, `CELERY_IMPORTS=("notifications.tasks",)`.
|
||||
|
||||
### Celery-поведение (`USE_ASYNC_FUNCTIONS`)
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `USE_ASYNC_FUNCTIONS` | bool | `True` | `True` — уведомления отправляются через `apply_async` (нужен работающий воркер и брокер); `False` — синхронно внутри запроса |
|
||||
|
||||
## Переменные из Helm-чарта (`.helm/values.yaml`)
|
||||
|
||||
Чарт — обёртка над `universal-chart` (`oci://cr.yandex/.../charts`, версия 0.1.7). Описаны два сервиса: `api` и `celery` (одинаковый образ `cr.yandex/crp3ccidau046kdj8g9q/rfi-backend`, разные команды и ресурсы). Значения выбираются по ключу `global.env` (`_default`/`stage`/`preprod`/`production`).
|
||||
|
||||
Обычные значения (`services.<svc>.envs`): `JWT_AUTH_ENABLE`, `SAREX_BACKEND_URL`, `EAV_URL`, `GATEWAY_URL`, `NOTIFICATIONS_ENABLE`, `NOTIFICATIONS_EMAIL_FROM`, `NOTIFICATIONS_SERVICE_URL`, `RABBITMQ_PORT`, `RABBITMQ_HOST`, `RESOURCES_API_HOST`.
|
||||
|
||||
Значения из секретов (`services.<svc>.secretEnvs`, монтируются как env через `secretKeyRef`):
|
||||
|
||||
| Переменная | Секрет (`_default`) | Секрет (`production`) | Ключ |
|
||||
| --- | --- | --- | --- |
|
||||
| `DJANGO_SECRET_KEY` | `rfi-backend-api-django-secret` | `django-secret` | `django_secret_key` |
|
||||
| `DB_HOST` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `host` |
|
||||
| `DB_PORT` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `port` |
|
||||
| `DB_NAME` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `dbname` |
|
||||
| `DB_USER` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `user` |
|
||||
| `DB_PASSWORD` | `rfi-backend-api-ya-pg-secret` | `ya-pg-secret` | `password` |
|
||||
| `SAREX_BACKEND_AUTH` | `django-secret` | `django-secret` | `token` |
|
||||
| `YC_S3_ACCESS_KEY_ID` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `key_id` |
|
||||
| `YC_S3_SECRET_ACCESS_KEY` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `access_key` |
|
||||
| `YC_S3_BUCKET_NAME` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `storage_bucket_name` |
|
||||
| `YC_S3_ENDPOINT_URL` | `rfi-backend-api-yc-s3-secret` | `yc-s3-secret` | `endpoint_url` |
|
||||
| `RABBITMQ_VHOST` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `vhost` |
|
||||
| `RABBITMQ_USERNAME` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `username` |
|
||||
| `RABBITMQ_PASSWORD` | `rfi-backend-api-rabbitmq-secret` | `rabbitmq-secret` | `password` |
|
||||
|
||||
Значения `envs` по окружениям (ключевые различия):
|
||||
|
||||
| Переменная | stage | preprod | production |
|
||||
| --- | --- | --- | --- |
|
||||
| `SAREX_BACKEND_URL` | `https://stage.sarex.io` | `https://perprod.sarex.io` | `https://lk.sarex.io` |
|
||||
| `EAV_URL` | `http://eav-service.eav-stage` | `http://eav-service.eav-preprod` | `http://eav-service.eav-prod` |
|
||||
| `GATEWAY_URL` | `https://stage-api.sarex.io/gateway` | `https://api.perprod.sarex.io/gateway` | `https://api.sarex.io/gateway` |
|
||||
| `NOTIFICATIONS_SERVICE_URL` | `https://stage.sarex.io/rfi` | `https://perprod.sarex.io/rfi` | `https://lk.sarex.io/rfi` |
|
||||
| `RABBITMQ_HOST` | `rabbitmq.rabbitmq.svc.cluster.local` | `rabbitmq.rabbitmq.svc.cluster.local` | `default-rabbit-cluster.rabbitmq.svc` |
|
||||
| `RESOURCES_API_HOST` | `http://iams.platform.svc.cluster.local:8080` | `http://sarex-resources-service.resources-preprod` | `http://iams.iam.svc.cluster.local:8080` |
|
||||
|
||||
Прочие параметры чарта (не переменные приложения): `deployment.*` (имя, реплики — prod: api 2 / celery 2, ресурсы — api 1024Mi/1cpu, celery 512Mi/1cpu), `image.*`, `service.*` (ClusterIP, 80→8000), `imagePullSecrets: dockerhub`, `probes` (liveness/readiness выключены).
|
||||
|
||||
## Переменные в CI (`.gitlab-ci.yml`)
|
||||
|
||||
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Общие переменные: `SERVICE_NAME=rfi-backend`, `DOCKERFILE_PATH=./docker/http/Dockerfile`, `CI_TRIGGER_SOURCE=app`.
|
||||
|
||||
Переключение окружения (`workflow.rules`):
|
||||
|
||||
| Условие | STAND | NAMESPACE | CHART_VERSION |
|
||||
| --- | --- | --- | --- |
|
||||
| ветка `stage` | `stage` | `proc` | `0.0.1-stage` |
|
||||
| ветка `master` | `preprod` | `rfi-preprod` | `0.0.1-preprod` |
|
||||
| тег (`CI_COMMIT_TAG`) | `production` | `rfi-prod` | `0.0.1-prod` |
|
||||
| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
|
||||
|
||||
Через `HELM_SET_ARGS` в чарт прокидываются образ (`services.api.image.name.<env>`, `services.celery.image.name.<env>`), `global.env`, а также метаданные коммита/джобы (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`). Job `lint` (образ `ghcr.io/astral-sh/uv`) прогоняет `ruff check` и `ruff format --check`.
|
||||
|
||||
## Замечания и потенциальные проблемы
|
||||
|
||||
- `PYTHONPATH=src` обязателен: без него не находятся пакеты (`config`, `core`, `request` и т.д.). Задан в `.env.template` и в `Dockerfile` (`ENV PYTHONPATH=src`).
|
||||
- Приложение не загружает `.env` автоматически: Django-настройки читают `os.getenv`, а `.env` подхватывается только через `Makefile` (`set -a; source .env`). При ручном запуске переменные нужно экспортировать самому.
|
||||
- `JWT_AUTH_ENABLE=false` включает `DefaultUser` с захардкоженными `id=356`, `company_ids=[1, 38]` и полным набором прав — это только для локальной отладки, в проде должно быть `true`.
|
||||
- S3 (`YC_S3_*`) читается только в `config.settings.prod`. При запуске с `config.settings.base` (локально) хранилище S3 не используется — файлы (аватар RFI) сохраняются локально.
|
||||
- В `.env.template` переменные `RABBITMQ_*` и `RESOURCES_API_HOST` отсутствуют — для Celery используются дефолты из кода (`mcc`/`rabbitmq-service`/`api`), в кластере они приходят из Helm (`envs` + `secretEnvs`).
|
||||
- В values.yaml значения preprod для sarex/gateway записаны как `perprod` (`https://perprod.sarex.io`, `.../api.perprod.sarex.io/...`) — так в исходнике; при необходимости свериться с фактическими адресами.
|
||||
- `USE_ASYNC_FUNCTIONS` через `os.getenv("USE_ASYNC_FUNCTIONS", True)` возвращает строку при заданной переменной; пустая/незаданная даёт `True`. Любая непустая строка (в т.ч. `"False"`) трактуется как истинное значение в условии `if settings.USE_ASYNC_FUNCTIONS`.
|
||||
|
||||
## Минимальный набор для локального запуска
|
||||
|
||||
С включённым `config.settings.base` и `JWT_AUTH_ENABLE=false` минимально необходимо:
|
||||
|
||||
- `PYTHONPATH=src`, `DJANGO_SECRET_KEY`
|
||||
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` (поднять PostgreSQL)
|
||||
- `SAREX_BACKEND_URL`, `SAREX_BACKEND_AUTH` (для обращений к sarex-backend)
|
||||
- `EAV_URL`, `GATEWAY_URL` (для атрибутов и ресурсов)
|
||||
- `NOTIFICATIONS_ENABLE=False` — чтобы не требовался mailer/брокер; либо `True` + `MAILER_URL` и работающий RabbitMQ
|
||||
- `USE_ASYNC_FUNCTIONS=False` — чтобы уведомления шли синхронно без Celery-воркера
|
||||
|
||||
Порядок: `make migrate` → `make run` (API) и при необходимости `uv run celery -A config worker -l info` (воркер). Готовые значения-примеры — в `.env.example`.
|
||||
@ -1,100 +0,0 @@
|
||||
# Эндпоинты, с которыми взаимодействует rfi-frontend
|
||||
|
||||
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `rfi-frontend`).
|
||||
|
||||
## Как устроено взаимодействие
|
||||
|
||||
Все запросы собраны в реестре `module/api/index.ts` и сгруппированы по объектам-«API»: `RfiAPI`, `AttachmentsAPI`, `CoreAPI`, `ResourcesAPI` и функция `getAttributes`. Каждый вызов идёт через единый `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх axios).
|
||||
|
||||
Вызов задаётся объектом:
|
||||
|
||||
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
|
||||
- `url` — путь запроса (дописывается к базовому хосту сервиса);
|
||||
- `data` — тело запроса (для POST/PUT/PATCH);
|
||||
- `axiosConfig` — доп. настройки axios, чаще всего `params` (query-параметры: `limit`, `offset`, `company_id` и т.п.);
|
||||
- `showErrorNotification` — показывать ли уведомление об ошибке;
|
||||
- `controller` — `AbortController` для отмены запроса.
|
||||
|
||||
Методы `httpService`: `getRequest`, `postRequest`, `putRequest`, `patchRequest`, `deleteRequest`. Базовый хост подставляется по `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV`.
|
||||
|
||||
## Базовые хосты по сервисам и окружениям
|
||||
|
||||
Значения из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url`. Использовано пять сервисов (остальные ключи в `hosts.ts` объявлены, но модулем не вызываются).
|
||||
|
||||
| Сервис (`service`) | Назначение | `stage` | `prod` |
|
||||
| --- | --- | --- | --- |
|
||||
| `rfi` | Собственный backend RFI (этот сервис) | `https://stage-api.sarex.io/rfi/api/v1` | `https://api.sarex.io/rfi/api/v1` |
|
||||
| `sarexApi` | Gateway/API Sarex (attachments, users v2) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
|
||||
| `sarex` | Локальный backend Sarex (`/api/core`, `/api/client`) | `https://stage.sarex.io` | `https://lk.sarex.io` |
|
||||
| `gateway_api_v1` | Gateway API v1 (resources) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` |
|
||||
| `eav_api_v0` | EAV — сервис атрибутов/схем | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` |
|
||||
|
||||
> Также определены окружения `local`, `preprod` и `contour`. В `local` сервис `sarex` проксируется на `/sarex-backend`; в `contour` используются относительные пути. Подключаемый удалённый модуль documentations описан отдельно в `module/api/module-hosts.ts` (Module Federation `remoteEntry.js`).
|
||||
|
||||
## Эндпоинты по сервисам
|
||||
|
||||
### `rfi` — Backend RFI (этот сервис)
|
||||
|
||||
`RfiAPI` из `module/api/index.ts`. Пути указаны относительно базы `.../rfi/api/v1`.
|
||||
|
||||
| Метод (`RfiAPI`) | HTTP | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `getRfi` | POST | `/rfi/filter/` | Список RFI по фильтру (query `limit`/`offset`, тело — фильтры) |
|
||||
| `postRfi` | POST | `/rfi/` | Создать RFI |
|
||||
| `putRfi` | PUT | `/rfi/{rfiId}/` | Полное обновление RFI |
|
||||
| `patchRfi` | PATCH | `/rfi/{rfiId}/` | Частичное обновление RFI |
|
||||
| `copyRfi` | POST | `/rfi/{rfiId}/copy/` | Скопировать RFI (тело `{ name }`) |
|
||||
| `getRfiById` | GET | `/rfi/{id}/` | RFI по id |
|
||||
| `deleteRfi` | DELETE | `/rfi/{id}/` | Удалить RFI (soft-delete) |
|
||||
| `getHistory` | GET | `/rfi/history/` | История изменений по списку RFI (query-параметры фильтра) |
|
||||
| `getStatusCount` | POST | `/rfi/status-count/` | Количество RFI по статусам (по `resource_id`) |
|
||||
| `getPriorityCount` | POST | `/rfi/priority-count/` | Количество RFI по приоритетам (по `resource_id`) |
|
||||
| `createRfiMessage` | POST | `/messages/` | Создать сообщение в RFI |
|
||||
| `getMessagesByRfiId` | GET | `/messages/?request_id={id}` | Сообщения по id запроса |
|
||||
| `patchRfiMessage` | PATCH | `/messages/{id}/` | Отметить сообщение решением (`is_solution`) |
|
||||
| `getStatuses` | GET | `/statuses/` | Список статусов (query `company_id`, `limit`, `offset`) |
|
||||
| `getStatusModels` | GET | `/status-models/` | Модели статусов компании (query `company_id`) |
|
||||
| `getPriorities` | GET | `/priorities/` | Список приоритетов (query `company_id`, `limit`, `offset`) |
|
||||
| `getPriorityModels` | GET | `/priority-models/` | Модели приоритетов компании (query `company_id`) |
|
||||
|
||||
### `sarexApi` — Gateway/API Sarex
|
||||
|
||||
`AttachmentsAPI` и `CoreAPI.getUsersV2`. Пути указаны относительно базы `https://(stage-)api.sarex.io`.
|
||||
|
||||
| Метод | HTTP | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `AttachmentsAPI.uploadFiles` | POST | `/gateway/api/v1/attachments/` | Загрузить файлы (`multipart/form-data`) |
|
||||
| `AttachmentsAPI.deleteFile` | DELETE | `/gateway/api/v1/attachments/{id}` | Удалить файл |
|
||||
| `AttachmentsAPI.getFilesByRfiId` | GET | `/gateway/api/v1/attachments/?company_id={companyId}&instance_id={rfiId}&model_name={ATTACHMENTS_MODEL_NAME}` | Файлы, привязанные к RFI |
|
||||
| `CoreAPI.getUsersV2` | GET | `/gateway/api/v2/users/` | Пользователи (query `company_id`, `permissions`, `resource_id`, `limit`, `offset`) |
|
||||
|
||||
### `sarex` — Backend Sarex
|
||||
|
||||
`CoreAPI`. Пути указаны относительно базы `https://stage.sarex.io` / `https://lk.sarex.io`.
|
||||
|
||||
| Метод | HTTP | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `CoreAPI.getUsers` | GET | `/api/core/users/` | Пользователи (query `company`, `perm`, `resource_id`, `limit`, `offset`) |
|
||||
| `CoreAPI.getDepartments` | GET | `/api/core/admin/departments/?company={companyId}&{query}` | Отделы компании |
|
||||
| `CoreAPI.getPositions` | GET | `/api/core/admin/positions/?company={companyId}&{query}` | Должности компании |
|
||||
| `CoreAPI.getSettings` | GET | `/api/client/settings/` | Клиентские настройки |
|
||||
|
||||
### `gateway_api_v1` — Gateway API v1
|
||||
|
||||
`ResourcesAPI`. База уже включает `/gateway/api/v1`.
|
||||
|
||||
| Метод | HTTP | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `ResourcesAPI.getResources` | GET | `/resources/` | Список ресурсов (query `company_id`) |
|
||||
|
||||
### `eav_api_v0` — EAV (атрибуты)
|
||||
|
||||
Функция `getAttributes`. База уже включает `/eav/api/v0`.
|
||||
|
||||
| Метод | HTTP | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `getAttributes` | GET | `/schema/?model_name=flow&company_id={companyId}` | Схема атрибутов по компании |
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Ошибки обрабатываются в `httpService` (`@sarex-team/sdk-js`). Тип ответа с ошибкой описан в `module/api/types.ts` (`ErrorResponse` — `{ response?.data?.detail }`). Часть запросов включает показ уведомления об ошибке флагом `showErrorNotification: true` (например `getSettings`, `AttachmentsAPI.*`). Права доступа (`core.can_*_RFI`) описаны в `module/api/permissions.ts` и проверяются на стороне backend RFI (`RFITokenBasedPermission`).
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,70 +0,0 @@
|
||||
# =============================================================================
|
||||
# stamp-verification-frontend — пример переменных окружения
|
||||
# =============================================================================
|
||||
#
|
||||
# ВАЖНО: само приложение (статическая страница на nginx) НЕ читает переменные
|
||||
# окружения во время выполнения. Конфигурация «зашита» в код:
|
||||
# - базовый URL API выбирается по window.location.host (см. static/js/index.js);
|
||||
# - nginx слушает порт 8080 (см. nginx.conf);
|
||||
# - health-check отдаётся статически по пути /ping.
|
||||
#
|
||||
# Поэтому файл .env НЕ подхватывается образом. Перечисленные ниже переменные
|
||||
# участвуют только в СБОРКЕ и ДЕПЛОЕ (GitLab CI + Helm/universal-chart) и
|
||||
# приведены здесь как справочный шаблон значений для каждого окружения.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --- GitLab CI (.gitlab-ci.yml) ---------------------------------------------
|
||||
# Задаются пайплайном/переменными проекта, переключаются по ветке/тегу.
|
||||
SERVICE_NAME=stamp-verification-frontend
|
||||
DOCKERFILE_PATH=Dockerfile
|
||||
BUILD_ARGS=
|
||||
CI_TRIGGER_SOURCE=app
|
||||
|
||||
# STAND / NAMESPACE выставляются автоматически по правилам workflow:
|
||||
# ветка stage -> STAND=stage NAMESPACE=documentations
|
||||
# ветка master -> STAND=preprod NAMESPACE=stamp-verification-preprod
|
||||
# тег (release) -> STAND=production NAMESPACE=stamp-verification-prod
|
||||
STAND=stage
|
||||
NAMESPACE=documentations
|
||||
RELEASE_NAME=stamp-verification-frontend
|
||||
CHART_NAME=stamp-verification-frontend
|
||||
CHART_VERSION=0.0.1-stage
|
||||
K8S_HUSTLER_BRANCH=universal-chart-stage
|
||||
|
||||
# Имя собранного образа (подставляется в HELM_SET_ARGS как
|
||||
# universal-chart.services.frontend.image.name.<env>)
|
||||
IMAGE_NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest
|
||||
|
||||
|
||||
# --- Helm / universal-chart (.helm/values.yaml) -----------------------------
|
||||
# Значения чарта universal-chart (dependency 0.1.7). env выбирает набор *.env.
|
||||
UNIVERSAL_CHART__GLOBAL__ENV=stage # _default | stage | preprod | production
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__NAME=stamp-verification-frontend
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__PORT=8080
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__REPLICA_COUNT=1
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__PULL_POLICY=IfNotPresent
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__NAME=frontend-service
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__PORT=8080 # production: 80
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TARGET_PORT=8080
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TYPE=ClusterIP
|
||||
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE_PULL_SECRETS__NAME=dockerhub
|
||||
|
||||
|
||||
# --- Docker build (Dockerfile) ----------------------------------------------
|
||||
# Базовый образ фиксирован в Dockerfile: nginx:mainline-alpine-otel.
|
||||
# BUILD_ARGS пуст — build-time аргументов у образа нет.
|
||||
|
||||
|
||||
# --- «Runtime»-конфигурация приложения (справочно, НЕ через env) -------------
|
||||
# Значения ниже НЕ настраиваются переменными окружения — они захардкожены в
|
||||
# static/js/index.js и приведены только для понимания поведения:
|
||||
#
|
||||
# window.location.host == "stamp-verification.sarex.io" -> API = api.sarex.io (prod)
|
||||
# иначе (напр. stamp-verification.stage.sarex.io) -> API = stage-api.sarex.io (stage)
|
||||
#
|
||||
# ВНИМАНИЕ (безопасность): в static/js/index.js в заголовок Authorization
|
||||
# захардкожен Bearer-JWT (истёк в 2023 г.). Это должно быть удалено —
|
||||
# публичный эндпоинт QR не требует пользовательского токена. Секрета для env
|
||||
# здесь нет и быть не должно.
|
||||
@ -1,132 +0,0 @@
|
||||
# Конфигурация проекта stamp-verification-frontend
|
||||
|
||||
Документ описывает, как конфигурируется и разворачивается модуль **stamp-verification-frontend** — публичная статическая страница проверки штампа (QR) на документе.
|
||||
|
||||
## Что это за сервис
|
||||
|
||||
`stamp-verification-frontend` — это статический фронтенд (обычный HTML + ванильный JavaScript + `moment.js`), который раздаётся через **nginx**. По QR-коду со штампа на PDF пользователь попадает на страницу с параметрами `?id=<qrId>&page_number=<n>`, страница запрашивает данные документа у backend-сервиса `documentations` и отображает информацию о версии, авторе, дате загрузки, статусе согласования и ссылке на документ в Sarex.
|
||||
|
||||
Ключевое отличие от backend-сервисов: **у приложения нет разбора переменных окружения**. Здесь нет `pydantic-settings`, нет `.env`, нет секций конфигурации. Всё «runtime»-поведение либо статично, либо определяется по хосту в браузере (`window.location.host`). Конфигурируется только **сборка и деплой** (Docker, Helm/`universal-chart`, GitLab CI).
|
||||
|
||||
## Способы конфигурирования
|
||||
|
||||
| Слой | Где задаётся | Что настраивает |
|
||||
| --- | --- | --- |
|
||||
| Runtime (браузер) | `static/js/index.js` | Выбор базового URL API по `window.location.host`; параметры запроса берутся из query-строки (`id`, `page_number`, `v`) |
|
||||
| Веб-сервер | `nginx.conf` | Порт прослушивания `8080`, корень `/dist`, health-check `GET /ping` → `200 {"result": "ok"}`, gzip, логи в stdout/stderr |
|
||||
| Образ | `Dockerfile` | Базовый образ `nginx:mainline-alpine-otel`, копирование `static/` и `index.html` в `/dist`, `EXPOSE 8080` |
|
||||
| Helm | `.helm/values.yaml`, `.helm/Chart.yaml` | Значения чарта-зависимости `universal-chart` (deployment, image, service, imagePullSecrets) |
|
||||
| CI/CD | `.gitlab-ci.yml` | Выбор окружения по ветке/тегу, имя релиза/чарта, `HELM_SET_ARGS` |
|
||||
| IaC (kustomize) | `iac/apps/stamp-verification/**` | Namespace, Deployment, Service и оверлеи кластеров (этот репозиторий) |
|
||||
|
||||
Отдельного конфиг-файла (yaml/toml/env) приложение не читает.
|
||||
|
||||
## Runtime-поведение (`static/js/index.js`)
|
||||
|
||||
Скрипт выполняется на `DOMContentLoaded`. Логика:
|
||||
|
||||
- читает query-параметры: `id` (идентификатор QR), `page_number`, `v` (пока не используется);
|
||||
- запрос выполняется только если заданы одновременно `id` и `page_number`;
|
||||
- выбирает базовый URL API:
|
||||
|
||||
| `window.location.host` | Базовый URL API | Окружение |
|
||||
| --- | --- | --- |
|
||||
| `stamp-verification.sarex.io` | `api.sarex.io` | prod |
|
||||
| любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage |
|
||||
|
||||
- делает `fetch` `GET https://<apiBaseUrl>/documentations/api/v1/public/qr/{id}/document_info?number_page={page_number}` с `mode: cors`, `credentials: include`, `cache: no-cache`;
|
||||
- по ответу заполняет поля страницы (название, версия, номер страницы, автор, дата загрузки), блок изменений (changelog), индикаторы актуальности/аннулирования версии, статус согласования и ссылку на документ.
|
||||
|
||||
Локаль дат — `ru` (`moment.locale("ru")`), формат отображения даты — `DD.MM.YYYY / HH:mm`.
|
||||
|
||||
Полный перечень внешних вызовов см. в `ENDPOINTS.md`, контракт эндпоинта — в `openapi.yaml`.
|
||||
|
||||
## nginx (`nginx.conf`)
|
||||
|
||||
| Параметр | Значение | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `listen` | `8080` | Порт HTTP |
|
||||
| `root` | `/dist` | Корень статики (туда Dockerfile кладёт `static/` и `index.html`) |
|
||||
| `location = /ping` | `return 200 '{"result": "ok"}'` | Health-check для k8s-проб/балансировщика |
|
||||
| `access_log` | `/dev/stdout` | Логи доступа в stdout |
|
||||
| `error_log` | `stderr warn` | Логи ошибок в stderr |
|
||||
| `gzip` | `on` | Сжатие ответов |
|
||||
| `expires` | `off` | Без заголовков кеширования |
|
||||
|
||||
## Docker (`Dockerfile`, `docker-compose.yml`)
|
||||
|
||||
Образ на базе `nginx:mainline-alpine-otel`. Сборка копирует `nginx.conf` в `/etc/nginx/nginx.conf`, каталог `static` и `index.html` в `/dist`, выставляет права на `/var` и `/run`, объявляет `EXPOSE 8080` и запускает `nginx -g "daemon off;"`. Build-time аргументов нет (`BUILD_ARGS=""`).
|
||||
|
||||
`docker-compose.yml` — только для локального прогона (`image: sarex/landing:latest`, публикует порт `8000:8000`; обратите внимание, что nginx внутри слушает `8080` — маппинг в compose оставлен историческим).
|
||||
|
||||
## Helm / universal-chart (`.helm/`)
|
||||
|
||||
`Chart.yaml` подключает зависимость `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`). Значения задаются в `values.yaml` под ключом `universal-chart` и разбиты по окружениям через суффиксы (`_default`, `stage`, `preprod`, `production`).
|
||||
|
||||
| Ключ (`universal-chart.services.frontend.*`) | Значение (`_default`) | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `deployment.enabled` | `true` | Создавать Deployment |
|
||||
| `deployment.name` | `stamp-verification-frontend` | Имя Deployment |
|
||||
| `deployment.port` | `8080` | Порт контейнера |
|
||||
| `deployment.replicaCount` | `1` | Число реплик |
|
||||
| `image.name` | `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest` | Образ |
|
||||
| `image.pullPolicy` | `IfNotPresent` | Политика загрузки образа |
|
||||
| `service.enabled` | `true` | Создавать Service |
|
||||
| `service.name` | `frontend-service` | Имя Service |
|
||||
| `service.port` | `8080` (в `production` — `80`) | Порт сервиса |
|
||||
| `service.targetPort` | `8080` | Целевой порт |
|
||||
| `service.type` | `ClusterIP` | Тип сервиса |
|
||||
| `imagePullSecrets.name` | `dockerhub` | Секрет для доступа к реестру |
|
||||
|
||||
`global.env` (`_default`) выбирает активный набор значений по окружению.
|
||||
|
||||
## CI/CD (`.gitlab-ci.yml`)
|
||||
|
||||
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Общие переменные: `SERVICE_NAME=stamp-verification-frontend`, `DOCKERFILE_PATH=Dockerfile`, `BUILD_ARGS=""`, `CI_TRIGGER_SOURCE=app`.
|
||||
|
||||
Окружение переключается правилами `workflow.rules`:
|
||||
|
||||
| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `merge_request_event` | — | — | — | `ENABLE_BUILD_IMAGE="false"` (только проверки, без сборки образа) |
|
||||
| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | `universal-chart-stage` |
|
||||
| ветка `master` | `preprod` | `stamp-verification-preprod` | `0.0.1-preprod` | `universal-chart-preprod` |
|
||||
| тег (`CI_COMMIT_TAG`) | `production` | `stamp-verification-prod` | `0.0.1-prod` | `universal-chart-production` |
|
||||
| иначе | `when: never` | | | |
|
||||
|
||||
Для каждого деплой-окружения `HELM_SET_ARGS` передаёт: образ (`universal-chart.services.frontend.image.name.<env>=${IMAGE_NAME}`), `universal-chart.global.env=<env>` и метаданные коммита (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`).
|
||||
|
||||
## Инфраструктура (kustomize, этот репозиторий)
|
||||
|
||||
Каталог `iac/apps/stamp-verification/` содержит kustomize-манифесты (альтернатива/дополнение к Helm-деплою из CI):
|
||||
|
||||
- `base/namespace.yaml` — namespace `stamp-verification` с `istio-injection: enabled`;
|
||||
- `base/deployment.yaml` — Deployment `frontend` (образ `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:<sha>`, `imagePullPolicy: IfNotPresent`, `containerPort: 80`, requests `cpu: 25m`/`memory: 100Mi`, `imagePullSecrets: regcred`);
|
||||
- `base/service.yaml` — Service `frontend-service` (`ClusterIP`, `port: 80` → `targetPort: 80`);
|
||||
- `base/kustomization.yaml` — сборка base (namespace + deployment + service);
|
||||
- `yc-k8s-test/kustomization.yaml` — оверлей кластера (`resources: ../base`, патчи закомментированы);
|
||||
- `yc-k8s-test/replicas.yaml` — заготовка патча реплик (`replicas: 1`).
|
||||
|
||||
## Замечания и потенциальные проблемы
|
||||
|
||||
- **Захардкоженный JWT.** В `static/js/index.js` в заголовок `Authorization: Bearer …` вшит истёкший (2023 г.) токен. Публичный эндпоинт `/public/qr/...` не должен требовать пользовательский токен — строку следует удалить. Держать секреты в статике недопустимо.
|
||||
- **Рассогласование портов.** nginx и Helm/`universal-chart` используют порт **8080**, а kustomize-манифесты в этом репозитории (`base/deployment.yaml`, `base/service.yaml`) — порт **80**. Нужно привести к одному значению (образ слушает 8080), иначе проброс/пробы могут не сходиться.
|
||||
- **Дублирование деплоя.** Приложение может разворачиваться и через Helm (CI, чарт `universal-chart`), и через kustomize (этот репозиторий). Следует зафиксировать единый источник истины, чтобы образ/порт/namespace не расходились.
|
||||
- **NAMESPACE для stage.** На ветке `stage` деплой идёт в namespace `documentations` (общий), тогда как preprod/prod — в выделенные `stamp-verification-*`. Это осознанное решение или наследие — стоит проверить.
|
||||
- **Конфиг только по хосту.** Выбор prod/stage жёстко завязан на строку `stamp-verification.sarex.io`. Любой новый прод-домен потребует правки кода, а не конфигурации.
|
||||
- **Mock-данные в бандле.** В `index.js` присутствуют объекты `mockData`/`mockChangeLog` — тестовые данные, оставшиеся в продовом бандле. На поведение не влияют (не используются), но их стоит убрать.
|
||||
|
||||
## Минимальный набор для локального запуска
|
||||
|
||||
Отдельная конфигурация не требуется — приложение статично:
|
||||
|
||||
```
|
||||
# сборка и запуск образа
|
||||
docker build -t stamp-verification-frontend .
|
||||
docker run --rm -p 8080:8080 stamp-verification-frontend
|
||||
# проверка
|
||||
curl http://localhost:8080/ping # {"result": "ok"}
|
||||
# страница: http://localhost:8080/?id=<qrId>&page_number=1
|
||||
```
|
||||
|
||||
Для реальных данных нужен доступный backend `documentations` (по умолчанию для не-prod хоста используется `https://stage-api.sarex.io`).
|
||||
@ -1,69 +0,0 @@
|
||||
# Эндпоинты, с которыми взаимодействует stamp-verification-frontend
|
||||
|
||||
Документ описывает все внешние HTTP-вызовы, которые выполняет статическая страница проверки штампа (`stamp-verification-frontend`).
|
||||
|
||||
## Как устроено взаимодействие
|
||||
|
||||
В отличие от микрофронтендов с декларативным реестром эндпоинтов, здесь весь сетевой код сосредоточен в одном файле — `static/js/index.js`. Страница обычным `fetch` обращается к **одному** публичному эндпоинту backend-сервиса `documentations`, а также подгружает сторонние ресурсы аналитики и статику.
|
||||
|
||||
Логика вызова:
|
||||
|
||||
1. из query-строки берутся параметры `id` (идентификатор QR) и `page_number`; запрос выполняется только если оба заданы;
|
||||
2. базовый хост API выбирается по `window.location.host`;
|
||||
3. выполняется `GET`-запрос с `mode: "cors"`, `credentials: "include"`, `cache: "no-cache"`;
|
||||
4. ответ (JSON) маппится на поля страницы.
|
||||
|
||||
## Базовые хосты по окружениям
|
||||
|
||||
Значение определяется в `index.js` по хосту страницы. Итоговый URL = `https://<базовый хост>` + путь эндпоинта.
|
||||
|
||||
| Хост страницы (`window.location.host`) | Базовый хост API | Окружение |
|
||||
| --- | --- | --- |
|
||||
| `stamp-verification.sarex.io` | `api.sarex.io` | prod |
|
||||
| любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage |
|
||||
|
||||
> Промежуточных окружений (preprod/local) в коде не предусмотрено: всё, что не prod-домен, трактуется как stage.
|
||||
|
||||
## Эндпоинты по сервисам
|
||||
|
||||
### `documentations` — Сервис документации
|
||||
|
||||
| Ключ | Метод | Путь | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `getQrDocumentInfo` | GET | `/documentations/api/v1/public/qr/{qrId}/document_info?number_page={pageNumber}` | Публичная информация о документе по QR-коду штампа: название, версия, автор, дата загрузки, актуальность/аннулирование версии, статус согласования, changelog и ссылка на документ в Sarex |
|
||||
|
||||
Параметры запроса:
|
||||
|
||||
| Параметр | Расположение | Обязателен | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `qrId` | path (`{qrId}`) | да | Идентификатор QR-кода (из query `?id=`) |
|
||||
| `number_page` | query | да | Номер страницы документа (из query `?page_number=`) |
|
||||
|
||||
Заголовки запроса (как в текущем коде): `Content-Type: application/json` и `Authorization: Bearer <jwt>`. **Токен захардкожен и истёк** — см. раздел «Замечания». Запрос идёт с `credentials: "include"` (куки/учётные данные), `redirect: "follow"`, `referrerPolicy: "no-referrer"`.
|
||||
|
||||
Используемые поля ответа (по `index.js`): `document_name`, `version_number`, `page_number`, `author`, `upload_date`, `is_actual_version`, `is_canceled`, `does_bundle_have_flows`, `is_actual_version_approved`, `document_review_status`, `document_url`, `is_latest_changelog`, `changelog` (`no`, `description`, `author` и др.). Полная схема — в `openapi.yaml`.
|
||||
|
||||
Формирование ссылки на документ: при наличии `document_url` ссылка на странице собирается как `${data.document_url}&page_number=${data.page_number}` и открывается в Sarex.
|
||||
|
||||
## Внешние (сторонние) ресурсы
|
||||
|
||||
Не относятся к API Sarex, но выполняются страницей:
|
||||
|
||||
| Ресурс | Метод | URL | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| Яндекс.Метрика (tag.js) | GET | `https://mc.yandex.ru/metrika/tag.js` | Загрузка счётчика аналитики (id `93441026`) |
|
||||
| Яндекс.Метрика (noscript) | GET | `https://mc.yandex.ru/watch/93441026` | Пиксель для окружения без JS |
|
||||
| Статика приложения | GET | `static/css/*`, `static/js/*`, `static/icons/*`, `static/fonts/*`, `static/img/*` | CSS, `moment-with-locales.min.js`, иконки, шрифты, фон — раздаются самим nginx |
|
||||
|
||||
Health-check (со стороны инфраструктуры, не самой страницы): `GET /ping` → `200 {"result": "ok"}` (отдаётся nginx).
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Специализированного маппинга кодов ошибок в человекочитаемые сообщения (как в backend-сервисах) в коде нет. Ошибки обрабатываются минимально: `fetch` обёрнут в `.catch(...)`, ошибки логируются в консоль (`console.error`). При пустом/отсутствующем ответе поля страницы заполняются плейсхолдером `–––` (`PLACEHOLDER_STR`). Пользовательских уведомлений об ошибке нет.
|
||||
|
||||
## Замечания
|
||||
|
||||
- **Захардкоженный JWT.** В заголовке `Authorization` в `index.js` вшит истёкший (2023 г.) Bearer-токен. Эндпоинт публичный (`/public/qr/...`) — заголовок с токеном следует удалить; хранить токены в статике недопустимо.
|
||||
- **`credentials: "include"`** при кросс-доменном запросе к `*-api.sarex.io` требует корректной CORS-конфигурации на стороне `documentations` (`Access-Control-Allow-Credentials` + конкретный `Access-Control-Allow-Origin`).
|
||||
- **Параметр `v`** (версия) читается из query, но пока не используется.
|
||||
- **Выбор окружения только по домену** — новый prod-домен потребует правки кода.
|
||||
@ -1,216 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: Stamp Verification — Public QR API (consumed)
|
||||
version: "1.0.0"
|
||||
description: |
|
||||
Контракт публичного эндпоинта, который потребляет фронтенд
|
||||
**stamp-verification-frontend** (`pdm/stamp-verification-frontend`).
|
||||
|
||||
Сам фронтенд — статическая страница на nginx (ванильный JS, `static/js/index.js`)
|
||||
и **не предоставляет** собственного HTTP API. Данная спецификация описывает
|
||||
единственный внешний эндпоинт backend-сервиса **documentations**, к которому
|
||||
страница обращается для получения информации о документе по QR-коду штампа.
|
||||
Спецификация восстановлена по клиентскому коду (`index.js`) и служит
|
||||
справочным контрактом (потребитель, а не поставщик).
|
||||
|
||||
### Базовый хост
|
||||
Выбирается на клиенте по `window.location.host`:
|
||||
- `stamp-verification.sarex.io` → `https://api.sarex.io` (prod);
|
||||
- любой другой хост → `https://stage-api.sarex.io` (stage).
|
||||
|
||||
### Аутентификация
|
||||
Эндпоинт публичный (сегмент пути `/public/`). Текущий клиент тем не менее
|
||||
отправляет заголовок `Authorization: Bearer <jwt>` с **захардкоженным и
|
||||
истёкшим** токеном — это дефект клиента, а не требование API (см. замечания
|
||||
в `ENDPOINTS.md`). Запрос выполняется с `credentials: include` (CORS с
|
||||
учётными данными).
|
||||
|
||||
### Формат
|
||||
Ответ — `application/json`. Все поля опциональны/nullable: клиент подставляет
|
||||
плейсхолдер `–––` при отсутствии значения.
|
||||
|
||||
servers:
|
||||
- url: https://api.sarex.io
|
||||
description: prod
|
||||
- url: https://stage-api.sarex.io
|
||||
description: stage
|
||||
|
||||
paths:
|
||||
/documentations/api/v1/public/qr/{qr_id}/document_info:
|
||||
get:
|
||||
operationId: getQrDocumentInfo
|
||||
summary: Информация о документе по QR-коду штампа
|
||||
description: |
|
||||
Возвращает публичную информацию о документе, на который указывает QR-код
|
||||
штампа: название, версию, автора, дату загрузки, актуальность и статус
|
||||
согласования версии, сведения об изменениях (changelog) и ссылку на
|
||||
документ в Sarex.
|
||||
tags:
|
||||
- public-qr
|
||||
parameters:
|
||||
- name: qr_id
|
||||
in: path
|
||||
required: true
|
||||
description: Идентификатор QR-кода (на клиенте — query-параметр `id`).
|
||||
schema:
|
||||
type: string
|
||||
example: "b46d6b82-9e51-48e3-8dce-a38dfc6b2a26"
|
||||
- name: number_page
|
||||
in: query
|
||||
required: true
|
||||
description: Номер страницы документа (на клиенте — query-параметр `page_number`).
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
example: 1
|
||||
responses:
|
||||
"200":
|
||||
description: Информация о документе
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QrDocumentInfo"
|
||||
"404":
|
||||
description: Документ/QR не найден
|
||||
"422":
|
||||
description: Некорректные параметры запроса
|
||||
"500":
|
||||
description: Внутренняя ошибка сервиса
|
||||
|
||||
components:
|
||||
schemas:
|
||||
QrDocumentInfo:
|
||||
type: object
|
||||
description: |
|
||||
Набор полей, используемых клиентом (`static/js/index.js`). Поля
|
||||
опциональны — при отсутствии клиент отображает плейсхолдер `–––`.
|
||||
properties:
|
||||
document_name:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Имя файла документа
|
||||
example: some-file-01.pdf
|
||||
version_number:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Номер версии документа
|
||||
example: v2
|
||||
page_number:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Номер страницы (эхо параметра запроса); участвует в сборке ссылки на документ
|
||||
example: 1
|
||||
author:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Автор версии (отображаемое имя)
|
||||
example: Андрей Алешков
|
||||
upload_date:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Дата и время загрузки версии (ISO 8601); на клиенте форматируется как `DD.MM.YYYY / HH:mm`
|
||||
example: "2023-05-23T20:01:55.496768Z"
|
||||
is_actual_version:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: >-
|
||||
`true` — последняя версия; `false` — не последняя; отсутствие/`null` —
|
||||
актуальность неизвестна
|
||||
example: true
|
||||
is_canceled:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: >-
|
||||
`true` — QR-код/версия аннулированы (клиент показывает предупреждение
|
||||
«QR-код аннулирован»)
|
||||
example: false
|
||||
does_bundle_have_flows:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Есть ли у бандла процессы согласования (управляет показом блока статуса согласования)
|
||||
example: false
|
||||
is_actual_version_approved:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Согласована ли текущая версия (при `does_bundle_have_flows = true`)
|
||||
example: true
|
||||
document_review_status:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Человекочитаемый статус документа (показывается при согласованной актуальной версии)
|
||||
example: Согласован
|
||||
document_url:
|
||||
type: string
|
||||
format: uri
|
||||
nullable: true
|
||||
description: >-
|
||||
Ссылка на документ в Sarex. Клиент открывает её как
|
||||
`${document_url}&page_number=${page_number}`
|
||||
example: https://stage.sarex.io/workspaces-v2/0244d9f9-8f32-4b65-80c7-7c403b85c81e?type=pdf
|
||||
is_latest_changelog:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: >-
|
||||
Является ли `changelog` последним. Если `false` при наличии
|
||||
`changelog` — клиент показывает предупреждение «Была выпущена новая
|
||||
версия изменения»
|
||||
example: true
|
||||
changelog:
|
||||
$ref: "#/components/schemas/Changelog"
|
||||
|
||||
Changelog:
|
||||
type: object
|
||||
nullable: true
|
||||
description: Сведения об изменении версии документа
|
||||
properties:
|
||||
bundle_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: b46d6b82-9e51-48e3-8dce-a38dfc6b2a26
|
||||
"no":
|
||||
type: string
|
||||
description: Порядковый номер изменения (на клиенте выводится как `№{no}`)
|
||||
example: "2"
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Описание изменения. Если длиннее 20 символов — клиент включает
|
||||
сворачивание текста; при отсутствии показывает «Нет описания изменений»
|
||||
example: Внесены изменения в структуру документа. Добавлена подпись руководителя проекта.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-23T20:01:55.496768Z"
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-23T20:01:55.496768Z"
|
||||
created_by:
|
||||
type: integer
|
||||
example: 2203
|
||||
author:
|
||||
$ref: "#/components/schemas/ChangelogAuthor"
|
||||
|
||||
ChangelogAuthor:
|
||||
type: object
|
||||
description: Автор изменения
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 2203
|
||||
username:
|
||||
type: string
|
||||
example: a.aleshkov
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: a.aleshkov@sarex.io
|
||||
first_name:
|
||||
type: string
|
||||
example: Андрей
|
||||
last_name:
|
||||
type: string
|
||||
example: Алешков
|
||||
@ -1,906 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: workspaces-api
|
||||
description: >-
|
||||
HTTP API сервиса workspaces-api (репозиторий `pdm/workspaces-api`).
|
||||
Хранит рабочие области (workspaces), документы, состояния (states),
|
||||
динамические состояния и приложения (apps). Схема составлена по
|
||||
маршрутам `cmd/api/bootstrap.go` и структурам пакетов `workspace` и `apps`.
|
||||
|
||||
Группы маршрутов:
|
||||
* публичные (`/ping`, `/metrics`) — без middleware;
|
||||
* `/api/v1` — внешний API, требует JWT (middleware `auth.JWTToCtx`);
|
||||
* `/internal/v1`, `/internal/v2` — внутренний API без JWT
|
||||
(доступ ограничивается сетевой политикой/ingress).
|
||||
version: "1.0.0"
|
||||
|
||||
servers:
|
||||
- url: https://api.sarex.io/workspaces
|
||||
description: production
|
||||
- url: https://api.preprod.sarex.io/workspaces
|
||||
description: preprod
|
||||
- url: https://stage-api.sarex.io/workspaces
|
||||
description: stage
|
||||
- url: http://localhost:6666
|
||||
description: local
|
||||
|
||||
tags:
|
||||
- name: health
|
||||
description: Liveness/readiness и метрики
|
||||
- name: workspaces
|
||||
description: Рабочие области
|
||||
- name: documents
|
||||
description: Документы рабочих областей
|
||||
- name: states
|
||||
description: Состояния рабочих областей
|
||||
- name: dynamic-states
|
||||
description: Динамические состояния
|
||||
- name: apps
|
||||
description: Приложения и их инстансы
|
||||
- name: internal
|
||||
description: Внутренние эндпоинты (без JWT)
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
paths:
|
||||
# ------------------------------------------------------------------ health
|
||||
/ping:
|
||||
get:
|
||||
tags: [health]
|
||||
summary: Liveness/readiness проба
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Сервис жив
|
||||
|
||||
/metrics:
|
||||
get:
|
||||
tags: [health]
|
||||
summary: Метрики Prometheus
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Метрики в формате Prometheus
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
# -------------------------------------------------------------- api/v1 · ws
|
||||
/api/v1/workspaces:
|
||||
post:
|
||||
tags: [workspaces]
|
||||
summary: Создать рабочую область (v2)
|
||||
description: Создаёт рабочую область по списку id документов (`CreateV2Workspace`).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateV2WorkspaceRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Созданная рабочая область
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workspace"
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{ws_id}:
|
||||
get:
|
||||
tags: [workspaces]
|
||||
summary: Получить рабочую область
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WsId"
|
||||
- name: archived
|
||||
in: query
|
||||
description: Включать архивные сущности
|
||||
schema: { type: integer, enum: [0, 1] }
|
||||
- name: state
|
||||
in: query
|
||||
description: uuid состояния; при указании в ответ добавляется последнее состояние
|
||||
schema: { type: string, format: uuid }
|
||||
responses:
|
||||
"200":
|
||||
description: Рабочая область
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workspace"
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{id}/archive:
|
||||
post:
|
||||
tags: [workspaces]
|
||||
summary: Архивировать рабочую область
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{id}/unarchive:
|
||||
post:
|
||||
tags: [workspaces]
|
||||
summary: Разархивировать рабочую область
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{id}/cache:
|
||||
delete:
|
||||
tags: [workspaces]
|
||||
summary: Очистить кеш бандлов рабочей области
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { description: Кеш очищен }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{ws_id}/states:
|
||||
get:
|
||||
tags: [states]
|
||||
summary: Список состояний рабочей области
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WsId"
|
||||
- name: expand
|
||||
in: query
|
||||
description: "`state_json` — вернуть полные данные состояний (иначе — краткий список)"
|
||||
schema: { type: string, enum: [state_json] }
|
||||
responses:
|
||||
"200":
|
||||
description: Состояния
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/State" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
post:
|
||||
tags: [states]
|
||||
summary: Создать состояние рабочей области
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateStateRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Созданное состояние
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/State" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{ws_id}/documents:
|
||||
post:
|
||||
tags: [documents]
|
||||
summary: Добавить документ в рабочую область
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AddDocumentRequest" }
|
||||
responses:
|
||||
"200": { description: Документ добавлен }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{ws_id}/documents/{doc_id}:
|
||||
delete:
|
||||
tags: [documents]
|
||||
summary: Удалить документ из рабочей области
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WsId"
|
||||
- $ref: "#/components/parameters/DocId"
|
||||
responses:
|
||||
"200": { description: Документ удалён }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{ws_id}/dynamic_states:
|
||||
get:
|
||||
tags: [dynamic-states]
|
||||
summary: Список динамических состояний рабочей области
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
responses:
|
||||
"200":
|
||||
description: Динамические состояния
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/DynamicState" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
post:
|
||||
tags: [dynamic-states]
|
||||
summary: Создать динамическое состояние
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateDynamicStateRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Созданное динамическое состояние
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DynamicState" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/workspaces/{workspace_id}/apps/{app_id}/instances:
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Создать инстанс приложения в рабочей области
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/AppId"
|
||||
responses:
|
||||
"200":
|
||||
description: Созданный инстанс
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppInstance" }
|
||||
"400":
|
||||
description: Некорректный запрос или дубликат инстанса
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# -------------------------------------------------------- api/v1 · documents
|
||||
/api/v1/documents/{doc_id}:
|
||||
patch:
|
||||
tags: [documents]
|
||||
summary: Обновить документ
|
||||
description: "Не реализовано в текущем коде — всегда возвращает 400 `Implement me`."
|
||||
parameters: [ { $ref: "#/components/parameters/DocId" } ]
|
||||
responses:
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
|
||||
/api/v1/documents/{id}/archive:
|
||||
post:
|
||||
tags: [documents]
|
||||
summary: Архивировать документ
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/documents/{id}/unarchive:
|
||||
post:
|
||||
tags: [documents]
|
||||
summary: Разархивировать документ
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# ----------------------------------------------------------- api/v1 · states
|
||||
/api/v1/states/{id}:
|
||||
get:
|
||||
tags: [states]
|
||||
summary: Получить состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200":
|
||||
description: Состояние
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/State" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
patch:
|
||||
tags: [states]
|
||||
summary: Изменить состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SetStateRequest" }
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
delete:
|
||||
tags: [states]
|
||||
summary: Удалить (архивировать) состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/states/{id}/archive:
|
||||
post:
|
||||
tags: [states]
|
||||
summary: Архивировать состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/states/{id}/unarchive:
|
||||
post:
|
||||
tags: [states]
|
||||
summary: Разархивировать состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# -------------------------------------------------- api/v1 · dynamic states
|
||||
/api/v1/dynamic_states/{id}:
|
||||
get:
|
||||
tags: [dynamic-states]
|
||||
summary: Получить динамическое состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200":
|
||||
description: Динамическое состояние
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DynamicState" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
patch:
|
||||
tags: [dynamic-states]
|
||||
summary: Изменить динамическое состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SetDynamicStateRequest" }
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/Ok" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
delete:
|
||||
tags: [dynamic-states]
|
||||
summary: Удалить динамическое состояние
|
||||
parameters: [ { $ref: "#/components/parameters/Id" } ]
|
||||
responses:
|
||||
"200": { description: Удалено }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# ------------------------------------------------------------ api/v1 · apps
|
||||
/api/v1/company/{company_id}/apps:
|
||||
get:
|
||||
tags: [apps]
|
||||
summary: Приложения компании
|
||||
parameters: [ { $ref: "#/components/parameters/CompanyId" } ]
|
||||
responses:
|
||||
"200":
|
||||
description: Список приложений
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/App" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Создать приложение
|
||||
parameters: [ { $ref: "#/components/parameters/CompanyId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppAttributes" }
|
||||
responses:
|
||||
"200":
|
||||
description: Созданное приложение
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/App" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/apps/{app_id}:
|
||||
patch:
|
||||
tags: [apps]
|
||||
summary: Изменить приложение
|
||||
parameters: [ { $ref: "#/components/parameters/AppId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ChangeAppRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Обновлённое приложение
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/App" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/apps/{app_id}/archive:
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Архивировать приложение
|
||||
parameters: [ { $ref: "#/components/parameters/AppId" } ]
|
||||
responses:
|
||||
"200": { description: Приложение архивировано }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/apps/{app_id}/unarchive:
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Разархивировать приложение
|
||||
parameters: [ { $ref: "#/components/parameters/AppId" } ]
|
||||
responses:
|
||||
"200": { description: Приложение разархивировано }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/app-instances/{instance_id}/archive:
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Архивировать инстанс приложения
|
||||
parameters: [ { $ref: "#/components/parameters/InstanceId" } ]
|
||||
responses:
|
||||
"200": { description: Инстанс архивирован }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/api/v1/app-instances/{instance_id}/unarchive:
|
||||
post:
|
||||
tags: [apps]
|
||||
summary: Разархивировать инстанс приложения
|
||||
parameters: [ { $ref: "#/components/parameters/InstanceId" } ]
|
||||
responses:
|
||||
"200": { description: Инстанс разархивирован }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# -------------------------------------------------------------- internal/v1
|
||||
/internal/v1/workspaces:
|
||||
post:
|
||||
tags: [internal]
|
||||
summary: Создать рабочую область (v1, по ссылкам на бандлы)
|
||||
description: Внутренний эндпоинт без JWT. Создаёт рабочую область по списку URL бандлов.
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Созданная рабочая область
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Workspace" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/internal/v1/workspaces/{ws_id}/documents:
|
||||
post:
|
||||
tags: [internal]
|
||||
summary: Добавить документ в рабочую область (без проверки прав)
|
||||
security: []
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AddDocumentRequest" }
|
||||
responses:
|
||||
"200": { description: Документ добавлен }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/internal/v1/workspaces/{ws_id}/states:
|
||||
get:
|
||||
tags: [internal]
|
||||
summary: Список состояний рабочей области (internal)
|
||||
security: []
|
||||
parameters: [ { $ref: "#/components/parameters/WsId" } ]
|
||||
responses:
|
||||
"200":
|
||||
description: Состояния
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/State" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
# -------------------------------------------------------------- internal/v2
|
||||
/internal/v2/workspaces:
|
||||
post:
|
||||
tags: [internal]
|
||||
summary: Создать рабочую область (v2, internal)
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateV2WorkspaceRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Созданная рабочая область
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Workspace" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/internal/v2/workspaces/is_workspaces_v2:
|
||||
post:
|
||||
tags: [internal]
|
||||
summary: Проверить, являются ли рабочие области v2
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/IsWorkspaceV2Request" }
|
||||
responses:
|
||||
"200":
|
||||
description: Карта id → признак v2
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/IsWorkspaceV2Response" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/internal/v2/documents/{docs_id}:
|
||||
delete:
|
||||
tags: [internal]
|
||||
summary: Удалить документ (internal)
|
||||
security: []
|
||||
parameters:
|
||||
- name: docs_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200": { description: Документ удалён }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
/internal/v2/documents/restore:
|
||||
patch:
|
||||
tags: [internal]
|
||||
summary: Восстановить документы и рабочие области (internal)
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/RestoreDocumentsRequest" }
|
||||
responses:
|
||||
"200": { description: Восстановлено }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"500": { $ref: "#/components/responses/InternalError" }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: >-
|
||||
JWT в заголовке `Authorization: Bearer <token>`. Токен также может
|
||||
передаваться query-параметром `jwt` (удаляется middleware после разбора).
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
WsId:
|
||||
name: ws_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
WorkspaceId:
|
||||
name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
DocId:
|
||||
name: doc_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
AppId:
|
||||
name: app_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
InstanceId:
|
||||
name: instance_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
CompanyId:
|
||||
name: company_id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer, format: int64 }
|
||||
|
||||
responses:
|
||||
Ok:
|
||||
description: Успех
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/OkResponse" }
|
||||
BadRequest:
|
||||
description: Некорректный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
NotFound:
|
||||
description: Ресурс не найден
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
InternalError:
|
||||
description: Внутренняя ошибка сервера
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
error: { type: string }
|
||||
required: [error]
|
||||
|
||||
OkResponse:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean }
|
||||
required: [ok]
|
||||
|
||||
# ---- requests ----
|
||||
CreateWorkspaceRequest:
|
||||
type: object
|
||||
description: "`workspace/api/workspace/create.go` (v1)"
|
||||
properties:
|
||||
title: { type: string, maxLength: 254 }
|
||||
subtitle: { type: string, maxLength: 254 }
|
||||
target: { type: integer, nullable: true }
|
||||
files:
|
||||
type: array
|
||||
description: Ссылки на бандлы документов
|
||||
items: { type: string }
|
||||
features:
|
||||
type: array
|
||||
items: { type: string }
|
||||
required: [title, files]
|
||||
|
||||
CreateV2WorkspaceRequest:
|
||||
type: object
|
||||
description: "`workspace/api/workspace/createV2.go` (v2)"
|
||||
properties:
|
||||
title: { type: string, maxLength: 254 }
|
||||
subtitle: { type: string, maxLength: 254 }
|
||||
target: { type: integer, nullable: true }
|
||||
documents:
|
||||
type: array
|
||||
description: id документов сервиса documentation
|
||||
items: { type: integer, format: int64 }
|
||||
features:
|
||||
type: array
|
||||
items: { type: string }
|
||||
auto_created: { type: boolean }
|
||||
required: [documents]
|
||||
|
||||
IsWorkspaceV2Request:
|
||||
type: object
|
||||
properties:
|
||||
ids:
|
||||
type: array
|
||||
items: { type: string }
|
||||
required: [ids]
|
||||
|
||||
IsWorkspaceV2Response:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
description: Карта uuid рабочей области → является ли она v2
|
||||
|
||||
AddDocumentRequest:
|
||||
type: object
|
||||
properties:
|
||||
document_id: { type: integer, format: int64 }
|
||||
required: [document_id]
|
||||
|
||||
RestoreDocumentsRequest:
|
||||
type: object
|
||||
properties:
|
||||
document_ids:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { type: integer, format: int64 }
|
||||
required: [document_ids]
|
||||
|
||||
CreateStateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string, maxLength: 254 }
|
||||
data: { type: string }
|
||||
dynamic_state_id: { type: string, format: uuid, nullable: true }
|
||||
required: [name, data]
|
||||
|
||||
SetStateRequest:
|
||||
type: object
|
||||
description: Частичное обновление; все поля опциональны
|
||||
properties:
|
||||
name: { type: string, nullable: true }
|
||||
is_default: { type: boolean, nullable: true }
|
||||
data: { type: string, nullable: true }
|
||||
|
||||
CreateDynamicStateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string, maxLength: 254 }
|
||||
autoload: { type: boolean }
|
||||
required: [name]
|
||||
|
||||
SetDynamicStateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string, nullable: true }
|
||||
autoload: { type: boolean, nullable: true }
|
||||
|
||||
AppAttributes:
|
||||
type: object
|
||||
description: "`apps/models.go`"
|
||||
properties:
|
||||
name: { type: string, minLength: 1, maxLength: 100 }
|
||||
entrypoint: { type: string, minLength: 1, maxLength: 1024, format: uri }
|
||||
devices:
|
||||
type: array
|
||||
items: { type: string, enum: [mobile, tablet, desktop] }
|
||||
module_name: { type: string, minLength: 1, maxLength: 100 }
|
||||
required: [name, entrypoint, devices, module_name]
|
||||
|
||||
ChangeAppRequest:
|
||||
type: object
|
||||
description: >-
|
||||
Частичное обновление приложения; должно быть задано хотя бы одно поле
|
||||
(`apps/api/changeapp.go`).
|
||||
properties:
|
||||
name: { type: string, maxLength: 100 }
|
||||
entrypoint: { type: string, maxLength: 1024, format: uri }
|
||||
devices:
|
||||
type: array
|
||||
items: { type: string, enum: [mobile, tablet, desktop] }
|
||||
module_name: { type: string, maxLength: 100 }
|
||||
|
||||
# ---- models ----
|
||||
Workspace:
|
||||
type: object
|
||||
description: "Агрегат рабочей области (`workspace.Model`)"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
title: { type: string }
|
||||
subtitle: { type: string }
|
||||
features:
|
||||
type: array
|
||||
items: { type: string }
|
||||
target: { type: integer, nullable: true }
|
||||
auto_created: { type: boolean, nullable: true }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
documents:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Document" }
|
||||
documents_v2:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/DocumentV2" }
|
||||
state:
|
||||
allOf: [ { $ref: "#/components/schemas/State" } ]
|
||||
nullable: true
|
||||
app_instances:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/AppInstance" }
|
||||
|
||||
Document:
|
||||
type: object
|
||||
description: "`workspace.ModelDocument`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
workspace_id: { type: string, format: uuid }
|
||||
is_archived: { type: boolean }
|
||||
kind: { type: string }
|
||||
url: { type: string }
|
||||
bundle: { type: string }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
|
||||
DocumentV2:
|
||||
type: object
|
||||
description: "`workspace.ModelDocumentV2`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
document_id: { type: integer, format: int64 }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
State:
|
||||
type: object
|
||||
description: "`workspace.ModelState`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
workspace_id: { type: string, format: uuid }
|
||||
created_at: { type: string, format: date-time }
|
||||
data: { type: string }
|
||||
author_id: { type: integer, format: int64, nullable: true }
|
||||
is_default: { type: boolean }
|
||||
indestructible: { type: boolean }
|
||||
dynamic_state_id: { type: string, format: uuid, nullable: true }
|
||||
|
||||
DynamicState:
|
||||
type: object
|
||||
description: "`workspace.ModelDynamicState`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
author_id: { type: integer, format: int64, nullable: true }
|
||||
autoload: { type: boolean }
|
||||
workspace_id: { type: string, format: uuid }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
states:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/State" }
|
||||
|
||||
App:
|
||||
type: object
|
||||
description: "`apps.App`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
company_id: { type: integer, format: int64 }
|
||||
name: { type: string }
|
||||
entrypoint: { type: string }
|
||||
devices:
|
||||
type: array
|
||||
items: { type: string, enum: [mobile, tablet, desktop] }
|
||||
module_name: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
|
||||
AppInstance:
|
||||
type: object
|
||||
description: "`apps.AppInstance`"
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
app_id: { type: string, format: uuid }
|
||||
workspace_id: { type: string, format: uuid }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
app:
|
||||
allOf: [ { $ref: "#/components/schemas/App" } ]
|
||||
nullable: true
|
||||
Loading…
Reference in New Issue
Block a user