BACKEND_NOTES.md 13 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)

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