VEU.WathBody/1-docs/4-modules/m02-rest-api/module_reqs.md

148 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Требования к модулю: m02-rest-api
> Модуль — REST API сервера v1: проект `Server.WatchBody.Web`
> (ASP.NET Core, Kestrel — только localhost), middleware ключа атлета,
> `POST /api/v1/sync`, чтение тренировок и активной программы.
> Самодостаточная копия требований (d-dev-6): внешние документы
> продублированы по содержанию, ссылки — только для контекста.
## 1. Назначение и границы
**В границах модуля:**
- проект `Server.WatchBody.Web` (net10.0, ASP.NET Core MVC) —
каркас, конфигурация (`appsettings.json`, валидация при старте —
tech-requirements 4.2: `Api.*`, `Db.ConnectionString` — фатально при
неудаче), запуск по HTTPS-прокси снаружи (сам Kestrel — localhost);
таблица маршрутов — fluent `Kit.Helpers.Routes`
(`Config\RouteConfig.cs`, паттерн — Kit.Task.WebAp\Task.WebApp),
обработчики — контроллеры `Controllers\` (решение владельца
2026-10-18);
- сопоставление атлетов конфига `Api.Athletes` (`{name, apiKey}`) с
`src.athlete` при старте: upsert по SHA-256-хэшу ключа
(идемпотентно; запись в БД — источник `is_blocked`);
- middleware аутентификации `X-Api-Key` (см. разд. 3);
- `POST /api/v1/sync` — приём пакета записей телефона (разд. 4);
- `GET /api/v1/workouts`, `GET /api/v1/workouts/{id}`,
`GET /api/v1/programs/active` (разд. 5);
- интеграционные тесты эндпоинтов (WebApplicationFactory + тестовая БД).
**Вне границ модуля:** AdminWeb (m08: admin REST API `/api/admin/*` в
этом же проекте + UI-приложение Flutter Web `3-app\3-web\admin-web` —
решение владельца 2026-10-17), fetch/import .fit (m06, m07),
метрики/правила (m04, m05), ProgramStore как сервис правил (m03;
здесь — только запись версии из sync), всё на телефоне и часах.
Доступ к БД — только DAL-репозитории модуля m01
(`Server.WatchBody.Core`); прямой SQL в Web-проекте запрещён.
## 2. Стек и соглашения
- C# / .NET 10, ASP.NET Core (minimal API), `Server.WatchBody.Core`
(домен + репозитории m01) и Kit.Helpers (`Log.Logger` — файловые
логи, уровень Info).
- Конфигурация — `appsettings.json` (`Api.Key`, `Api.Athletes[]`,
`Db.ConnectionString`); валидация при старте, ошибки — в лог и
stderr, код возврата 1.
- Идентификация атлета — по хэшу ключа через `AthleteRepository`
(`athlete_select` по `p_api_key_hash`); middleware кладёт
`athlete_id` в `HttpContext.Items`.
- Формат ответов — JSON (System.Text.Json, camelCase);
ошибки — `{ "error": "<текст>" }` с соответствующим кодом HTTP.
## 3. Аутентификация (requirements Ф-8.5, Ф-7.2 — 🛡)
- Каждый запрос (кроме health-check `GET /health`) обязан нести
заголовок `X-Api-Key`.
- Сервер считает SHA-256 ключа и ищет атлета в `src.athlete` по
`api_key_hash`. Ключ не найден или `is_blocked = true` → `401`
(тело: `{"error": "unauthorized"}`), запрос не доходит до обработчиков.
- 🛡 Атлета определяет только ключ: телефон не передаёт идентификатор
атлета в данных пакета; `athlete_id` для всех записей пакета берётся
из middleware (изоляция — каждый запрос видит данные только своего
атлета).
## 4. `POST /api/v1/sync` (Ф-7.3, tech-requirements 2.1–2.2)
Запрос — JSON-массив записей (пакет ≤ 50), каждая:
```json
{
"client_guid": "uuid-v4",
"entity": "workout | workout_set | exercise | program_version | profile | daily_log | break",
"op": "upsert",
"data": { "...схема сущности..." }
}
```
**Ключевые поля `data`** (tech-requirements 2.2):
- `exercise`: `exercise_id` (uuid), `name`, `group` (push|pull|legs|core|cardio), `is_base`, `is_active`;
повторная отправка упражнения с `is_active=false` — **логическое удаление**
(`src.exercise_delete`, З-41(а): единственный серверный путь установки
флага в v1; идемпотентно, GUID включается в `accepted` в обоих случаях,
Ф-7.4);
- `workout`: `workout_id` (uuid), `started_at`/`ended_at` (UTC ISO 8601), `status` (active|finished|cancelled), `session_kind` (normal|return|deload|test), `program_version_id` (nullable), `avg_hr`;
- `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`, `done_at`, `source` (voice|watch|edit), `in_plan`, `rpe` (int? 1–10), `knee_pain` (int? 0–10, только «ноги»);
- `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable);
- `break`: `kind` (nullable), `source` (телефон передаёт только `manual`), `started_on`, `planned_return`, `note`;
- `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`, `hr_resting`, `wellbeing` (1–5);
- `program_version`: JSON-схема программы из дизайна LLM-планирования (разд. 2) — пишется в `body` как есть; `athlete_id` в теле не передаётся (выводится из ключа).
**Правила обработки:**
1. Пакет атомарен — «все или ничего»: одна транзакция на пакет;
при любой ошибке записи пакет целиком отклоняется.
2. Успех → `200 { "accepted": ["guid1", ...] }` — все GUID пакета.
3. 🛡 Идемпотентность: повтор пакета безопасен — записи по `client_guid`
(PK uuid) уже существуют, `workout_insert` и insert-функции uuid-
сущностей возвращают существующий id; сервер снова отвечает тем же
`accepted` (ответ идемпотентен).
4. `409` — конфликт версии программы (`UNIQUE(program_id, version)` в
`program_version_insert`); телефон перечитывает программу и
перезаписывает локально. Тело: `{"error": "program_version_conflict"}`.
5. `break` с `source = "auto"` от телефона отклоняется (`400`) —
`auto` проставляет только сервер (Ф-14.1); аналогично прочие
нарушения схем/диапазонов (`rpe` 1–10, `wellbeing` 1–5, пакет > 50,
неизвестная `entity`) — `400` с текстом ошибки.
6. Маппинг записей на функции БД (репозитории m01):
workout → `workout_insert`/`workout_update` (update — при
`ended_at`/`status`/`avg_hr`); workout_set → `workout_set_insert`;
exercise → `exercise_insert`; program_version → `program_version_insert`
(при первой версии атлета — `program_insert`); profile →
`athlete_profile_upsert`; daily_log → `daily_log_upsert`;
break → `break_insert` (`source='manual'`).
## 5. Чтение (Ф-8.5)
| Эндпоинт | Ответ | Реализация |
|---|---|---|
| `GET /api/v1/workouts?limit=&offset=` | массив карточек тренировок (с подходами), пагинация (limit ≤ 100, умолчание 20) | `workout_select` (карточки + sets агрегатом) |
| `GET /api/v1/workouts/{id}` | полная карточка тренировки + command_log | `workout_get`; чужой id → `404` |
| `GET /api/v1/programs/active` | активная версия программы атлета (с `body`) или `404` | `program_select` + `program_version_select (status=active)` + `program_version_get` |
Все выборки — в пределах `athlete_id` из middleware (🛡 изоляция).
## 6. Инварианты (🛡 — нарушать запрещено)
1. Изоляция: каждый запрос видит и пишет данные только своего атлета
(атлет — по `X-Api-Key`; в теле пакета атлет не передаётся).
2. Идемпотентность: повтор пакета не создаёт дублей, ответ повторения —
тот же `accepted`.
3. Атомарность пакета: «все или ничего».
4. API не редактирует факт-данные в обход sync: ручного ввода на
сервере нет (Ф-8.6); пишет только через репозитории m01.
5. `break.source = 'auto'` из sync не принимается.
## 7. Критерии приёмки модуля
1. Без ключа / с чужим / заблокированным ключом — `401` на все
эндпоинты; изоляция подтверждена тестом (атлет Б не видит данные
атлета А, запись под чужой `workout_id` отклоняется).
2. Пакет sync: успешный приём возвращает все GUID; повтор пакета —
тот же ответ, дублей в БД нет; конфликт версии программы — `409`;
нарушение схемы — `400`.
3. `GET workouts` (пагинация), `GET workouts/{id}` (карточка, чужой id →
404), `GET programs/active` (или 404) работают в пределах атлета.
4. Тесты эндпоинтов зелёные; валидация конфига при старте (пустой
`Api.Key`, дубликаты ключей атлетов — код возврата 1).