--- 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.