# API Контракт (OpenAPI 3.0) ## Базовый URL - Локально: `http://localhost:8080/api/v1` - Стенд: `https://staging.api.photoplaces.ru/api/v1` - Прод: `https://api.photoplaces.ru/api/v1` ## Аутентификация - **Access Token**: JWT, срок 15 мин, в заголовке `Authorization: Bearer ` - **Refresh Token**: HTTP-only cookie, срок 30 дней, ротация при использовании - **Роли**: `superadmin`, `moderator`, `landlord`, `executor`, `customer` ## Формат ошибок (RFC 7807) ```json { "type": "https://api.photoplaces.ru/errors/validation-error", "title": "Ошибка валидации", "status": 422, "detail": "Некорректные входные данные", "instance": "/api/v1/places", "errors": [ { "field": "title", "message": "Название обязательно" } ] } ``` --- ## Эндпоинты авторизации ### POST /auth/register ```json // Запрос { "email": "user@example.com", "password": "securePass123", "role": "customer" // customer | landlord | executor } // Ответ 201 { "user": { "id": "uuid", "email": "...", "role": "customer", "created_at": "..." }, "access_token": "eyJ...", "refresh_token": "eyJ..." // также устанавливается HttpOnly cookie } ``` ### POST /auth/login ```json // Запрос { "email": "user@example.com", "password": "securePass123" } // Ответ 200 { "user": { ... }, "access_token": "eyJ...", "refresh_token": "eyJ..." } ``` ### POST /auth/refresh - На основе cookie, тело не требуется - Ответ 200: новый access_token + обновлённый refresh_token ### POST /auth/logout - Инвалидирует refresh token - Ответ 204 ### GET /auth/me - Возвращает профиль текущего пользователя - Ответ 200: объект User --- ## Эндпоинты мест (Places) ### GET /places **Параметры запроса:** - `type`: `place` | `studio` (по умолчанию оба) - `tags`: ID тегов через запятую - `min_rating`: число 0-5 - `features`: ID характеристик через запятую - `price_min`, `price_max`: для студий - `bounds`: `sw_lat,sw_lng,ne_lat,ne_lng` (область карты) - `cursor`: курсор пагинации - `limit`: 1-100 (по умолчанию 20) - `sort`: `rating` | `created_at` | `distance` (требует `user_lat,user_lng`) **Ответ 200:** ```json { "data": [ { "id": "uuid", "type": "place", "title": "Красивый парк", "cover_image": "https://cdn.../uuid.jpg", "coordinates": { "lat": 55.75, "lng": 37.61 }, "address": "Москва, Парковая 1", "rating": 4.5, "tags": ["nature", "architecture"], "features": ["parking", "public_transport"], "status": "published", "created_at": "2024-01-15T10:30:00Z" } ], "next_cursor": "eyJpZCI6InV1aWQifQ==", "has_more": true } ``` ### GET /places/:id **Ответ 200:** Полная карточка PlaceCard / StudioCard ```json { "id": "uuid", "type": "studio", "title": "Студия 'Свет'", "description": "Профессиональная фотостудия...", "address": "Москва, ул. Ленина 10, офис 5", "coordinates": { "lat": 55.75, "lng": 37.61 }, "cover_image": "https://cdn.../cover.jpg", "images": ["https://cdn.../1.jpg", "https://cdn.../2.jpg"], "access_info": "Метро 'Площадь Революции', 5 мин пешком. Парковка во дворе.", "tags": ["portrait", "fashion", "product"], "features": [ { "id": "flash", "name": "Импульсные источники" }, { "id": "continuous", "name": "Постоянный свет" }, { "id": "cyclorama", "name": "Циклорама" } ], "pricing": { "hourly_rate": 1500, "currency": "RUB", "min_hours": 2 }, "booking_url": "https://booking.photoplaces.ru/studio/uuid", "executor_services": [ { "id": "uuid", "title": "Портретная съемка", "price": 5000, "executor_name": "Иван Фотограф" } ], "rating": 4.8, "reviews_count": 24, "owner": { "id": "uuid", "name": "Студия 'Свет'", "avatar": "..." }, "status": "published", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T14:22:00Z" } ``` ### POST /places **Роли:** `customer` (type=place), `landlord` (type=studio) ```json // Запрос { "type": "studio", "title": "Новая студия", "description": "Описание...", "address": "Москва, ул. Новая 5", "coordinates": { "lat": 55.76, "lng": 37.62 }, "access_info": "Как добраться...", "tags": ["portrait", "fashion"], "features": ["flash", "cyclorama"], "pricing": { "hourly_rate": 2000, "currency": "RUB", "min_hours": 3 } } // Ответ 201 { "id": "uuid", "status": "pending_moderation", "message": "Отправлено на проверку" } ``` ### PATCH /places/:id **Роли:** Владелец, `moderator`, `superadmin` - Редактирование владельцем переводит в статус `pending_moderation` ### DELETE /places/:id **Роли:** Владелец, `moderator`, `superadmin` - Мягкое удаление ### POST /places/:id/moderate **Роли:** `moderator`, `superadmin` ```json // Запрос { "action": "approve", "comment": "Всё хорошо" } // action: approve | reject | request_changes ``` --- ## Эндпоинты услуг исполнителей (Services) ### GET /services **Параметры:** `tags`, `min_rating`, `price_min`, `price_max`, `cursor`, `limit` ### GET /services/:id **Ответ 200:** Полная карточка ServiceCard ### POST /services **Роль:** `executor` ```json { "title": "Портретная съемка в студии", "description": "Профессиональная портретная съемка...", "price": 5000, "currency": "RUB", "duration_minutes": 120, "tags": ["portrait", "studio"], "portfolio_images": ["https://cdn.../1.jpg"] } ``` ### PATCH /services/:id **Роль:** Владелец, `moderator`, `superadmin` ### DELETE /services/:id **Роль:** Владелец, `moderator`, `superadmin` --- ## Эндпоинты пользователей (Users) ### GET /users/:id **Публичный профиль** ### PATCH /users/me **Обновление своего профиля** ### GET /users/me/places **Свои места (все статусы)** ### GET /users/me/services **Свои услуги (для исполнителей)** --- ## Административные эндпоинты ### GET /admin/users **Роли:** `moderator`, `superadmin` **Параметры:** `role`, `status`, `cursor`, `limit` ### PATCH /admin/users/:id **Роли:** `superadmin` (все), `moderator` (кроме superadmin/moderator) ```json { "role": "customer", "status": "active" } // status: active | banned | pending_verification ``` ### GET /admin/moderation/queue **Роли:** `moderator`, `superadmin` **Параметры:** `type`: place | studio | service, `status`: pending | rejected ### POST /admin/moderation/:entityType/:id **Роли:** `moderator`, `superadmin` ```json { "action": "approve", "comment": "..." } ``` --- ## Теги и характеристики (справочные данные) ### GET /tags **Ответ:** Список тегов стилей съёмки ```json [{ "id": "portrait", "name": "Портрет", "category": "style" }, ...] ``` ### GET /features **Ответ:** Список характеристик мест ```json [{ "id": "flash", "name": "Импульсный свет", "category": "equipment" }, ...] ``` --- ## Загрузка файлов ### POST /upload/presigned-url ```json // Запрос { "content_type": "image/jpeg", "max_size_mb": 10 } // Ответ { "upload_url": "https://s3...", "file_url": "https://cdn...", "fields": { "key": "...", "policy": "...", "signature": "..." } } ``` - Прямая загрузка в S3/MinIO с фронтенда - Бэкенд возвращает политику presigned POST --- ## Вебхуки ### CloudPayments Webhook `POST /webhooks/cloudpayments` - Проверка подписи - Обновление статуса подписки/платежа --- ## Лимиты запросов - Авторизация: 10 запросов/мин на IP - Чтение мест: 60 запросов/мин на пользователя - Запись мест: 10 запросов/мин на пользователя - Администрирование: 100 запросов/мин --- ## Пагинация Курсорная: - `cursor` = base64(JSON) ключа сортировки последнего элемента + ID - `limit` макс. 100 - Ответ содержит `next_cursor` и `has_more`