- Добвлен @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
- Обновлены тесты
8.7 KiB
Задача: Синхронизация бэкенда с документацией
Контекст
По итогам аудита документации vs кода приняты решения. Данная задача — изменения в backend-репозитории.
0. Настройка Cookie в Fastify
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
});
Порядок плагинов: redis → database → security → cookie → rateLimit → auth → subscription → routes.
0.3 Чтение cookie в роуте
const refreshToken = req.cookies.refreshToken; // string | undefined
Если cookie не передан, refreshToken будет undefined.
0.4 Установка cookie в ответе
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)
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:
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:
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 |
Действия:
- Добавить в схему
questionCacheMeta:valid(boolean),retryCount(integer),questionsGenerated(integer). - Создать миграцию Drizzle.
- Расширить
LlmGenerationMetaв llm.service:valid,retryCount,questionsGenerated. - В
LlmService.generateQuestionsиchatWithMeta: при необходимости возвращать retry count. - В
QuestionService.generateAndPersistQuestions: при вставке вquestionCacheMetaпередавать новые поля.
9. Cookie-настройки
- Путь для 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
- Обновить тесты под новые контракты