"""Управление зарегистрированными устройствами Seaca.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.orm import Session from app.models.mobile_device import MobileDevice from app.models.user import User from app.services.mobile_tokens import revoke_refresh_tokens_for_device @dataclass(frozen=True) class MobileDeviceSummary: id: int user_id: int username: str device_uuid: str display_name: str platform: str app_version: str | None has_fcm_token: bool enrolled_at: datetime last_seen_at: datetime | None revoked_at: datetime | None is_active: bool def list_mobile_devices(db: Session) -> list[MobileDeviceSummary]: rows = db.scalars(select(MobileDevice).order_by(MobileDevice.enrolled_at.desc())).all() user_ids = {r.user_id for r in rows} usernames: dict[int, str] = {} if user_ids: for user in db.scalars(select(User).where(User.id.in_(user_ids))).all(): usernames[user.id] = user.username return [_to_summary(row, usernames.get(row.user_id, "?")) for row in rows] def _to_summary(row: MobileDevice, username: str) -> MobileDeviceSummary: return MobileDeviceSummary( id=row.id, user_id=row.user_id, username=username, device_uuid=row.device_uuid, display_name=row.display_name or "", platform=row.platform or "android", app_version=row.app_version, has_fcm_token=bool((row.fcm_token or "").strip()), enrolled_at=row.enrolled_at, last_seen_at=row.last_seen_at, revoked_at=row.revoked_at, is_active=row.revoked_at is None, ) def get_device_or_raise(db: Session, device_id: int) -> MobileDevice: row = db.get(MobileDevice, device_id) if row is None: raise ValueError("device not found") return row def assert_device_active(device: MobileDevice) -> None: if device.revoked_at is not None: raise PermissionError("device has been revoked") def touch_device(db: Session, device: MobileDevice, *, commit: bool = False) -> None: device.last_seen_at = datetime.now(timezone.utc) if commit: db.commit() def revoke_device(db: Session, device_id: int) -> MobileDeviceSummary: device = get_device_or_raise(db, device_id) if device.revoked_at is None: device.revoked_at = datetime.now(timezone.utc) revoke_refresh_tokens_for_device(db, device.id) db.commit() db.refresh(device) user = db.get(User, device.user_id) username = user.username if user else "?" return _to_summary(device, username) def delete_device_record(db: Session, device_id: int) -> None: device = get_device_or_raise(db, device_id) revoke_refresh_tokens_for_device(db, device.id) db.delete(device) db.commit() def update_fcm_token(db: Session, device: MobileDevice, fcm_token: str) -> None: cleaned = (fcm_token or "").strip() if not cleaned: raise ValueError("fcm_token is required") now = datetime.now(timezone.utc) device.fcm_token = cleaned device.fcm_token_updated_at = now device.last_seen_at = now db.commit()