# Задача: Синхронизация бэкенда с документацией ## Контекст По итогам аудита документации vs кода приняты решения. Данная задача — изменения в backend-репозитории. --- ## 0. Настройка Cookie в Fastify Cookie нужны для хранения refresh token (httpOnly). Без этого разделы 3, 4 и 5 (logout, refresh, login Set-Cookie) не реализуемы. ### 0.1 Установка плагина ```bash npm install @fastify/cookie ``` ### 0.2 Регистрация в app.ts Зарегистрировать **до** auth-роутов (cookie должны быть доступны в onRequest): ```ts import cookie from '@fastify/cookie'; // После securityPlugin, до authPlugin await app.register(cookie, { secret: env.JWT_SECRET, // для подписанных cookie (опционально) parseOptions: {}, // опции для парсинга входящих cookie }); ``` Порядок плагинов: `redis` → `database` → `security` → `cookie` → `rateLimit` → `auth` → `subscription` → routes. ### 0.3 Чтение cookie в роуте ```ts const refreshToken = req.cookies.refreshToken; // string | undefined ``` Если cookie не передан, `refreshToken` будет `undefined`. ### 0.4 Установка cookie в ответе ```ts reply.setCookie('refreshToken', token, { httpOnly: true, secure: env.NODE_ENV === 'production', sameSite: 'strict', path: '/api/v1/auth', maxAge: 604800, // 7 дней в секундах signed: false, // если не используем подпись }); ``` Для production: `secure: true` (только HTTPS). Для development: `secure: false`, иначе cookie не установится на localhost:3000 (если без HTTPS). ### 0.5 Очистка cookie (logout) ```ts reply.clearCookie('refreshToken', { path: '/api/v1/auth', httpOnly: true, secure: env.NODE_ENV === 'production', sameSite: 'strict', }); ``` Либо `reply.setCookie('refreshToken', '', { ... same opts ..., maxAge: 0 })`. ### 0.6 CORS и credentials Убедиться, что CORS настроен с `credentials: true` (уже есть в `@fastify/cors`), иначе браузер не будет отправлять cookie с cross-origin запросами. Origin фронтенда должен быть в whitelist `CORS_ORIGINS`. ### 0.7 Согласование пути cookie и роутов Cookie с `path: '/api/v1/auth'` отправляется только на запросы к `/api/v1/auth/*`. Refresh и logout должны вызываться по этим путям. --- ## 1. Префикс /api/v1 **Файл:** `src/app.ts` При регистрации роутов добавить префикс `/api/v1`: ```ts await app.register(authRoutes, { prefix: '/api/v1/auth' }); await app.register(profileRoutes, { prefix: '/api/v1/profile' }); await app.register(testsRoutes, { prefix: '/api/v1/tests' }); await app.register(adminQuestionsRoutes, { prefix: '/api/v1/admin' }); ``` Health check оставить без префикса (например `/health`) или перенести по необходимости. --- ## 2. Auth: Login — вернуть объект user в ответ **Файлы:** `src/routes/auth.ts`, `src/services/auth/auth.service.ts` - `AuthService.login()` должен возвращать `{ user, accessToken, refreshToken, expiresIn }`. - `user`: `{ id, email, nickname, avatarUrl, role, emailVerified }` — нужно подтянуть из БД. - Фронтенду нужны id, nickname, avatarUrl для стейта (шапка, Redux/Pinia). --- ## 3. Auth: Logout — по документации (cookie, Bearer) **Файлы:** `src/routes/auth.ts`, `src/services/auth/auth.service.ts` - **Авторизация:** Bearer token в заголовке. - **Request:** пустое тело (refresh token читается из cookie). - **Response:** 200 с `{ message: "Logged out successfully" }`. - Set-Cookie: `refreshToken=; ... Max-Age=0` для очистки cookie. Требуется: - Читать refresh token из cookie `refreshToken` (httpOnly cookie). - Удалять сессию по хешу refresh token. - Очищать cookie в ответе. --- ## 4. Auth: Refresh — по документации (cookie) **Файлы:** `src/routes/auth.ts`, `src/services/auth/auth.service.ts` - **Request:** пустое тело (refresh token из cookie). - **Response:** 200 с `{ accessToken }`. - Set-Cookie: новый refreshToken (ротация). Требуется: - Читать refresh token из cookie. - Убрать `refreshToken` из body в schema и коде. - Устанавливать httpOnly cookie с новым refresh token в ответе. --- ## 5. Auth: Login — установка cookie при успехе При успешном login устанавливать refresh token в httpOnly cookie: ```text Set-Cookie: refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800 ``` В development (`NODE_ENV=development`) Secure можно опустить, если используется HTTP. --- ## 6. Profile: добавить role и plan в getPrivateProfile **Файлы:** `src/services/user/user.service.ts`, `src/routes/profile.ts` - Расширить `PrivateProfile`: добавить `role`, `plan`. - `role` — из `users.role`. - `plan` — из `subscriptions` через subscription middleware (уже загружается в `req.subscription` для GET /profile). - В route GET /profile после `getPrivateProfile` добавить в ответ `role: req.user` (из users) и `plan: req.subscription?.plan ?? 'free'`. Либо загружать subscription в UserService при запросе профиля и включать plan/role в ответ. --- ## 7. Tests: score — хранить и отдавать количество правильных **Файлы:** `src/services/tests/tests.service.ts`, `src/db/schema/tests.ts` (если нужно) - `score` в БД и API = количество правильных ответов (integer), не процент. - В `finishTest`: `score = correctCount` (не `Math.round((correctCount / questions.length) * 100)`). - В ответе finish и results: `{ score, totalQuestions, percentage }`, где `percentage = (score / totalQuestions) * 100`. --- ## 8. question_cache_meta: привести схему к документации **Файлы:** `src/db/schema/questionCacheMeta.ts`, миграция, `src/services/llm/llm.service.ts`, `src/services/questions/question.service.ts` Документация (llm/strategy.md) требует: | Поле | Тип | Описание | | - | - | - | | model | varchar | Уже есть как llm_model | | generation_time_ms | integer | Есть | | prompt_hash | varchar | Есть | | valid | boolean | Прошёл ли валидацию с первого раза | | retry_count | integer | Сколько retry потребовалось | | questions_generated | integer | Сколько вопросов вернул LLM | Действия: 1. Добавить в схему `questionCacheMeta`: `valid` (boolean), `retryCount` (integer), `questionsGenerated` (integer). 2. Создать миграцию Drizzle. 3. Расширить `LlmGenerationMeta` в llm.service: `valid`, `retryCount`, `questionsGenerated`. 4. В `LlmService.generateQuestions` и `chatWithMeta`: при необходимости возвращать retry count. 5. В `QuestionService.generateAndPersistQuestions`: при вставке в `questionCacheMeta` передавать новые поля. --- ## 9. Cookie-настройки - Путь для cookie: `/api/v1/auth` (совпадает с префиксом auth-роутов). - Max-Age для refresh: 604800 (7 дней). - Secure: только в production (NODE_ENV=production). - SameSite=Strict. --- ## Чеклист - [x] @fastify/cookie установлен и зарегистрирован (раздел 0) - [x] Префикс /api/v1 для роутов - [x] Login: user в ответе (id, nickname, avatarUrl, role, emailVerified) - [x] Login: Set-Cookie refreshToken - [x] Logout: Bearer + cookie, пустое тело, 200 + message - [x] Refresh: cookie, пустое тело, 200 + accessToken + Set-Cookie - [x] getPrivateProfile: role, plan - [x] Tests finish: score = correctCount, возвращать score, totalQuestions, percentage - [x] question_cache_meta: valid, retryCount, questionsGenerated - [x] Обновить тесты под новые контракты