Files
romm/backend/endpoints/responses/device.py
nendo 519abc1645 Add device authorization flow for TV-app-style pairing (RFC 8628)
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.
2026-06-18 05:24:32 +09:00

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