BACKEND_NOTES.md 18 KB

MyBeacon Backend - Технические заметки

Технические детали и особенности реализации.

Критические решения

1. JWT Subject как string

Проблема: python-jose требует JWT sub поле как string, но мы используем integer ID.

Решение:

# При создании токена
if "sub" in to_encode and isinstance(to_encode["sub"], int):
    to_encode["sub"] = str(to_encode["sub"])

# При парсинге
user_id_str: str = payload.get("sub")
user_id = int(user_id_str)

Файлы:

  • app/core/security.py:26-27 - конвертация при создании
  • app/api/deps.py:48-62 - парсинг обратно в int

2. JWT type field для разных токенов

Проблема: Нужны разные типы токенов (access, refresh, device).

Решение: Добавлено поле type в JWT payload:

# Access token для пользователей
{"sub": "user_id", "type": "access", "exp": ...}

# Refresh token
{"sub": "user_id", "type": "refresh", "exp": ...}

# Device token
{"sub": "device_id", "type": "device", "mac": "...", "org_id": ..., "exp": ...}

Важно: При создании токена НЕ перезаписываем type если он уже есть:

# app/core/security.py:31-34
if "type" not in to_encode:
    to_encode["type"] = "access"
to_encode["exp"] = expire

Файлы:

  • app/core/security.py:31-34 - не перезаписываем type
  • app/api/deps.py:182-187 - проверка type="device"

3. Device simple_id auto-increment

Проблема: NULL constraint violation при создании Device без simple_id.

Решение: Добавить server_default в SQLAlchemy модель:

simple_id: Mapped[int] = mapped_column(
    Integer,
    unique=True,
    nullable=False,
    server_default=text("nextval('device_simple_id_seq')"),
)

Критично: server_default должен быть в модели, даже если sequence создан в миграции!

Файлы:

  • alembic/versions/001_initial_schema.py - создание sequence
  • app/models/device.py:23-28 - server_default в модели

4. Bcrypt vs Passlib

Проблема: Passlib с bcrypt backend имеет compatibility issues:

ValueError: password cannot be longer than 72 bytes

Решение: Использовать bcrypt напрямую, убрать passlib:

import bcrypt

def hash_password(password: str) -> str:
    salt = bcrypt.gensalt()
    hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
    return hashed.decode("utf-8")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return bcrypt.checkpw(
        plain_password.encode("utf-8"),
        hashed_password.encode("utf-8")
    )

Файлы:

  • app/core/security.py:83-103 - прямой bcrypt
  • pyproject.toml - только bcrypt, без passlib

5. Stub-compatible Device API

Проблема: Демон на железке ожидает stub API (192.168.5.2:5000).

Решение: Реализованы endpoints полностью совместимые со stub:

POST /api/v1/registration
GET  /api/v1/config?device_id=MAC
POST /api/v1/ble
POST /api/v1/wifi

Важно:

  • device_id передаётся как query parameter в GET /config
  • Authorization header с Bearer token обязателен (кроме /registration)
  • Content-Encoding: gzip поддерживается для events

Файлы:

  • app/api/v1/registration.py - POST /registration
  • app/api/v1/config.py - GET /config
  • app/api/v1/events.py - POST /ble, POST /wifi

6. Автоматическая регистрация устройств

Проблема: Каждое новое устройство регистрировать руками неудобно.

Решение: Settings table с JSON value + toggle в superadmin API:

# settings table
{
  "key": "auto_registration",
  "value": {
    "enabled": true,
    "last_device_at": "2025-12-27T11:39:14Z"
  }
}

Механизм:

  1. Superadmin включает авторегистрацию через API
  2. Устройство отправляет POST /registration
  3. Если enabled=true, создаётся Device с organization_id=NULL
  4. При создании обновляется last_device_at
  5. Автоотключение через 1 час после last_device_at (проверяется в GET /auto-registration)

Файлы:

  • app/models/settings.py - модель Settings
  • app/services/device_auth_service.py:36-62 - авторегистрация
  • app/api/v1/superadmin/settings.py - toggle API

7. FastAPI Depends в Annotated

Проблема:

db: Annotated[AsyncSession, Depends(get_db)] = Depends(get_db)
# AssertionError: Cannot specify Depends in Annotated and default value

Решение: Параметры БЕЗ default value должны идти ПЕРЕД параметрами с default:

# Правильно
async def endpoint(
    db: Annotated[AsyncSession, Depends(get_db)],  # БЕЗ default
    param: str = Query(None),  # С default
):
    pass

# Неправильно
async def endpoint(
    param: str = Query(None),
    db: Annotated[AsyncSession, Depends(get_db)],  # ОШИБКА!
):
    pass

Файлы: Все endpoints с Depends

8. Device token vs Device password

Два поля в devices:

  • device_token - Bearer token для API (base64, 32 bytes)
  • device_password - 8-digit password для dashboard admin

device_token - используется в Authorization header device_password - для POST /admin/update_device в будущем

Генерация:

def _generate_token() -> str:
    return b64encode(secrets.token_bytes(32)).decode("ascii")

def _generate_password() -> str:
    n = secrets.randbelow(10**8)
    return f"{n:08d}"

Файлы:

  • app/api/v1/registration.py:29-37 - генерация
  • app/models/device.py:61-62 - поля в модели

Особенности архитектуры

Multi-tenant isolation

SQL уровень:

# Автоматическая фильтрация по organization_id
result = await db.execute(
    select(Device).where(
        Device.organization_id == current_user.organization_id
    )
)

Проверки:

# Проверка принадлежности к организации
if device.organization_id != current_user.organization_id:
    raise HTTPException(status_code=403, detail="...")

Файлы:

  • app/api/v1/client/devices.py:72-76 - проверка organization_id
  • app/api/v1/client/users.py:81-85 - multi-tenant для users

RBAC Dependencies

Иерархия:

get_current_user()                # Любой авторизованный
  └─ get_current_owner()          # owner или superadmin
       └─ get_current_superadmin() # только superadmin

Использование:

@router.post("/users")
async def create_user(
    current_user: Annotated[User, Depends(get_current_owner)],  # owner/admin only
):
    pass

Файлы:

  • app/api/deps.py:83-127 - все dependencies

Config JSONB merge

Device config - это JSONB поле для гибких настроек:

# Default config (hardcoded)
default_config = {
    "ble": {"enabled": True, "batch_interval_ms": 2500},
    "wifi": {"monitor_enabled": False}
}

# Device-specific overrides (в БД)
device.config = {
    "wifi": {"monitor_enabled": True}  # override
}

# Merged config (отправляется устройству)
merged = {
    "ble": {"enabled": True, "batch_interval_ms": 2500},
    "wifi": {"monitor_enabled": True}  # OVERRIDDEN
}

Файлы:

  • app/api/v1/config.py:56-95 - merge logic

Database особенности

Async SQLAlchemy

# asyncpg driver
DATABASE_URL = "postgresql+asyncpg://user:password@host/db"

# async session
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

# usage
async with async_session() as session:
    result = await session.execute(select(User))
    user = result.scalar_one_or_none()

Важно: expire_on_commit=False чтобы можно было использовать объекты после commit.

Файлы:

  • app/core/database.py:11-14 - async session config

JSONB columns

PostgreSQL JSONB для гибких данных:

  • devices.config - конфигурация устройства
  • settings.value - системные настройки

Преимущества:

  • Можно добавлять новые поля без миграций
  • Можно делать partial updates
  • Можно query по содержимому (GIN индексы)

Файлы:

  • app/models/device.py:58 - config JSONB
  • app/models/settings.py:14 - value JSONB

Sequences

PostgreSQL sequence для simple_id:

CREATE SEQUENCE device_simple_id_seq START 1;
ALTER TABLE devices
  ALTER COLUMN simple_id
  SET DEFAULT nextval('device_simple_id_seq');

Важно: SQLAlchemy модель ДОЛЖНА иметь server_default!

Файлы:

  • alembic/versions/001_initial_schema.py - CREATE SEQUENCE
  • app/models/device.py:23-28 - server_default

Тестирование

Тестовые данные в БД

Superadmin:

  • email: superadmin@mybeacon.com
  • password: Admin123!
  • role: superadmin

Owner:

  • email: admin@mybeacon.com
  • password: Admin123!
  • role: owner
  • organization_id: 4

Devices:

  • #2 (AA:BB:CC:DD:EE:02) - тестовое
  • #4 (38:54:39:4b:1b:ac) - реальное железо на 192.168.5.244

Тестовые скрипты

/tmp/test_client.sh       # Client API test
/tmp/test_device_api.sh   # Device API test
/tmp/demo_hardware.sh     # Hardware integration demo
/tmp/enable_autoreg.sh    # Enable auto-registration
/tmp/assign_device.sh     # Assign device to org

Deployment

Environment

  • Server: 192.168.5.4:8000
  • Database: PostgreSQL на localhost
  • Redis: localhost:6379 (optional)

Hardware

  • Device: Luckfox Pico Ultra W (192.168.5.244)
  • Daemon: /etc/init.d/S98mybeacon
  • Server URL: обновлён на http://192.168.5.4:8000

Startup

cd /home/user/work/luckfox/alpine/mybeacon-backend/backend
poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000

Production: Добавить systemd service + nginx reverse proxy.

Known Issues

Resolved

✅ JWT Subject must be string - конвертируем int → str ✅ Device simple_id NULL - добавлен server_default ✅ Bcrypt/passlib compatibility - используем прямой bcrypt ✅ Device token type - не перезаписываем type в create_access_token ✅ FastAPI Depends ordering - параметры без default идут первыми

TODO

  • ClickHouse для хранения событий (BLE/WiFi)
  • Audit logs middleware
  • Email verification
  • Password reset flow
  • Rate limiting на login
  • WebSocket для real-time
  • Frontend (Vue 3)

ClickHouse Integration

Setup

ClickHouse Version: 25.12.1.649 (official build)

Installation:

sudo systemctl start clickhouse-server
clickhouse-client

Database Creation:

CREATE DATABASE mybeacon;

Tables:

-- BLE events
CREATE TABLE mybeacon.ble_events (
    timestamp DateTime64(3),
    device_id UInt32,
    device_mac String,
    organization_id UInt32 DEFAULT 0,
    beacon_mac String,
    rssi Int8,
    uuid String DEFAULT '',
    major UInt16 DEFAULT 0,
    minor UInt16 DEFAULT 0,
    tx_power Int8 DEFAULT 0,
    raw_data String DEFAULT ''
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (organization_id, device_id, timestamp);

-- WiFi events
CREATE TABLE mybeacon.wifi_events (
    timestamp DateTime64(3),
    device_id UInt32,
    device_mac String,
    organization_id UInt32 DEFAULT 0,
    client_mac String,
    ssid String DEFAULT '',
    rssi Int8,
    channel UInt8 DEFAULT 0,
    frame_type String DEFAULT 'probe_request',
    raw_data String DEFAULT ''
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (organization_id, device_id, timestamp);

Implementation

Service: app/services/clickhouse_service.py

  • insert_ble_events() - batch insert BLE events
  • insert_wifi_events() - batch insert WiFi events
  • Timestamp parsing from unix milliseconds (ts field)
  • Organization ID: 0 for unassigned devices

Integration: app/api/v1/events.py

  • POST /ble → ClickHouse insert
  • POST /wifi → ClickHouse insert
  • Gzip compression support

Event Format (from device):

{
  "mac": "f9:29:de:16:b4:f3",
  "rssi": -64,
  "ts": 1766923995934,
  "type": "ibeacon",
  "uuid": "b9407f30f5f8466eaff925556b57fe6d",
  "major": 1,
  "minor": 2
}

Performance

  • Partitioning: Monthly by timestamp (toYYYYMM(timestamp))
  • Ordering: (organization_id, device_id, timestamp) for fast multi-tenant queries
  • Batch inserts: All events in one INSERT statement
  • Data type: DateTime64(3) for millisecond precision

Queries

Total events:

SELECT count(*) FROM mybeacon.ble_events;
SELECT count(*) FROM mybeacon.wifi_events;

Unique beacons:

SELECT uniq(beacon_mac) FROM mybeacon.ble_events;

Latest events:

SELECT timestamp, beacon_mac, rssi, uuid
FROM mybeacon.ble_events
ORDER BY timestamp DESC
LIMIT 10;

Events by organization:

SELECT organization_id, count(*) as events
FROM mybeacon.ble_events
GROUP BY organization_id;

Performance

Текущее состояние

  • BLE events: 2-46 событий в батче
  • Logging: только в консоль (print)
  • Database: PostgreSQL без индексов (пока)

Оптимизации (when needed)

  1. ClickHouse для событий (миллионы records)
  2. Redis для кеширования config
  3. PostgreSQL индексы (organization_id, mac_address, created_at)
  4. Connection pooling (asyncpg имеет встроенный)
  5. Batch inserts для событий

Monitoring

Logs

[REGISTRATION] device=38:54:39:4b:1b:ac simple_id=4
[BLE BATCH] device=38:54:39:4b:1b:ac simple_id=4 count=46
[WIFI BATCH] device=38:54:39:4b:1b:ac simple_id=4 count=10

Metrics (future)

  • Prometheus + Grafana
  • Device online/offline count
  • Events per second
  • API latency

Security

JWT Security

  • Access token: 15 minutes (короткий срок)
  • Refresh token: 30 days (хранится в БД с возможностью отзыва)
  • Device token: не expire (можно добавить rotation)

Password Security

  • Bcrypt с default cost factor (12)
  • Min length: 8 символов (в Pydantic schema)
  • Strength: рекомендуется uppercase + lowercase + digit + special

API Security

  • HTTPS: рекомендуется в production (nginx SSL termination)
  • CORS: настроить allowed origins
  • Rate limiting: добавить для /auth/login
  • SQL injection: защищено через SQLAlchemy ORM

Git

Repository: https://h2.e-bash.ru/root/mybeacon-backend.git Branch: master Latest commit: Implement complete MyBeacon backend MVP

Frontend

Technology Stack

  • Framework: Vue 3 (Composition API)
  • Build Tool: Vite 5
  • State Management: Pinia
  • Routing: Vue Router 4
  • HTTP Client: Axios
  • Internationalization: vue-i18n 9

Internationalization (i18n)

Supported Languages:

  • Russian (ru) - default
  • English (en)

Implementation:

  • Language selector in sidebar (RU/EN buttons)
  • Locale stored in localStorage
  • All UI strings translated in /src/i18n/index.js
  • Usage: {{ $t('nav.dashboard') }}

Adding new translations:

// src/i18n/index.js
const messages = {
  en: {
    mySection: {
      myKey: 'My English Text'
    }
  },
  ru: {
    mySection: {
      myKey: 'Мой русский текст'
    }
  }
}

Navigation Structure

Superadmin:

  1. Dashboard
  2. Devices
  3. Organizations
  4. Users

Client (owner/admin):

  1. Dashboard
  2. Devices
  3. Users (owner/admin only)

Host Monitoring (TODO)

Dashboard должен отображать метрики хоста в реальном времени:

Метрики:

  • CPU Usage (%)
  • Memory Usage (GB/%)
  • Network Traffic (rx/tx)
  • Load Average (1m, 5m, 15m)
  • Disk I/O (read/write)
  • Disk Usage (%)

Варианты реализации:

  1. Custom daemon - написать свой демон на Python/Go для сбора метрик
  2. Node Exporter (Prometheus) - использовать готовое решение
  3. Netdata - полноценный мониторинг с API
  4. collectd - легковесный сборщик метрик

API Endpoint (планируется):

GET /api/v1/monitoring/host
{
  "cpu": {"usage": 45.2, "cores": 4},
  "memory": {"total": 16384, "used": 8192, "percent": 50},
  "network": {"rx_bytes": 1024000, "tx_bytes": 512000},
  "load": {"1m": 0.5, "5m": 0.7, "15m": 0.6},
  "disk": {
    "io": {"read_bytes": 2048000, "write_bytes": 1024000},
    "usage": {"total": 512000, "used": 256000, "percent": 50}
  },
  "timestamp": "2025-12-28T16:00:00Z"
}

Frontend компонент:

  • Real-time графики (Chart.js / Apache ECharts)
  • WebSocket для live updates
  • Алерты при превышении порогов