venthrive-api / routes /admin.py
GitHub Actions
Sync from GitHub · 2026-10-22 20:43 UTC
89a70e2
Raw History Blame Contribute Delete
6.21 kB
"""
Admin Routes — Protected by admin role verification
──────────────────────────────────────────────────────────────────────────────
All admin actions use Firebase Admin SDK (bypasses Firestore rules).
The requesting user must have role: 'admin' in their Firestore document.
Routes:
GET /api/admin/users → list all users (paginated)
PUT /api/admin/users/{uid} → update locked fields (fullName, phone, role)
DELETE /api/admin/users/{uid} → delete user account + Firestore data
GET /api/admin/stats → platform overview stats
GET /api/admin/audit → audit trail of manual admin actions
GET /api/admin/niche-requests → niches people couldn't find, most requested first
PUT /api/admin/niche-requests/{id} → mark one added / dismissed / open
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from firebase_admin import auth as fb_auth, firestore
from auth import verify_firebase_token
from routes.account import delete_all_user_data
from utils.supabase_client import get_supabase
from utils import niche_requests
from utils.audit import log_action
router = APIRouter()
def _verify_admin(user: dict) -> str:
"""Verifies the requesting user has role: 'admin'. Returns their UID."""
uid = user.get("uid")
if not uid:
raise HTTPException(status_code=401, detail="Unauthorized.")
db = firestore.client()
doc = db.collection("user_data").document(uid).get()
role = doc.to_dict().get("role", "user") if doc.exists else "user"
if role != "admin":
raise HTTPException(status_code=403, detail="Admin access required.")
return uid
class UpdateUserRequest(BaseModel):
fullName: str | None = None
phone: str | None = None
role: str | None = None # only 'user' or 'admin'
@router.get("/users")
def list_users(
limit: int = 50,
user: dict = Depends(verify_firebase_token),
):
_verify_admin(user)
db = firestore.client()
docs = db.collection("user_data").limit(limit).stream()
users = []
for doc in docs:
d = doc.to_dict()
users.append({
"uid": doc.id,
"fullName": d.get("fullName", ""),
"email": d.get("email", ""),
"phone": d.get("phone", ""),
"role": d.get("role", "user"),
"xpTotal": d.get("xpTotal", 0),
"createdAt": str(d.get("createdAt", "")),
})
return {"users": users, "count": len(users)}
@router.put("/users/{target_uid}")
def update_user(
target_uid: str,
body: UpdateUserRequest,
user: dict = Depends(verify_firebase_token),
):
_verify_admin(user)
if body.role and body.role not in ("user", "admin"):
raise HTTPException(status_code=400, detail="role must be 'user' or 'admin'")
db = firestore.client()
updates = {}
if body.fullName is not None: updates["fullName"] = body.fullName
if body.phone is not None: updates["phone"] = body.phone
if body.role is not None: updates["role"] = body.role
if not updates:
raise HTTPException(status_code=400, detail="Nothing to update.")
db.collection("user_data").document(target_uid).update(updates)
# Sync display name to Firebase Auth if fullName changed
if body.fullName:
try:
fb_auth.update_user(target_uid, display_name=body.fullName)
except Exception:
pass # non-critical
return {"success": True, "updated": list(updates.keys())}
@router.delete("/users/{target_uid}")
def delete_user(
target_uid: str,
user: dict = Depends(verify_firebase_token),
):
admin_uid = _verify_admin(user)
if target_uid == admin_uid:
# Self-deletion as an admin goes through DELETE /api/account/me instead,
# same as any other user — this endpoint is for deleting *other* accounts.
raise HTTPException(status_code=400, detail="Admins cannot delete their own account.")
delete_all_user_data(target_uid)
# Delete Firebase Auth account
try:
fb_auth.delete_user(target_uid)
except fb_auth.UserNotFoundError:
pass # already deleted
return {"success": True, "deleted": target_uid}
@router.get("/audit")
def audit_trail(kind: str | None = None, limit: int = 200, user: dict = Depends(verify_firebase_token)):
"""Every manual admin action (review decisions, source toggles, run starts),
newest first — see backend/sql/admin_audit_schema.sql."""
_verify_admin(user)
db = get_supabase()
query = db.table("admin_audit_log").select("*").order("at", desc=True).limit(limit)
if kind:
query = query.eq("kind", kind)
return {"entries": query.execute().data}
@router.get("/stats")
def platform_stats(user: dict = Depends(verify_firebase_token)):
_verify_admin(user)
db = firestore.client()
user_count = len(list(db.collection("user_data").stream()))
# Count businesses across all users (approximate - stream limit applies)
return {
"totalUsers": user_count,
"note": "Deep counts require Cloud Functions — this shows top-level totals only."
}
class NicheStatusRequest(BaseModel):
status: str # open | added | dismissed
@router.get("/niche-requests")
def list_niche_requests(status: str | None = None, user: dict = Depends(verify_firebase_token)):
_verify_admin(user)
return {"requests": niche_requests.list_requests(status)}
@router.put("/niche-requests/{key}")
def update_niche_request(key: str, body: NicheStatusRequest, user: dict = Depends(verify_firebase_token)):
admin_uid = _verify_admin(user)
try:
text = niche_requests.set_status(key, body.status)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except KeyError:
raise HTTPException(status_code=404, detail="Niche request not found.")
log_action(admin_uid, "niche", body.status, text, target=key)
return {"success": True}