10 KiB
Требования к модулю: 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); таблица маршрутов — fluentKit.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), каждая:
{
"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в теле не передаётся (выводится из ключа).
Правила обработки:
- Пакет атомарен — «все или ничего»: одна транзакция на пакет; при любой ошибке записи пакет целиком отклоняется.
- Успех →
200 { "accepted": ["guid1", ...] }— все GUID пакета. - 🛡 Идемпотентность: повтор пакета безопасен — записи по
client_guid(PK uuid) уже существуют,workout_insertи insert-функции uuid- сущностей возвращают существующий id; сервер снова отвечает тем жеaccepted(ответ идемпотентен). 409— конфликт версии программы (UNIQUE(program_id, version)вprogram_version_insert); телефон перечитывает программу и перезаписывает локально. Тело:{"error": "program_version_conflict"}.breakсsource = "auto"от телефона отклоняется (400) —autoпроставляет только сервер (Ф-14.1); аналогично прочие нарушения схем/диапазонов (rpe1–10,wellbeing1–5, пакет > 50, неизвестнаяentity) —400с текстом ошибки.- Маппинг записей на функции БД (репозитории 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. Инварианты (🛡 — нарушать запрещено)
- Изоляция: каждый запрос видит и пишет данные только своего атлета
(атлет — по
X-Api-Key; в теле пакета атлет не передаётся). - Идемпотентность: повтор пакета не создаёт дублей, ответ повторения —
тот же
accepted. - Атомарность пакета: «все или ничего».
- API не редактирует факт-данные в обход sync: ручного ввода на сервере нет (Ф-8.6); пишет только через репозитории m01.
break.source = 'auto'из sync не принимается.
7. Критерии приёмки модуля
- Без ключа / с чужим / заблокированным ключом —
401на все эндпоинты; изоляция подтверждена тестом (атлет Б не видит данные атлета А, запись под чужойworkout_idотклоняется). - Пакет sync: успешный приём возвращает все GUID; повтор пакета —
тот же ответ, дублей в БД нет; конфликт версии программы —
409; нарушение схемы —400. GET workouts(пагинация),GET workouts/{id}(карточка, чужой id → 404),GET programs/active(или 404) работают в пределах атлета.- Тесты эндпоинтов зелёные; валидация конфига при старте (пустой
Api.Key, дубликаты ключей атлетов — код возврата 1).