107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
import secrets
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from app.models import CommandPayload
|
|
from app.internal import firestore as fstore
|
|
from app.internal.mqtt_handler import mqtt_handler
|
|
from app.internal.auth import require_admin_token
|
|
from app.routers.tokens import assign_token, release_token
|
|
|
|
router = APIRouter(prefix="/nodes", tags=["nodes"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_nodes():
|
|
return await fstore.collection_list("nodes")
|
|
|
|
|
|
@router.get("/{node_id}")
|
|
async def get_node(node_id: str):
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
return node
|
|
|
|
|
|
@router.post("/{node_id}/approve")
|
|
async def approve_node(node_id: str, _: dict = Depends(require_admin_token)):
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
|
|
api_key = secrets.token_hex(32)
|
|
await fstore.doc_set("node_keys", node_id, {"node_id": node_id, "api_key": api_key}, merge=False)
|
|
await fstore.doc_update("nodes", node_id, {"approval_status": "approved"})
|
|
mqtt_handler.publish_node_key(node_id, api_key)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{node_id}/reject")
|
|
async def reject_node(node_id: str, _: dict = Depends(require_admin_token)):
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
await fstore.doc_update("nodes", node_id, {"approval_status": "rejected"})
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{node_id}/command")
|
|
async def send_command(node_id: str, cmd: CommandPayload):
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
|
|
payload = cmd.model_dump(exclude_none=True)
|
|
|
|
if cmd.action == "discord_join":
|
|
preferred = payload.pop("preferred_token_id", None)
|
|
token = await assign_token(node_id, preferred_token_id=preferred)
|
|
if not token:
|
|
raise HTTPException(503, "No Discord bot tokens available in the pool.")
|
|
payload["token"] = token
|
|
|
|
elif cmd.action == "discord_leave":
|
|
await release_token(node_id)
|
|
|
|
if not mqtt_handler.send_command(node_id, payload):
|
|
raise HTTPException(503, "MQTT broker unavailable — command not delivered.")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{node_id}/reissue-key")
|
|
async def reissue_node_key(node_id: str, _: dict = Depends(require_admin_token)):
|
|
"""Generate a new API key for the node and push it via MQTT (retained).
|
|
Use this to rotate a key or recover a node whose key was lost."""
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
api_key = secrets.token_hex(32)
|
|
await fstore.doc_set("node_keys", node_id, {"node_id": node_id, "api_key": api_key}, merge=False)
|
|
mqtt_handler.publish_node_key(node_id, api_key)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{node_id}/config/{system_id}")
|
|
async def assign_system(node_id: str, system_id: str):
|
|
"""
|
|
Assign a system to a node. Fetches the system config from Firestore
|
|
and pushes it to the node via MQTT, then marks the node as configured.
|
|
"""
|
|
node = await fstore.doc_get("nodes", node_id)
|
|
if not node:
|
|
raise HTTPException(404, f"Node '{node_id}' not found.")
|
|
|
|
system = await fstore.doc_get("systems", system_id)
|
|
if not system:
|
|
raise HTTPException(404, f"System '{system_id}' not found.")
|
|
|
|
# Push config to the node via MQTT
|
|
mqtt_handler.push_config(node_id, system)
|
|
|
|
# Update Firestore
|
|
await fstore.doc_update("nodes", node_id, {
|
|
"assigned_system_id": system_id,
|
|
"configured": True,
|
|
})
|
|
|
|
return {"ok": True}
|