File size: 6,205 Bytes
e561008
 
 
 
 
 
 
 
 
 
 
a32af27
89a70e2
 
e561008
 
 
 
 
 
889e86b
a32af27
89a70e2
 
e561008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889e86b
 
e561008
 
889e86b
e561008
 
 
 
 
 
 
 
 
 
a32af27
 
 
 
 
 
 
 
 
 
 
 
e561008
 
 
 
 
 
 
 
 
 
89a70e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
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}