Files
samreshu_backend/AGENT_TASK_BACKEND_SYNC.md
Anton 223feed0e0 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
- Обновлены тесты
2026-03-06 13:58:34 +03:00

8.7 KiB
Raw Blame History

Задача: Синхронизация бэкенда с документацией

Контекст

По итогам аудита документации vs кода приняты решения. Данная задача — изменения в backend-репозитории.


Cookie нужны для хранения refresh token (httpOnly). Без этого разделы 3, 4 и 5 (logout, refresh, login Set-Cookie) не реализуемы.

0.1 Установка плагина

npm install @fastify/cookie

0.2 Регистрация в app.ts

Зарегистрировать до auth-роутов (cookie должны быть доступны в onRequest):

import cookie from '@fastify/cookie';

// После securityPlugin, до authPlugin
await app.register(cookie, {
  secret: env.JWT_SECRET,  // для подписанных cookie (опционально)
  parseOptions: {},       // опции для парсинга входящих cookie
});

Порядок плагинов: redisdatabasesecuritycookierateLimitauthsubscription → routes.

const refreshToken = req.cookies.refreshToken;  // string | undefined

Если cookie не передан, refreshToken будет undefined.

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

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.

Cookie с path: '/api/v1/auth' отправляется только на запросы к /api/v1/auth/*. Refresh и logout должны вызываться по этим путям.


1. Префикс /api/v1

Файл: src/app.ts

При регистрации роутов добавить префикс /api/v1:

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

Файлы: 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 в ответе.

Файлы: 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 в ответе.

При успешном login устанавливать refresh token в httpOnly cookie:

Set-Cookie: refreshToken=<token>; 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 передавать новые поля.

  • Путь для cookie: /api/v1/auth (совпадает с префиксом auth-роутов).
  • Max-Age для refresh: 604800 (7 дней).
  • Secure: только в production (NODE_ENV=production).
  • SameSite=Strict.

Чеклист

  • @fastify/cookie установлен и зарегистрирован (раздел 0)
  • Префикс /api/v1 для роутов
  • Login: user в ответе (id, nickname, avatarUrl, role, emailVerified)
  • Login: Set-Cookie refreshToken
  • Logout: Bearer + cookie, пустое тело, 200 + message
  • Refresh: cookie, пустое тело, 200 + accessToken + Set-Cookie
  • getPrivateProfile: role, plan
  • Tests finish: score = correctCount, возвращать score, totalQuestions, percentage
  • question_cache_meta: valid, retryCount, questionsGenerated
  • Обновить тесты под новые контракты