Spaces:
Running
Running
| from typing import Optional | |
| from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query | |
| from pydantic import BaseModel | |
| from .agents.clients import get_redis_client | |
| from .auth import _service_client, invalidate_status_cache, verify_token | |
| router = APIRouter(prefix="/admin", tags=["Admin"]) | |
| # Prefix used by the multi-agent response cache (_cache_key in | |
| # agents_orchestration.py builds keys as "mac:<route>:<scope>:<hash>"). | |
| _PROMPT_CACHE_PREFIX = "mac:" | |
| _ALLOWED_STATUSES = {"pending", "approved", "rejected"} | |
| def require_admin(token_user_id: str = Depends(verify_token)) -> str: | |
| """Only users whose profiles.role is 'admin' may manage approvals.""" | |
| try: | |
| res = _service_client().table("profiles").select("role").eq("id", token_user_id).execute() | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| role = res.data[0].get("role") if res.data else None | |
| if role != "admin": | |
| raise HTTPException(status_code=403, detail="Admin access required") | |
| return token_user_id | |
| async def list_users( | |
| status: Optional[str] = Query(default=None), | |
| _admin_id: str = Depends(require_admin), | |
| ): | |
| if status is not None and status not in _ALLOWED_STATUSES: | |
| raise HTTPException(status_code=422, detail=f"status must be one of {sorted(_ALLOWED_STATUSES)}") | |
| try: | |
| query = ( | |
| _service_client() | |
| .table("profiles") | |
| .select("id, email, full_name, role, status, created_at") | |
| .order("created_at", desc=True) | |
| ) | |
| if status is not None: | |
| query = query.eq("status", status) | |
| res = query.execute() | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| return {"data": res.data or []} | |
| class StatusUpdate(BaseModel): | |
| status: str | |
| async def set_user_status( | |
| user_id: str = Path(...), | |
| payload: StatusUpdate = Body(...), | |
| admin_id: str = Depends(require_admin), | |
| ): | |
| if payload.status not in ("approved", "rejected"): | |
| raise HTTPException(status_code=422, detail="status must be 'approved' or 'rejected'") | |
| if user_id == admin_id: | |
| raise HTTPException(status_code=400, detail="Admins cannot change their own status") | |
| try: | |
| res = ( | |
| _service_client() | |
| .table("profiles") | |
| .update({"status": payload.status}) | |
| .eq("id", user_id) | |
| .execute() | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| if not res.data: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| invalidate_status_cache(user_id) | |
| return {"data": res.data[0]} | |
| async def clear_prompt_cache(_admin_id: str = Depends(require_admin)): | |
| """Delete every cached multi-agent response (the "mac:*" keys in Redis). | |
| These are the prompt/response cache entries built by the chat pipeline; they | |
| are regenerated on demand, so clearing them just forces fresh answers. | |
| """ | |
| try: | |
| redis = get_redis_client() | |
| keys = [key async for key in redis.scan_iter(match=f"{_PROMPT_CACHE_PREFIX}*", count=500)] | |
| deleted = await redis.delete(*keys) if keys else 0 | |
| except Exception as e: | |
| raise HTTPException(status_code=502, detail=f"Failed to clear cache: {e}") | |
| return {"deleted": deleted} | |