API_CONTRACT.md 8.9 KB

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 <token>
  • Refresh Token: HTTP-only cookie, срок 30 дней, ротация при использовании
  • Роли: superadmin, moderator, landlord, executor, customer

Формат ошибок (RFC 7807)

{
  "type": "https://api.photoplaces.ru/errors/validation-error",
  "title": "Ошибка валидации",
  "status": 422,
  "detail": "Некорректные входные данные",
  "instance": "/api/v1/places",
  "errors": [
    { "field": "title", "message": "Название обязательно" }
  ]
}

Эндпоинты авторизации

POST /auth/register

// Запрос
{
  "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

// Запрос
{
  "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:

{
  "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

{
  "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)

// Запрос
{
  "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

// Запрос
{ "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

{
  "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)

{ "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

{ "action": "approve", "comment": "..." }

Теги и характеристики (справочные данные)

GET /tags

Ответ: Список тегов стилей съёмки

[{ "id": "portrait", "name": "Портрет", "category": "style" }, ...]

GET /features

Ответ: Список характеристик мест

[{ "id": "flash", "name": "Импульсный свет", "category": "equipment" }, ...]

Загрузка файлов

POST /upload/presigned-url

// Запрос
{ "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