device_auth_service.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """
  2. Device authentication service.
  3. """
  4. from datetime import datetime, timezone
  5. from typing import Any
  6. from sqlalchemy import select, update
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from app.core.security import create_access_token
  9. from app.models.device import Device
  10. from app.models.settings import Settings
  11. async def authenticate_device(db: AsyncSession, mac_address: str) -> dict[str, Any]:
  12. """
  13. Authenticate device by MAC address and return JWT token.
  14. If device not found and auto_registration is enabled, creates new device automatically.
  15. Args:
  16. db: Database session
  17. mac_address: Device MAC address
  18. Returns:
  19. Dict with access_token, device_id, simple_id, organization_id
  20. Raises:
  21. ValueError: If device not found and auto_registration disabled, or if device is inactive
  22. """
  23. # Find device by MAC address
  24. result = await db.execute(select(Device).where(Device.mac_address == mac_address))
  25. device = result.scalar_one_or_none()
  26. if not device:
  27. # Check auto-registration setting
  28. settings_result = await db.execute(
  29. select(Settings).where(Settings.key == "auto_registration")
  30. )
  31. auto_reg_setting = settings_result.scalar_one_or_none()
  32. if not auto_reg_setting or not auto_reg_setting.value.get("enabled", False):
  33. raise ValueError(
  34. f"Device with MAC {mac_address} not found. "
  35. "Contact administrator to enable auto-registration."
  36. )
  37. # Auto-register new device
  38. device = Device(
  39. mac_address=mac_address,
  40. organization_id=None, # Unassigned, admin will assign later
  41. status="online",
  42. config={},
  43. )
  44. db.add(device)
  45. await db.flush() # Get device.id
  46. # Update last_device_at timestamp
  47. auto_reg_setting.value["last_device_at"] = datetime.now(timezone.utc).isoformat()
  48. await db.commit()
  49. await db.refresh(device)
  50. if device.status == "inactive":
  51. raise ValueError(f"Device {device.simple_id} is inactive")
  52. # Create JWT token with device info
  53. token_data = {
  54. "sub": str(device.id), # Device ID as subject
  55. "type": "device", # Token type
  56. "mac": mac_address,
  57. "org_id": device.organization_id,
  58. }
  59. access_token = create_access_token(token_data)
  60. # Update last_seen_at
  61. await db.execute(
  62. update(Device)
  63. .where(Device.id == device.id)
  64. .values(
  65. last_seen_at=datetime.now(timezone.utc),
  66. status="online",
  67. )
  68. )
  69. await db.commit()
  70. return {
  71. "access_token": access_token,
  72. "token_type": "bearer",
  73. "device_id": device.id,
  74. "simple_id": device.simple_id,
  75. "organization_id": device.organization_id,
  76. }
  77. async def update_device_heartbeat(
  78. db: AsyncSession, device_id: int, status: str, metadata: dict[str, Any] | None = None
  79. ) -> datetime:
  80. """
  81. Update device heartbeat (last_seen_at and status).
  82. Args:
  83. db: Database session
  84. device_id: Device ID
  85. status: Device status
  86. metadata: Optional metadata to update
  87. Returns:
  88. Updated last_seen_at timestamp
  89. """
  90. now = datetime.now(timezone.utc)
  91. update_values = {
  92. "last_seen_at": now,
  93. "status": status,
  94. }
  95. # Update config metadata if provided
  96. if metadata:
  97. result = await db.execute(select(Device).where(Device.id == device_id))
  98. device = result.scalar_one()
  99. # Merge metadata into config
  100. config = device.config or {}
  101. config.update(metadata)
  102. update_values["config"] = config
  103. await db.execute(
  104. update(Device).where(Device.id == device_id).values(**update_values)
  105. )
  106. await db.commit()
  107. return now