Auth Service — API Contract

Этот документ является единственным источником правды для API Auth Service.

Бэкенд реализует контракт. Фронтенд потребляет контракт. Отклонения запрещены.

Содержание

Public API

Internal API (Service Token)


POST /auth/login

Авторизация сотрудника по email и паролю.

ПараметрЗначение
AuthPublic
Content-Typeapplication/json

Request Body

{
  "email": "string, required — email сотрудника",
  "password": "string, required — пароль"
}

Response 200

{
  "data": {
    "access_token": "string — JWT access token",
    "refresh_token": "string — JWT refresh token",
    "expires_in": "number — TTL access token в секундах",
    "user": {
      "id": "uuid",
      "email": "string",
      "first_name": "string",
      "last_name": "string",
      "franchise": {
        "id": "uuid",
        "type": "corporate | individual"
      },
      "scope": {
        "type": "all_franchise | legal_entity_ids | store_ids",
        "legal_entity_ids": ["uuid"],
        "store_ids": ["uuid"]
      },
      "role_ids": ["uuid"],
      "permissions": ["string"]
    }
  }
}

(Расширено в BR 1.4.3: поля role_ids и permissions — permissions-слой для фронтенда.)

(Обновлено в BR 1.4.4: удалены поля role, store_ids, legal_entity_id. Добавлены franchise: { id, type } и scope: { type, legal_entity_ids?, store_ids? }. Scope вычисляется по правилам Ролевая модель: владельцы получают all_franchise или legal_entity_ids; обычные сотрудники — store_ids из агрегата permissions-ролей. Поля legal_entity_ids / store_ids присутствуют только при соответствующем type.)

Errors

CodeHTTPКогда
INVALID_CREDENTIALS401Неверный email или пароль
ACCOUNT_DISABLED403Сотрудник деактивирован
ACCOUNT_LOCKED429Превышен лимит попыток (5), аккаунт заблокирован на 15 мин
VALIDATION_ERROR400Невалидный формат email
NO_BACKOFFICE_ACCESS403В permissions нет ни одного backoffice *.read — только POS-операции. Сотрудник может работать только на POS (BR 1.5)

POST /auth/pin-login

Авторизация кассира на POS-терминале по PIN-коду. PIN уникален в рамках ТТ.

ПараметрЗначение
AuthPublic
Content-Typeapplication/json

Request Body

{
  "pin": "string, required — 4-значный PIN",
  "store_id": "uuid, required — ID торговой точки",
  "employee_id": "uuid, optional — ID сотрудника. Требуется при коллизии PIN (несколько сотрудников с одним PIN в одной ТТ) для явного выбора"
}

Response 200

Та же структура что и у /auth/login (access_token + refresh_token + user).

BR 5.1 v2: kitchen_station_id для KDS

При вызове из KDS-клиента (с X-Device-Id) pos-bff дополнительно проксирует поле kitchen_station_id (uuid | null) в response — это цех, к которому привязано устройство в kds_devices. Клиент использует, чтобы пропустить экран выбора станций после PIN. Auth Service сам это поле не возвращает; добавляется pos-bff’ом по результатам внутреннего вызова GET /internal/kds-devices/validate в User Service.

Errors

CodeHTTPКогда
INVALID_PIN401Неверный PIN, нет сотрудника с таким PIN в ТТ, или PIN не назначен сотруднику
PIN_COLLISION409В одной ТТ несколько сотрудников с совпадающим PIN. Клиент должен повторить запрос, передав employee_id
ACCOUNT_DISABLED403Сотрудник деактивирован
ACCOUNT_LOCKED429Превышен лимит попыток
POS_ACCESS_DENIED403В permissions сотрудника нет pos.access (BR 1.4.4)
DEVICE_NOT_REGISTERED403(добавляется pos-bff’ом до вызова этого endpoint’а) KDS-клиент прислал X-Device-Id, но в kds_devices нет соответствия (device_id, store_id) или запись revoked. Клиент чистит localStorage и заново проходит wizard регистрации
VALIDATION_ERROR400PIN не 4 цифры / store_id невалиден

Логика POS_ACCESS_DENIED

Проверяется что в permissions сотрудника (по набору назначенных ему permissions-ролей) присутствует ключ pos.access. Без него вход на POS невозможен даже при валидном PIN.

Логика DEVICE_NOT_REGISTERED (BR 5.1 — KDS device validation)

Проверка выполняется в pos-bff до вызова Auth Service: если в headers пришёл X-Device-Id, pos-bff дёргает User Service GET /internal/kds-devices/validate?device_id=X&store_id=Y. При valid=false сразу 403, Auth Service не вызывается. Без X-Device-Id (обычный POS-кассир) проверка пропускается — поведение не меняется.


POST /auth/refresh

Обновление access token по refresh token.

ПараметрЗначение
AuthRefresh token в body
Content-Typeapplication/json

Request Body

{
  "refresh_token": "string, required"
}

Response 200

{
  "data": {
    "access_token": "string — новый JWT access token",
    "refresh_token": "string — новый refresh token (rotation)",
    "expires_in": "number"
  }
}

Errors

CodeHTTPКогда
INVALID_REFRESH_TOKEN401Токен невалиден или истёк
TOKEN_REVOKED401Токен был отозван (logout)

POST /auth/logout

Выход — инвалидация текущего refresh token.

ПараметрЗначение
AuthBearer JWT
Content-Typeapplication/json

Request Body

{
  "refresh_token": "string, required"
}

Response 204

Нет тела ответа.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT

POST /auth/forgot-password

Запрос сброса пароля. Отправляет email со ссылкой через Resend.

ПараметрЗначение
AuthPublic
Content-Typeapplication/json

Request Body

{
  "email": "string, required"
}

Response 200

{
  "data": {
    "message": "Если аккаунт существует, на email отправлена ссылка для сброса пароля"
  }
}

Всегда 200 — не раскрываем существует ли email в системе.

Errors

CodeHTTPКогда
VALIDATION_ERROR400Невалидный формат email

POST /auth/reset-password

Установка нового пароля по токену из email-ссылки.

ПараметрЗначение
AuthPublic
Content-Typeapplication/json

Request Body

{
  "token": "string, required — токен из ссылки",
  "password": "string, required — новый пароль",
  "password_confirmation": "string, required — подтверждение"
}

Response 200

{
  "data": {
    "message": "Пароль успешно изменён"
  }
}

Errors

CodeHTTPКогда
VALIDATION_ERROR400Пароли не совпадают или слишком короткий
INVALID_RESET_TOKEN422Токен невалиден или истёк (TTL = 1 час)
TOKEN_ALREADY_USED422Токен уже использован

POST /internal/auth/validate

Internal endpoint — только для service-to-service вызовов

Валидация JWT-токена. Используется API Gateway и BFF для проверки запросов.

ПараметрЗначение
AuthService token (X-Service-Token header)
Content-Typeapplication/json

Request Body

{
  "token": "string, required — JWT access token"
}

Response 200

{
  "data": {
    "valid": true,
    "user_id": "uuid",
    "franchise_id": "uuid",
    "role_ids": ["uuid"],
    "permissions": ["string — agg granted=true из всех ролей"],
    "scope": {
      "type": "all_franchise | legal_entity_ids | store_ids",
      "legal_entity_ids": ["uuid"],
      "store_ids": ["uuid"]
    }
  }
}

(Расширено в BR 1.4.3: поля role_ids и permissions. Значения берутся из Redis-кэша user_permissions:{user_id} с TTL 60 сек; при промахе — fetch через GET /internal/users/{user_id}/permissions в User Service.)

(Обновлено в BR 1.4.4: удалены role, store_ids, legal_entity_id. Добавлено поле scope — берётся из Redis-кэша user_scope:{user_id} (TTL 60 сек), при промахе — fetch через GET /internal/users/{user_id}/scope в User Service. Все downstream-сервисы используют scope и permissions для авторизации; enum role больше не передаётся.)

Response 200 (невалидный токен)

{
  "data": {
    "valid": false,
    "reason": "expired | invalid_signature | revoked"
  }
}

GET /auth/me

(Введено в BR 1.4.3)

Полный профиль авторизованного сотрудника с актуальным списком ролей и permissions. Фронт вызывает при старте сессии и/или после логина для получения permissions-каталога.

ПараметрЗначение
AuthBearer JWT

Response 200

{
  "data": {
    "user": {
      "id": "uuid",
      "email": "string",
      "first_name": "string",
      "last_name": "string",
      "franchise": {
        "id": "uuid",
        "type": "corporate | individual"
      },
      "scope": {
        "type": "all_franchise | legal_entity_ids | store_ids",
        "legal_entity_ids": ["uuid"],
        "store_ids": ["uuid"]
      }
    },
    "roles": [
      {
        "id": "uuid",
        "name": "string",
        "is_system": "boolean",
        "store_ids": ["uuid — где эта роль действует у данного сотрудника"]
      }
    ],
    "permissions": ["string — агрегат granted=true permission-ключей"]
  }
}

(Обновлено в BR 1.4.4: удалены role, store_ids, legal_entity_id. Добавлены franchise и scope — аналогично /auth/login. franchise.type управляет видимостью раздела «Юр. лица» во фронте.)

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена или невалидный

JWT Payload

Формат JWT access token, единый для всех сервисов:

{
  "sub": "uuid — user_id",
  "franchise_id": "uuid",
  "role_ids": ["uuid"],
  "iat": 1234567890,
  "exp": 1234567890
}

(Обновлено в BR 1.4.3: добавлено поле role_ids — список permissions-ролей. Permissions в JWT НЕ кладутся — только role_ids. Полный список прав — через /auth/me или /internal/auth/validate.)

(Упрощено в BR 1.4.4: удалены поля role (enum), store_ids, legal_entity_id. Scope считается на бэке Auth Service по правилам Ролевая модель с Redis-кэшем user_scope:{user_id} и отдаётся через /auth/me и /internal/auth/validate.)

Общий формат ошибок

{
  "error": {
    "code": "string — машиночитаемый код (UPPER_SNAKE_CASE)",
    "message": "string — человекочитаемое описание",
    "details": [
      {
        "field": "string — путь к полю",
        "message": "string — описание ошибки"
      }
    ]
  }
}