POS Desktop · Регистрация устройства
Wizard первого запуска. Пользователь видит его если в localStorage нет pos.registered=true и нет fallback’а через VITE_DEV_STORE_ID.
Триггер показа (в App.tsx)
const cfg = getDeviceConfig();
const hasEnvFallback = !!import.meta.env.VITE_DEV_STORE_ID;
if (cfg.registered || hasEnvFallback || config.useMockBff) {
// обычный flow с PIN-логином
} else {
// <RegistrationScreen />
}То есть dev-разработчик с env-настройками wizard не видит — старый workflow продолжает работать.
Шаги wizard
Шаг 0 — URL франшизы
┌─ ERP POS · Шаг 1/4 ─┐
│ URL франшизы: │
│ [https://...........] │
│ Например: │
│ https://erp-test. │
│ nirbi.ru/pos │
│ │
│ [Далее] │
└──────────────────────┘
При нажатии «Далее» — pingHealth(bffUrl) → GET /health. Если 200 → шаг 1, иначе ошибка «Сервер не отвечает».
URL должен содержать префикс
/posНа test VPS nginx роутит
/pos/api/...→ pos-bff. Без префикса попадаем в default health-check fallback.
Шаг 1 — Логин админа
┌─ Шаг 2/4 ─┐
│ Email менеджера: │
│ [............] │
│ Пароль: │
│ [............] │
│ │
│ [Назад] [Далее] │
└─────────────────┘
Запросы:
POST /api/v1/admin/auth/login(admin-bff) — body{ email, password }. Ответ:{ data: { access_token, user: { permissions: [...] } } }.- Проверка
permissions.includes("pos.settings.edit"). Если нет → ошибка «У вас нет прав для регистрации кассы». GET /api/v1/admin/stores— список доступных пользователю ТТ.
Шаг 2 — Выбор ТТ
┌─ Шаг 3/4 ─┐
│ Куда устанавливаете кассу? │
│ ◉ Кафе на Тверской 1 │
│ ○ Кафе на Покровке │
│ │
│ [Назад] [Далее] │
└────────────────────────────┘
Если ТТ только одна — выбирается автоматически. Кнопка «Далее» disabled пока ТТ не выбрана.
Шаг 3 — Имя устройства
┌─ Шаг 4/4 ─┐
│ Имя кассы: │
│ [POS-a3f29b...............]│
│ Имя видно админу в списке │
│ устройств. Например, «Касса│
│ бар Тверская». │
│ │
│ [Назад] [Зарегистрировать] │
└────────────────────────────┘
При нажатии «Зарегистрировать»:
POST /api/v1/admin/pos/devices/register(admin-bff → user-service). Body:{ device_id, store_id, name, app_version }+ Bearer admin-токен.- На успех:
setRegistered(bffUrl, storeId)в localStorage →window.location.reload(). - После reload
App.tsxвидитregistered=trueи пропускает кLoginScreen(PIN-логин кассира).
localStorage ключи
| Ключ | Значение | Когда выставляется |
|---|---|---|
pos.device_id | UUID v4 | при первом запуске (ensureDeviceId()) |
pos.bff_url | URL франшизы | после успешной регистрации |
pos.store_id | UUID ТТ | после успешной регистрации |
pos.registered | "true" | после успешной регистрации |
Force-logout (revoke)
Когда админ удаляет кассу через админку:
- На следующем API-запросе
pos-bff/middleware/auth.tsheartbeat-проверка возвращает404 DEVICE_NOT_FOUNDот user-service. - Pos-bff отвечает клиенту
401с кодомDEVICE_REVOKED. BffClient.onDeviceRevokedcallback:clearRegistration()+window.location.reload().- После reload
App.tsxснова показывает<RegistrationScreen>.
device_id остаётся в localStorage — переиспользуется при повторной регистрации (опционально админ может одобрить заново).
Технические детали
- Файлы:
apps/desktop/src/screens/RegistrationScreen.tsx— wizardapps/desktop/src/api/registration.ts— клиент admin-bffapps/desktop/src/lib/storage.ts— localStorage обёрткаapps/desktop/src/config.ts— runtime getter (localStorage с fallback на VITE_*)packages/api-client/src/bff-client.ts— добавлены опцииgetDeviceId,getAppVersion,onDeviceRevoked
- HTTP headers, отправляемые с каждым запросом после регистрации:
X-Device-Id: <pos.device_id>X-App-Version: 0.1.1X-Active-Store: <pos.store_id>(старая фича для multi-store кассиров)Authorization: Bearer <jwt>