Перейти к содержанию

Аутентификация

Защищённые эндпоинты (помечены 🔒 в референсе) требуют заголовок:

Authorization: Bearer <access_token>

Без токена и без OAuth доступны только каталог и сам раздел auth.

Модель токенов

Вход выдаёт пару токенов:

Токен Назначение Срок жизни
access_token кладётся в Authorization к каждому защищённому запросу ~15 минут
refresh_token только для обновления пары; больше нигде не шлётся ~30 дней

Поле expires_in в ответе — срок access в секундах. Refresh ротируется: при обновлении старый refresh гасится и выдаётся новый. Опциональный device_id привязывает сессию к устройству (выход на одном устройстве не рушит остальные).

Храни токены безопасно

refresh_token = долгий доступ к аккаунту. На мобильном — Keychain (iOS) / Keystore (Android), не в UserDefaults/SharedPreferences открытым текстом.

Гостевой аккаунт

Пользователь генерирует до регистрации. При первом запуске приложение получает гостевой аккаунт со стартовыми кредитами — с этого момента у него есть настоящая пара токенов, и почти весь API работает как у зарегистрированного.

# device_id обязателен; ref_code необязателен → 200 TokenResponse
curl -X POST "$BASE_URL/v1/auth/anonymous" -H 'Content-Type: application/json' \
  -d '{"device_id":"<стабильный id устройства>"}'

В ответе — обычный TokenResponse, в котором user.is_anonymous: true, а email, name пустые и providers — пустой список. Баланс приходит в user.balance: показывай его, а не зашитое в приложение число (сейчас это 100 кредитов, хватает на несколько дешёвых эффектов).

device_id — это ключ от аккаунта

Предъявив его, любой получает токены гостя вместе с его балансом и сгенерированными фотографиями. Это должен быть случайный идентификатор, который приложение сгенерировало само и хранит в Keychain (iOS) / Keystore (Android) — не идентификатор от ОС, не что-либо предсказуемое. Тот же device_id передавай в register, login, refresh и OAuth: по нему сервер узнаёт устройство.

Ситуация Ответ
первый вызов с этим device_id 200, новый гость, начислены стартовые кредиты
повторный вызов 200, тот же аккаунт и баланс, новая пара токенов; кредиты не начисляются снова
устройство уже принадлежит полноценному аккаунту 403 forbidden — гостя на нём больше не создать

Стартовые кредиты даются один раз на устройство за всю его жизнь: переустановка приложения и даже DELETE /v1/me не выдадут их второй раз.

Превращение гостя в полноценный аккаунт

Регистрация и вход через провайдера принимают необязательный Authorization с токеном гостя. Тогда новый аккаунт не создаётся: полноценным становится тот же user.id.

# Тот же /register, но с токеном гостя в заголовке → 201, тот же user.id
curl -X POST "$BASE_URL/v1/auth/register" -H "Authorization: Bearer $GUEST_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"email":"u@example.com","password":"s3cret99"}'

Работает для register, google, apple, vk и yandex. Баланс, генерации и загрузки остаются на месте, is_anonymous становится false, приветственные кредиты начисляются сверх гостевых. Если гость приходил по ref_code, бонусы обеим сторонам начисляются именно здесь, в момент апгрейда.

Два случая, которые стоит обработать в приложении:

  • Почта уже занята → 409 email_taken. Гость не тронут: он остаётся гостем со своим балансом и продолжает работать по тем же токенам. Предложи войти в существующий аккаунт.
  • Вход в аккаунт, который уже существует (та же почта у провайдера) → 200 с токенами того аккаунта. Гостевые баланс и работы туда не переносятся. Предупреди об этом до входа, иначе пользователь решит, что его фотографии пропали.

Что гостю закрыто

Оплата и всё, что требует настоящего аккаунта, отвечают 403 registration_required — отправляй пользователя на экран регистрации:

Закрыто Открыто
POST /v1/payments, POST /v1/subscriptions и остальные операции с платежами и подписками витрины: GET /v1/payments/products, GET /v1/subscriptions/plans
весь раздел /v1/user-effects (маркетплейс) каталог, загрузки, генерации, кредиты, /v1/devices
POST /v1/auth/change-password профиль /v1/me, включая удаление аккаунта
POST /v1/effects/{id}/complaints — жалоба действует на чужой эффект, а гостевой аккаунт заводится одним запросом просмотр каталога целиком
GET /v1/referral — свой код и статистика: зарабатывать на приглашениях, сам не зарегистрировавшись, нельзя приход по чужому коду, см. ниже

Реферальная ссылка на госте работает

Закрыта только выдача СВОЕГО кода. Прийти по чужому гость может, и убирать реферальные ссылки из онбординга не нужно.

Основное место для кода — регистрация, в том числе регистрация гостя: приложение заводит гостя при первом запуске, когда кода ещё неоткуда взять, а человек вводит его позже. Гость, созданный без кода, передаёт ref_code в register (или во вход через провайдера) со своим токеном в заголовке — привязка и бонусы случаются там.

ref_code в POST /v1/auth/anonymous — для случая поуже: приложение открыли по ссылке-приглашению и код известен сразу. Бонусы и тогда платятся не сразу, а при превращении гостя в полноценный аккаунт. Подробности — Реферальная программа.

registration_required — это не повод обновлять токен

Код отличается от auth_required намеренно: токен гостя валиден, и обновление пары ничего не изменит. Обработчик 401 → refresh → повтор на этот ответ срабатывать не должен, иначе клиент зациклится.

Витрина открыта специально: чтобы захотеть купить, цены надо сначала увидеть. Отказ приходит на попытке заплатить.

Срок жизни брошенного гостя

Гость, в который не заходили больше 30 дней, удаляется вместе со своими фотографиями и результатами генераций. Отсчёт идёт от последнего входа, так что гость, который пользуется приложением, не удаляется никогда. После уборки устройство помнит, что стартовые кредиты уже выдавались: новый гость на нём начнётся с нулевым балансом.

Вход

Все варианты возвращают одинаковый объект TokenResponse (access_token, refresh_token, expires_in, user). Полные тела — в референсе.

# Регистрация → 201, начисляет приветственные кредиты
curl -X POST "$BASE_URL/v1/auth/register" -H 'Content-Type: application/json' \
  -d '{"email":"u@example.com","password":"s3cret99","name":"User"}'

# Вход → 200
curl -X POST "$BASE_URL/v1/auth/login" -H 'Content-Type: application/json' \
  -d '{"email":"u@example.com","password":"s3cret99"}'

Правила регистрации

  • Пароль — от 8 до 128 символов. Короче → 422 validation_error (упавшее поле password приходит в details).
  • Email уже зарегистрирован → 409 email_taken. Это не фатальная ошибка: предложи вход вместо регистрации. Полный список кодов — Ошибки.
# id_token получает мобильный SDK Google на устройстве
curl -X POST "$BASE_URL/v1/auth/google" -H 'Content-Type: application/json' \
  -d '{"id_token":"<google_id_token>"}'
# id_token и authorization_code - из одного ответа Sign in with Apple
curl -X POST "$BASE_URL/v1/auth/apple" -H 'Content-Type: application/json' \
  -d '{"id_token":"<apple_id_token>","authorization_code":"<apple_authorization_code>"}'

authorization_code передавай при каждом входе: по нему сервер получает у Apple токен и отзывает его, когда пользователь удаляет аккаунт, как требует App Store. Поле необязательное, вход работает и без него, но тогда отзывать при удалении нечего.

# access_token выдаёт VK ID SDK на устройстве
curl -X POST "$BASE_URL/v1/auth/vk" -H 'Content-Type: application/json' \
  -d '{"access_token":"<vk_access_token>","oauth_provider":"ok"}'

Один роут на все три кнопки VK ID: ВКонтакте, Одноклассники и Mail. В oauth_provider передай, какой кнопкой вошёл пользователь: vk, ok или mail. Поле необязательное, без него считается vk. На сам вход оно не влияет, только на providers в профиле: там показывается кнопка последнего входа.

# access_token (OAuth-токен) выдаёт Яндекс LoginSDK на устройстве
curl -X POST "$BASE_URL/v1/auth/yandex" -H 'Content-Type: application/json' \
  -d '{"access_token":"<yandex_oauth_token>"}'

Токен должен быть выдан приложению Mivio. Токен другого Яндекс-приложения отклоняется с 401 invalid_credentials.

Google и Apple присылают id_token, VK ID и Яндекс ID присылают access_token: сервер проверяет его запросом к провайдеру, поэтому вход через них на одну сетевую поездку дольше. Новому аккаунту из VK ID и Яндекс ID сразу проставляется имя из профиля провайдера. У существующего аккаунта имя не меняется.

При входе можно передать ref_code (реферальный код пригласившего) — в register, google, apple, vk, yandex.

Ошибки входа через провайдера

Одинаковы для google, apple, vk и yandex:

HTTP code Когда Что делать клиенту
401 invalid_credentials провайдер не принял токен: протух, отозван, выдан другому приложению показать «не удалось войти», дать повторить
503 provider_unavailable вход через этого провайдера не настроен на сервере, или провайдер не ответил (сбой, таймаут около 4 секунд) сказать, что способ временно недоступен; остальные кнопки входа оставить активными
422 validation_error пустой или недопустимый по формату токен (access_token - печатные ASCII-символы без пробелов, до 4096), неизвестное значение oauth_provider ошибка в запросе, повторять бессмысленно
429 rate_limited превышен лимит на операцию (см. Лимиты) подождать Retry-After секунд

Успешный вход через провайдера отвечает 200, в том числе когда аккаунт создаётся впервые. 201 отдаёт только register.

Привязка OAuth к существующему email-аккаунту

flowchart TD
    A[OAuth-логин: provider + subject] --> B{identity\nуже есть?}
    B -- да --> C[получить пользователя → выдать токены]
    B -- нет --> D{подтверждённый email\nпровайдера есть в БД?}
    D -- да --> E[привязать identity к существующему аккаунту\nбез welcome-кредитов и реферала]
    D -- нет --> F[создать нового пользователя]
    F --> G[начислить welcome-кредиты + реферальный бонус]
    G --> H[выдать токены]
    E --> H

Привязка вместо дублирования

Вход через Google с email, который уже зарегистрирован email+пароль, не создаёт второй аккаунт — Google-identity линкуется к существующему. Пользователь может потом входить обоими способами. Welcome-кредиты при этом не начисляются повторно.

Привязка по почте срабатывает, только если провайдер ручается, что почта подтверждена:

Провайдер Привязка к аккаунту с той же почтой
Google, Apple да, если почта подтверждена (email_verified)
Яндекс ID да, по основной почте Яндекс-аккаунта
VK ID пока нет: VK не сообщает, подтверждена ли почта

Если привязки не произошло, создаётся отдельный аккаунт. При входе через VK ID он создаётся без почты (email: null), даже если VK её прислал: адрес с непроверенной почтой не должен открывать чужой аккаунт.

Обновление токена: паттерн 401 → refresh → повтор

Access живёт недолго. Клиент не должен угадывать срок — лови 401 auth_required и обновляйся:

запрос с access ──▶ 401 auth_required
                       │
                       ▼
   POST /v1/auth/refresh {refresh_token}
                       │
       ┌───────────────┴───────────────┐
       ▼                               ▼
   200: новая пара                 401: refresh истёк/отозван
   повторить исходный запрос       → разлогинить, показать вход
curl -X POST "$BASE_URL/v1/auth/refresh" -H 'Content-Type: application/json' \
  -d '{"refresh_token":"<refresh>"}'

Один refresh за раз

Из-за ротации не запускай несколько параллельных refresh с одним токеном — первый погасит остальные. Сериализуй обновление (один общий «refresh in flight») и пускай ждущие запросы по готовой новой паре.

Защита от перехвата refresh-токена (reuse detection)

При ротации старый refresh помечается как использованный (superseded). Если перехваченный ротированный токен предъявить повторно — система обнаруживает кражу и отзывает все refresh-токены пользователя разом:

sequenceDiagram
    participant C as Клиент
    participant A as API
    participant X as Злоумышленник

    C->>A: POST /auth/refresh {refresh_A}
    A-->>C: 200 {refresh_B, access_B}  (refresh_A → superseded)

    X->>A: POST /auth/refresh {refresh_A}  ← перехваченный старый
    A->>A: refresh_A.superseded=True → отозвать ВСЕ токены пользователя
    A-->>X: 401 auth_required

    C->>A: следующий запрос с access_B
    A-->>C: 401 auth_required (refresh_B тоже отозван)
    Note over C: необходим повторный вход

Полный разлогин при обнаружении кражи

Если внезапно все устройства разлогинились без действия пользователя — возможно, refresh-токен был перехвачен. Попроси пользователя войти заново и сменить пароль.

Выход

Logout — защищённый (🔒): нужен access-токен, гасится только refresh текущего пользователя.

# Гасит переданный refresh → 204. Access доживёт свой короткий срок сам.
curl -X POST "$BASE_URL/v1/auth/logout" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"refresh_token":"<refresh>"}'

Смена пароля

# Требует access-токен. Email-аккаунту нужен текущий пароль; OAuth-аккаунт с почтой
# (ещё без пароля) может задать его впервые, опустив current_password.
curl -X POST "$BASE_URL/v1/auth/change-password" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"current_password":"s3cret","new_password":"n3w-s3cret"}'

Смена пароля разлогинивает все другие устройства: все refresh-токены гасятся, а в ответе (TokenResponse) приходит свежая пара для текущего устройства — замени ею сохранённую. Минимальная длина нового пароля — 8 символов; опциональный device_id привязывает новую сессию к устройству.

HTTP code Когда
401 invalid_credentials у аккаунта уже есть пароль, а current_password не передан или неверный
403 forbidden у аккаунта нет почты (email: null, например вход через VK ID): вход по паролю ищет аккаунт по почте, поэтому пароль ему задать нельзя. Ничего не меняется, сессии не гасятся
403 registration_required запрос от гостя: пароль задаётся при регистрации, отдельной сменой он гостю не нужен

Задать пароль может только аккаунт с почтой. Кнопку «задать пароль» стоит показывать по user.email, а не по отсутствию email в providers.

Текущий пользователь и профиль

Раздел profile работает поверх access-токена:

curl "$BASE_URL/v1/me" -H "Authorization: Bearer $TOKEN"          # профиль + баланс
curl -X PATCH "$BASE_URL/v1/me" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"name":"Новое имя","locale":"en"}'
curl -X DELETE "$BASE_URL/v1/me" -H "Authorization: Bearer $TOKEN"  # удалить аккаунт → 204

GET /v1/me возвращает и balance — отдельный запрос к кредитам для общего профиля не нужен. PATCH /v1/me меняет name и locale; locale должен быть из поддерживаемых (ru, en — регион отбрасывается, en-US → en), иначе 422 validation_error.

Поле is_anonymous говорит, что перед нами гость: у такого аккаунта нет ни почты, ни способов входа, и часть разделов ему закрыта. У зарегистрированного оно false.

Поле providers перечисляет способы входа в аккаунт. Оно приходит и в /v1/me, и в user внутри TokenResponse:

Значение Что значит
email в аккаунт можно войти по почте и паролю: у него есть и email, и пароль
google, apple, yandex привязан этот провайдер
vk, ok, mail привязан VK ID; значение показывает кнопку последнего входа через VK ID

Порядок: email первым (если он есть), дальше провайдеры в порядке привязки.

"providers": ["email", "yandex"]

Удаление аккаунта необратимо (GDPR / требования сторов): identities, refresh-токены, генерации и кредиты удаляются каскадом, а сами файлы пользователя (загруженные фото, результаты генераций, аватар) стираются из объектного хранилища. Если пользователь входил через Apple и приложение передало authorization_code, доступ приложения к его Apple ID отзывается: в настройках Apple ID приложение пропадает из списка, а следующий вход через Apple снова спросит согласие. Отзыв идёт в фоне, ответ 204 его не ждёт.

Аватар

Загрузка — тем же двухшаговым presigned-потоком, что и фото генерации (байты летят прямо в хранилище, не через API). Бакет assets — публичный на чтение:

# 1. запросить presigned POST
curl -X POST "$BASE_URL/v1/me/avatar/upload-url" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"content_type":"image/jpeg"}'
# → {"url":"...","fields":{...},"object_key":"avatars/<uid>/<uuid>.jpg","expires_in":600}

# 2. залить файл: multipart-POST на url — все fields как есть, файл полем file ПОСЛЕДНИМ
#    (правила формы те же, что у загрузки фото — см. гайд «Загрузка фото»)
curl -X POST "<url>" -F 'Content-Type=<из fields>' -F 'key=<из fields>' \
  -F 'policy=<из fields>' -F 'x-amz-algorithm=<из fields>' -F 'x-amz-credential=<из fields>' \
  -F 'x-amz-date=<из fields>' -F 'x-amz-signature=<из fields>' -F 'file=@avatar.jpg'

# 3. привязать объект к профилю → вернёт обновлённый профиль с avatar_url
curl -X PUT "$BASE_URL/v1/me/avatar" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"object_key":"avatars/<uid>/<uuid>.jpg"}'

# удалить аватар → профиль с "avatar_url": null
curl -X DELETE "$BASE_URL/v1/me/avatar" -H "Authorization: Bearer $TOKEN"

avatar_url входит в объект user во всех ответах профиля и в TokenResponse (null → клиент рисует плейсхолдер из инициалов). Форматы: JPEG / PNG / WebP. Привязать можно только ключ под собственным префиксом avatars/<uid>/ — чужой объект отклоняется (403 forbidden).

Лимиты

Чувствительные операции (register, login, refresh, change-password, google, apple, vk, yandex) защищены rate-limit'ом (token bucket, отдельный лимит на операцию). При превышении — 429 rate_limited; выжди и повтори с экспоненциальной задержкой.

Если пароль к аккаунту подбирают с многих адресов, login на время (до часа) перестаёт принимать пароль с новых устройств и отвечает тем же 429 rate_limited. Устройство, с которого в этот аккаунт уже входили, продолжает входить, поэтому передавай в login тот же device_id, что и в остальных запросах: без него приложение для сервера новое устройство.