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
- Обновлены тесты
This commit is contained in:
Anton
2026-03-06 13:58:34 +03:00
parent 99a2686532
commit 223feed0e0
21 changed files with 2244 additions and 62 deletions

221
AGENT_TASK_BACKEND_SYNC.md Normal file
View File

@@ -0,0 +1,221 @@
# Задача: Синхронизация бэкенда с документацией
## Контекст
По итогам аудита документации 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] Обновить тесты под новые контракты