paijo77 commited on
Commit
396dcd5
·
verified ·
1 Parent(s): 8d6df0a

update app/routers/admin.py

Browse files
Files changed (1) hide show
  1. app/routers/admin.py +295 -0
app/routers/admin.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException, Query, Request
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, func
4
+ from app.database import get_db
5
+ from app.db_models import Proxy, User, CandidateSource, ProxySource
6
+ from app.models.candidate import CandidateResponse
7
+ from app.dependencies import require_admin
8
+ from app.hunter.service import HunterService
9
+ from app.db_storage import db_storage
10
+ from typing import List, Optional
11
+ from pydantic import BaseModel
12
+
13
+ # All admin endpoints require admin role
14
+ router = APIRouter(
15
+ prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)]
16
+ )
17
+
18
+ # Access limiter from app state
19
+ from slowapi import Limiter
20
+ from slowapi.util import get_remote_address
21
+
22
+ limiter = Limiter(key_func=get_remote_address)
23
+
24
+
25
+ class UserUpdateRole(BaseModel):
26
+ role: str
27
+
28
+
29
+ class UserResponse(BaseModel):
30
+ id: int
31
+ email: str
32
+ username: str
33
+ role: str
34
+ created_at: Optional[str]
35
+
36
+ class Config:
37
+ from_attributes = True
38
+
39
+
40
+ @router.get("/users", response_model=dict)
41
+ @limiter.limit("30/minute")
42
+ async def list_users(
43
+ request: Request,
44
+ limit: int = Query(50, ge=1, le=200),
45
+ offset: int = Query(0, ge=0),
46
+ session: AsyncSession = Depends(get_db),
47
+ ):
48
+ users, total = await db_storage.get_users(session, limit=limit, offset=offset)
49
+ return {
50
+ "total": total,
51
+ "count": len(users),
52
+ "offset": offset,
53
+ "limit": limit,
54
+ "users": [
55
+ {
56
+ "id": u.id,
57
+ "email": u.email,
58
+ "username": u.username,
59
+ "role": u.role,
60
+ "created_at": u.created_at.isoformat() if u.created_at else None,
61
+ }
62
+ for u in users
63
+ ],
64
+ }
65
+
66
+
67
+ @router.get("/users/{user_id}", response_model=UserResponse)
68
+ @limiter.limit("60/minute")
69
+ async def get_user_details(
70
+ request: Request, user_id: int, session: AsyncSession = Depends(get_db)
71
+ ):
72
+ user = await db_storage.get_user_by_id(session, user_id)
73
+ if not user:
74
+ raise HTTPException(status_code=404, detail="User not found")
75
+ return user
76
+
77
+
78
+ @router.put("/users/{user_id}/role", response_model=UserResponse)
79
+ @limiter.limit("10/minute")
80
+ async def update_user_role(
81
+ request: Request,
82
+ user_id: int,
83
+ role_data: UserUpdateRole,
84
+ session: AsyncSession = Depends(get_db),
85
+ ):
86
+ if role_data.role not in ["user", "admin"]:
87
+ raise HTTPException(status_code=400, detail="Invalid role")
88
+
89
+ user = await db_storage.update_user_role(session, user_id, role_data.role)
90
+ if not user:
91
+ raise HTTPException(status_code=404, detail="User not found")
92
+ return user
93
+
94
+
95
+ @router.delete("/users/{user_id}")
96
+ @limiter.limit("5/minute")
97
+ async def delete_user(
98
+ request: Request, user_id: int, session: AsyncSession = Depends(get_db)
99
+ ):
100
+ # Prevent self-deletion if current user is the target
101
+ # This would require current_user from dependency, but we'll stick to basic admin check for now
102
+ success = await db_storage.delete_user(session, user_id)
103
+ if not success:
104
+ raise HTTPException(status_code=404, detail="User not found")
105
+ return {"message": "User deleted successfully"}
106
+
107
+
108
+ @router.post("/hunter/trigger")
109
+ @limiter.limit("5/minute")
110
+ async def trigger_hunt(request: Request, background_tasks: BackgroundTasks):
111
+ """
112
+ Manually trigger the Hunter Protocol to find new proxy sources.
113
+ """
114
+ service = HunterService()
115
+ background_tasks.add_task(service.run_hunt)
116
+ return {"status": "Hunter Protocol initiated", "message": "Check logs for progress"}
117
+
118
+
119
+ @router.get("/candidates", response_model=List[CandidateResponse])
120
+ @limiter.limit("30/minute")
121
+ async def list_candidates(
122
+ request: Request,
123
+ status: str = "pending",
124
+ limit: int = 50,
125
+ offset: int = 0,
126
+ session: AsyncSession = Depends(get_db),
127
+ ):
128
+ """
129
+ List discovered candidate sources.
130
+ """
131
+ stmt = (
132
+ select(CandidateSource)
133
+ .where(CandidateSource.status == status)
134
+ .order_by(CandidateSource.confidence_score.desc())
135
+ .limit(limit)
136
+ .offset(offset)
137
+ )
138
+ result = await session.execute(stmt)
139
+ return result.scalars().all()
140
+
141
+
142
+ @router.post("/candidates/{id}/approve")
143
+ @limiter.limit("10/minute")
144
+ async def approve_candidate(
145
+ request: Request, id: int, session: AsyncSession = Depends(get_db)
146
+ ):
147
+ """
148
+ Approve a candidate source and promote it to a real ProxySource.
149
+ """
150
+ # Get candidate
151
+ stmt = select(CandidateSource).where(CandidateSource.id == id)
152
+ result = await session.execute(stmt)
153
+ candidate = result.scalar_one_or_none()
154
+
155
+ if not candidate:
156
+ raise HTTPException(status_code=404, detail="Candidate not found")
157
+
158
+ if candidate.status == "approved":
159
+ raise HTTPException(status_code=400, detail="Candidate already approved")
160
+
161
+ # Check if URL already exists in sources (double check)
162
+ stmt_source = select(ProxySource).where(ProxySource.url == candidate.url)
163
+ result_source = await session.execute(stmt_source)
164
+ if result_source.scalar_one_or_none():
165
+ # Just mark as approved/duplicate
166
+ candidate.status = "approved"
167
+ await session.commit()
168
+ return {"status": "merged", "message": "Source already existed"}
169
+
170
+ # Create new ProxySource
171
+ # We assume it's a public list found on the web
172
+ new_source = ProxySource(
173
+ url=candidate.url,
174
+ type="public", # or "url" depending on your convention
175
+ name=f"Hunter: {candidate.domain}",
176
+ description=f"Auto-discovered via {candidate.discovery_method}",
177
+ enabled=True,
178
+ is_admin_source=True,
179
+ )
180
+ session.add(new_source)
181
+
182
+ # Update candidate status
183
+ candidate.status = "approved"
184
+
185
+ await session.commit()
186
+
187
+ return {"status": "approved", "source_id": new_source.id}
188
+
189
+
190
+ @router.get("/validation-stats")
191
+ @limiter.limit("60/minute")
192
+ async def get_validation_stats(
193
+ request: Request, session: AsyncSession = Depends(get_db)
194
+ ):
195
+ result = await session.execute(
196
+ select(
197
+ Proxy.validation_status,
198
+ func.count(Proxy.id).label("count"),
199
+ func.avg(Proxy.quality_score).label("avg_quality"),
200
+ func.avg(Proxy.latency_ms).label("avg_latency"),
201
+ ).group_by(Proxy.validation_status)
202
+ )
203
+
204
+ stats_by_status = {}
205
+ for row in result.all():
206
+ stats_by_status[row.validation_status] = {
207
+ "count": row.count,
208
+ "avg_quality": round(row.avg_quality, 2) if row.avg_quality else None,
209
+ "avg_latency": round(row.avg_latency, 2) if row.avg_latency else None,
210
+ }
211
+
212
+ total_result = await session.execute(select(func.count()).select_from(Proxy))
213
+ total = total_result.scalar()
214
+
215
+ validated_count = stats_by_status.get("validated", {}).get("count", 0)
216
+ pending_count = stats_by_status.get("pending", {}).get("count", 0)
217
+ failed_count = stats_by_status.get("failed", {}).get("count", 0)
218
+
219
+ validation_rate = round((validated_count / total) * 100, 2) if total > 0 else 0
220
+
221
+ return {
222
+ "total_proxies": total,
223
+ "by_status": stats_by_status,
224
+ "summary": {
225
+ "validated": validated_count,
226
+ "pending": pending_count,
227
+ "failed": failed_count,
228
+ "validation_rate_percent": validation_rate,
229
+ },
230
+ }
231
+
232
+
233
+ @router.get("/quality-distribution")
234
+ @limiter.limit("60/minute")
235
+ async def get_quality_distribution(
236
+ request: Request, session: AsyncSession = Depends(get_db)
237
+ ):
238
+ result = await session.execute(
239
+ select(Proxy.quality_score, func.count(Proxy.id).label("count"))
240
+ .where(Proxy.validation_status == "validated")
241
+ .group_by(Proxy.quality_score)
242
+ .order_by(Proxy.quality_score.desc())
243
+ )
244
+
245
+ distribution = {
246
+ "excellent": 0,
247
+ "good": 0,
248
+ "fair": 0,
249
+ "poor": 0,
250
+ }
251
+
252
+ for row in result.all():
253
+ if row.quality_score:
254
+ if row.quality_score >= 80:
255
+ distribution["excellent"] += row.count
256
+ elif row.quality_score >= 60:
257
+ distribution["good"] += row.count
258
+ elif row.quality_score >= 40:
259
+ distribution["fair"] += row.count
260
+ else:
261
+ distribution["poor"] += row.count
262
+
263
+ return distribution
264
+
265
+
266
+ @router.get("/recent-validations")
267
+ @limiter.limit("60/minute")
268
+ async def get_recent_validations(
269
+ request: Request, limit: int = 20, session: AsyncSession = Depends(get_db)
270
+ ):
271
+ result = await session.execute(
272
+ select(Proxy)
273
+ .where(Proxy.last_validated.isnot(None))
274
+ .order_by(Proxy.last_validated.desc())
275
+ .limit(limit)
276
+ )
277
+
278
+ proxies = result.scalars().all()
279
+
280
+ return {
281
+ "recent_validations": [
282
+ {
283
+ "url": p.url,
284
+ "validation_status": p.validation_status,
285
+ "quality_score": p.quality_score,
286
+ "latency_ms": p.latency_ms,
287
+ "country_code": p.country_code,
288
+ "anonymity": p.anonymity,
289
+ "last_validated": p.last_validated.isoformat()
290
+ if p.last_validated
291
+ else None,
292
+ }
293
+ for p in proxies
294
+ ]
295
+ }