feat: синхронизация бэкенда с документацией (AGENT_TASK_BACKEND_SYNC)
- Добвлен @fastify/cookie и настройку httpOnly cookie для refresh token
- Добавлен префикс /api/v1 для auth, profile, tests, admin
- Скорректировано в Login: возвращать user (id, nickname, avatarUrl, role, emailVerified),
ставить refreshToken в Set-Cookie
- Скорректировано в Logout: Bearer + cookie, пустое тело, 200 + { message }, очищать cookie
- Скорректировано в Refresh: token из cookie, пустое тело, 200 + { accessToken }, Set-Cookie
- Добавлено в getPrivateProfile: поля role и plan
- Скорректировано в Tests: score = количество правильных, ответ { score, totalQuestions, percentage }
- Добавлено в question_cache_meta: поля valid, retryCount, questionsGenerated
- Обновлены тесты
This commit is contained in:
366
.cursor/plans/документация_vs_реализация_0932d1a0.plan.md
Normal file
366
.cursor/plans/документация_vs_реализация_0932d1a0.plan.md
Normal file
@@ -0,0 +1,366 @@
|
||||
---
|
||||
name: Документация vs Реализация
|
||||
overview: "Подробный отчёт о соответствии бэкенда документации samreshu_docs: выявлены расхождения в API контрактах, схеме БД, аутентификации, тестах и других компонентах."
|
||||
todos: []
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Отчёт: Соответствие бэкенда документации Samreshu
|
||||
|
||||
## 1. Базовый URL и структура API
|
||||
|
||||
**Документация** ([contracts.md](samreshu_docs/api/contracts.md)):
|
||||
|
||||
- Базовый URL: `/api/v1`
|
||||
- Все эндпоинты: `/api/v1/auth/`*, `/api/v1/profile/`*, `/api/v1/tests/*`, `/api/v1/admin/*`
|
||||
|
||||
**Реализация** ([app.ts](src/app.ts)):
|
||||
|
||||
- Префикс `/api/v1` отсутствует. Маршруты зарегистрированы как `/auth`, `/profile`, `/tests`, `/admin`
|
||||
- Фактические URL: `/auth/`*, `/profile/`*, `/tests/*`, `/admin/*`
|
||||
|
||||
**Вывод:** Критическое расхождение. Клиент, следующий документации, будет отправлять запросы на несуществующие эндпоинты.
|
||||
|
||||
---
|
||||
|
||||
## 2. Аутентификация (Auth)
|
||||
|
||||
### 2.1 POST /auth/register
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| -------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Response 201 | `{ user, accessToken }` + Set-Cookie refreshToken | `{ userId, message, verificationCode }` |
|
||||
| Логика | Сразу выдаёт токены, отправляет письмо | Требует верификацию email; не выдаёт токены; возвращает код (для dev) |
|
||||
| NICKNAME_TAKEN | — | Есть в коде; в документации не описан |
|
||||
|
||||
|
||||
**Вывод:** Разный flow. Документация описывает выдачу токенов сразу после регистрации; реализация ожидает верификацию email.
|
||||
|
||||
### 2.2 POST /auth/login
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ----------------------- | ------------------------------------ | ------------------------------------------------------- |
|
||||
| Response | `{ user, accessToken }` + Set-Cookie | `{ accessToken, refreshToken, expiresIn }` — без `user` |
|
||||
| Lockout при brute force | 403 `ACCOUNT_LOCKED` | 429 `RATE_LIMIT_EXCEEDED` |
|
||||
| Ошибка неверных данных | 401 `INVALID_CREDENTIALS` | 401 `UNAUTHORIZED` (общий код) |
|
||||
|
||||
|
||||
**Вывод:** Расхождения в формате ответа, коде ошибки и семантике блокировки (403 vs 429).
|
||||
|
||||
### 2.3 POST /auth/logout
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ------------ | ------------------------ | -------------------------- |
|
||||
| Авторизация | Bearer token | refreshToken в теле |
|
||||
| Request body | Пустое | `{ refreshToken: string }` |
|
||||
| Response | 200 `{ message: "..." }` | 204 (без тела) |
|
||||
|
||||
|
||||
**Вывод:** Другой механизм авторизации и формат ответа.
|
||||
|
||||
### 2.4 POST /auth/refresh
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ------------ | --------------- | -------------------------- |
|
||||
| Токен | httpOnly cookie | `{ refreshToken }` в body |
|
||||
| Request body | Пустое | `{ refreshToken: string }` |
|
||||
|
||||
|
||||
**Вывод:** Документация предполагает cookie; реализация использует body.
|
||||
|
||||
### 2.5 POST /auth/verify-email
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ----------- | ------------ | ------------------ |
|
||||
| Request | `{ code }` | `{ userId, code }` |
|
||||
| Авторизация | Bearer token | Не требуется |
|
||||
|
||||
|
||||
**Вывод:** Документация — только `code` с Bearer; реализация — `userId` и `code` без Bearer.
|
||||
|
||||
### 2.6 POST /auth/reset-password
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ----------- | ------------ | ------------- |
|
||||
| Поле пароля | `password` | `newPassword` |
|
||||
|
||||
|
||||
**Вывод:** Разные имена полей в теле запроса.
|
||||
|
||||
### 2.7 Set-Cookie для refresh token
|
||||
|
||||
**Документация:** RefreshToken в httpOnly cookie (`Path=/api/v1/auth`, `Max-Age=604800`).
|
||||
|
||||
**Реализация:** Cookie не устанавливаются; refresh token возвращается только в JSON.
|
||||
|
||||
---
|
||||
|
||||
## 3. Profile
|
||||
|
||||
### 3.1 GET /profile
|
||||
|
||||
**Документация:** Ответ включает `role`, `plan` (из subscriptions).
|
||||
|
||||
**Реализация** ([user.service.ts](src/services/user/user.service.ts)): `getPrivateProfile` возвращает `id`, `nickname`, `avatarUrl`, `country`, `city`, `selfLevel`, `isPublic`, `email`, `emailVerifiedAt`, `createdAt`, `updatedAt`, `stats`. Поля `role` и `plan` отсутствуют.
|
||||
|
||||
**Вывод:** В ответе нет полей, указанных в документации.
|
||||
|
||||
### 3.2 GET /profile/:username
|
||||
|
||||
**Документация:** `stats: { testsCompleted, averageScore }`.
|
||||
|
||||
**Реализация:** `stats: { byStack, totalTestsTaken, totalQuestions, correctAnswers, accuracy }` — другая структура.
|
||||
|
||||
**Вывод:** Формат статистики не совпадает.
|
||||
|
||||
### 3.3 PATCH /profile
|
||||
|
||||
**Документация:** Поле `avatarUrl` не описано.
|
||||
|
||||
**Реализация:** Поддерживается `avatarUrl`.
|
||||
|
||||
**Вывод:** Документация неполная (расхождение в пользу реализации).
|
||||
|
||||
---
|
||||
|
||||
## 4. Tests
|
||||
|
||||
### 4.1 POST /tests (создание)
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
|
||||
| questionCount | enum: 10 / 20 | integer 1–50 |
|
||||
| stack/level (MVP 0) | html, css / basic, beginner | Все значения enum |
|
||||
| Response структура | `{ id, stack, level, questionCount, status, currentQuestion, startedAt, timeLimitSeconds, question: {...} }` — только текущий вопрос | `{ ...test, questions: [...] }` — все вопросы |
|
||||
|
||||
|
||||
**Вывод:** Разная структура ответа и валидация параметров.
|
||||
|
||||
### 4.2 POST /tests/:id/answer
|
||||
|
||||
**Документация:** `{ answered: {...}, progress: {...}, nextQuestion: {...} }`.
|
||||
|
||||
**Реализация:** Возвращает полный `TestSnapshot` отвеченного вопроса, без `progress` и `nextQuestion`.
|
||||
|
||||
**Вывод:** Формат ответа не совпадает с документацией.
|
||||
|
||||
### 4.3 POST /tests/:id/finish
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| -------------- | --------------------------------------- | ----------------------- |
|
||||
| score | Количество правильных ответов (8 из 10) | Процент (0–100) |
|
||||
| totalQuestions | В ответе | Нет в ответе |
|
||||
| percentage | В ответе | Нет (score как процент) |
|
||||
|
||||
|
||||
**Реализация** ([tests.service.ts](src/services/tests/tests.service.ts) 239–241):
|
||||
|
||||
```ts
|
||||
const score = Math.round((correctCount / questions.length) * 100);
|
||||
```
|
||||
|
||||
**Вывод:** Критическое расхождение: `score` в документации — количество, в реализации — процент.
|
||||
|
||||
### 4.4 GET /tests/:id/results
|
||||
|
||||
**Документация:** Детальные результаты с `questions`, `userAnswer`, `correctAnswer`, `isCorrect`, `explanation`.
|
||||
|
||||
**Реализация:** Эндпоинт отсутствует.
|
||||
|
||||
**Вывод:** Критическое расхождение: описанный эндпоинт не реализован.
|
||||
|
||||
### 4.5 GET /tests/history
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| ------------- | ------------------------------------------------------ | ------------------------------ |
|
||||
| Путь | GET /tests/history | GET /tests (то же для истории) |
|
||||
| Пагинация | cursor-based (limit, cursor) | offset-based (limit, offset) |
|
||||
| Формат ответа | `{ data: [...], pagination: { nextCursor, hasMore } }` | `{ tests: [...], total }` |
|
||||
| Параметры | stack, status фильтры | Нет фильтров |
|
||||
|
||||
|
||||
**Вывод:** Другая схема пагинации и структура ответа.
|
||||
|
||||
### 4.6 Параметр теста
|
||||
|
||||
**Документация:** `:id`.
|
||||
|
||||
**Реализация:** `:testId` в params.
|
||||
|
||||
**Вывод:** Незначительное расхождение в именовании.
|
||||
|
||||
---
|
||||
|
||||
## 5. Admin
|
||||
|
||||
### 5.1 Список вопросов
|
||||
|
||||
|
||||
| Аспект | Документация | Реализация |
|
||||
| -------------- | ----------------------------- | ---------------------------- |
|
||||
| Эндпоинт | GET /admin/questions/queue | GET /admin/questions/pending |
|
||||
| Пагинация | cursor (limit, cursor) | offset (limit, offset) |
|
||||
| status в query | pending / approved / rejected | — |
|
||||
|
||||
|
||||
### 5.2 Редактирование
|
||||
|
||||
**Документация:** PATCH /admin/questions/:id с `status: approved | rejected` и полями для правок.
|
||||
|
||||
**Реализация:**
|
||||
|
||||
- POST /admin/questions/:questionId/approve
|
||||
- POST /admin/questions/:questionId/reject
|
||||
- PATCH /admin/questions/:questionId — для редактирования
|
||||
|
||||
**Вывод:** Другая схема: отдельные approve/reject вместо смены статуса через PATCH.
|
||||
|
||||
---
|
||||
|
||||
## 6. База данных
|
||||
|
||||
### 6.1 Таблицы
|
||||
|
||||
**Документация** ([schema.md](samreshu_docs/database/schema.md)): `verification_tokens` (обобщённо).
|
||||
|
||||
**Реализация:** Отдельные таблицы `email_verification_codes` и `password_reset_tokens` (в [verificationTokens.ts](src/db/schema/verificationTokens.ts)).
|
||||
|
||||
**Вывод:** Расхождение в модели хранения токенов.
|
||||
|
||||
### 6.2 Отсутствующие таблицы (Phase 2+)
|
||||
|
||||
В документации есть таблицы, которых нет в текущем коде: `oauth_accounts`, `totp_secrets`, `payments`, `payment_events`, `notifications_log`, `promo_codes`, `user_achievements`. Это ожидаемо для Phase 2+.
|
||||
|
||||
---
|
||||
|
||||
## 7. Безопасность
|
||||
|
||||
### 7.1 Argon2
|
||||
|
||||
**Документация:** argon2id, 19 MiB, 2 iterations.
|
||||
|
||||
**Реализация** ([password.ts](src/utils/password.ts)): `memoryCost: 19456` (KiB), `timeCost: 2`.
|
||||
|
||||
**Вывод:** Совпадение.
|
||||
|
||||
### 7.2 JWT
|
||||
|
||||
**Документация:** Access 15 мин, Refresh 7 дней, HS256.
|
||||
|
||||
**Реализация:** `JWT_ACCESS_TTL=15m`, `JWT_REFRESH_TTL=7d`.
|
||||
|
||||
**Вывод:** Совпадение.
|
||||
|
||||
### 7.3 Login lockout
|
||||
|
||||
**Документация:** 5/15 мин, 10/1 ч, 20/24 ч.
|
||||
|
||||
**Реализация** ([loginLockout.ts](src/utils/loginLockout.ts)): Те же пороги (5, 10, 20).
|
||||
|
||||
**Вывод:** Совпадение.
|
||||
|
||||
### 7.4 Rate limits
|
||||
|
||||
**Документация:** RATE_LIMIT_LOGIN, RATE_LIMIT_REGISTER, RATE_LIMIT_FORGOT_PASSWORD, RATE_LIMIT_VERIFY_EMAIL, RATE_LIMIT_API_AUTHED, RATE_LIMIT_API_GUEST.
|
||||
|
||||
**Реализация** ([env.ts](src/config/env.ts)): RATE_LIMIT_LOGIN отсутствует (используется progressive lockout). Остальные переменные есть.
|
||||
|
||||
**Вывод:** Незначительное расхождение; security.md упоминает RATE_LIMIT_LOGIN, но логика lockout иная.
|
||||
|
||||
### 7.5 CORS
|
||||
|
||||
**Документация:** `http://localhost:5173`, `https://samreshu.ru`, `credentials: true`, методы GET, POST, PATCH, DELETE.
|
||||
|
||||
**Реализация:** Origins из `CORS_ORIGINS`, `credentials: true`, методы включают PUT и OPTIONS.
|
||||
|
||||
**Вывод:** Реализация шире, расхождение несущественное.
|
||||
|
||||
### 7.6 Helmet
|
||||
|
||||
**Документация:** Полный набор заголовков, включая CSP.
|
||||
|
||||
**Реализация:** `contentSecurityPolicy: false`, `crossOriginEmbedderPolicy: false`.
|
||||
|
||||
**Вывод:** CSP и COEP отключены.
|
||||
|
||||
---
|
||||
|
||||
## 8. LLM
|
||||
|
||||
### 8.1 Конфигурация
|
||||
|
||||
**Документация:** LLM_BASE_URL, LLM_MODEL, LLM_API_KEY, LLM_TIMEOUT_MS, LLM_MAX_RETRIES, LLM_TEMPERATURE, LLM_MAX_TOKENS.
|
||||
|
||||
**Реализация:** Дополнительно `LLM_FALLBACK_MODEL`, `LLM_RETRY_DELAY_MS`.
|
||||
|
||||
**Вывод:** Реализация расширяет документацию.
|
||||
|
||||
### 8.2 question_cache_meta
|
||||
|
||||
**Документация:** model, generation_time_ms, prompt_hash. Также упоминаются valid, retry_count, questions_generated.
|
||||
|
||||
**Реализация:** Нужно проверить сохранение этих полей в [questionCacheMeta](src/db/schema/questionCacheMeta.ts) и связанных сервисах.
|
||||
|
||||
---
|
||||
|
||||
## 9. Код и инфраструктура
|
||||
|
||||
### 9.1 Onboarding / setup
|
||||
|
||||
**Документация:** docker-compose.dev.yml с postgres и redis.
|
||||
|
||||
**Реализация:** Соответствует.
|
||||
|
||||
### 9.2 .env.example
|
||||
|
||||
**Документация:** Полный перечень переменных.
|
||||
|
||||
**Реализация:** Совпадает, включая rate limits и LLM. JWT_SECRET требует не менее 32 символов — в примере выполнено.
|
||||
|
||||
---
|
||||
|
||||
## 10. Сводка расхождений
|
||||
|
||||
### Критические
|
||||
|
||||
1. Отсутствие префикса `/api/v1`
|
||||
2. Auth: другой flow (verify-email до токенов, refresh/logout через body вместо cookie)
|
||||
3. Tests: `score` как процент вместо количества
|
||||
4. Отсутствует GET /tests/:id/results
|
||||
5. Формат ответов create/answer/finish/history не совпадает с документацией
|
||||
|
||||
### Значительные
|
||||
|
||||
1. Login lockout: 429 вместо 403 ACCOUNT_LOCKED
|
||||
2. Login: нет поля `user` в ответе
|
||||
3. Profile: нет `role`, `plan`; другая структура `stats`
|
||||
4. Admin: другой набор эндпоинтов и логика approve/reject
|
||||
5. Пагинация: offset вместо cursor
|
||||
|
||||
### Незначительные
|
||||
|
||||
1. Имена полей (testId vs id, newPassword vs password)
|
||||
2. verify-email: userId + code вместо только code
|
||||
3. Эндпоинт admin: /pending вместо /queue
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Привести маршрутизацию к документации:** добавить префикс `/api/v1` при регистрации роутов.
|
||||
2. **Унифицировать auth:** реализовать cookie для refresh token и обновить logout/refresh под документацию, либо явно зафиксировать в документации текущий подход (body).
|
||||
3. **Исправить score в тестах:** хранить и возвращать количество правильных ответов, а процент считать отдельно.
|
||||
4. **Реализовать GET /tests/:id/results** по описанному в документации формату.
|
||||
5. **Привести ответы create/answer/finish/history** к формату из contracts.md.
|
||||
6. **Обновить документацию** под уже реализованные отличия (offset, admin approve/reject и т.д.), если менять реализацию не планируется.
|
||||
7. **Расширить getPrivateProfile** полями `role` и `plan` из subscription middleware.
|
||||
|
||||
Reference in New Issue
Block a user