148 lines
10 KiB
Markdown
148 lines
10 KiB
Markdown
# Требования к модулю: 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).
|