# MyBeacon Backend - Технические заметки Технические детали и особенности реализации. ## Критические решения ### 1. JWT Subject как string **Проблема:** python-jose требует JWT `sub` поле как string, но мы используем integer ID. **Решение:** ```python # При создании токена 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: ```python # 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` если он уже есть: ```python # 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 модель: ```python 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: ```python 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: ```python # 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 **Проблема:** ```python db: Annotated[AsyncSession, Depends(get_db)] = Depends(get_db) # AssertionError: Cannot specify Depends in Annotated and default value ``` **Решение:** Параметры БЕЗ default value должны идти ПЕРЕД параметрами с default: ```python # Правильно 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 в будущем **Генерация:** ```python 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 уровень:** ```python # Автоматическая фильтрация по organization_id result = await db.execute( select(Device).where( Device.organization_id == current_user.organization_id ) ) ``` **Проверки:** ```python # Проверка принадлежности к организации 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 **Иерархия:** ```python get_current_user() # Любой авторизованный └─ get_current_owner() # owner или superadmin └─ get_current_superadmin() # только superadmin ``` **Использование:** ```python @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 поле для гибких настроек: ```python # 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 ```python # 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: ```sql 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 ### Тестовые скрипты ```bash /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 ```bash 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 - [x] 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:** ```bash sudo systemctl start clickhouse-server clickhouse-client ``` **Database Creation:** ```sql CREATE DATABASE mybeacon; ``` **Tables:** ```sql -- 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):** ```json { "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:** ```sql SELECT count(*) FROM mybeacon.ble_events; SELECT count(*) FROM mybeacon.wifi_events; ``` **Unique beacons:** ```sql SELECT uniq(beacon_mac) FROM mybeacon.ble_events; ``` **Latest events:** ```sql SELECT timestamp, beacon_mac, rssi, uuid FROM mybeacon.ble_events ORDER BY timestamp DESC LIMIT 10; ``` **Events by organization:** ```sql 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:** ```javascript // 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 - Алерты при превышении порогов