diff --git a/AGENT_TASK_DOCS_SYNC.md b/AGENT_TASK_DOCS_SYNC.md new file mode 100644 index 0000000..62cf554 --- /dev/null +++ b/AGENT_TASK_DOCS_SYNC.md @@ -0,0 +1,157 @@ +# Задача: Синхронизация документации с реализацией + +## Контекст + +По итогам аудита документация должна быть приведена в соответствие с текущей реализацией backend. Данная задача — правки в samreshu_docs (submodule или основной репо с документацией). + +--- + +## 1. API: Базовый URL + +Убедиться, что базовый URL `/api/v1` соответствует backend (после внесения префикса агентом backend). Если в docs есть примеры с другим базовым путём — исправить. + +--- + +## 2. Auth: Register + +**Файл:** `samreshu_docs/api/contracts.md` + +- Регистрация **не выдаёт токены** до подтверждения email. +- Response 201: `{ userId, message, verificationCode }` (verificationCode — для dev/тестов, в prod не отдаётся). +- Добавить ошибку `NICKNAME_TAKEN` (409), если в коде она есть. + +--- + +## 3. Auth: Login + +- Ошибка при блокировке (lockout): **429** `RATE_LIMIT_EXCEEDED` (не 403 ACCOUNT_LOCKED). +- Response 200 должен включать объект `user`: `{ id, email, nickname, avatarUrl, role, emailVerified }`. +- Set-Cookie: refreshToken (httpOnly, Secure, SameSite=Strict, Path=/api/v1/auth). + +--- + +## 4. Auth: Verify-email + +- **Request:** `{ userId, code }` (не только code). +- **Авторизация:** не требуется (Bearer не нужен). + +--- + +## 5. Auth: Reset-password + +- **Request:** поле `newPassword` (не `password`). + +--- + +## 6. Profile: GET /profile + +- В ответе: `role`, `plan` (из subscriptions). + +--- + +## 7. Profile: GET /profile/:username (публичный профиль) + +- Структура `stats`: `{ byStack, totalTestsTaken, totalQuestions, correctAnswers, accuracy }` (а не только testsCompleted, averageScore). +- Описать формат `byStack` и остальных полей. + +--- + +## 8. Tests: POST /tests (создание) + +- Response: тест со списком **всех** вопросов в `questions` (не только текущий в `question`). +- Структура ответа: `{ id, stack, level, questionCount, status, startedAt, timeLimitSeconds, questions: [...] }`. + +--- + +## 9. Tests: POST /tests/:id/answer + +- Response: полный snapshot отвеченного вопроса (формат из реализации). +- Указать, что структура может отличаться от минимальной "answered + progress + nextQuestion". + +--- + +## 10. Tests: POST /tests/:id/finish + +- `score` — количество правильных ответов (integer). +- В ответе: `score`, `totalQuestions`, `percentage` (процент считает фронтенд или он приходит с бэка). + +--- + +## 11. Tests: GET /tests/history (или GET /tests для истории) + +- **Пагинация:** offset-based: `limit`, `offset` (не cursor). +- Формат: `{ tests: [...], total }` (не `{ data, pagination }`). +- Указать параметры `limit`, `offset`. + +--- + +## 12. Admin + +- Эндпоинт списка: **GET /admin/questions/pending** (не /queue). +- Отдельные эндпоинты: **POST /admin/questions/:id/approve**, **POST /admin/questions/:id/reject**. +- **PATCH /admin/questions/:id** — для редактирования контента (без смены статуса). +- Пагинация: limit/offset. + +--- + +## 13. Database: Токены верификации + +**Файл:** `samreshu_docs/database/schema.md` + +- Вместо общей таблицы `verification_tokens` описать: + - `email_verification_codes` (userId, code, expiresAt) + - `password_reset_tokens` (userId, tokenHash, expiresAt) + +--- + +## 14. Security: CORS + +**Файл:** `samreshu_docs/principles/security.md` + +- Origins задаются через переменную окружения **CORS_ORIGINS** (не хардкод localhost/prod). +- Методы: **GET, POST, PATCH, DELETE, PUT, OPTIONS** (OPTIONS нужен для preflight, PUT — для идемпотентных обновлений). + +--- + +## 15. Security: Helmet (CSP, COEP) + +- **CSP и COEP отключены** — бэкенд отдаёт только JSON API. +- Эти заголовки предназначены для HTML-страниц; для REST API они не нужны и могут мешать Swagger UI. + +--- + +## 16. LLM: Переменные окружения + +**Файлы:** `samreshu_docs/llm/strategy.md`, `samreshu_docs/onboarding/setup.md` + +Добавить в документацию: +- **LLM_FALLBACK_MODEL** — запасная модель при падении основной. +- **LLM_RETRY_DELAY_MS** — задержка между retry при ошибках API. + +--- + +## 17. LLM: Логирование в question_cache_meta + +**Файл:** `samreshu_docs/llm/strategy.md` + +- Актуальная структура: `llm_model`, `prompt_hash`, `generation_time_ms`, `valid`, `retry_count`, `questions_generated`, `raw_response` (опционально). + +--- + +## 18. Onboarding: .env.example + +- Закрепить правило: `.env.example` обновляется при добавлении новых фич (новые переменные, rate limits, LLM и т.д.). +- Упомянуть требование JWT_SECRET >= 32 символа. + +--- + +## Чеклист + +- [ ] api/contracts.md: register, login, verify-email, reset-password, logout, refresh +- [ ] api/contracts.md: profile (role, plan, stats) +- [ ] api/contracts.md: tests (create, answer, finish, history) +- [ ] api/contracts.md: admin (pending, approve, reject, PATCH) +- [ ] database/schema.md: email_verification_codes, password_reset_tokens +- [ ] principles/security.md: CORS, Helmet +- [ ] llm/strategy.md: LLM_FALLBACK_MODEL, LLM_RETRY_DELAY_MS, question_cache_meta +- [ ] onboarding/setup.md: правило .env.example diff --git a/api/contracts.md b/api/contracts.md index a6215e7..591d37c 100644 --- a/api/contracts.md +++ b/api/contracts.md @@ -35,34 +35,9 @@ | 429 | `RATE_LIMIT_EXCEEDED` | Превышен лимит запросов | | 500 | `INTERNAL_ERROR` | Внутренняя ошибка сервера | -### Пагинация (cursor-based) +### Пагинация -Для списков используется cursor-based пагинация на основе UUID v7 (сортируемый по времени). - -Запрос: - -```text -GET /tests/history?limit=10&cursor=0192a8b0-... -``` - -| Параметр | Тип | Обязательный | Описание | -| ---------- | ----- | -------------- | ---------- | -| limit | integer | нет | Количество записей (default 10, max 50) | -| cursor | uuid | нет | ID последнего элемента предыдущей страницы | - -Ответ всегда содержит: - -```json -{ - "data": [...], - "pagination": { - "nextCursor": "0192a8b0-...", - "hasMore": true - } -} -``` - -`nextCursor = null` и `hasMore = false` если это последняя страница. +Для списков тестов и админ-очередей используется **offset-based** пагинация: `limit`, `offset`. --- @@ -70,7 +45,7 @@ GET /tests/history?limit=10&cursor=0192a8b0-... ### POST /auth/register -Регистрация нового пользователя. Отправляет письмо с кодом подтверждения email. +Регистрация нового пользователя. Отправляет письмо с кодом подтверждения email. **Токены не выдаются** до подтверждения email. **Авторизация:** не требуется @@ -94,25 +69,20 @@ GET /tests/history?limit=10&cursor=0192a8b0-... ```json { - "user": { - "id": "0192a8b0-1234-7000-8000-000000000001", - "email": "user@example.com", - "nickname": "john_doe", - "role": "free", - "emailVerified": false, - "createdAt": "2026-03-03T12:00:00.000Z" - }, - "accessToken": "eyJhbGciOiJIUzI1NiIs..." + "userId": "0192a8b0-1234-7000-8000-000000000001", + "message": "Verification code sent to your email", + "verificationCode": "123456" } ``` -Set-Cookie: `refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800` +`verificationCode` — для dev/тестов; в production не отдаётся. **Ошибки:** | HTTP | Код | Когда | | ------ | ----- | ------- | | 409 | `EMAIL_TAKEN` | Email уже зарегистрирован | +| 409 | `NICKNAME_TAKEN` | Никнейм уже занят | | 422 | `VALIDATION_ERROR` | Невалидные данные | | 429 | `RATE_LIMIT_EXCEEDED` | Более 3 регистраций с IP за час | @@ -146,6 +116,7 @@ Set-Cookie: `refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/api/ "id": "0192a8b0-1234-7000-8000-000000000001", "email": "user@example.com", "nickname": "john_doe", + "avatarUrl": null, "role": "free", "emailVerified": true }, @@ -162,8 +133,7 @@ Set-Cookie: `refreshToken=; HttpOnly; Secure; SameSite=Strict; Path=/api/ | HTTP | Код | Когда | | ------ | ----- | ------- | | 401 | `INVALID_CREDENTIALS` | Неверный email или пароль | -| 403 | `ACCOUNT_LOCKED` | Прогрессивный lockout (brute force) | -| 429 | `RATE_LIMIT_EXCEEDED` | Превышен лимит попыток входа | +| 429 | `RATE_LIMIT_EXCEEDED` | Прогрессивный lockout (brute force) или превышен лимит попыток входа | --- @@ -218,16 +188,18 @@ Set-Cookie: новый `refreshToken` (ротация). Подтверждение email по коду из письма. -**Авторизация:** Bearer token +**Авторизация:** не требуется (Bearer не нужен) **Request:** | Поле | Тип | Обязательное | | ------ | ----- | -------------- | +| userId | uuid | да | | code | string | да | ```json { + "userId": "0192a8b0-1234-7000-8000-000000000001", "code": "123456" } ``` @@ -296,12 +268,12 @@ Set-Cookie: новый `refreshToken` (ротация). | Поле | Тип | Обязательное | | ------ | ----- | ------------- | | token | string | да | -| password | string | да | +| newPassword | string | да | ```json { "token": "reset-token-from-email", - "password": "newSecurePass456" + "newPassword": "newSecurePass456" } ``` @@ -408,12 +380,25 @@ Set-Cookie: новый `refreshToken` (ротация). "selfLevel": "jun", "createdAt": "2026-03-03T12:00:00.000Z", "stats": { - "testsCompleted": 42, - "averageScore": 78 + "byStack": { + "html": { "totalTestsTaken": 5, "totalQuestions": 50, "correctAnswers": 42, "accuracy": 84 }, + "css": { "totalTestsTaken": 3, "totalQuestions": 30, "correctAnswers": 24, "accuracy": 80 } + }, + "totalTestsTaken": 8, + "totalQuestions": 80, + "correctAnswers": 66, + "accuracy": 82.5 } } ``` +**Структура stats:** +- `byStack` — объект: ключ — стек (html, css и т.д.), значение — статистика по стеку (`totalTestsTaken`, `totalQuestions`, `correctAnswers`, `accuracy`) +- `totalTestsTaken` — общее количество пройденных тестов +- `totalQuestions` — общее количество отвеченных вопросов +- `correctAnswers` — количество правильных ответов +- `accuracy` — процент правильных ответов (0–100) + Не возвращает email, role, план — только публичная информация. **Ошибки:** @@ -457,25 +442,38 @@ Set-Cookie: новый `refreshToken` (ротация). "level": "basic", "questionCount": 10, "status": "in_progress", - "currentQuestion": 1, "startedAt": "2026-03-03T12:05:00.000Z", "timeLimitSeconds": null, - "question": { - "id": "0192a8b0-9abc-7000-8000-000000000003", - "orderNumber": 1, - "type": "single_choice", - "questionText": "What does the tag do?", - "options": [ - { "key": "A", "text": "Sets the character encoding" }, - { "key": "B", "text": "Sets the page title" }, - { "key": "C", "text": "Links a stylesheet" }, - { "key": "D", "text": "Defines a script" } - ] - } + "questions": [ + { + "id": "0192a8b0-9abc-7000-8000-000000000003", + "orderNumber": 1, + "type": "single_choice", + "questionText": "What does the tag do?", + "options": [ + { "key": "A", "text": "Sets the character encoding" }, + { "key": "B", "text": "Sets the page title" }, + { "key": "C", "text": "Links a stylesheet" }, + { "key": "D", "text": "Defines a script" } + ] + }, + { + "id": "0192a8b0-9abc-7000-8000-000000000004", + "orderNumber": 2, + "type": "single_choice", + "questionText": "Which HTML element is used for the largest heading?", + "options": [ + { "key": "A", "text": "
" }, + { "key": "B", "text": "

" }, + { "key": "C", "text": "" }, + { "key": "D", "text": "
" } + ] + } + ] } ``` -При создании теста вопросы копируются в `test_questions` (снепшот). Первый вопрос возвращается сразу. +При создании теста возвращается **полный список всех вопросов** в `questions`. Вопросы копируются в `test_questions` (снепшот). **Ошибки:** @@ -533,7 +531,7 @@ Set-Cookie: новый `refreshToken` (ротация). ### POST /tests/:id/answer -Ответ на текущий вопрос. Возвращает следующий вопрос. +Ответ на текущий вопрос. **Авторизация:** Bearer token (только свой тест) @@ -541,7 +539,7 @@ Set-Cookie: новый `refreshToken` (ротация). | Поле | Тип | Обязательное | Описание | | ------ | ----- | -------------- | ---------- | -| questionId | uuid | да | ID вопроса из `question` | +| questionId | uuid | да | ID вопроса из `question` / `questions` | | answer | string / string[] | да | Ключ ответа ("A") или массив (["A", "C"]) | ```json @@ -553,32 +551,7 @@ Set-Cookie: новый `refreshToken` (ротация). **Response 200:** -```json -{ - "answered": { - "questionId": "0192a8b0-9abc-7000-8000-000000000003", - "isCorrect": true - }, - "progress": { - "answeredCount": 1, - "totalCount": 10 - }, - "nextQuestion": { - "id": "0192a8b0-9abc-7000-8000-000000000004", - "orderNumber": 2, - "type": "single_choice", - "questionText": "Which HTML element is used for the largest heading?", - "options": [ - { "key": "A", "text": "
" }, - { "key": "B", "text": "

" }, - { "key": "C", "text": "" }, - { "key": "D", "text": "
" } - ] - } -} -``` - -`nextQuestion = null` если это был последний вопрос (клиент должен вызвать `/finish`). +Возвращает полный **snapshot отвеченного вопроса** (формат из реализации). Структура может отличаться от минимальной «answered + progress + nextQuestion» — контракт определяется backend. Может включать детали ответа, правильный ответ, объяснение, прогресс и/или следующий вопрос. **Ошибки:** @@ -613,6 +586,8 @@ Set-Cookie: новый `refreshToken` (ротация). } ``` +`score` — количество правильных ответов (integer). `percentage` приходит с backend (или считается на фронте). + **Ошибки:** | HTTP | Код | Когда | @@ -687,7 +662,7 @@ Set-Cookie: новый `refreshToken` (ротация). ### GET /tests/history -История тестов пользователя. Cursor-based пагинация, сортировка по дате (новые первые). +История тестов пользователя. Offset-based пагинация, сортировка по дате (новые первые). **Авторизация:** Bearer token @@ -696,7 +671,7 @@ Set-Cookie: новый `refreshToken` (ротация). | Параметр | Тип | Обязательный | Описание | | ---------- | ----- | -------------- | --------- | | limit | integer | нет | default 10, max 50 | -| cursor | uuid | нет | ID последнего теста предыдущей страницы | +| offset | integer | нет | Смещение (default 0) | | stack | string | нет | Фильтр по стеку | | status | string | нет | Фильтр: completed / abandoned | @@ -704,7 +679,7 @@ Set-Cookie: новый `refreshToken` (ротация). ```json { - "data": [ + "tests": [ { "id": "0192a8b0-5678-7000-8000-000000000002", "stack": "html", @@ -717,10 +692,7 @@ Set-Cookie: новый `refreshToken` (ротация). "finishedAt": "2026-03-03T12:10:42.000Z" } ], - "pagination": { - "nextCursor": "0192a8b0-5678-7000-8000-000000000002", - "hasMore": false - } + "total": 42 } ``` @@ -728,9 +700,9 @@ Set-Cookie: новый `refreshToken` (ротация). ## Admin -### GET /admin/questions/queue +### GET /admin/questions/pending -QA очередь вопросов для модерации. Cursor-based пагинация. +Список вопросов на модерацию (статус pending). Offset-based пагинация. **Авторизация:** Bearer token (role: admin) @@ -739,15 +711,14 @@ QA очередь вопросов для модерации. Cursor-based па | Параметр | Тип | Обязательный | Описание | | ---------- | ----- | -------------- | ---------- | | limit | integer | нет | default 20, max 50 | -| cursor | uuid | нет | | -| status | string | нет | pending / approved / rejected (default: pending) | +| offset | integer | нет | Смещение (default 0) | | stack | string | нет | Фильтр по стеку | **Response 200:** ```json { - "data": [ + "questions": [ { "id": "0192a8b0-def0-7000-8000-000000000010", "stack": "html", @@ -769,10 +740,7 @@ QA очередь вопросов для модерации. Cursor-based па "reportsCount": 0 } ], - "pagination": { - "nextCursor": "0192a8b0-def0-7000-8000-000000000010", - "hasMore": true - } + "total": 15 } ``` @@ -784,9 +752,37 @@ QA очередь вопросов для модерации. Cursor-based па --- +### POST /admin/questions/:id/approve + +Одобрение вопроса (смена статуса на approved). + +**Авторизация:** Bearer token (role: admin) + +**Request:** пустое тело + +**Response 200:** полный объект вопроса с обновлённым статусом. + +Действие записывается в `audit_logs`. + +--- + +### POST /admin/questions/:id/reject + +Отклонение вопроса (смена статуса на rejected). + +**Авторизация:** Bearer token (role: admin) + +**Request:** пустое тело + +**Response 200:** полный объект вопроса с обновлённым статусом. + +Действие записывается в `audit_logs`. + +--- + ### PATCH /admin/questions/:id -Одобрение, отклонение или редактирование вопроса. +Редактирование контента вопроса **без смены статуса** (текст, варианты, правильный ответ, объяснение). **Авторизация:** Bearer token (role: admin) @@ -794,7 +790,6 @@ QA очередь вопросов для модерации. Cursor-based па | Поле | Тип | Обязательное | Описание | | ------ | ----- | -------------- | ---------- | -| status | string | нет | approved / rejected | | questionText | string | нет | Отредактированный текст | | options | array | нет | Отредактированные варианты | | correctAnswer | string | нет | Исправленный ответ | @@ -802,24 +797,11 @@ QA очередь вопросов для модерации. Cursor-based па ```json { - "status": "approved" -} -``` - -Или с редактированием: - -```json -{ - "status": "approved", "explanation": "The tag specifies the character encoding for the HTML document, typically UTF-8." } ``` -**Response 200:** - -Полный объект вопроса (как в GET queue) с обновлёнными полями. - -Действие записывается в `audit_logs`. +**Response 200:** полный объект вопроса с обновлёнными полями. **Ошибки:** diff --git a/database/schema.md b/database/schema.md index fa677e6..a58c27e 100644 --- a/database/schema.md +++ b/database/schema.md @@ -8,6 +8,8 @@ erDiagram users ||--o{ subscriptions : has users ||--o{ sessions : has + users ||--o{ email_verification_codes : has + users ||--o{ password_reset_tokens : has users ||--o{ oauth_accounts : has users ||--o| totp_secrets : has users ||--o{ tests : takes @@ -74,6 +76,30 @@ erDiagram | expires_at | timestamptz | | | created_at | timestamptz | | +### email_verification_codes + +Коды подтверждения email (для регистрации). + +| Колонка | Тип | Описание | +|---------|-----|----------| +| id | uuid, PK | | +| user_id | uuid, FK → users | | +| code | varchar | Код из письма (обычно 6 цифр) | +| expires_at | timestamptz | Срок действия кода | +| created_at | timestamptz | | + +### password_reset_tokens + +Токены для сброса пароля. + +| Колонка | Тип | Описание | +|---------|-----|----------| +| id | uuid, PK | | +| user_id | uuid, FK → users | | +| token_hash | varchar | Хеш токена из письма | +| expires_at | timestamptz | Срок действия | +| created_at | timestamptz | | + ### oauth_accounts Привязанные OAuth-провайдеры (Phase 2). @@ -165,7 +191,11 @@ erDiagram | question_bank_id | uuid, FK → question_bank | | | llm_model | varchar | Модель, сгенерировавшая вопрос | | prompt_hash | varchar | Хеш промпта | -| generation_time_ms | integer | Время генерации | +| generation_time_ms | integer | Время генерации в мс | +| valid | boolean | Прошёл ли валидацию с первого раза | +| retry_count | integer | Количество retry при ошибках | +| questions_generated | integer | Сколько вопросов сгенерировано | +| raw_response | text, nullable | Сырой ответ LLM (опционально, для отладки) | | created_at | timestamptz | | ### question_reports diff --git a/llm/strategy.md b/llm/strategy.md index d149315..1e1c891 100644 --- a/llm/strategy.md +++ b/llm/strategy.md @@ -20,12 +20,17 @@ flowchart LR LLM_BASE_URL=http://localhost:11434/v1 LLM_MODEL=qwen2.5:14b LLM_API_KEY= +LLM_FALLBACK_MODEL=qwen2.5:7b LLM_TIMEOUT_MS=15000 LLM_MAX_RETRIES=1 +LLM_RETRY_DELAY_MS=2000 LLM_TEMPERATURE=0.7 LLM_MAX_TOKENS=2048 ``` +- **LLM_FALLBACK_MODEL** — запасная модель при падении основной. +- **LLM_RETRY_DELAY_MS** — задержка между retry при ошибках API (мс). + Все провайдеры используют OpenAI-совместимый API (`/v1/chat/completions`). Замена провайдера — изменение `.env`, код не меняется. ### Стратегия провайдеров @@ -466,14 +471,15 @@ Here are the questions: Каждый LLM-вызов записывается в `question_cache_meta`: -| Метрика | Что записываем | -| - | - | -| model | Модель (`qwen2.5:14b`, `gpt-4o-mini`, ...) | -| generation_time_ms | Время генерации | -| prompt_hash | SHA-256 промпта (для дедупликации) | -| valid | boolean — прошёл ли валидацию с первого раза | -| retry_count | Сколько retry потребовалось | -| questions_generated | Сколько вопросов вернул | +| Поле | Тип | Описание | +| - | - | - | +| llm_model | varchar | Модель (`qwen2.5:14b`, `gpt-4o-mini`, ...) | +| prompt_hash | varchar | SHA-256 промпта (для дедупликации) | +| generation_time_ms | integer | Время генерации в мс | +| valid | boolean | Прошёл ли валидацию с первого раза | +| retry_count | integer | Сколько retry потребовалось | +| questions_generated | integer | Сколько вопросов сгенерировано | +| raw_response | text, nullable | Сырой ответ LLM (опционально, для отладки) | Периодически анализируем: `% валидных ответов по модели`. Если ниже 80% — менять промпт или модель. diff --git a/onboarding/setup.md b/onboarding/setup.md index 1b04f93..d98946f 100644 --- a/onboarding/setup.md +++ b/onboarding/setup.md @@ -69,17 +69,22 @@ DATABASE_URL=postgresql://samreshu:samreshu_dev@localhost:5432/samreshu # Redis REDIS_URL=redis://localhost:6379 -# Auth +# Auth (JWT_SECRET минимум 32 символа в production) JWT_SECRET=dev-secret-change-in-production JWT_ACCESS_TTL=15m JWT_REFRESH_TTL=7d +# CORS (через запятую: http://localhost:5173,https://samreshu.ru) +CORS_ORIGINS=http://localhost:5173 + # LLM LLM_BASE_URL=http://localhost:11434/v1 LLM_MODEL=qwen2.5:14b LLM_API_KEY= +LLM_FALLBACK_MODEL=qwen2.5:7b LLM_TIMEOUT_MS=15000 LLM_MAX_RETRIES=1 +LLM_RETRY_DELAY_MS=2000 LLM_TEMPERATURE=0.7 LLM_MAX_TOKENS=2048 @@ -96,6 +101,8 @@ SENTRY_DSN= `.env` файл **не коммитится**. В репо лежит `.env.example` с теми же ключами и примерами значений. +**Правило:** `.env.example` обновляется при добавлении новых фич — новые переменные (rate limits, LLM, CORS и т.д.) должны быть зафиксированы в шаблоне. **JWT_SECRET** должен быть не менее 32 символов (рекомендуется `openssl rand -base64 32`). + ## Быстрый старт ```bash diff --git a/principles/security.md b/principles/security.md index 97e9ea6..f2b08c9 100644 --- a/principles/security.md +++ b/principles/security.md @@ -159,23 +159,23 @@ ${userAnswer} - `X-Frame-Options: DENY` - `Strict-Transport-Security: max-age=31536000; includeSubDomains` - `X-XSS-Protection: 0` (устаревший, отключаем — CSP заменяет) -- `Content-Security-Policy` — ограничение источников скриптов, стилей, шрифтов + +**CSP и COEP отключены** — бэкенд отдаёт только JSON API. Эти заголовки предназначены для HTML-страниц; для REST API они не нужны и могут мешать Swagger UI. ### CORS -Плагин: **`@fastify/cors`** с whitelist origins: +Плагин: **`@fastify/cors`**. Origins задаются через переменную окружения **`CORS_ORIGINS`** (не хардкод localhost/prod): ```ts { - origin: [ - 'http://localhost:5173', // dev (Vite) - 'https://samreshu.ru', // prod - ], + origin: process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:5173'], credentials: true, // для httpOnly cookies - methods: ['GET', 'POST', 'PATCH', 'DELETE'], + methods: ['GET', 'POST', 'PATCH', 'DELETE', 'PUT', 'OPTIONS'], } ``` +Методы: **GET, POST, PATCH, DELETE, PUT, OPTIONS** (OPTIONS для preflight, PUT для идемпотентных обновлений). + ### HTTPS SSL termination на nginx (reverse proxy), не в Node.js: