mirror of
https://github.com/rommapp/romm.git
synced 2026-06-27 22:35:57 +00:00
Implements RFC 8628-style device authorization so clients (argosy-launcher, grout) can pair by display instead of manually copying tokens. Device posts to an open /api/auth/device/init with its identifier and requested scopes; the server returns device_code + user_code + QR URL. User scans QR, lands at /pair/device, approves (optionally editing name/scopes/expiry); the device's next poll on /api/auth/device/token returns a ClientToken bound 1:1 to a newly- created (or deduped) Device record. Downstream endpoints (/play-sessions, /sync/negotiate) infer device_id from the bound token so the client doesn't have to ship it on every call. - Migrations 0080/0081: devices.client_device_identifier (unique per user) and client_tokens.device_id FK (ON DELETE SET NULL) - Five new endpoints under /api/auth/device (init/pending/approve/ deny/token) with Redis-backed state, per-IP rate limits, and RFC-compliant error codes (authorization_pending, slow_down, expired_token, access_denied) - HybridAuthBackend surfaces bound device_id on request.state and bumps devices.last_seen with a 5-minute debounce - /api/users/me returns current_device_id for bound tokens so a device can identify itself from its token alone - Frontend approval screen at /pair/device with editable scopes/ name/expiry (defaults to Never), 3s auto-close countdown - ClientApiTokens settings list shows bound-device chip - 20 i18n keys added to all 17 locales; generated models updated - 52 new tests across 13 classes; full suite 1334 passed Planning and review assisted by Claude Code.
57 lines
1.3 KiB
Python
57 lines
1.3 KiB
Python
from typing import Any
|
|
|
|
from pydantic import ConfigDict, field_serializer
|
|
|
|
from models.device import SyncMode
|
|
|
|
from .base import BaseModel, UTCDatetime
|
|
|
|
SENSITIVE_SYNC_CONFIG_KEYS = {"ssh_password", "ssh_key_path"}
|
|
|
|
|
|
class DeviceSyncSchema(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
device_id: str
|
|
device_name: str | None
|
|
last_synced_at: UTCDatetime
|
|
is_untracked: bool
|
|
is_current: bool
|
|
|
|
|
|
class DeviceSchema(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: str
|
|
user_id: int
|
|
name: str | None
|
|
platform: str | None
|
|
client: str | None
|
|
client_version: str | None
|
|
ip_address: str | None
|
|
mac_address: str | None
|
|
hostname: str | None
|
|
client_device_identifier: str | None
|
|
sync_mode: SyncMode
|
|
sync_enabled: bool
|
|
sync_config: dict | None
|
|
last_seen: UTCDatetime | None
|
|
created_at: UTCDatetime
|
|
updated_at: UTCDatetime
|
|
|
|
@field_serializer("sync_config")
|
|
@classmethod
|
|
def mask_sensitive_fields(cls, v: dict | None) -> dict[str, Any] | None:
|
|
if not v:
|
|
return v
|
|
return {
|
|
k: "********" if k in SENSITIVE_SYNC_CONFIG_KEYS else val
|
|
for k, val in v.items()
|
|
}
|
|
|
|
|
|
class DeviceCreateResponse(BaseModel):
|
|
device_id: str
|
|
name: str | None
|
|
created_at: UTCDatetime
|