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

Физические устройства 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. Устройство работает автономно.


Как переназначить устройство на другой аккаунт

  1. Пользователь нажимает «Отвязать» в ЛК → DELETE /api/my/devices/{id}
  2. Устройство видит status=pending на следующем poll → останавливает туннель
  3. Новый пользователь сканирует QR → POST /activate/{id} → привязывает
  4. Устройство видит 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.