# Карта API-маршрутов

## Паспорт документа

- Статус документа: living document
- Актуально на: 2 апреля 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении endpoint'ов, auth-flow или API-контракта
- Область применения: обзорная карта живого backend API и точек входа OpenAPI/Swagger
- Связанные документы:
  - [Памятка для frontend-команды](../frontend/frontend-handoff.md)
  - [Change Log для frontend-команды](../frontend/frontend-change-log.md)
  - [Платформенный implementation status](../../specs/qadam-platform/implementation.md)

> Base prefix: `/api/v1/`

---

## Авторизация и токены

Web-клиент использует **httpOnly cookies**. Дополнительно `JwtAuthGuard` умеет читать Bearer token из `Authorization` header, что используется для non-web клиентов и служебных сценариев.

| Cookie | Содержимое | TTL | Path |
|--------|-----------|-----|------|
| `qadam_at` | Access JWT `{sub, email, type, tokenType:'access'}` | 15 мин | `/` |
| `qadam_rt` | Refresh JWT `{tokenType:'refresh', jti}` | 7 дней | `/` |

**JwtAuthGuard** применён глобально (APP_GUARD). Сначала читает Bearer token из `Authorization`, если его нет — fallback на cookie `qadam_at`. Затем проверяет подпись и `tokenType === 'access'`. Маршруты с `@Public()` пропускают проверку.

**Роли** проверяются в контроллерах через `assertSeller(user)` / `assertBuyer(user)` и аналогичные проверки по типу пользователя, без отдельного role-guard слоя. При несоответствии бросается `ForbiddenException`.

**Ротация токена:** `POST /auth/refresh` читает `qadam_rt` из cookie или `refreshToken` из body, верифицирует его, проверяет `jti`, генерирует **оба** новых токена и для web-клиента заново устанавливает cookies.

> `qadam_rt` теперь intentionally доступна всему web runtime по `path=/`, чтобы server-side proxy и server components могли читать refresh cookie при полной перезагрузке страницы. При выдаче новой пары токенов backend дополнительно чистит legacy `qadam_rt` с `path=/api/v1/auth`, чтобы не оставлять дублирующую refresh-cookie после миграции.

---

## OpenAPI и Swagger

Backend теперь публикует машиночитаемый и человекочитаемый API-контур:

| Назначение | URL | Доступ |
|-----------|-----|--------|
| Swagger UI | `/api/docs` | Внутренний инженерный доступ |
| OpenAPI JSON | `/api/openapi.json` | Внутренний инженерный доступ |

Именно `OpenAPI JSON` должен использоваться как источник истины для frontend codegen, contract review и дальнейшего выноса frontend в отдельный репозиторий. `docs/architecture/api-routes.md` остаётся обзорной картой маршрутов, но не заменяет машиночитаемый контракт.

Дополнительно в репозитории зафиксирован versioned artifact:

- `apps/api/openapi/openapi.json` — канонический snapshot контракта для CI и frontend codegen;
- `apps/web/src/shared/api/generated/openapi.d.ts` — generated TypeScript contract для web;
- `pnpm sync:api-contract` — обновить artifact и generated types;
- `pnpm check:api-contract` — проверить drift между backend-кодом, artifact и generated types.

---

## Health

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| GET | `/health` | Public | Быстрый health check API |
| GET | `/health/live` | Public | Liveness probe процесса API |
| GET | `/health/ready` | Public | Readiness probe с проверкой PostgreSQL и Redis |
| GET | `/metrics` | Public route, loopback-only runtime access | Prometheus-compatible runtime/process/http metrics |

> `GET /metrics` публикуется в OpenAPI как часть system API, но runtime сознательно режет его на `403`, если запрос пришёл не с loopback-адреса сервера. Это инженерный endpoint для локального scrape и operational диагностики, а не публичный маршрут для браузера.

---

## Auth

| Метод | Path | Auth | Cookies | Описание |
|-------|------|------|---------|----------|
| POST | `/auth/register` | Public | web: ← sets `qadam_at` + `qadam_rt`; mobile: ← returns both in body | Регистрация (email/phone, пароль, тип аккаунта) |
| POST | `/auth/check-availability` | Public | n/a | Проверка доступности email и/или телефона перед регистрацией |
| POST | `/auth/register/buyer` | Public | web: ← sets `qadam_at` + `qadam_rt`; mobile: ← returns both in body | Единая регистрация buyer с онбордингом |
| POST | `/auth/register/seller` | Public | web: ← sets `qadam_at` + `qadam_rt`; mobile: ← returns both in body | Единая регистрация seller с профилем, адресами и направлениями |
| POST | `/auth/login` | Public | web: ← sets `qadam_at` + `qadam_rt`; mobile: ← returns both in body | Вход по email/phone + пароль |
| GET | `/auth/me` | `qadam_at` или Bearer | → reads access token | Текущий пользователь |
| POST | `/auth/refresh` | Public | web: → reads `qadam_rt`, mobile: → reads `refreshToken` body | Ротация токенов |
| POST | `/auth/logout` | `qadam_at` или Bearer | web: ← clears both cookies | Выход, очистка сессии |
| POST | `/auth/forgot-password` | Public | n/a | Начать восстановление пароля |
| POST | `/auth/verify-reset-code` | Public | n/a | Проверить SMS-код для сброса пароля |
| POST | `/auth/reset-password` | Public | web: ← sets `qadam_at` + `qadam_rt`; mobile: ← returns both in body | Сбросить пароль и открыть сессию |
| POST | `/auth/change-password` | `qadam_at` или Bearer | → reads access token | Сменить пароль из авторизованного кабинета |
| POST | `/auth/add-buyer-role` | `qadam_at` или Bearer | → reads access token | Добавить buyer-профиль к seller/seller_staff аккаунту |

---

## Buyer (роль: BUYER)

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| GET | `/me/profile` | `qadam_at` или Bearer | Получить профиль покупателя |
| POST | `/me/profile` | `qadam_at` или Bearer | Создать профиль |
| PATCH | `/me/profile` | `qadam_at` или Bearer | Обновить account-данные buyer-профиля |
| PUT | `/me/profile` | `qadam_at` или Bearer | Legacy alias для PATCH |
| GET | `/me/children` | `qadam_at` или Bearer | Получить список детей parent-buyer |
| POST | `/me/children` | `qadam_at` или Bearer | Добавить ребёнка |
| PATCH | `/me/children/:studentId` | `qadam_at` или Bearer | Обновить ребёнка |
| DELETE | `/me/children/:studentId` | `qadam_at` или Bearer | Удалить ребёнка |
| PATCH | `/me/interests` | `qadam_at` или Bearer | Полностью заменить интересы student-buyer |
| POST | `/leads` | `qadam_at` или Bearer | Оставить заявку на товар |
| GET | `/me/leads` | `qadam_at` или Bearer | Мои заявки |
| GET | `/me/reviews` | `qadam_at` или Bearer | Мои отзывы |
| POST | `/me/reviews` | `qadam_at` или Bearer | Написать отзыв |

> Review flow больше не подразумевает немедленную публикацию: новые отзывы создаются в `PENDING`, `GET /me/reviews` публикует `status` и `moderationNote`, а публичные страницы и агрегаты учитывают только `PUBLISHED`.

---

## Seller (роль: SELLER)

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| GET | `/seller/profile` | `qadam_at` или Bearer | Профиль продавца |
| POST | `/seller/profile` | `qadam_at` или Bearer | Создать профиль |
| PATCH | `/seller/profile` | `qadam_at` или Bearer | Обновить профиль |
| PUT | `/seller/profile` | `qadam_at` или Bearer | Legacy alias для PATCH |
| GET | `/seller/addresses` | `qadam_at` или Bearer | Список адресов продавца |
| POST | `/seller/addresses` | `qadam_at` или Bearer | Добавить адрес продавца |
| PATCH | `/seller/addresses/:addressId` | `qadam_at` или Bearer | Обновить адрес продавца |
| DELETE | `/seller/addresses/:addressId` | `qadam_at` или Bearer | Удалить адрес продавца |
| PATCH | `/seller/addresses/:addressId/set-primary` | `qadam_at` или Bearer | Сделать адрес основным |
| GET | `/seller/notification-settings` | `qadam_at` или Bearer | Получить настройки уведомлений продавца |
| PATCH | `/seller/notification-settings` | `qadam_at` или Bearer | Обновить настройки уведомлений продавца |
| GET | `/seller/telegram/connect-link` | `qadam_at` или Bearer | Получить deep link для запуска seller Telegram onboarding flow |
| POST | `/seller/telegram/verify` | `qadam_at` или Bearer | Привязать Telegram по верификационному коду |
| DELETE | `/seller/telegram` | `qadam_at` или Bearer | Отвязать Telegram |
| GET | `/seller/items` | `qadam_at` или Bearer | Список своих товаров |
| GET | `/seller/items/:id` | `qadam_at` или Bearer | Товар по ID |
| POST | `/seller/items` | `qadam_at` или Bearer | Создать товар |
| PUT | `/seller/items/:id` | `qadam_at` или Bearer | Обновить товар |
| POST | `/seller/items/:id/submit` | `qadam_at` или Bearer | Отправить на модерацию |
| POST | `/seller/items/:id/withdraw` | `qadam_at` или Bearer | Отозвать pending-товар с модерации обратно в draft |
| POST | `/seller/items/:id/archive` | `qadam_at` или Bearer | Архивировать товар |
| DELETE | `/seller/items/:id` | `qadam_at` или Bearer | Удалить товар |
| GET | `/seller/leads` | `qadam_at` или Bearer | Заявки на мои товары; доступно owner и `SELLER_STAFF` |
| PUT | `/seller/leads/:id/status` | `qadam_at` или Bearer | Изменить статус заявки (CREATED / CONTACTED / ENROLLED / REJECTED); доступно owner и `SELLER_STAFF` |
| GET | `/seller/reviews` | `qadam_at` или Bearer | Отзывы по товарам продавца |
| PATCH | `/seller/reviews/:id/reply` | `qadam_at` или Bearer | Оставить или обновить ответ продавца на отзыв |
| POST | `/seller/reviews/:id/complaint` | `qadam_at` или Bearer | Подать жалобу на опубликованный отзыв и отправить его на повторную модерацию |
| GET | `/seller/staff` | `qadam_at` или Bearer | Список сотрудников |
| POST | `/seller/staff` | `qadam_at` или Bearer | Добавить сотрудника |
| PUT | `/seller/staff/:id` | `qadam_at` или Bearer | Обновить сотрудника |
| DELETE | `/seller/staff/:id` | `qadam_at` или Bearer | Удалить сотрудника |

> Для `PATCH /seller/profile` backend возвращает специфичные `PHONE_TAKEN` и `EMAIL_TAKEN`, если contact phone/email конфликтуют с другим аккаунтом. `GET /seller/telegram/connect-link` отдаёт готовый deep link с коротким signed token; frontend больше не должен собирать bot URL вручную. `POST /seller/telegram/verify` и `DELETE /seller/telegram` возвращают `{ success, username }`, где `username` может быть `null`.

> Seller item status-модель теперь включает `DRAFT`, `PENDING`, `ACTIVE`, `REJECTED`. `GET /seller/items` и `GET /seller/items/:id` дополнительно публикуют `latestModerationRecord`, чтобы frontend мог показать последнюю seller-visible причину/комментарий модерации. `PUT /seller/items/:id` больше не разрешён для `PENDING` айтемов: seller должен сначала вызвать `POST /seller/items/:id/withdraw`.

> Seller review surface теперь публикуется отдельно: `GET /seller/reviews` отдаёт список отзывов по айтемам продавца со статусом, `sellerReply`, `sellerReplyAt`, `canReply`, `canEditReply` и `activeComplaint`. `POST /seller/reviews/:id/complaint` доступен только для `PUBLISHED`-отзывов, переводит отзыв в `PENDING_MODERATION` и временно убирает его из public aggregates, а `PATCH /seller/reviews/:id/reply` разрешён только в течение 48 часов после последнего ответа продавца.

> Seller notification baseline теперь публикует `GET/PATCH /seller/notification-settings` с полями `notifyNewLeadTelegram`, `notifyNewLeadEmail`, `notifyStatusChangeTelegram`, `telegramConnected`, `telegramUsername`, `sellerEmail`. После создания лида `LeadService` сначала пытается Telegram-доставку продавцу, а при её отказе/невозможности и включённом `notifyNewLeadEmail` переходит на SMTP-capable email fallback; если SMTP не настроен на конкретном окружении, backend фиксирует `SKIPPED`, но не ломает создание лида. `sellerEmail` теперь резолвится из seller profile email с fallback на account email. Для owner-facing status-change notifications `notifyStatusChangeTelegram` больше не является пустым флагом: если статус меняет активный `SELLER_STAFF`, backend шлёт owner-уведомление по выбранным статусам из `SELLER_STATUS_CHANGE_NOTIFY_STATUSES` и при недоступном Telegram так же переходит на email fallback.

---

## Catalog (Public)

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| GET | `/catalog/items` | Public | Список опубликованных товаров с фильтрами |
| GET | `/catalog/items/:slug` | Public | Товар по slug (трекается событие `view_item`) |
| GET | `/catalog/items/:slug/reviews` | Public | Отзывы на товар |
| GET | `/subjects` | Public | Публичный alias справочника направлений |
| GET | `/catalog/subjects` | Public | Категории / предметы |
| GET | `/catalog/locations` | Public | Локации |
| GET | `/sellers/:id` | Public | Публичный профиль seller |

---

> `GET /sellers/:id` теперь публикует не только витринные поля seller и список опубликованных айтемов, но и явный `seo` block (`title`, `description`, `canonicalUrl`, `openGraph`, `jsonLd`) для SSR/metadata слоя. Профиль отдаётся только для `ACTIVE` seller-аккаунтов; несуществующий или непубличный seller должен давать `404`.

---

## Admin (роль: ADMIN)

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| GET | `/admin/stats` | `qadam_at` или Bearer | Статистика дашборда |
| GET | `/admin/leads` | `qadam_at` или Bearer | Все заявки в системе |
| GET | `/admin/sellers` | `qadam_at` или Bearer | Пагинированный список seller-аккаунтов для admin-операций |
| PATCH | `/admin/sellers/:sellerId/status` | `qadam_at` или Bearer | Изменить account status seller-профиля |
| GET | `/admin/moderation/items` | `qadam_at` или Bearer | Товары на модерации |
| GET | `/admin/moderation/items/:id` | `qadam_at` или Bearer | Товар для проверки |
| POST | `/admin/moderation/items/:id/approve` | `qadam_at` или Bearer | Одобрить товар (опциональный комментарий) |
| POST | `/admin/moderation/items/:id/reject` | `qadam_at` или Bearer | Отклонить товар (причина обязательна) |
| GET | `/admin/moderation/reviews` | `qadam_at` или Bearer | Очередь отзывов на модерацию |
| GET | `/admin/moderation/reviews/:id` | `qadam_at` или Bearer | Отзыв для модерации |
| PATCH | `/admin/moderation/reviews/:id` | `qadam_at` или Bearer | Принять moderation decision по отзыву |
| POST | `/catalog/subjects` | `qadam_at` или Bearer | Создать категорию |
| PUT | `/catalog/subjects/:id` | `qadam_at` или Bearer | Обновить категорию |
| DELETE | `/catalog/subjects/:id` | `qadam_at` или Bearer | Удалить категорию |
| POST | `/catalog/locations` | `qadam_at` или Bearer | Создать локацию |
| PUT | `/catalog/locations/:id` | `qadam_at` или Bearer | Обновить локацию |
| DELETE | `/catalog/locations/:id` | `qadam_at` или Bearer | Удалить локацию |

---

> `GET /admin/sellers` принимает фильтры `search`, `status`, `sellerType`, `page`, `limit` и возвращает seller summary с `displayName`, `accountStatus`, contact fields и item counters (`totalItems`, `pendingItems`, `activeItems`), чтобы admin мог работать со статусами без ручного знания `sellerId`.

> После `PATCH /admin/sellers/:sellerId/status` seller-facing защищённые маршруты больше не маскируют статус аккаунта под generic token error: backend отдаёт явные `403 ACCOUNT_UNDER_REVIEW` или `403 ACCOUNT_BLOCKED`, если seller пытается идти в защищённый контур со старым access token.

> Review moderation теперь живёт на отдельном backend-срезе: `ReviewStatus` включает `PENDING`, `PUBLISHED`, `REJECTED`, `PENDING_MODERATION`; admin принимает решение через `PATCH /admin/moderation/reviews/:id`, catalog/public seller aggregates считают только `PUBLISHED`, а seller complaint flow переводит спорный отзыв обратно в `PENDING_MODERATION`.

---

## Uploads

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| POST | `/upload/image` | `qadam_at` или Bearer | Загрузить JPG/PNG/WebP до 5 МБ и получить публичный URL |
| GET | `/uploads/images/:fileName` | Public | Публичная выдача уже загруженного изображения |

> Текущий production уже переведён на S3-compatible storage `DigitalOcean Spaces`, поэтому новые upload'ы обычно возвращают абсолютный публичный URL. Локальный `/uploads/images/<fileName>` остаётся compatibility/fallback-вариантом для `OBJECT_STORAGE_DRIVER=local` и старых локальных файлов. Canonical transport contract для upload endpoint остаётся тем же: `{ url: string }`, где `url` является готовым публичным URL.

---

## Tracking

| Метод | Path | Auth | Описание |
|-------|------|------|----------|
| POST | `/track` | Public | Отправить событие трекинга. Если пользователь авторизован — привязывается к нему, иначе по IP. Cookie `qadam_cid` используется как client ID. |
> Admin moderation endpoints теперь принимают решение только по `PENDING` айтемам. Если айтем уже ушёл в `ACTIVE` или `REJECTED`, backend возвращает конфликт статуса, а `GET /admin/moderation/items/:id` публикует `latestModerationRecord` и `moderationHistory`.
