| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- """
- 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
|