# 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**: JWT, срок 30 дней, передаётся в заголовке `X-Refresh-Token` - **Роли**: `superadmin`, `moderator`, `landlord`, `executor`, `customer` ## Формат ошибок ```json { "error": "описание ошибки" } ``` HTTP статусы: 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 409 (Conflict), 422 (Validation Error), 500 (Internal Server Error) --- ## Эндпоинты авторизации ### POST /auth/register ```json // Запрос { "email": "user@example.com", "password": "securePass123", "role": "customer", // customer | landlord | executor "name": "Иван Иванов" } // Ответ 201 { "user": { "id": "uuid", "email": "...", "role": "customer", "created_at": "..." }, "access_token": "eyJ...", "refresh_token": "eyJ..." } ``` ### POST /auth/login ```json // Запрос { "email": "user@example.com", "password": "securePass123" } // Ответ 200 { "user": { ... }, "access_token": "eyJ...", "refresh_token": "eyJ..." } ``` ### POST /auth/refresh - Заголовок: `X-Refresh-Token: ` - Ответ 200: новый access_token + обновлённый refresh_token ### POST /auth/logout - Заголовок: `Authorization: Bearer ` - Ответ 204 ### GET /auth/me - Возвращает профиль текущего пользователя - Ответ 200: объект User --- ## Эндпоинты мест (Places) ### GET /places **Параметры запроса:** - `type`: `place` | `studio` (по умолчанию оба) - `tags`: ID тегов (можно несколько, напр. `?tags=portrait&tags=nature`) - `min_rating`: число 0-5 - `features`: ID характеристик (можно несколько) - `price_min`, `price_max`: для студий - `bounds`: `sw_lat,sw_lng,ne_lat,ne_lng` (область карты) - `limit`: 1-100 (по умолчанию 20) - `sort`: `rating` | `created_at` | `distance` (требует `user_lat,user_lng`) **Ответ 200:** ```json { "data": [ { "id": "uuid", "type": "place", "owner_id": "uuid", "title": "Красивый парк", "description": null, "address": "Москва, Парковая 1", "lat": 55.75, "lng": 37.61, "cover_image": "https://cdn.../uuid.jpg", "access_info": null, "status": "published", "rating": 4.5, "reviews_count": 12, "hourly_rate": null, "currency": "RUB", "min_hours": 0, "tags": [{ "id": "portrait", "name": "Портрет", "sort_order": 1 }], "features": [{ "id": "parking", "name": "Парковка", "sort_order": 1 }], "created_at": "2024-01-15T10:30:00Z" } ] } ``` ### GET /places/:id **Ответ 200:** Полная карточка PlaceCard / StudioCard ```json { "id": "uuid", "type": "studio", "title": "Студия 'Свет'", "description": "Профессиональная фотостудия...", "address": "Москва, ул. Ленина 10, офис 5", "lat": 55.75, "lng": 37.61, "cover_image": "https://cdn.../cover.jpg", "access_info": "Метро 'Площадь Революции', 5 мин пешком. Парковка во дворе.", "tags": [{ "id": "portrait", "name": "Портрет" }], "features": [{ "id": "flash", "name": "Импульсные источники" }], "hourly_rate": 1500, "currency": "RUB", "min_hours": 2, "rating": 4.8, "reviews_count": 24, "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", "lat": 55.76, "lng": 37.62, "access_info": "Как добраться...", "tags": ["portrait", "fashion"], "features": ["flash", "cyclorama"], "hourly_rate": 2000, "currency": "RUB", "min_hours": 3 } // Ответ 201 — полный объект Place в статусе pending_moderation ``` ### PATCH /places/:id **Роли:** Владелец, `moderator`, `superadmin` - Редактирование владельцем переводит в статус `pending_moderation` ### DELETE /places/:id **Роли:** Владелец (свои места), `moderator`, `superadmin` - Проверка: `owner_id` == текущий пользователь || moderator || superadmin - Мягкое удаление (`deleted_at`) ### 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`, `limit` **Ответ 200:** `{ "data": [ Service ] }` ### GET /services/:id **Ответ 200:** Полный объект Service (с тегами) ### POST /services **Роль:** `executor` ```json { "title": "Портретная съемка в студии", "description": "Профессиональная портретная съемка...", "price": 5000, "currency": "RUB", "duration_minutes": 120, "tags": ["portrait", "studio"] } ``` - Ответ 201: полный объект Service со статусом `published` ### PATCH /services/:id **Роль:** Владелец (свои услуги), `moderator`, `superadmin` - Проверка: `executor_id` == текущий пользователь || moderator || superadmin ### DELETE /services/:id **Роль:** Владелец (свои услуги), `moderator`, `superadmin` - Проверка: `executor_id` == текущий пользователь || moderator || superadmin --- ## Эндпоинты пользователей (Users) ### GET /users/:id **Публичный профиль** ### PATCH /users/me **Обновление своего профиля** ```json { "name": "Новое имя", "phone": "+79991234567", "bio": "Фотограф с 10-летним стажем", "avatar_url": "https://cdn.../avatar.jpg", "country": "RU" } ``` --- ## Административные эндпоинты ### GET /admin/users **Роли:** `moderator`, `superadmin` **Параметры:** `role`, `status`, `limit` ### PATCH /admin/users/:id **Роли:** `superadmin` (все), `moderator` (кроме superadmin/moderator) ```json { "role": "customer", "status": "active" } // status: active | banned | pending_verification ``` --- ## Модерация ## Бронирования (Bookings) ### POST /bookings **Роль:** Любой авторизованный пользователь ```json { "place_id": "uuid", "start_time": "2024-02-01T10:00:00Z", "end_time": "2024-02-01T14:00:00Z", "comment": "Нужна циклорама" } ``` - Проверка доступности слота - Расчёт цены на основе `hourly_rate` ### GET /bookings/me **Список своих бронирований** ### PATCH /bookings/:id/cancel **Роль:** Владелец брони - Проверка: `user_id` == текущий пользователь --- ## Отзывы (Reviews) ### GET /reviews ```json // Параметры запроса // ?target_type=place&target_id=uuid // Ответ 200 { "data": [ { "id": "uuid", "user_id": "uuid", "target_type": "place", "target_id": "uuid", "rating": 5, "text": "Отличное место", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "user": { "name": "Иван", "avatar_url": "https://cdn.../avatar.jpg" } } ] } ``` ### POST /reviews **Роль:** Любой авторизованный пользователь ```json { "target_type": "place", "target_id": "uuid", "rating": 5, "text": "Отличное место" } ``` - `rating`: 1-5 - `text`: опционально - Ответ 201: полный объект Review - Ограничение: один отзыв на пользователя на цель (409 Conflict при повторе) --- ## WebSocket — точки посетителей ### GET /ws/visitors - WebSocket-соединение для получения точек других посетителей на карте - Клиент отправляет `{ "user_id": "...", "lat": 55.75, "lng": 37.61 }` - Сервер рассылает всем подключённым клиентам список всех активных точек: ```json { "visitors": [{ "user_id": "...", "lat": 55.75, "lng": 37.61 }] } ``` --- ## Теги и характеристики (справочные данные) ### 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 (`POST /webhooks/cloudpayments`), но пока не реализована. --- ## Лимиты запросов - Авторизация: 10 запросов/мин на IP - Чтение мест: 60 запросов/мин на пользователя - Запись мест: 10 запросов/мин на пользователя - Администрирование: 100 запросов/мин ---