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

Client Guide

Руководство по интеграции с REST API EFKO Kernel для веб и мобильных клиентов.

Полная документация API доступна в Swagger: GET /api/swagger или GET /api/swagger/json.

Общие сведения

  • Base URL: http://localhost:3000/api (dev) или ваш production URL
  • Формат данных: JSON (Content-Type: application/json)
  • Аутентификация: Bearer JWT в заголовке Authorization
  • Корреляция: requestId проставляется автоматически через middleware; можно передать вручную заголовком x-request-id
  • Язык ответов: Русский (сообщения об ошибках)

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

Схема токенов

Система использует двухтокенную схему:

Токен Где хранится (Web) Где хранится (Mobile) TTL Назначение
accessToken Память приложения Память приложения 15 минут (по умолчанию) Bearer в Authorization
refreshToken httpOnly cookie Secure storage (Keychain/Keystore) 7 дней (по умолчанию) Обновление сессии

Access token передаётся в каждом защищённом запросе:

Authorization: Bearer <accessToken>

Жизненный цикл сессии

register / login
    └─> получить accessToken + refreshToken
    использовать accessToken для запросов
           ▼ (accessToken истёк → 401)
    POST /auth/refresh-session
    новый accessToken + ротация refreshToken
           ▼ (выход)
    POST /auth/logout

Веб клиенты (Browser)

Особенности

  • Используют cookies для refresh токена
  • Требуют CSRF защиту (Double Submit Cookie)
  • Доступен XSRF-TOKEN cookie (не httpOnly)

Хранение токенов

// Храните accessToken в памяти (не в localStorage)
let accessToken: string | null = null;

// Refresh token хранится в httpOnly cookie (автоматически)
// Не храните в localStorage для защиты от XSS

Чтение CSRF токена

function getCsrfToken(): string {
  return document.cookie
    .split('; ')
    .find(row => row.startsWith('XSRF-TOKEN='))
    ?.split('=')[1] ?? '';
}

Пример: Вход в систему

async function login(email: string, password: string) {
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  });

  const data = await response.json();

  // Сохраняем accessToken в памяти
  accessToken = data.accessToken;

  // Cookies (refreshToken и XSRF-TOKEN) устанавливаются автоматически
  return data;
}

Пример: Защищенный запрос

async function createDepartment(name: string, code: string) {
  const response = await fetch('/api/personnel/departments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,
      'X-CSRF-Token': getCsrfToken(), // Обязательно для POST/PATCH/DELETE
    },
    body: JSON.stringify({ name, code, type: 'DIVISION' }),
  });

  if (response.status === 401) {
    // Попробовать обновить сессию
    await refreshSession();
    // Повторить запрос
    return createDepartment(name, code);
  }

  return response.json();
}

Пример: Обновление сессии

async function refreshSession() {
  const response = await fetch('/api/auth/refresh-session', {
    method: 'POST',
    headers: {
      'X-CSRF-Token': getCsrfToken(), // Обязательно
    },
    // Cookies отправляются автоматически
  });

  if (!response.ok) {
    // Редирект на логин
    window.location.href = '/login';
    return;
  }

  const data = await response.json();
  accessToken = data.accessToken;
  // Cookies ротируются автоматически
}

Пример: Выход из системы

async function logout() {
  await fetch('/api/auth/logout', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'X-CSRF-Token': getCsrfToken(),
    },
  });

  accessToken = null;
  // Cookies очищаются автоматически
  window.location.href = '/login';
}

Обработка ошибок

async function apiRequest(url: string, options: RequestInit = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,
      ...options.headers,
    },
  });

  if (response.status === 401) {
    // Попробовать refresh
    await refreshSession();
    // Повторить запрос
    return apiRequest(url, options);
  }

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Request failed');
  }

  return response.json();
}

Мобильные клиенты (Mobile)

Особенности

  • Не используют cookies
  • CSRF проверка автоматически пропускается
  • Refresh token хранится в защищенном хранилище
  • Refresh token передается в теле запроса

Хранение токенов

// iOS (Swift)
// Храните accessToken в памяти
var accessToken: String?

// Храните refreshToken в Keychain
KeychainHelper.set("refreshToken", value: refreshToken)

// Android (Kotlin)
// Храните accessToken в памяти
var accessToken: String? = null

// Храните refreshToken в EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(...)
sharedPreferences.edit().putString("refreshToken", refreshToken).apply()

Пример: Вход в систему (React Native)

import SecureStore from 'expo-secure-store';

async function login(email: string, password: string) {
  const response = await fetch(API_URL + '/auth/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  });

  const data = await response.json();

  // Сохраняем accessToken в памяти
  accessToken = data.accessToken;

  // Сохраняем refreshToken в SecureStore
  await SecureStore.setItemAsync('refreshToken', data.refreshToken);

  return data;
}

Пример: Защищенный запрос (React Native)

async function createDepartment(name: string, code: string) {
  const response = await fetch(API_URL + '/personnel/departments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,
      // X-CSRF-Token НЕ нужен для мобильных клиентов
    },
    body: JSON.stringify({ name, code, type: 'DIVISION' }),
  });

  if (response.status === 401) {
    // Попробовать обновить сессию
    await refreshSession();
    return createDepartment(name, code);
  }

  return response.json();
}

Пример: Обновление сессии (React Native)

async function refreshSession() {
  const refreshToken = await SecureStore.getItemAsync('refreshToken');

  if (!refreshToken) {
    // Редирект на логин
    navigation.navigate('Login');
    return;
  }

  const response = await fetch(API_URL + '/auth/refresh-session', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ refreshToken }), // Refresh токен в теле запроса
  });

  if (!response.ok) {
    // Удалить refresh токен и редирект на логин
    await SecureStore.deleteItemAsync('refreshToken');
    navigation.navigate('Login');
    return;
  }

  const data = await response.json();
  accessToken = data.accessToken;

  // Сохранить новый refresh токен (rotation)
  await SecureStore.setItemAsync('refreshToken', data.refreshToken);
}

Пример: Выход из системы (React Native)

async function logout() {
  const refreshToken = await SecureStore.getItemAsync('refreshToken');

  await fetch(API_URL + '/auth/logout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,
    },
    body: JSON.stringify({ userId, refreshToken }),
  });

  accessToken = null;
  await SecureStore.deleteItemAsync('refreshToken');
  navigation.navigate('Login');
}

Ключевые различия Web vs Mobile

Аспект Web (Браузер) Mobile (Приложение)
Refresh Token httpOnly cookie Secure storage (Keychain/Keystore/SecureStore)
CSRF Token Требуется (X-CSRF-Token header) Не требуется (автоматический пропуск)
Refresh Session Cookies отправляются автоматически Refresh токен в теле запроса
Хранение Access Token Память приложения Память приложения
Logout Cookies очищаются автоматически Ручное удаление из secure storage

Эндпоинты аутентификации

POST /auth/register

Регистрация нового пользователя.

  • Аутентификация: не требуется
  • CSRF: требуется для браузера
  • Rate limit: отдельный (строгий)
  • Тело: { email, password, fullName, role? }
  • Ответ: { id, email, fullName, role, isActive, employeeId }

POST /auth/login

Вход в систему.

  • Аутентификация: не требуется
  • CSRF: требуется для браузера
  • Rate limit: отдельный (строгий)
  • Тело: { email, password }
  • Ответ: { accessToken, refreshToken }
  • Побочный эффект (Web): устанавливает cookies refreshToken и XSRF-TOKEN

POST /auth/refresh-session

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

  • Аутентификация: refresh token из httpOnly cookie refreshToken
  • CSRF: требуется для браузера (заголовок X-CSRF-Token)
  • Ответ: { accessToken, refreshToken }
  • Побочный эффект: ротирует refreshToken cookie и XSRF-TOKEN

GET /auth/me

Получить профиль текущего пользователя.

  • Аутентификация: Bearer accessToken
  • Ответ: { id, email, fullName, role, isActive, employeeId }

POST /auth/logout

Завершение сессии.

  • Аутентификация: Bearer accessToken
  • CSRF: требуется для браузера
  • Ответ: { success: true }
  • Побочный эффект (Web): очищает cookies refreshToken и XSRF-TOKEN

Ролевая модель

Роли

Роль Описание
ADMIN Полный доступ, управление пользователями
MANAGER Управление персоналом и производством
SHIFT_MANAGER Операции со сменами и выпуском
ANALYST Доступ на чтение к аналитическим данным
EMPLOYEE Базовый доступ (только собственные данные)

Роль пользователя возвращается в GET /auth/me и в payload JWT.

Проверка авторизации

При недостаточной роли → 403 Forbidden.
При отсутствии или невалидном токене → 401 Unauthorized.


Personnel API

Маршруты защищены — требуемые роли зависят от операции (чтение: ANALYST+, запись: MANAGER+).

Ресурс Маршруты
Подразделения GET/POST /personnel/departments, PATCH /personnel/departments/:id
Должности GET/POST /personnel/positions, PATCH /personnel/positions/:id
Сотрудники GET/POST /personnel/employees, PATCH /personnel/employees/:id, POST /personnel/employees/:id/terminate
Шаблоны смен GET/POST /personnel/shift-templates, PATCH /personnel/shift-templates/:id

Production API

Маршруты защищены — требуемые роли зависят от операции.

Ресурс Маршруты
Продукты GET/POST /production/products
Производственные заказы GET/POST /production/orders, GET /production/orders/:id, PATCH /production/orders/:id/status
Выпуск продукции GET/POST /production/output
Продажи GET/POST /production/sales, GET /production/sales/summary
Складские остатки GET/POST /production/inventory
Контроль качества GET/POST /production/quality
Показания датчиков GET/POST /production/sensors
KPI GET /production/kpi

ETL API

Требуется роль ADMIN.

Метод Путь Описание
POST /etl/import Запустить импорт (JSON payload)
POST /etl/import/file Загрузить файл (multipart, макс. 20 MB)
GET /etl/imports Список импортов
GET /etl/imports/:id Детали импорта
GET /etl/imports/:id/file Скачать исходный файл
POST /etl/imports/:id/retry Повторить неудавшийся импорт

Обработка ошибок

Стандартная структура ошибки

{
  "statusCode": 401,
  "message": "Unauthorized",
  "error": "Unauthorized"
}

Коды ошибок аутентификации

HTTP Причина
400 Ошибка валидации
401 Невалидный/истёкший токен, неверные credentials, ошибка CSRF
403 Недостаточная роль
404 Ресурс не найден
409 Конфликт (дубликат, недопустимый переход)
429 Rate limit превышен
503 Downstream-сервис недоступен
504 Downstream-сервис не ответил вовремя (RPC timeout)

Rate Limiting

Три профиля, применяются глобально:

Профиль Лимит
short 20 req / 1 s
medium 100 req / 10 s
long 500 req / 60 s

/auth/register и /auth/login имеют более строгие отдельные лимиты.


Рекомендации для клиентов

Браузер (Web) - Хранить accessToken в памяти (не в localStorage) — защита от XSS - Читать XSRF-TOKEN из cookie и передавать в X-CSRF-Token при каждом мутирующем запросе - При получении 401 на защищённом маршруте — пробовать POST /auth/refresh-session; если снова 401 — редиректить на логин

Мобильные клиенты - CSRF-заголовок не нужен - Хранить refreshToken в защищённом хранилище (Keychain / Keystore / SecureStore) - При POST /auth/refresh-session передавать refreshToken в теле запроса - refreshToken cookie не устанавливается для мобильных клиентов (нет cookie-механизма)

AI-агенты - Аутентифицироваться через POST /auth/login, сохранить accessToken - Обновлять через POST /auth/refresh-session при 401 - CSRF-заголовок не нужен (нет cookie) - Полные контракты запросов и ответов — в GET /api/swagger/json