Skip to content

Doc Public API

PlutoGr edited this page Oct 17, 2026 · 4 revisions

Публичный API

Всё, что видно снаружи, проходит через Gateway по префиксу /api/v1. Таблицы ниже собраны из конфигурации маршрутов Gateway (src/Gateway/TravelAgency.Gateway/appsettings.json, секция ReverseProxy), YarpExtensions.cs и контроллеров сервисов, права сверены с обработчиками команд и запросов.

На dev весь сайт дополнительно закрыт basic auth на nginx, поэтому снаружи API доступен только после ввода пароля стенда. Ниже описаны правила самого приложения.

Простой пример

Гость открывает каталог:

GET /api/v1/catalog/tours?country=Турция&sortBy=Price&sortDirection=Asc&page=1&pageSize=20
{
  "items": [
    {
      "id": "3f2a…",
      "title": "Неделя в Стамбуле",
      "country": "Турция",
      "durationDays": 7,
      "coverMediaFileId": "9b1c…",
      "priceFrom": 52000,
      "currency": "RUB",
      "nearestDate": "2026-11-10T00:00:00Z",
      "previews": [{ "mediaFileId": "9b1c…", "alt": "Вид на Босфор", "isCover": true, "sortOrder": 0 }]
    }
  ],
  "totalCount": 1,
  "page": 1,
  "pageSize": 20,
  "totalPages": 1,
  "hasNextPage": false,
  "hasPreviousPage": false
}

Фото браузер берёт по GET /api/v1/media/files/9b1c…/w800. Поля ответа сокращены; точный состав в DTO TourSummaryDto и PagedResult<T>.

Общие правила

Тема Как устроено
Аутентификация httpOnly cookie access_token и refresh_token. Их ставит Gateway при входе; на каждом запросе Gateway превращает cookie в заголовок Authorization: Bearer. Подробно: Вход и сессия
Роли Client (выдаётся при регистрации), Manager, Admin
Формат JSON, имена полей в camelCase
Ошибки ProblemDetails: type, title, status, detail; у некоторых ошибок есть расширения (например, missing)
Перечисления статус заявки (status, newStatus) передаётся числом: 0 New, 1 InProgress, 2 ProposalSent, 3 Confirmed, 4 Closed, 5 Cancelled
Лимиты на IP: вход, регистрация, обновление токена и выход 5 запросов в секунду, остальное 200 в минуту; сверх лимита 429. Identity добавляет свой лимит 5 попыток за 60 секунд (скользящее окно) на register, login, refresh
Версии ломающие изменения только в /api/v2; новые необязательные поля добавляются в /api/v1

Коды ответов

Код Когда
400 ошибка валидации запроса
401 нет входа или токен истёк (фронтенд сам пробует refresh и повторяет запрос)
403 вход есть, прав нет (не та роль, чужой тур, чужая заявка)
404 ресурса нет или он скрыт (черновик тура, непубличное фото, закрытый маршрут presign)
409 конфликт: устаревшая версия тура, удаление не черновика, дубль email или избранного
413 файл больше лимита nginx (12 МБ)
422 тур не проходит проверку публикации, фото чужое, тур недоступен для заявки, недопустимый переход статуса заявки
428 для изменения тура не передан If-Match
429 превышен лимит запросов
503 сервис не смог обратиться к другому сервису по gRPC (например, Catalog к Media при публикации). Исключение: Chat при недоступном Booking отвечает 403

Маршруты Gateway

Gateway выбирает маршрут по пути и методу; при пересечении побеждает меньший Order. Затем он убирает префикс и отправляет запрос в сервис.

Маршрут Внешний путь Методы Доступ на Gateway Сервис и путь внутри
auth-route /api/v1/auth/** GET, POST, PATCH все Identity /identity/**
catalog-wizard-route (Order -1) /api/v1/catalog/manage/** все Manager, Admin Catalog /catalog/manage/**
catalog-public-route /api/v1/catalog/** GET все Catalog /catalog/**
catalog-manage-route /api/v1/catalog/** POST, PUT, PATCH, DELETE Manager, Admin Catalog (эндпоинтов записи вне manage нет, запрос до действия не доходит; ответ 404 или 405)
bookings-route /api/v1/bookings/** GET, POST, PUT, PATCH, DELETE вошедший Booking /bookings/**
favorites-route /api/v1/favorites/** GET, POST, DELETE вошедший Booking /favorites/**
chat-hub-route (Order 0) /api/v1/chat/hubs/** все вошедший Chat /hubs/**
chat-route (Order 1) /api/v1/chat/** GET, POST вошедший Chat /chat/**
media-files-public-route (Order 0) /api/v1/media/files/** GET все Media /media/files/**
media-manage-files-route (Order 0) /api/v1/media/manage/** GET Manager, Admin Media /media/manage/**
media-route (Order 100) /api/v1/media/** GET, POST, DELETE вошедший Media /media/**
заглушка в YarpExtensions /api/v1/media/presign GET, POST, PUT, DELETE закрыт ответ 404, в сервис не идёт

«Вошедший» означает любую из ролей Client, Manager, Admin (политика RequireAuthenticated).

Вход и профиль (Identity)

Метод Путь Доступ Тело Ответ
POST /api/v1/auth/register все { email, password, firstName, lastName, phone? } 201, cookie + { "success": true }; 409, если email занят
POST /api/v1/auth/login все { email, password } 200, cookie + { "success": true }
POST /api/v1/auth/refresh все пусто (refresh-токен берётся из cookie) или { refreshToken } 200, новые cookie
POST /api/v1/auth/logout вошедший пусто или { refreshToken } 204, cookie удаляются
GET /api/v1/auth/me вошедший { id, email, firstName, lastName, phone?, role, createdAt }
PATCH /api/v1/auth/me вошедший { firstName?, lastName?, phone? } обновлённый профиль

Сами токены в теле ответа не возвращаются: Gateway забирает их из ответа Identity и кладёт в cookie.

Каталог: публичная часть (Catalog)

Отдаются только опубликованные туры (Published).

Метод Путь Доступ Параметры Ответ
GET /api/v1/catalog/tours все country, directionId, dateFrom, dateTo, minPrice, maxPrice, tourType, sortBy (CreatedAt, Title, Price, DurationDays), sortDirection (Asc, Desc), page, pageSize (по умолчанию 20) PagedResult<TourSummaryDto>: цена «от» и ближайшая дата по будущим предложениям, до 5 превью
GET /api/v1/catalog/tours/{id} все PublicTourDto: описание, программа по дням, включено и не включено, питание, проживание, предложения с ценами, фото, components: []. Черновик и снятый тур: 404
GET /api/v1/catalog/tours/cards?ids=… все до 50 id карточки для избранного. Снятый тур: только id, title, обложка и available: false. Черновики и несуществующие id в ответ не попадают
GET /api/v1/catalog/directions все список направлений

Параметр isActive в фильтре остался от старой модели и на выдачу не влияет: черновики и снятые туры он не открывает.

Каталог: управление турами (Catalog)

Только Manager и Admin. Менеджер работает со своими турами (владелец OwnerId), на чужой получает 403. Admin видит и меняет все туры.

Метод Путь Тело Ответ
GET /api/v1/catalog/manage/tours список туров: у менеджера свои, у админа все. Фильтров в запросе нет
GET /api/v1/catalog/manage/tours/{id} тур с чек-листом готовности к публикации; версия в заголовке ETag
POST /api/v1/catalog/manage/tours необязательно: { title?, shortDescription?, description?, departureCity?, country?, tourType?, durationDays?, directionId? } 201, черновик. If-Match не нужен
PUT …/{id}/basics { title, shortDescription, departureCity, country, tourType, durationDays, directionId } тур с новой версией
PUT …/{id}/description { description }
PUT …/{id}/program { days: [{ dayNumber, title, description }] }
PUT …/{id}/conditions { inclusions: [{ text, kind, sortOrder }], mealPlan, accommodationText }
PUT …/{id}/prices { offers: [{ validFrom, validTo, pricePerPerson, currency, availableSeats }] }
PUT …/{id}/images { images: [{ mediaFileId, sortOrder, isCover, alt }] } 422, если фото не принадлежит владельцу тура
POST …/{id}/publish тур в статусе Published; 422 со списком missing, если не готов
POST …/{id}/unpublish тур в статусе Unpublished; 409 для черновика
DELETE …/{id} 204; удалить можно только черновик, иначе 409

Все запросы, кроме GET и POST черновика, требуют заголовок If-Match с версией из ETag. Без него 428, с устаревшей версией 409. Как это работает: Конструктор и публикация тура.

Заявки и избранное (Booking)

Метод Путь Доступ (Gateway и сервис) Тело Ответ
POST /api/v1/bookings вошедший { tourId, comment? } 201, заявка в статусе New; 422 tour-unavailable, если тур не опубликован
GET /api/v1/bookings Manager, Admin все заявки
GET /api/v1/bookings/my вошедший заявки текущего пользователя
GET /api/v1/bookings/{id} клиент только свою (иначе 403), Manager и Admin любую заявка с предложениями; менеджер и админ дополнительно получают имя и email клиента
PATCH /api/v1/bookings/{id}/status клиент только свою и только в Cancelled; Manager и Admin любые допустимые переходы { newStatus } заявка; 400, если newStatus = Confirmed (для этого есть confirm); 422 на недопустимый переход
POST /api/v1/bookings/{id}/proposal Manager, Admin { notes? } 201, предложение со снимком тура
POST /api/v1/bookings/{id}/confirm клиент свою заявку; Manager и Admin тоже могут { proposalId } заявка в статусе Confirmed
GET /api/v1/favorites вошедший избранное текущего пользователя
POST /api/v1/favorites/{tourId} вошедший 201; 409, если тур уже в избранном
DELETE /api/v1/favorites/{tourId} вошедший 204

Переходы статусов и снимок тура: Заявки и чат.

Чат (Chat)

Метод Путь Доступ Ответ
GET /api/v1/chat/booking/{bookingId}/messages клиент заявки, Manager, Admin; остальным 403 список { id, bookingId, senderId, senderName, senderRole, text, attachments?, createdAt }
SignalR /api/v1/chat/hubs/chat вошедший методы JoinBookingGroup(bookingId), SendMessage(bookingId, text, attachments?); событие MessageReceived

Доступ к заявке проверяется на каждом вызове через Booking.

Файлы (Media)

Метод Путь Доступ Ответ
POST /api/v1/media/upload вошедший; с ?purpose=tour-image только Manager и Admin multipart, поле file. 201: { id, fileName, contentType, sizeBytes, thumbnails: [{ id, width, height }], uploadedAt, width?, height?, isPublic? }. Адресов в ответе нет
GET /api/v1/media/files/{id}/{w200|w800|w1600} все превью фото тура; только для публичных файлов, иначе 404. Cache-Control: public, max-age=31536000, immutable
GET /api/v1/media/manage/files/{id}/{size} Manager и Admin на Gateway; в сервисе владелец файла или Admin, иначе 403 превью черновика. Cache-Control: private, no-store
GET /api/v1/media/{id} вошедший. Публичный tour-image, владелец, Manager или Admin. Публичный файл другого purpose и чужой непубличный файл: 404, как неизвестный id оригинал потоком. Ответ, кроме публичного фото тура: Cache-Control: private, no-store. Аноним: 401
DELETE /api/v1/media/{id} только владелец файла, иначе 403 204, файл и превью удаляются из хранилища
любой /api/v1/media/presign закрыт на Gateway 404

Ограничения загрузки: до 10 МБ; общие типы image/jpeg, image/png, image/webp, image/gif, application/pdf; для фото туров только jpeg, png и webp. Содержимое файла сверяется с заявленным типом. Подробно: Картинки.

Служебное

Путь Где Что
/healthz nginx без пароля; Healthy, если Gateway и все сервисы готовы
/health/live, /health/ready Gateway и каждый сервис внутри сети; снаружи не опубликованы

Clone this wiki locally