Аутентификация¶
Защищённые эндпоинты (помечены 🔒 в референсе) требуют заголовок:
Без токена и без 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 и 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 первым (если он есть), дальше провайдеры в порядке привязки.
Удаление аккаунта необратимо (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, что и в остальных запросах: без него приложение для сервера новое устройство.