""" Device authentication service. """ from datetime import datetime, timezone from typing import Any from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import create_access_token from app.models.device import Device from app.models.settings import Settings async def authenticate_device(db: AsyncSession, mac_address: str) -> dict[str, Any]: """ Authenticate device by MAC address and return JWT token. If device not found and auto_registration is enabled, creates new device automatically. Args: db: Database session mac_address: Device MAC address Returns: Dict with access_token, device_id, simple_id, organization_id Raises: ValueError: If device not found and auto_registration disabled, or if device is inactive """ # Find device by MAC address result = await db.execute(select(Device).where(Device.mac_address == mac_address)) device = result.scalar_one_or_none() if not device: # Check auto-registration setting settings_result = await db.execute( select(Settings).where(Settings.key == "auto_registration") ) auto_reg_setting = settings_result.scalar_one_or_none() if not auto_reg_setting or not auto_reg_setting.value.get("enabled", False): raise ValueError( f"Device with MAC {mac_address} not found. " "Contact administrator to enable auto-registration." ) # Auto-register new device device = Device( mac_address=mac_address, organization_id=None, # Unassigned, admin will assign later status="online", config={}, ) db.add(device) await db.flush() # Get device.id # Update last_device_at timestamp auto_reg_setting.value["last_device_at"] = datetime.now(timezone.utc).isoformat() await db.commit() await db.refresh(device) if device.status == "inactive": raise ValueError(f"Device {device.simple_id} is inactive") # Create JWT token with device info token_data = { "sub": str(device.id), # Device ID as subject "type": "device", # Token type "mac": mac_address, "org_id": device.organization_id, } access_token = create_access_token(token_data) # Update last_seen_at await db.execute( update(Device) .where(Device.id == device.id) .values( last_seen_at=datetime.now(timezone.utc), status="online", ) ) await db.commit() return { "access_token": access_token, "token_type": "bearer", "device_id": device.id, "simple_id": device.simple_id, "organization_id": device.organization_id, } async def update_device_heartbeat( db: AsyncSession, device_id: int, status: str, metadata: dict[str, Any] | None = None ) -> datetime: """ Update device heartbeat (last_seen_at and status). Args: db: Database session device_id: Device ID status: Device status metadata: Optional metadata to update Returns: Updated last_seen_at timestamp """ now = datetime.now(timezone.utc) update_values = { "last_seen_at": now, "status": status, } # Update config metadata if provided if metadata: result = await db.execute(select(Device).where(Device.id == device_id)) device = result.scalar_one() # Merge metadata into config config = device.config or {} config.update(metadata) update_values["config"] = config await db.execute( update(Device).where(Device.id == device_id).values(**update_values) ) await db.commit() return now