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

222 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задача: Синхронизация бэкенда с документацией
## Контекст
По итогам аудита документации 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=<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` передавать новые поля.
---
## 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] Обновить тесты под новые контракты