Repository navigation
Doc Public 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 выбирает маршрут по пути и методу; при пересечении побеждает меньший 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).
| Метод | Путь | Доступ | Тело | Ответ |
|---|---|---|---|---|
| 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.
Отдаются только опубликованные туры (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 в фильтре остался от старой модели и на выдачу не влияет: черновики и снятые туры он не открывает.
Только 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. Как это работает: Конструктор и публикация тура.
| Метод | Путь | Доступ (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 |
Переходы статусов и снимок тура: Заявки и чат.
| Метод | Путь | Доступ | Ответ |
|---|---|---|---|
| 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.
| Метод | Путь | Доступ | Ответ |
|---|---|---|---|
| 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 и каждый сервис | внутри сети; снаружи не опубликованы |
Система
API
Сервисы
Как реализовано
Справочное
- Видение продукта
- Дорожная карта
- Этап 1: что осталось
- Этап 2: расширение
- Этап 3: масштабирование
- Концепция сайта и интерфейса
- Шаблон ТЗ