paijo77 commited on
Commit
a8009db
·
verified ·
1 Parent(s): cdd38c8

update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +481 -0
app/main.py ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FastAPI Application Entry Point
2
+ from app.config import settings
3
+
4
+ from fastapi import FastAPI, HTTPException, Query, Depends, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse, FileResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from typing import Optional
9
+ import os
10
+ from slowapi import Limiter, _rate_limit_exceeded_handler
11
+ from slowapi.util import get_remote_address
12
+ from slowapi.errors import RateLimitExceeded
13
+ from app.models import SourceConfig, SourceType
14
+ from app.grabber import GitHubGrabber
15
+ from app.sources import SourceRegistry
16
+ from app.database import init_db, AsyncSessionLocal, get_db, AsyncSession
17
+ from app.db_storage import db_storage
18
+ from app.routers import auth, sources, proxies, notifications, validation, admin
19
+ from app.dependencies import require_admin
20
+ from app.db_models import User
21
+ from app.background_validator import background_validation_worker
22
+ import asyncio
23
+ import logging
24
+
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
28
+ )
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Configure rate limiting
32
+ limiter = Limiter(key_func=get_remote_address)
33
+
34
+ app = FastAPI(
35
+ title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json"
36
+ )
37
+
38
+ # Add rate limiter state
39
+ app.state.limiter = limiter
40
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
41
+
42
+
43
+ # Global exception handler to prevent leaking internal errors
44
+ @app.exception_handler(Exception)
45
+ async def global_exception_handler(request: Request, exc: Exception):
46
+ """
47
+ Catch all unhandled exceptions and return a safe error message.
48
+ Log the full error details for debugging.
49
+ """
50
+ import uuid
51
+ import traceback
52
+
53
+ error_id = str(uuid.uuid4())[:8]
54
+ logger.error(
55
+ f"Unhandled exception [{error_id}]: {exc}",
56
+ exc_info=True,
57
+ extra={
58
+ "error_id": error_id,
59
+ "path": request.url.path,
60
+ "method": request.method,
61
+ "traceback": traceback.format_exc(),
62
+ },
63
+ )
64
+
65
+ return JSONResponse(
66
+ status_code=500,
67
+ content={
68
+ "detail": "An internal error occurred. Please try again later.",
69
+ "error_id": error_id,
70
+ },
71
+ )
72
+
73
+
74
+ # CORS middleware configuration - support HF Spaces, GitHub Pages, and local development
75
+ app.add_middleware(
76
+ CORSMiddleware,
77
+ allow_origins=[
78
+ settings.FRONTEND_URL,
79
+ settings.API_URL,
80
+ "http://localhost:3000",
81
+ "http://localhost:8000",
82
+ "https://*.hf.space",
83
+ "https://*.spaces.huggingface.tech",
84
+ "https://*.github.io", # GitHub Pages support
85
+ ],
86
+ allow_credentials=True,
87
+ allow_methods=["*"],
88
+ allow_headers=["*"],
89
+ )
90
+
91
+ # Serve Next.js frontend static files (built with standalone output)
92
+ # Check multiple possible locations for the frontend build
93
+ frontend_paths = [
94
+ "/app/frontend",
95
+ "/app/1proxy-frontend",
96
+ os.path.join(os.path.dirname(__file__), "../../1proxy-frontend"),
97
+ ]
98
+
99
+ frontend_path = None
100
+ for fp in frontend_paths:
101
+ if os.path.exists(os.path.join(fp, "server.js")):
102
+ frontend_path = fp
103
+ break
104
+
105
+ if frontend_path:
106
+ logger.info(f"📦 Serving frontend from: {frontend_path}")
107
+ # Mount the Next.js build output
108
+ app.mount(
109
+ "/static",
110
+ StaticFiles(directory=os.path.join(frontend_path, ".next/static")),
111
+ name="static",
112
+ )
113
+ app.mount("/_next", StaticFiles(directory=frontend_path), name="next")
114
+
115
+ @app.get("/favicon.ico")
116
+ async def favicon():
117
+ favicon_path = os.path.join(frontend_path, "public/favicon.ico")
118
+ if os.path.exists(favicon_path):
119
+ return FileResponse(favicon_path)
120
+ return JSONResponse(status_code=204, content={})
121
+
122
+
123
+ app.include_router(auth.router)
124
+ app.include_router(sources.router)
125
+ app.include_router(proxies.router)
126
+ app.include_router(notifications.router)
127
+ app.include_router(validation.router)
128
+ app.include_router(admin.router)
129
+
130
+ from app.admin.scraping_admin import router as scraping_admin_router
131
+
132
+ app.include_router(scraping_admin_router)
133
+
134
+ grabber = GitHubGrabber()
135
+
136
+
137
+ @app.on_event("startup")
138
+ async def startup():
139
+ await init_db()
140
+
141
+ async with AsyncSessionLocal() as session:
142
+ try:
143
+ admin_user = await db_storage.get_or_create_user(
144
+ session=session,
145
+ oauth_provider="local",
146
+ oauth_id="admin",
147
+ email="admin@1proxy.local",
148
+ username="admin",
149
+ role="admin",
150
+ )
151
+ await db_storage.seed_admin_sources(session, admin_user.id)
152
+ await session.commit()
153
+ logger.info(
154
+ f"✅ Admin user created/verified: {admin_user.username} (ID: {admin_user.id})"
155
+ )
156
+ logger.info("✅ Admin sources seeded")
157
+ except Exception as e:
158
+ logger.warning(f"⚠️ Startup error (non-critical): {e}")
159
+ await session.rollback()
160
+
161
+ # STARTUP STABILIZER: Wait for HF Space to pass health check before spawning workers
162
+ async def delayed_workers():
163
+ logger.info("⏳ Stabilizer: Waiting 15s before starting background workers...")
164
+ await asyncio.sleep(15)
165
+
166
+ logger.info("🚀 Stabilizer: Spawning background workers...")
167
+ # Start validation worker with reduced batch size for HF
168
+ asyncio.create_task(
169
+ background_validation_worker(batch_size=20, interval_seconds=60)
170
+ )
171
+
172
+ # Import and start auto-scraper
173
+ from app.background_validator import background_scraper_worker
174
+
175
+ asyncio.create_task(background_scraper_worker(interval_minutes=10))
176
+ logger.info("✅ Stabilizer: Background workers active")
177
+
178
+ asyncio.create_task(delayed_workers())
179
+
180
+
181
+ @app.get("/")
182
+ async def root():
183
+ return {
184
+ "name": "1proxy API",
185
+ "version": "2.0.0",
186
+ "status": "running",
187
+ "features": {
188
+ "multi_user": True,
189
+ "oauth": ["github", "google"],
190
+ "advanced_filtering": True,
191
+ "export_formats": ["txt", "json", "csv"],
192
+ },
193
+ "endpoints": {
194
+ "health": "/health",
195
+ "auth": "/auth/*",
196
+ "my_sources": "/api/v1/my-sources",
197
+ "advanced_search": "/api/v1/proxies/advanced",
198
+ "export": "/api/v1/proxies/export",
199
+ "public_sources": "/api/v1/sources",
200
+ },
201
+ }
202
+
203
+
204
+ @app.get("/health")
205
+ async def health_check(session: AsyncSession = Depends(get_db)):
206
+ proxy_count = await db_storage.count_proxies(session)
207
+ source_count = await db_storage.count_sources(session)
208
+ user_count = await db_storage.count_users(session)
209
+ return {
210
+ "status": "healthy",
211
+ "database": "connected",
212
+ "proxies": proxy_count,
213
+ "sources": source_count,
214
+ "users": user_count,
215
+ }
216
+
217
+
218
+ @app.post("/api/v1/proxies/scrape", response_model=dict)
219
+ async def scrape_proxies(
220
+ source: SourceConfig, current_user: User = Depends(require_admin)
221
+ ):
222
+ async with AsyncSessionLocal() as session:
223
+ try:
224
+ proxies = await grabber.extract_proxies(source)
225
+ proxies_data = []
226
+ for p in proxies:
227
+ data = p.model_dump() if hasattr(p, "model_dump") else p.__dict__
228
+ proxies_data.append(
229
+ {
230
+ "url": f"{data.get('protocol', 'http')}://{data.get('ip')}:{data.get('port')}",
231
+ "protocol": data.get("protocol", "http"),
232
+ "ip": data.get("ip"),
233
+ "port": data.get("port"),
234
+ "country_code": data.get("country_code"),
235
+ "country_name": data.get("country_name"),
236
+ "city": data.get("city"),
237
+ "latency_ms": data.get("latency_ms"),
238
+ "speed_mbps": data.get("speed_mbps"),
239
+ "anonymity": data.get("anonymity"),
240
+ "proxy_type": data.get("proxy_type"),
241
+ }
242
+ )
243
+ added = await db_storage.add_proxies(session, proxies_data)
244
+
245
+ validation_results = await db_storage.validate_and_update_proxies(
246
+ session, limit=min(added, 100)
247
+ )
248
+
249
+ return {
250
+ "source": str(source.url),
251
+ "scraped": len(proxies),
252
+ "added": added,
253
+ "validated": validation_results["validated"],
254
+ "failed": validation_results["failed"],
255
+ "total": await db_storage.count_proxies(session),
256
+ }
257
+ except FileNotFoundError as e:
258
+ raise HTTPException(status_code=404, detail=str(e))
259
+ except asyncio.TimeoutError:
260
+ raise HTTPException(status_code=504, detail="Request timeout")
261
+ except Exception as e:
262
+ raise HTTPException(status_code=500, detail=str(e))
263
+
264
+
265
+ @app.get("/api/v1/proxies", response_model=dict)
266
+ async def list_proxies(
267
+ protocol: Optional[str] = Query(
268
+ None, description="Filter by protocol (http, vmess, vless, trojan, shadowsocks)"
269
+ ),
270
+ limit: int = Query(10, ge=1, le=100, description="Number of results"),
271
+ offset: int = Query(0, ge=0, description="Offset for pagination"),
272
+ session: AsyncSession = Depends(get_db),
273
+ ):
274
+ proxies, total = await db_storage.get_proxies(
275
+ session=session, protocol=protocol, limit=limit, offset=offset, is_working=True
276
+ )
277
+
278
+ proxy_list = []
279
+ for p in proxies:
280
+ proxy_list.append(
281
+ {
282
+ "id": p.id,
283
+ "url": p.url,
284
+ "protocol": p.protocol,
285
+ "ip": p.ip,
286
+ "port": p.port,
287
+ "country_code": p.country_code,
288
+ "country_name": p.country_name,
289
+ "city": p.city,
290
+ "latency_ms": p.latency_ms,
291
+ "speed_mbps": p.speed_mbps,
292
+ "anonymity": p.anonymity,
293
+ "quality_score": p.quality_score,
294
+ "is_working": p.is_working,
295
+ "last_validated": p.last_validated.isoformat()
296
+ if p.last_validated
297
+ else None,
298
+ "source": str(p.source_id),
299
+ }
300
+ )
301
+
302
+ return {
303
+ "total": total,
304
+ "count": len(proxies),
305
+ "offset": offset,
306
+ "limit": limit,
307
+ "proxies": proxy_list,
308
+ }
309
+
310
+ return {
311
+ "total": total,
312
+ "count": len(proxies),
313
+ "offset": offset,
314
+ "limit": limit,
315
+ "proxies": proxy_list,
316
+ }
317
+
318
+
319
+ @app.get("/api/v1/stats")
320
+ async def get_stats(session: AsyncSession = Depends(get_db)):
321
+ stats = await db_storage.get_stats(session)
322
+ user_count = await db_storage.count_users(session)
323
+ source_count = await db_storage.count_sources(session)
324
+
325
+ stats["total_users"] = user_count
326
+ stats["total_sources"] = source_count
327
+
328
+ return stats
329
+
330
+
331
+ @app.post("/api/v1/proxies/demo")
332
+ async def demo_scrape(current_user: User = Depends(require_admin)):
333
+ async with AsyncSessionLocal() as session:
334
+ source = SourceConfig(
335
+ url="https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list-raw.txt",
336
+ type=SourceType.GITHUB_RAW,
337
+ )
338
+ try:
339
+ proxies = await grabber.extract_proxies(source)
340
+ proxies_data = []
341
+ sample_list = []
342
+ for p in proxies:
343
+ data = p.model_dump() if hasattr(p, "model_dump") else p.__dict__
344
+ proxy_data = {
345
+ "url": f"{data.get('protocol', 'http')}://{data.get('ip')}:{data.get('port')}",
346
+ "protocol": data.get("protocol", "http"),
347
+ "ip": data.get("ip"),
348
+ "port": data.get("port"),
349
+ "country_code": data.get("country_code"),
350
+ "country_name": data.get("country_name"),
351
+ "city": data.get("city"),
352
+ "latency_ms": data.get("latency_ms"),
353
+ "speed_mbps": data.get("speed_mbps"),
354
+ "anonymity": data.get("anonymity"),
355
+ "proxy_type": data.get("proxy_type"),
356
+ }
357
+ proxies_data.append(proxy_data)
358
+ if len(sample_list) < 5:
359
+ sample_list.append(data)
360
+
361
+ added = await db_storage.add_proxies(session, proxies_data)
362
+
363
+ validation_results = await db_storage.validate_and_update_proxies(
364
+ session, limit=min(added, 50)
365
+ )
366
+
367
+ return {
368
+ "message": "Demo scrape completed",
369
+ "source": str(source.url),
370
+ "scraped": len(proxies),
371
+ "added": added,
372
+ "validated": validation_results["validated"],
373
+ "failed": validation_results["failed"],
374
+ "total_stored": await db_storage.count_proxies(session),
375
+ "sample": sample_list,
376
+ }
377
+ except Exception as e:
378
+ raise HTTPException(status_code=500, detail=str(e))
379
+
380
+
381
+ @app.get("/api/v1/sources")
382
+ async def list_sources(session: AsyncSession = Depends(get_db)):
383
+ sources = await db_storage.get_sources(session, enabled_only=False)
384
+ return {
385
+ "total": len(sources),
386
+ "enabled": len([s for s in sources if s.enabled]),
387
+ "sources": [
388
+ {
389
+ "id": s.id,
390
+ "url": s.url,
391
+ "type": s.type,
392
+ "enabled": s.enabled,
393
+ "name": s.name,
394
+ "is_admin_source": s.is_admin_source,
395
+ "validated": s.validated,
396
+ "total_scraped": s.total_scraped,
397
+ }
398
+ for s in sources
399
+ ],
400
+ }
401
+
402
+
403
+ @app.post("/api/v1/proxies/scrape-all")
404
+ async def scrape_all_sources(current_user: User = Depends(require_admin)):
405
+ async with AsyncSessionLocal() as session:
406
+ sources = SourceRegistry.get_enabled_sources()
407
+ results = []
408
+ total_scraped = 0
409
+ total_added = 0
410
+ total_validated = 0
411
+ total_failed = 0
412
+
413
+ for source in sources:
414
+ try:
415
+ proxies = await grabber.extract_proxies(source)
416
+ proxies_data = []
417
+ for p in proxies:
418
+ data = p.model_dump() if hasattr(p, "model_dump") else p.__dict__
419
+ proxies_data.append(
420
+ {
421
+ "url": f"{data.get('protocol', 'http')}://{data.get('ip')}:{data.get('port')}",
422
+ "protocol": data.get("protocol", "http"),
423
+ "ip": data.get("ip"),
424
+ "port": data.get("port"),
425
+ "country_code": data.get("country_code"),
426
+ "country_name": data.get("country_name"),
427
+ "city": data.get("city"),
428
+ "latency_ms": data.get("latency_ms"),
429
+ "speed_mbps": data.get("speed_mbps"),
430
+ "anonymity": data.get("anonymity"),
431
+ "proxy_type": data.get("proxy_type"),
432
+ }
433
+ )
434
+ added = await db_storage.add_proxies(session, proxies_data)
435
+
436
+ validation_results = await db_storage.validate_and_update_proxies(
437
+ session, limit=min(added, 50)
438
+ )
439
+
440
+ total_scraped += len(proxies)
441
+ total_added += added
442
+ total_validated += validation_results["validated"]
443
+ total_failed += validation_results["failed"]
444
+ results.append(
445
+ {
446
+ "url": str(source.url),
447
+ "status": "success",
448
+ "scraped": len(proxies),
449
+ "added": added,
450
+ "validated": validation_results["validated"],
451
+ "failed": validation_results["failed"],
452
+ }
453
+ )
454
+ except Exception as e:
455
+ results.append(
456
+ {
457
+ "url": str(source.url),
458
+ "status": "failed",
459
+ "error": str(e),
460
+ "scraped": 0,
461
+ "added": 0,
462
+ "validated": 0,
463
+ "failed": 0,
464
+ }
465
+ )
466
+
467
+ return {
468
+ "message": f"Scraped {len(sources)} sources",
469
+ "total_scraped": total_scraped,
470
+ "total_added": total_added,
471
+ "total_validated": total_validated,
472
+ "total_failed": total_failed,
473
+ "total_stored": await db_storage.count_proxies(session),
474
+ "results": results,
475
+ }
476
+
477
+
478
+ if __name__ == "__main__":
479
+ import uvicorn
480
+
481
+ uvicorn.run(app, host="0.0.0.0", port=8000)