Физические устройства dix.su¶
Документ описывает архитектуру zero-touch provisioning для готовых устройств, продаваемых через shopdix.su: как работает привязка к аккаунту, какие компоненты задействованы и как это расширять.
Гайды по использованию и продаже — в guides/device-user.md
и guides/device-seller.md.
Концепция¶
Пользователь покупает физическое устройство (Raspberry Pi, смартфон на
postmarketOS, мини-ПК). На нём предустановлен device_client и скрипт
provision.sh. Устройство само получает slug и токен с сервера — пользователю
не нужно ничего настраивать вручную.
Ключевое отличие от ручной установки (dixsu-install.sh): при ручной установке
пользователь сам вводит slug и токен из личного кабинета. При zero-touch — устройство
идентифицируется по UUID, а slug/токен приходят с сервера после привязки через QR.
Флоу целиком¶
┌─────────────────────────────────────────────────────────────────────────┐
│ ПРОДАВЕЦ (один раз перед отправкой) │
│ │
│ sudo sh flash_device.sh --label "RPi 5 #007" │
│ │ │
│ ├─ генерирует UUID → /etc/dixsu/device_id │
│ ├─ скачивает provision.sh с dix.su/api/client/provision.sh │
│ ├─ устанавливает как сервис (systemd / OpenRC) │
│ └─ сохраняет QR PNG → /etc/dixsu/activation_qr.png │
│ │
│ Распечатать QR на карточку, вложить в коробку │
└─────────────────────────────────────────────────────────────────────────┘
│ устройство в коробке
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ УСТРОЙСТВО (при каждой загрузке) │
│ │
│ provision.sh (запущен как сервис) │
│ │ │
│ └─ каждые 60 сек: GET dix.su/api/provision/{UUID} │
│ │ │
│ status=pending → ждём, ничего не делаем │
│ status=active → сохраняем slug/token, запускаем device_client│
│ нет ответа → работаем на последнем сохранённом конфиге │
└─────────────────────────────────────────────────────────────────────────┘
│ покупатель получил коробку
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ПОКУПАТЕЛЬ (один раз при активации) │
│ │
│ 1. Сканирует QR → браузер открывает dix.su/activate/{UUID} │
│ 2. Логинится (или уже залогинен) │
│ 3. Нажимает «Привязать устройство к аккаунту» │
│ │ │
│ └─ POST /activate/{UUID} │
│ → provisioned_devices.user_id = id покупателя │
│ → provisioned_devices.slug = slug покупателя │
│ → provisioned_devices.token = token покупателя │
│ │
│ 4. Через ≤60 сек устройство видит status=active → туннель поднят │
└─────────────────────────────────────────────────────────────────────────┘
База данных¶
Таблица provisioned_devices¶
| Колонка | Тип | Описание |
|---|---|---|
device_id |
UUID PK | Уникальный ID устройства, прошивается один раз |
slug |
VARCHAR | Slug туннеля (берётся из users.slug) |
token |
VARCHAR | Токен туннеля (берётся из users.token) |
user_id |
BIGINT FK→users | Пользователь-владелец, NULL если не привязано |
label |
VARCHAR | Метка устройства (напр. "RPi 5 #007"), задаётся продавцом |
assigned_at |
TIMESTAMP | Время последней привязки к аккаунту |
created_at |
TIMESTAMP | Время первой регистрации (первый poll или flash) |
Состояния устройства:
user_id |
slug |
Состояние |
|---|---|---|
| NULL | NULL | Не привязано, ожидает активации |
| ≠ NULL | ≠ NULL | Активировано, туннель работает |
| NULL | NULL | Отвязано пользователем (после unlink) |
API-эндпоинты¶
Все эндпоинты в ProvisionController.java и ActivateDeviceController.java.
Устройство (без авторизации)¶
| Метод | URL | Описание |
|---|---|---|
GET |
/api/provision/{deviceId} |
Опрос: вернёт {status:"pending"} или {status:"active",slug,token} |
GET |
/api/client/provision.sh |
Скачать скрипт provisioning-демона |
Пользователь (авторизация обязательна)¶
| Метод | URL | Описание |
|---|---|---|
GET |
/activate/{deviceId} |
Страница активации (Thymeleaf) |
POST |
/activate/{deviceId} |
Привязать устройство к текущему аккаунту |
GET |
/api/my/devices |
Список своих устройств |
DELETE |
/api/my/devices/{deviceId} |
Отвязать устройство |
Администратор¶
| Метод | URL | Описание |
|---|---|---|
GET |
/api/admin/provision/devices |
Все устройства в системе |
POST |
/api/admin/provision/assign |
Переназначить устройство на другой slug |
DELETE |
/api/admin/provision/{deviceId} |
Снять привязку (без удаления записи) |
Расположение кода¶
| Компонент | Файл |
|---|---|
| Polling API + POST activate + admin | modulauth/.../provision/ProvisionController.java |
| Thymeleaf страница активации | modulauth/.../provision/ActivateDeviceController.java |
| Репозиторий устройств | modulauth/.../provision/ProvisionedDeviceRepository.java |
| Модель устройства | modulauth/.../provision/ProvisionedDevice.java |
| Шаблон страницы активации | modulauth/src/main/resources/templates/activate-device.html |
| Скрипт provisioning-демона | device/provision.sh + resources/scripts/provision.sh |
| Скрипт прошивки продавца | device/flash_device.sh |
| Вкладка «Мои устройства» в ЛК | protected-page.html → #sec-devices |
Поддерживаемые платформы¶
| ОС | Архитектура | Init | Бинарник |
|---|---|---|---|
| postmarketOS / Alpine | ARM64 | OpenRC | linux_arm64 |
| Debian / Raspberry Pi OS | ARM64 | systemd | linux_arm64 |
| Debian / Ubuntu | x86_64 | systemd | linux_x86_64 |
| (планируется) macOS | ARM64 / x86_64 | launchd | macos_arm64 / macos_x86_64 |
| (планируется) Windows | x86_64 | Service | windows_x86_64 |
provision.sh определяет платформу автоматически через uname -m. Расширение
на macOS и Windows — добавить _install_launchd / _install_windows блоки по
аналогии с существующими.
Жизненный цикл после активации¶
Устройство включилось
│
├─ provision.sh читает /etc/dixsu/provision.env
│ (slug/token сохранены с прошлого раза)
│
├─ сразу запускает device_client ← туннель поднимается без ожидания сервера
│
└─ фоновый цикл опроса (60 сек):
• slug/token изменились (переназначение аккаунта) → перезапуск
• device_client упал → перезапуск
• устройство отвязали → остановить device_client, удалить env
Почему slug/token сохраняются локально: если сервер недоступен при загрузке устройства, туннель всё равно поднимется с последними известными credentials. Устройство работает автономно.
Как переназначить устройство на другой аккаунт¶
- Пользователь нажимает «Отвязать» в ЛК →
DELETE /api/my/devices/{id} - Устройство видит
status=pendingна следующем poll → останавливает туннель - Новый пользователь сканирует QR →
POST /activate/{id}→ привязывает - Устройство видит
status=activeс новыми slug/token → перезапускает туннель
Или — напрямую через API администратора POST /api/admin/provision/assign
без участия пользователей (например, при возврате товара).
Контейнерные стеки как альтернативный тип клиента¶
Помимо бинарного device_client, туннель можно поднять через Docker-стек.
Пользователь запускает готовый docker-compose.yml — сервис + туннельный
контейнер стартуют вместе, без ручной установки бинарника.
Образ туннельного клиента: ramanzes/dixsu-tunnel:latest
FROM elixir:1.19-slim
RUN apt-get install -y --no-install-recommends ca-certificates
COPY device_client.exs .
ENTRYPOINT ["elixir", "device_client.exs"]
Клиент читает конфигурацию из env-переменных (DIXSU_SLUG, DIXSU_TOKEN,
LOCAL_PORT) — или из CLI-аргументов при прямом запуске.
Переменная TUNNEL_SOURCE=docker передаётся как ?source=docker в WebSocket-URL.
Сервер сохраняет её в Registry-метаданных и отдаёт через /api/profile/tunnel/status.
В личном кабинете отображается бейдж 📦 docker-стек.
Подробнее: CONTAINER_STACKS.md (техническая документация),
guides/container-stacks.md (гайд для пользователя).
Известные ограничения¶
- Один туннель на устройство: одно устройство = одна запись в
provisioned_devices. Если нужно несколько туннелей — используйте дополнительные слаги (PRO: до 2, PERS: без ограничений). Подробнее: guides/multi-slug.md. - UUID не меняется: если QR-карточка потеряна — покупатель может ввести UUID вручную на странице «Мои устройства» в ЛК.
- Нет проверки подлинности устройства: сервер не верифицирует, что запрос
/api/provision/{UUID}идёт именно от устройства с этим UUID. Любой, знающий UUID, может получить slug/token после привязки. Это осознанное упрощение для v1 — в v2 можно добавить challenge-response при polling.