from datetime import datetime, timezone from typing import Any, Dict, List, Optional from fastapi import APIRouter, Body, Header, HTTPException import requests from app.core.config import ( SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_TIMEOUT_SECONDS, supabase_configured, LOGGER, ) from app.security.auth import require_authenticated_user, _supabase_headers, _normalize_spaces router = APIRouter(tags=["notifications"]) def _upsert_notification_device_token( *, user_id: str, fcm_token: str, platform: Optional[str], device_id: Optional[str], ) -> Optional[Dict[str, Any]]: payload: Dict[str, Any] = { "user_id": user_id, "fcm_token": fcm_token, "is_active": True, "last_seen_at": datetime.now(timezone.utc).isoformat(), } if platform: payload["platform"] = platform if device_id: payload["device_id"] = device_id response = requests.post( f"{SUPABASE_URL}/rest/v1/notification_device_tokens", params={"on_conflict": "fcm_token"}, json=payload, headers=_supabase_headers( api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY, content_type="application/json", prefer="resolution=merge-duplicates,return=representation", ), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code not in {200, 201}: raise HTTPException(status_code=502, detail=f"Failed to save device token: {response.text[:220]}") rows = response.json() if response.text else [] if isinstance(rows, list) and rows and isinstance(rows[0], dict): return rows[0] return None def _list_notification_events_for_user(user_id: str, limit: int) -> List[Dict[str, Any]]: response = requests.get( f"{SUPABASE_URL}/rest/v1/notification_events", params={ "select": "id,event_type,title,body,data,read_at,created_at", "user_id": f"eq.{user_id}", "order": "created_at.desc", "limit": str(limit), }, headers=_supabase_headers( api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY, ), timeout=SUPABASE_TIMEOUT_SECONDS, ) if response.status_code != 200: raise HTTPException(status_code=502, detail=f"Failed to load notifications: {response.text[:240]}") rows = response.json() if not isinstance(rows, list): return [] return [row for row in rows if isinstance(row, dict)] @router.post("/notifications/fcm-token") def notifications_register_fcm_token( payload: Dict[str, Any] = Body(...), authorization: Optional[str] = Header(default=None), ) -> Dict[str, Any]: if not supabase_configured(): raise HTTPException(status_code=503, detail="Supabase is not configured.") request_user = require_authenticated_user(authorization) if request_user is None: raise HTTPException(status_code=401, detail="Authentication is required.") requester_id = str(request_user.get("id") or "").strip() if not requester_id: raise HTTPException(status_code=401, detail="Authenticated user id is missing.") fcm_token = _normalize_spaces(str(payload.get("fcm_token") or payload.get("token") or "")) if not fcm_token: raise HTTPException(status_code=400, detail="fcm_token is required.") platform = _normalize_spaces(str(payload.get("platform") or "")) or None device_id = _normalize_spaces(str(payload.get("device_id") or "")) or None row = _upsert_notification_device_token( user_id=requester_id, fcm_token=fcm_token, platform=platform, device_id=device_id, ) return {"status": "ok", "device": row, "message": "FCM token registered."} @router.get("/notifications/my") def notifications_my( limit: int = 50, authorization: Optional[str] = Header(default=None), ) -> Dict[str, Any]: request_user = require_authenticated_user(authorization) if request_user is None: raise HTTPException(status_code=401, detail="Authentication is required.") requester_id = str(request_user.get("id") or "").strip() if not requester_id: raise HTTPException(status_code=401, detail="Authenticated user id is missing.") resolved_limit = max(1, min(200, int(limit))) rows = _list_notification_events_for_user(requester_id, resolved_limit) return {"status": "ok", "count": len(rows), "notifications": rows}