registration.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """
  2. Device registration endpoint.
  3. """
  4. import asyncio
  5. import copy
  6. import json
  7. import secrets
  8. import subprocess
  9. from base64 import b64encode
  10. from datetime import datetime, timezone
  11. from pathlib import Path
  12. from typing import Annotated
  13. from fastapi import APIRouter, Depends, HTTPException, status
  14. from pydantic import BaseModel
  15. from sqlalchemy import select, update
  16. from sqlalchemy.ext.asyncio import AsyncSession
  17. from app.core.database import async_session_maker, get_db
  18. from app.models.device import Device
  19. from app.models.settings import Settings
  20. router = APIRouter()
  21. # Load default config from JSON file
  22. DEFAULT_CONFIG_PATH = Path(__file__).parent.parent.parent / "default_config.json"
  23. with open(DEFAULT_CONFIG_PATH, "r") as f:
  24. DEFAULT_CONFIG = json.load(f)
  25. class RegistrationRequest(BaseModel):
  26. """Device registration request."""
  27. device_id: str # MAC address
  28. ssh_public_key: str | None = None
  29. class RegistrationResponse(BaseModel):
  30. """Device registration response."""
  31. device_token: str
  32. device_password: str
  33. def _generate_token() -> str:
  34. """Generate 32-byte base64 token."""
  35. return b64encode(secrets.token_bytes(32)).decode("ascii")
  36. def _generate_password() -> str:
  37. """Generate 8-digit password."""
  38. n = secrets.randbelow(10**8)
  39. return f"{n:08d}"
  40. async def _sync_authorized_keys():
  41. """Sync all device SSH keys to /home/tunnel/.ssh/authorized_keys"""
  42. try:
  43. async with async_session_maker() as session:
  44. result = await session.execute(select(Device))
  45. devices = result.scalars().all()
  46. keys = []
  47. for device in devices:
  48. if device.config and 'ssh_public_key' in device.config:
  49. ssh_key = device.config['ssh_public_key'].strip()
  50. if ssh_key:
  51. keys.append(f"{ssh_key} # {device.mac_address}")
  52. authorized_keys_content = "\n".join(keys) + "\n" if keys else ""
  53. # Write using sudo
  54. subprocess.run(
  55. ["sudo", "tee", "/home/tunnel/.ssh/authorized_keys"],
  56. input=authorized_keys_content.encode(),
  57. stdout=subprocess.DEVNULL,
  58. check=True
  59. )
  60. subprocess.run(
  61. ["sudo", "chmod", "600", "/home/tunnel/.ssh/authorized_keys"],
  62. check=True
  63. )
  64. subprocess.run(
  65. ["sudo", "chown", "tunnel:tunnel", "/home/tunnel/.ssh/authorized_keys"],
  66. check=True
  67. )
  68. print(f"[SSH] Synced {len(keys)} keys to authorized_keys")
  69. except Exception as e:
  70. print(f"[SSH] Failed to sync authorized_keys: {e}")
  71. @router.post("/registration", response_model=RegistrationResponse, status_code=201)
  72. async def register_device(
  73. data: RegistrationRequest,
  74. db: Annotated[AsyncSession, Depends(get_db)],
  75. ):
  76. """
  77. Register new device or return existing credentials.
  78. Requires auto_registration to be enabled in settings.
  79. """
  80. mac_address = data.device_id.lower().strip()
  81. if not mac_address:
  82. raise HTTPException(
  83. status_code=status.HTTP_400_BAD_REQUEST,
  84. detail="Missing device_id",
  85. )
  86. # Check if device already exists
  87. result = await db.execute(select(Device).where(Device.mac_address == mac_address))
  88. device = result.scalar_one_or_none()
  89. if device:
  90. # Return existing credentials
  91. if not device.device_token or not device.device_password:
  92. # Re-generate if missing
  93. device.device_token = _generate_token()
  94. device.device_password = _generate_password()
  95. await db.commit()
  96. return RegistrationResponse(
  97. device_token=device.device_token,
  98. device_password=device.device_password,
  99. )
  100. # Check auto-registration setting
  101. settings_result = await db.execute(
  102. select(Settings).where(Settings.key == "auto_registration")
  103. )
  104. auto_reg_setting = settings_result.scalar_one_or_none()
  105. if not auto_reg_setting or not auto_reg_setting.value.get("enabled", False):
  106. raise HTTPException(
  107. status_code=status.HTTP_401_UNAUTHORIZED,
  108. detail="Registration disabled. Contact administrator.",
  109. )
  110. # Create new device with default config
  111. # Deep copy to avoid modifying the global default
  112. device_config = copy.deepcopy(DEFAULT_CONFIG)
  113. # Add SSH public key if provided
  114. if data.ssh_public_key:
  115. device_config["ssh_public_key"] = data.ssh_public_key
  116. device = Device(
  117. mac_address=mac_address,
  118. organization_id=None, # Unassigned
  119. status="online",
  120. config=device_config,
  121. device_token=_generate_token(),
  122. device_password=_generate_password(),
  123. )
  124. db.add(device)
  125. await db.flush()
  126. # Update last_device_at
  127. auto_reg_setting.value["last_device_at"] = datetime.now(timezone.utc).isoformat()
  128. await db.commit()
  129. await db.refresh(device)
  130. print(f"[REGISTRATION] device={mac_address} simple_id={device.simple_id}")
  131. # Sync SSH keys to authorized_keys (background task)
  132. if data.ssh_public_key:
  133. asyncio.create_task(_sync_authorized_keys())
  134. return RegistrationResponse(
  135. device_token=device.device_token,
  136. device_password=device.device_password,
  137. )