diff --git a/backend/handler/database/devices_handler.py b/backend/handler/database/devices_handler.py index d0ef1dc28..6be412fad 100644 --- a/backend/handler/database/devices_handler.py +++ b/backend/handler/database/devices_handler.py @@ -36,6 +36,7 @@ class DBDevicesHandler(DBBaseHandler): user_id: int, mac_address: str | None = None, hostname: str | None = None, + ip_address: str | None = None, platform: str | None = None, session: Session = None, # type: ignore ) -> Device | None: @@ -48,6 +49,13 @@ class DBDevicesHandler(DBBaseHandler): if device: return device + if ip_address and platform: + return session.scalar( + select(Device) + .filter_by(user_id=user_id, ip_address=ip_address, platform=platform) + .limit(1) + ) + if hostname and platform: return session.scalar( select(Device) diff --git a/backend/models/device.py b/backend/models/device.py index 911471d26..fc3c663a1 100644 --- a/backend/models/device.py +++ b/backend/models/device.py @@ -1,6 +1,7 @@ from __future__ import annotations import enum +from dataclasses import dataclass from datetime import datetime from typing import TYPE_CHECKING @@ -20,6 +21,24 @@ class SyncMode(enum.StrEnum): PUSH_PULL = "push_pull" +@dataclass(frozen=True) +class DeviceType: + """A preconfigured device type with its client identifier and sync mode.""" + + platform: str + client: str + sync_mode: SyncMode + + +KNOWN_DEVICES: dict[str, DeviceType] = { + "web": DeviceType(platform="Web", client="web", sync_mode=SyncMode.API), + "grout": DeviceType(platform="muOS", client="grout", sync_mode=SyncMode.API), + "argosy-launcher": DeviceType( + platform="Android", client="argosy-launcher", sync_mode=SyncMode.API + ), +} + + class Device(BaseModel): __tablename__ = "devices" __table_args__ = {"extend_existing": True} diff --git a/backend/utils/auth.py b/backend/utils/auth.py index 9b8af20cb..e862a66c7 100644 --- a/backend/utils/auth.py +++ b/backend/utils/auth.py @@ -8,7 +8,7 @@ from handler.database import db_device_handler from logger.formatter import CYAN from logger.formatter import highlight as hl from logger.logger import log -from models.device import Device +from models.device import KNOWN_DEVICES, Device from models.user import User @@ -38,31 +38,35 @@ def create_or_find_web_device(request: Request, user: User) -> Device: Uses parsed browser/OS family + client IP as fingerprint to avoid creating duplicate devices for the same browser. """ + device_type = KNOWN_DEVICES["web"] + user_agent = request.headers.get("user-agent", "") platform = _parse_platform(user_agent) - client_host = request.client.host if request.client else None - ip_address = request.headers.get("x-forwarded-for", client_host) - # TODO: differentiate name vs platform vs client better + + hostname = request.client.host if request.client else None + ip_address = request.headers.get("x-forwarded-for", None) + existing = db_device_handler.get_device_by_fingerprint( user_id=user.id, mac_address=None, - hostname=ip_address, + hostname=hostname, + ip_address=ip_address, platform=platform, ) if existing: db_device_handler.update_last_seen(device_id=existing.id, user_id=user.id) return existing - now = datetime.now(timezone.utc) device = Device( id=str(uuid.uuid4()), user_id=user.id, - name="Web Browser", - platform=platform, - client="web", - hostname=ip_address, + name=platform, + platform=device_type.platform, + client=device_type.client, + sync_mode=device_type.sync_mode, + hostname=hostname, ip_address=ip_address, - last_seen=now, + last_seen=datetime.now(timezone.utc), ) device = db_device_handler.add_device(device) log.info(