import uuid from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, HTTPException, Depends, Query from app.models import AlertRule, AlertRuleUpdate from app.internal import firestore as fstore from app.internal.auth import require_admin_token, require_service_or_firebase_token, resolve_caller_org_id from app.internal.tenancy import FOUNDING_ORG_ID router = APIRouter(tags=["alerts"]) # --------------------------------------------------------------------------- # Alert events (triggered alerts) # --------------------------------------------------------------------------- @router.get("/alerts") async def list_alerts( acknowledged: Optional[bool] = None, decoded: dict = Depends(require_service_or_firebase_token), ): filters = {} if acknowledged is not None: filters["acknowledged"] = acknowledged org_id = await resolve_caller_org_id(decoded) if org_id is not None: filters["org_id"] = org_id return await fstore.collection_list("alert_events", **filters) @router.post("/alerts/{alert_id}/acknowledge") async def acknowledge_alert(alert_id: str, decoded: dict = Depends(require_service_or_firebase_token)): doc = await fstore.doc_get("alert_events", alert_id) if not doc: raise HTTPException(404, f"Alert '{alert_id}' not found.") # SAAS_PLAN.md B2c: previously any authenticated viewer could acknowledge # any org's alerts. org_id may be absent on a pre-tenancy alert_event # that hasn't been through scripts/backfill_org_id.py yet — allow those # through rather than making them permanently unacknowledgeable. org_id = await resolve_caller_org_id(decoded) if org_id is not None and doc.get("org_id") and doc.get("org_id") != org_id: raise HTTPException(404, f"Alert '{alert_id}' not found.") await fstore.doc_update("alert_events", alert_id, {"acknowledged": True}) return {"ok": True} # --------------------------------------------------------------------------- # Alert rules # --------------------------------------------------------------------------- @router.get("/alert-rules") async def list_alert_rules(decoded: dict = Depends(require_service_or_firebase_token)): org_id = await resolve_caller_org_id(decoded) if org_id is not None: return await fstore.collection_list("alert_rules", org_id=org_id) return await fstore.collection_list("alert_rules") @router.post("/alert-rules") async def create_alert_rule( body: AlertRule, org_id: Optional[str] = Query(None, description="Platform-admin only — defaults to the founding org."), _: dict = Depends(require_admin_token), ): rule_id = str(uuid.uuid4()) doc = { "rule_id": rule_id, "org_id": org_id or FOUNDING_ORG_ID, "name": body.name, "keywords": body.keywords, "talkgroup_ids": body.talkgroup_ids, "enabled": body.enabled, "discord_webhook": body.discord_webhook, "created_at": datetime.now(timezone.utc).isoformat(), } await fstore.doc_set("alert_rules", rule_id, doc, merge=False) return doc @router.put("/alert-rules/{rule_id}") async def update_alert_rule(rule_id: str, body: AlertRuleUpdate, _: dict = Depends(require_admin_token)): doc = await fstore.doc_get("alert_rules", rule_id) if not doc: raise HTTPException(404, f"Alert rule '{rule_id}' not found.") updates = body.model_dump(exclude_none=True) await fstore.doc_update("alert_rules", rule_id, updates) return {**doc, **updates} @router.delete("/alert-rules/{rule_id}") async def delete_alert_rule(rule_id: str, _: dict = Depends(require_admin_token)): doc = await fstore.doc_get("alert_rules", rule_id) if not doc: raise HTTPException(404, f"Alert rule '{rule_id}' not found.") await fstore.doc_delete("alert_rules", rule_id) return {"ok": True}