Files
boc/iom/quixzoom-auth-service/services/device_service.py
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

253 lines
7.1 KiB
Python

"""
Device Service - Manages device registration and operations
"""
from typing import Optional, List
from datetime import datetime
from models.device import (
Device, DeviceCreate, DeviceUpdate, DeviceResponse,
DeviceStatus, DevicePlatform
)
from utils.security import security_manager
from utils.device_fingerprint import DeviceFingerprint
class DeviceService:
"""
Service for managing registered devices
Handles:
- Device registration
- Device lookup
- Device revocation
- Device fingerprint validation
"""
def __init__(self, db_pool=None):
self.db = db_pool
async def register_device(self, device_data: DeviceCreate) -> Device:
"""
Register a new device
Args:
device_data: Device registration data
Returns:
Device: Created device
"""
# Hash the device fingerprint for storage
fingerprint_hash = security_manager.hash_fingerprint(device_data.device_fingerprint)
device = Device(
user_id=device_data.user_id,
device_name=device_data.device_name,
platform=device_data.platform,
public_key=device_data.public_key,
device_fingerprint=fingerprint_hash,
push_token=device_data.push_token,
status=DeviceStatus.ACTIVE,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
# Store in database
if self.db:
await self._save_device_to_db(device)
return device
async def get_device(self, device_id: str) -> Optional[Device]:
"""Get device by ID"""
if not self.db:
return None
row = await self.db.fetchrow(
"""
SELECT * FROM devices WHERE id = $1
""",
device_id
)
if row:
return Device(**dict(row))
return None
async def get_device_by_fingerprint(
self,
user_id: str,
fingerprint: str
) -> Optional[Device]:
"""Get device by user ID and fingerprint"""
if not self.db:
return None
fingerprint_hash = security_manager.hash_fingerprint(fingerprint)
row = await self.db.fetchrow(
"""
SELECT * FROM devices
WHERE user_id = $1 AND device_fingerprint = $2 AND status = 'active'
""",
user_id,
fingerprint_hash
)
if row:
return Device(**dict(row))
return None
async def get_user_devices(self, user_id: str) -> List[DeviceResponse]:
"""Get all devices for a user"""
if not self.db:
return []
rows = await self.db.fetch(
"""
SELECT id, user_id, device_name, platform, status, created_at, last_used_at
FROM devices
WHERE user_id = $1
ORDER BY last_used_at DESC NULLS LAST
""",
user_id
)
return [DeviceResponse(**dict(row)) for row in rows]
async def update_device(
self,
device_id: str,
update_data: DeviceUpdate
) -> Optional[Device]:
"""Update device information"""
device = await self.get_device(device_id)
if not device:
return None
# Update fields
if update_data.device_name:
device.device_name = update_data.device_name
if update_data.push_token is not None:
device.push_token = update_data.push_token
if update_data.status:
device.status = update_data.status
device.updated_at = datetime.utcnow()
if self.db:
await self._update_device_in_db(device)
return device
async def revoke_device(self, device_id: str, reason: str = "") -> bool:
"""
Revoke a device
Args:
device_id: Device to revoke
reason: Reason for revocation
Returns:
bool: True if revoked successfully
"""
device = await self.get_device(device_id)
if not device:
return False
device.status = DeviceStatus.REVOKED
device.updated_at = datetime.utcnow()
if self.db:
await self.db.execute(
"""
UPDATE devices
SET status = 'revoked', updated_at = NOW()
WHERE id = $1
""",
device_id
)
return True
async def validate_device(
self,
device_id: str,
fingerprint: Optional[str] = None
) -> bool:
"""
Validate device is active and optionally check fingerprint
Args:
device_id: Device ID
fingerprint: Optional fingerprint to validate
Returns:
bool: True if device is valid
"""
device = await self.get_device(device_id)
if not device:
return False
if device.status != DeviceStatus.ACTIVE:
return False
if fingerprint:
fingerprint_hash = security_manager.hash_fingerprint(fingerprint)
if device.device_fingerprint != fingerprint_hash:
return False
return True
async def update_last_used(self, device_id: str, ip_address: Optional[str] = None):
"""Update last used timestamp"""
if self.db:
await self.db.execute(
"""
UPDATE devices
SET last_used_at = NOW(), last_ip = $2
WHERE id = $1
""",
device_id,
ip_address
)
async def _save_device_to_db(self, device: Device):
"""Save device to database"""
await self.db.execute(
"""
INSERT INTO devices (
id, user_id, device_name, platform, public_key,
device_fingerprint, push_token, status, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
""",
device.id,
device.user_id,
device.device_name,
device.platform.value,
device.public_key,
device.device_fingerprint,
device.push_token,
device.status.value,
device.created_at,
device.updated_at
)
async def _update_device_in_db(self, device: Device):
"""Update device in database"""
await self.db.execute(
"""
UPDATE devices
SET device_name = $2, push_token = $3, status = $4, updated_at = $5
WHERE id = $1
""",
device.id,
device.device_name,
device.push_token,
device.status.value,
device.updated_at
)
# Global device service instance
device_service = DeviceService()