settings.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. """
  2. Superadmin endpoints for system settings.
  3. """
  4. from datetime import datetime, timedelta, timezone
  5. from typing import Annotated
  6. from fastapi import APIRouter, Depends, HTTPException, status
  7. from pydantic import BaseModel
  8. from sqlalchemy import select
  9. from sqlalchemy.ext.asyncio import AsyncSession
  10. from app.api.deps import get_current_superadmin
  11. from app.core.database import get_db
  12. from app.models.settings import Settings
  13. from app.models.user import User
  14. router = APIRouter()
  15. class AutoRegistrationResponse(BaseModel):
  16. """Auto-registration status response."""
  17. enabled: bool
  18. last_device_at: str | None
  19. time_left: int # Seconds until auto-disable (0 if disabled)
  20. class AutoRegistrationToggleRequest(BaseModel):
  21. """Auto-registration toggle request."""
  22. enabled: bool
  23. @router.get("/auto-registration", response_model=AutoRegistrationResponse)
  24. async def get_auto_registration_status(
  25. db: Annotated[AsyncSession, Depends(get_db)],
  26. current_user: Annotated[User, Depends(get_current_superadmin)],
  27. ):
  28. """
  29. Get auto-registration status.
  30. Returns current state and last device registration timestamp.
  31. """
  32. result = await db.execute(
  33. select(Settings).where(Settings.key == "auto_registration")
  34. )
  35. setting = result.scalar_one_or_none()
  36. if not setting:
  37. # Create default setting if not exists
  38. setting = Settings(
  39. key="auto_registration",
  40. value={"enabled": False, "last_device_at": None},
  41. )
  42. db.add(setting)
  43. await db.commit()
  44. # Check if should auto-disable (1 hour since last device)
  45. enabled = setting.value.get("enabled", False)
  46. last_device_at_str = setting.value.get("last_device_at")
  47. time_left = 0
  48. if enabled and last_device_at_str:
  49. last_device_at = datetime.fromisoformat(last_device_at_str)
  50. time_since = datetime.now(timezone.utc) - last_device_at
  51. time_left = max(0, int(3600 - time_since.total_seconds())) # 1 hour = 3600 seconds
  52. if time_since > timedelta(hours=1):
  53. # Auto-disable - must reassign whole dict for SQLAlchemy
  54. setting.value = {**setting.value, "enabled": False}
  55. await db.commit()
  56. enabled = False
  57. time_left = 0
  58. return AutoRegistrationResponse(
  59. enabled=enabled,
  60. last_device_at=last_device_at_str,
  61. time_left=time_left,
  62. )
  63. @router.post("/auto-registration", response_model=AutoRegistrationResponse)
  64. async def toggle_auto_registration(
  65. data: AutoRegistrationToggleRequest,
  66. db: Annotated[AsyncSession, Depends(get_db)],
  67. current_user: Annotated[User, Depends(get_current_superadmin)],
  68. ):
  69. """
  70. Enable or disable auto-registration of new devices.
  71. When enabled, devices that authenticate with unknown MAC addresses will be
  72. automatically registered in the system (unassigned to any organization).
  73. Auto-registration will automatically disable after 1 hour of the last registered device.
  74. """
  75. result = await db.execute(
  76. select(Settings).where(Settings.key == "auto_registration")
  77. )
  78. setting = result.scalar_one_or_none()
  79. if not setting:
  80. setting = Settings(
  81. key="auto_registration",
  82. value={"enabled": data.enabled, "last_device_at": None},
  83. )
  84. db.add(setting)
  85. else:
  86. # Must reassign the whole dict for SQLAlchemy to detect the change
  87. new_value = {**setting.value, "enabled": data.enabled}
  88. if not data.enabled:
  89. # Reset last_device_at when disabling
  90. new_value["last_device_at"] = None
  91. setting.value = new_value
  92. await db.commit()
  93. # Calculate time_left
  94. time_left = 0
  95. enabled = setting.value.get("enabled", False)
  96. last_device_at_str = setting.value.get("last_device_at")
  97. if enabled and last_device_at_str:
  98. last_device_at = datetime.fromisoformat(last_device_at_str)
  99. time_since = datetime.now(timezone.utc) - last_device_at
  100. time_left = max(0, int(3600 - time_since.total_seconds()))
  101. return AutoRegistrationResponse(
  102. enabled=enabled,
  103. last_device_at=last_device_at_str,
  104. time_left=time_left,
  105. )
  106. class SettingUpdate(BaseModel):
  107. """Generic setting update request."""
  108. value: dict
  109. class SettingResponse(BaseModel):
  110. """Generic setting response."""
  111. key: str
  112. value: dict
  113. updated_at: str | None
  114. @router.get("/setting/{key}", response_model=SettingResponse)
  115. async def get_setting(
  116. key: str,
  117. db: Annotated[AsyncSession, Depends(get_db)],
  118. current_user: Annotated[User, Depends(get_current_superadmin)],
  119. ):
  120. """Get a specific setting by key."""
  121. result = await db.execute(select(Settings).where(Settings.key == key))
  122. setting = result.scalar_one_or_none()
  123. if not setting:
  124. raise HTTPException(
  125. status_code=status.HTTP_404_NOT_FOUND,
  126. detail=f"setting_not_found:{key}",
  127. )
  128. return SettingResponse(
  129. key=setting.key,
  130. value=setting.value,
  131. updated_at=setting.updated_at.isoformat() if setting.updated_at else None,
  132. )
  133. @router.put("/setting/{key}", response_model=SettingResponse)
  134. async def update_setting(
  135. key: str,
  136. data: SettingUpdate,
  137. db: Annotated[AsyncSession, Depends(get_db)],
  138. current_user: Annotated[User, Depends(get_current_superadmin)],
  139. ):
  140. """Update a specific setting by key."""
  141. result = await db.execute(select(Settings).where(Settings.key == key))
  142. setting = result.scalar_one_or_none()
  143. if not setting:
  144. raise HTTPException(
  145. status_code=status.HTTP_404_NOT_FOUND,
  146. detail=f"setting_not_found:{key}",
  147. )
  148. setting.value = data.value
  149. setting.updated_at = datetime.now(timezone.utc)
  150. await db.commit()
  151. await db.refresh(setting)
  152. return SettingResponse(
  153. key=setting.key,
  154. value=setting.value,
  155. updated_at=setting.updated_at.isoformat(),
  156. )