From cc3640f13c9c3eb62e74a277ea63838b54af525b Mon Sep 17 00:00:00 2001 From: embeddevio Date: Thu, 22 Oct 2026 00:20:36 +0300 Subject: [PATCH] =?UTF-8?q?docs(api):=20OpenAPI=203.1=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BA=D1=82=20API=20v1=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20source=20of=20truth=20(Refs=20#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/openapi.yaml: все эндпоинты ТЗ, единый формат ошибок {code,message,details}, writeOnly для private_key/preshared_key/password, WS-кадры metrics/stream. - requirements-contract.txt: зависимости контрактных тестов (jsonschema, openapi-spec-validator, PyYAML) до мержа каркаса PR #47. - .gitea/workflows/contract-openapi.yml: lint + контрактный тест + проверка актуальности сгенерированного TS-клиента. --- .gitea/workflows/contract-openapi.yml | 55 ++ docs/openapi.yaml | 1254 +++++++++++++++++++++++++ requirements-contract.txt | 5 + 3 files changed, 1314 insertions(+) create mode 100644 .gitea/workflows/contract-openapi.yml create mode 100644 docs/openapi.yaml create mode 100644 requirements-contract.txt diff --git a/.gitea/workflows/contract-openapi.yml b/.gitea/workflows/contract-openapi.yml new file mode 100644 index 0000000..6745e31 --- /dev/null +++ b/.gitea/workflows/contract-openapi.yml @@ -0,0 +1,55 @@ +name: contract-openapi +# Контракт OpenAPI 3.1 (issue #19): линтеры, контрактный тест «схема ↔ реализация» +# и проверка, что сгенерированный TS-клиент актуален относительно docs/openapi.yaml. +on: + push: + branches: + - "**" + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + contract: + name: openapi-contract + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt -r requirements-contract.txt + + - name: lint (ruff + mypy) + run: | + ruff check backend + ruff format --check backend + mypy backend + + - name: contract and unit tests (coverage >= 70%) + run: pytest + + ts-client: + name: ts-client-is-current + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: node 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: regenerate client and compare + run: | + bash ui/scripts/generate-api.sh + git diff --exit-code -- ui/src/api/generated/schema.d.ts diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..4f46a1e --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,1254 @@ +openapi: 3.1.0 +info: + title: awg-dash API + version: 0.1.0 + summary: REST + WebSocket control plane of the AmneziaWG gateway dashboard + description: | + Контракт API v1 (source of truth, ADR-008). Спецификация — источник истины: + реализация и UI-клиент генерируются/проверяются от неё. + + Соглашения: + * Единый формат ошибки — схема `Error` (`{code, message, details}`) для любого + не-2xx ответа, включая 422 и ошибки внутренних исключений. + * Секреты (`private_key`, `preshared_key`, `password`) помечены `writeOnly`: + они допустимы в запросах и ЗАПРЕЩЕНЫ в ответах (`additionalProperties: false` + + удаление `writeOnly`-свойств из ответной валидации). + * Аутентификация: `Authorization: Bearer ` для REST, cookie-сессия + + `X-CSRF-Token` для UI. `/api/v1/health`, `/api/v1/ready`, `/metrics` — открыты. + * Все таймстемпы — RFC 3339 (UTC), целые размеры — байты, длительности — секунды. + + Генерация TS-клиента: `ui/scripts/generate-api.sh` → + `ui/src/api/generated/schema.d.ts`. Контрактный тест: + `pytest backend/tests/test_contract_responses.py`. + license: + name: MIT + identifier: MIT +servers: + - url: / + description: Встроенный uvicorn (ADR-007), тот же origin, что и UI +tags: + - name: ops + description: Служебные эндпоинты (health/ready/metrics), без аутентификации + - name: status + description: Состояние туннеля и сводный статус + - name: config + description: Чтение, запись и валидация конфигурации AmneziaWG + - name: routing + description: Правила маршрутизации, режим split-tunnel, GeoIP, импорт + - name: devices + description: Устройства за шлюзом и их маршрутизация + - name: logs + description: Журнал событий шлюза + - name: auth + description: Аутентификация UI и API + - name: metrics + description: Потоковые метрики (WebSocket) + +security: + - bearerAuth: [] + +paths: + /api/v1/health: + get: + tags: [ops] + operationId: getHealth + summary: Liveness-проба + security: [] + responses: + "200": + description: Процесс жив + content: + application/json: + schema: + $ref: "#/components/schemas/HealthResponse" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/ready: + get: + tags: [ops] + operationId: getReady + summary: Readiness-проба (готовность агента и зависимостей) + security: [] + responses: + "200": + description: Состояние готовности (в том числе при ready=false) + content: + application/json: + schema: + $ref: "#/components/schemas/ReadyResponse" + "503": + $ref: "#/components/responses/AgentUnavailable" + "500": + $ref: "#/components/responses/InternalError" + + /metrics: + get: + tags: [ops] + operationId: getPrometheusMetrics + summary: Метрики в формате Prometheus text exposition + security: [] + responses: + "200": + description: Текстовый экспорт метрик + content: + text/plain: + schema: + type: string + description: Prometheus text exposition format (version 0.0.4) + + /api/v1/status: + get: + tags: [status] + operationId: getStatus + summary: Сводный статус шлюза, туннеля и GeoIP + responses: + "200": + description: Статус + content: + application/json: + schema: + $ref: "#/components/schemas/StatusResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "503": + $ref: "#/components/responses/AgentUnavailable" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/config: + get: + tags: [config] + operationId: getConfig + summary: Текущая конфигурация (без секретов) + responses: + "200": + description: Конфигурация; секретные поля отсутствуют + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + put: + tags: [config] + operationId: putConfig + summary: Атомарное применение конфигурации (validate → staging → apply → rollback) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigUpdateRequest" + responses: + "200": + description: Конфигурация применена + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/config/validate: + post: + tags: [config] + operationId: validateConfig + summary: Сухая валидация конфигурации без применения + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigUpdateRequest" + responses: + "200": + description: Результат валидации (в том числе при valid=false) + content: + application/json: + schema: + $ref: "#/components/schemas/ValidateResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/routing/rules: + get: + tags: [routing] + operationId: listRoutingRules + summary: Список правил маршрутизации + parameters: + - name: enabled + in: query + required: false + description: Фильтр по признаку включённости + schema: + type: boolean + - name: kind + in: query + required: false + schema: + $ref: "#/components/schemas/RuleKind" + responses: + "200": + description: Список правил + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRuleList" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + post: + tags: [routing] + operationId: createRoutingRule + summary: Создать правило маршрутизации + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRuleCreate" + responses: + "201": + description: Правило создано + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRule" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/routing/rules/{rule_id}: + parameters: + - $ref: "#/components/parameters/RuleId" + get: + tags: [routing] + operationId: getRoutingRule + summary: Правило по идентификатору + responses: + "200": + description: Правило + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRule" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalError" + put: + tags: [routing] + operationId: updateRoutingRule + summary: Изменить правило + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRuleUpdate" + responses: + "200": + description: Обновлённое правило + content: + application/json: + schema: + $ref: "#/components/schemas/RoutingRule" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + delete: + tags: [routing] + operationId: deleteRoutingRule + summary: Удалить правило + responses: + "204": + description: Правило удалено + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/routing/mode: + put: + tags: [routing] + operationId: putRoutingMode + summary: Переключить режим split-tunnel + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ModeUpdateRequest" + responses: + "200": + description: Режим применён + content: + application/json: + schema: + $ref: "#/components/schemas/ModeUpdateResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "503": + $ref: "#/components/responses/AgentUnavailable" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/routing/geoip/update: + post: + tags: [routing] + operationId: updateGeoIp + summary: Обновить снапшот GeoIP (RIPE Stat → локальный кэш) + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/GeoIpUpdateRequest" + responses: + "200": + description: Обновление выполнено (или взято из кэша) + content: + application/json: + schema: + $ref: "#/components/schemas/GeoIpUpdateResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "503": + $ref: "#/components/responses/UpstreamUnavailable" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/routing/import: + post: + tags: [routing] + operationId: importRoutingConfig + summary: Импорт .conf / JSON (пиры и правила) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ImportRequest" + responses: + "200": + description: Результат импорта + content: + application/json: + schema: + $ref: "#/components/schemas/ImportResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/metrics/stream: + get: + tags: [metrics] + operationId: streamMetrics + summary: WebSocket-поток метрик и статуса + description: | + WebSocket-апгрейд на том же пути. Клиент передаёт токен либо заголовком + `Authorization: Bearer `, либо query-параметром `token`. + Сервер отправляет текстовые JSON-кадры схемы `WsFrame`; при `type=metrics` + поле `data` соответствует `MetricsSample`, при `type=status` — `StatusResponse`, + при `type=error` — `Error`. + parameters: + - name: token + in: query + required: false + description: Токен доступа для сред, где нельзя задать заголовок + schema: + type: string + - name: interval_ms + in: query + required: false + description: Период отправки кадров, мс + schema: + type: integer + minimum: 250 + maximum: 60000 + default: 1000 + responses: + "101": + description: Переключение протокола на WebSocket + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/devices: + get: + tags: [devices] + operationId: listDevices + summary: Устройства за шлюзом + responses: + "200": + description: Список устройств + content: + application/json: + schema: + $ref: "#/components/schemas/DeviceList" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/logs: + get: + tags: [logs] + operationId: listLogs + summary: Журнал событий + parameters: + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 500 + default: 100 + - name: cursor + in: query + required: false + description: Курсор пагинации из предыдущего ответа (`next_cursor`) + schema: + type: string + - name: level + in: query + required: false + schema: + $ref: "#/components/schemas/LogLevel" + - name: q + in: query + required: false + description: Подстрока поиска по сообщению + schema: + type: string + responses: + "200": + description: Страница журнала + content: + application/json: + schema: + $ref: "#/components/schemas/LogList" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/auth/login: + post: + tags: [auth] + operationId: login + summary: Аутентификация (сессия UI + токен API) + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AuthLoginRequest" + responses: + "200": + description: Сессия создана + content: + application/json: + schema: + $ref: "#/components/schemas/AuthSession" + "401": + $ref: "#/components/responses/Unauthorized" + "422": + $ref: "#/components/responses/ValidationFailed" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/auth/logout: + post: + tags: [auth] + operationId: logout + summary: Завершить сессию + responses: + "200": + description: Сессия завершена + content: + application/json: + schema: + $ref: "#/components/schemas/AuthSession" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + + /api/v1/auth/session: + get: + tags: [auth] + operationId: getAuthSession + summary: Текущая сессия и CSRF-токен + responses: + "200": + description: Состояние сессии + content: + application/json: + schema: + $ref: "#/components/schemas/AuthSession" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: opaque + description: "Токен доступа API (заголовок `Authorization: Bearer `)" + sessionCookie: + type: apiKey + in: cookie + name: awg_session + description: Сессионная cookie UI (требуется вместе с `X-CSRF-Token` для изменяющих запросов) + + parameters: + RuleId: + name: rule_id + in: path + required: true + description: Идентификатор правила + schema: + type: string + format: uuid + + responses: + BadRequest: + description: Некорректный запрос + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + Unauthorized: + description: Отсутствует или недействителен токен доступа + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + NotFound: + description: Объект не найден + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + Conflict: + description: Конфликт состояния (например, apply уже выполняется) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + ValidationFailed: + description: Тело запроса не соответствует схеме + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + RateLimited: + description: Превышен лимит запросов + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + UpstreamUnavailable: + description: Внешний источник (RIPE Stat) недоступен + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + AgentUnavailable: + description: Агент шлюза недоступен (unix-socket не отвечает) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + InternalError: + description: Внутренняя ошибка + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + schemas: + ErrorCode: + type: string + description: Машиночитаемый код ошибки + enum: + - bad_request + - validation_failed + - unauthorized + - forbidden + - not_found + - conflict + - rate_limited + - upstream_unavailable + - agent_unavailable + - internal_error + + Error: + type: object + description: Единый формат ошибки для всех не-2xx ответов + additionalProperties: false + required: [code, message, details] + properties: + code: + $ref: "#/components/schemas/ErrorCode" + message: + type: string + minLength: 1 + description: Человекочитаемое описание + details: + description: Машиночитаемые детали (для validation_failed — список issues) + type: [object, "null"] + additionalProperties: true + default: null + + Check: + type: object + additionalProperties: false + required: [name, ok] + properties: + name: + type: string + ok: + type: boolean + detail: + type: [string, "null"] + + HealthResponse: + type: object + additionalProperties: false + required: [status, version, uptime_s, checks] + properties: + status: + type: string + enum: [ok, degraded, down] + version: + type: string + uptime_s: + type: integer + minimum: 0 + checks: + type: array + items: + $ref: "#/components/schemas/Check" + + ReadyResponse: + type: object + additionalProperties: false + required: [ready, checks] + properties: + ready: + type: boolean + checks: + type: array + items: + $ref: "#/components/schemas/Check" + + RoutingMode: + type: string + description: Режим split-tunnel + enum: [geoip, dns, all-vpn, direct-only] + + RuleKind: + type: string + enum: [domain, cidr, geoip, asn] + + RuleAction: + type: string + enum: [vpn, direct] + + LogLevel: + type: string + enum: [debug, info, warning, error] + + TunnelState: + type: string + enum: [up, down, error] + + TunnelStatus: + type: object + additionalProperties: false + required: [interface, state, handshake_age_s, rx_bytes, tx_bytes, endpoint, last_handshake_at] + properties: + interface: + type: string + examples: [awg0] + state: + $ref: "#/components/schemas/TunnelState" + handshake_age_s: + type: [integer, "null"] + minimum: 0 + rx_bytes: + type: integer + minimum: 0 + tx_bytes: + type: integer + minimum: 0 + endpoint: + type: [string, "null"] + last_handshake_at: + type: [string, "null"] + format: date-time + + GeoIpSource: + type: string + enum: [ripe-stat, cache, none] + + GeoIpStatus: + type: object + additionalProperties: false + required: [source, prefix_count, updated_at] + properties: + source: + $ref: "#/components/schemas/GeoIpSource" + prefix_count: + type: integer + minimum: 0 + updated_at: + type: [string, "null"] + format: date-time + + StatusResponse: + type: object + additionalProperties: false + required: [version, uptime_s, mode, tunnel, geoip, kill_switch, dns_leak_protection] + properties: + version: + type: string + uptime_s: + type: integer + minimum: 0 + mode: + $ref: "#/components/schemas/RoutingMode" + tunnel: + $ref: "#/components/schemas/TunnelStatus" + geoip: + $ref: "#/components/schemas/GeoIpStatus" + kill_switch: + type: boolean + dns_leak_protection: + type: boolean + + TunnelConfig: + type: object + additionalProperties: false + required: [address, listen_port, mtu, private_key] + properties: + address: + type: string + description: Адрес интерфейса в CIDR + examples: ["10.8.0.2/32"] + listen_port: + type: integer + minimum: 1 + maximum: 65535 + mtu: + type: integer + minimum: 576 + maximum: 1500 + private_key: + type: string + writeOnly: true + description: Приватный ключ интерфейса; только запись, в ответах отсутствует + dns: + type: array + items: + type: string + + PeerConfig: + type: object + additionalProperties: false + required: [public_key, endpoint, allowed_ips] + properties: + public_key: + type: string + preshared_key: + type: string + writeOnly: true + description: Preshared key; только запись, в ответах отсутствует + endpoint: + type: string + allowed_ips: + type: array + items: + type: string + persistent_keepalive: + type: [integer, "null"] + minimum: 0 + maximum: 65535 + + DnsConfig: + type: object + additionalProperties: false + required: [mode, servers] + properties: + mode: + type: string + enum: [internal, external] + servers: + type: array + items: + type: string + + Ipv6Policy: + type: string + enum: [direct, tunnel, block] + + RoutingConfig: + type: object + additionalProperties: false + required: [mode, kill_switch, mtu_clamping, ipv6, geoip] + properties: + mode: + $ref: "#/components/schemas/RoutingMode" + kill_switch: + type: boolean + mtu_clamping: + type: boolean + ipv6: + $ref: "#/components/schemas/Ipv6Policy" + geoip: + $ref: "#/components/schemas/GeoIpStatus" + + ConfigResponse: + type: object + additionalProperties: false + required: [tunnel, peers, dns, routing, updated_at] + properties: + tunnel: + $ref: "#/components/schemas/TunnelConfig" + peers: + type: array + items: + $ref: "#/components/schemas/PeerConfig" + dns: + $ref: "#/components/schemas/DnsConfig" + routing: + $ref: "#/components/schemas/RoutingConfig" + updated_at: + type: string + format: date-time + + ConfigUpdateRequest: + type: object + description: Частичное обновление конфигурации (merge в текущее состояние) + additionalProperties: false + minProperties: 1 + properties: + tunnel: + $ref: "#/components/schemas/TunnelConfig" + peers: + type: array + items: + $ref: "#/components/schemas/PeerConfig" + dns: + $ref: "#/components/schemas/DnsConfig" + routing: + $ref: "#/components/schemas/RoutingConfig" + + ValidationIssue: + type: object + additionalProperties: false + required: [path, code, message, severity] + properties: + path: + type: string + description: JSON Pointer до проблемного поля + code: + type: string + message: + type: string + severity: + type: string + enum: [error, warning] + + ValidateResponse: + type: object + additionalProperties: false + required: [valid, errors, warnings] + properties: + valid: + type: boolean + errors: + type: array + items: + $ref: "#/components/schemas/ValidationIssue" + warnings: + type: array + items: + $ref: "#/components/schemas/ValidationIssue" + + RoutingRule: + type: object + additionalProperties: false + required: [id, name, kind, value, action, enabled, priority, comment, created_at, updated_at] + properties: + id: + type: string + format: uuid + name: + type: string + kind: + $ref: "#/components/schemas/RuleKind" + value: + type: string + action: + $ref: "#/components/schemas/RuleAction" + enabled: + type: boolean + priority: + type: integer + comment: + type: [string, "null"] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RoutingRuleCreate: + type: object + additionalProperties: false + required: [name, kind, value, action] + properties: + name: + type: string + minLength: 1 + maxLength: 128 + kind: + $ref: "#/components/schemas/RuleKind" + value: + type: string + minLength: 1 + action: + $ref: "#/components/schemas/RuleAction" + enabled: + type: boolean + default: true + priority: + type: integer + minimum: 0 + maximum: 10000 + default: 100 + comment: + type: [string, "null"] + + RoutingRuleUpdate: + type: object + additionalProperties: false + minProperties: 1 + properties: + name: + type: string + minLength: 1 + maxLength: 128 + kind: + $ref: "#/components/schemas/RuleKind" + value: + type: string + minLength: 1 + action: + $ref: "#/components/schemas/RuleAction" + enabled: + type: boolean + priority: + type: integer + minimum: 0 + maximum: 10000 + comment: + type: [string, "null"] + + RoutingRuleList: + type: object + additionalProperties: false + required: [items, total] + properties: + items: + type: array + items: + $ref: "#/components/schemas/RoutingRule" + total: + type: integer + minimum: 0 + + ModeUpdateRequest: + type: object + additionalProperties: false + required: [mode] + properties: + mode: + $ref: "#/components/schemas/RoutingMode" + + ModeUpdateResponse: + type: object + additionalProperties: false + required: [mode, applied_at, reconcile_id] + properties: + mode: + $ref: "#/components/schemas/RoutingMode" + applied_at: + type: string + format: date-time + reconcile_id: + type: string + format: uuid + + GeoIpUpdateRequest: + type: object + additionalProperties: false + properties: + force: + type: boolean + default: false + description: Игнорировать локальный кэш и обновить из RIPE Stat + + GeoIpUpdateResponse: + type: object + additionalProperties: false + required: [status, source, prefix_count, updated_at, duration_ms] + properties: + status: + type: string + enum: [updated, cached, failed] + source: + $ref: "#/components/schemas/GeoIpSource" + prefix_count: + type: integer + minimum: 0 + updated_at: + type: string + format: date-time + duration_ms: + type: integer + minimum: 0 + + ImportRequest: + type: object + additionalProperties: false + required: [format, content] + properties: + format: + type: string + enum: [amneziawg-conf, wireguard-conf, json] + content: + type: string + minLength: 1 + apply: + type: boolean + default: false + description: Применить импорт сразу, а не только вернуть разбор + + ImportResponse: + type: object + additionalProperties: false + required: [imported_peers, imported_rules, warnings] + properties: + imported_peers: + type: integer + minimum: 0 + imported_rules: + type: integer + minimum: 0 + warnings: + type: array + items: + type: string + + Device: + type: object + additionalProperties: false + required: [name, ip, mac, vpn, ipsets, last_seen_at] + properties: + name: + type: string + ip: + type: string + mac: + type: [string, "null"] + vpn: + type: boolean + ipsets: + type: array + items: + type: string + last_seen_at: + type: [string, "null"] + format: date-time + + DeviceList: + type: object + additionalProperties: false + required: [items, total] + properties: + items: + type: array + items: + $ref: "#/components/schemas/Device" + total: + type: integer + minimum: 0 + + LogEntry: + type: object + additionalProperties: false + required: [ts, level, logger, message, fields] + properties: + ts: + type: string + format: date-time + level: + $ref: "#/components/schemas/LogLevel" + logger: + type: string + message: + type: string + fields: + type: object + additionalProperties: true + + LogList: + type: object + additionalProperties: false + required: [items, total, next_cursor] + properties: + items: + type: array + items: + $ref: "#/components/schemas/LogEntry" + total: + type: integer + minimum: 0 + next_cursor: + type: [string, "null"] + + AuthLoginRequest: + type: object + additionalProperties: false + required: [username, password] + properties: + username: + type: string + minLength: 1 + password: + type: string + minLength: 1 + writeOnly: true + + AuthSession: + type: object + additionalProperties: false + required: [authenticated, username, expires_at, csrf_token] + properties: + authenticated: + type: boolean + username: + type: [string, "null"] + expires_at: + type: [string, "null"] + format: date-time + csrf_token: + type: [string, "null"] + + MetricsSample: + type: object + additionalProperties: false + required: [rx_bytes, tx_bytes, rx_rate_bps, tx_rate_bps, handshake_age_s, active_peers] + properties: + rx_bytes: + type: integer + minimum: 0 + tx_bytes: + type: integer + minimum: 0 + rx_rate_bps: + type: integer + minimum: 0 + tx_rate_bps: + type: integer + minimum: 0 + handshake_age_s: + type: [integer, "null"] + minimum: 0 + active_peers: + type: integer + minimum: 0 + + WsFrame: + type: object + additionalProperties: false + required: [type, ts, data] + properties: + type: + type: string + enum: [metrics, status, error] + ts: + type: string + format: date-time + data: + description: | + MetricsSample при type=metrics, StatusResponse при type=status, + Error при type=error. + type: object + additionalProperties: true diff --git a/requirements-contract.txt b/requirements-contract.txt new file mode 100644 index 0000000..d5faa3e --- /dev/null +++ b/requirements-contract.txt @@ -0,0 +1,5 @@ +# Зависимости контрактных тестов API (issue #19). +# После мержа каркаса монорепо (PR #47) строки переезжают в requirements-dev.txt. +jsonschema>=4.23 +openapi-spec-validator>=0.7 +PyYAML>=6