- Добвлен @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
- Обновлены тесты
222 lines
8.7 KiB
Markdown
222 lines
8.7 KiB
Markdown
# Задача: Синхронизация бэкенда с документацией
|
||
|
||
## Контекст
|
||
|
||
По итогам аудита документации 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] Обновить тесты под новые контракты
|