Spaces:
Paused
Paused
| import asyncio | |
| import sys | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import JSONResponse | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from contextlib import asynccontextmanager | |
| import logging | |
| import uuid | |
| # Fix crítico para Windows: Playwright necesita ProactorEventLoop para subprocesses. | |
| # SelectorEventLoop causa NotImplementedError en _make_subprocess_transport. | |
| # Python 3.8+ usa WindowsProactorEventLoopPolicy por defecto — NO sobreescribir. | |
| if sys.platform == "win32": | |
| pass # Keep default ProactorEventLoop — Playwright requires it for subprocesses | |
| from app.config import get_settings | |
| from app.database import init_db | |
| from app.auth.router import router as auth_router | |
| from app.utils.logging_structured import CorrelationMiddleware, set_correlation_id, get_correlation_id | |
| settings = get_settings() | |
| # Use structured JSON logging in production, human-readable in dev | |
| if settings.debug: | |
| logging.basicConfig( | |
| level=logging.DEBUG, | |
| format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", | |
| ) | |
| else: | |
| # Production: JSON structured logging | |
| import logging.handlers | |
| handler = logging.StreamHandler() | |
| handler.setFormatter(logging.Formatter('%(message)s')) | |
| root_logger = logging.getLogger() | |
| root_logger.handlers = [handler] | |
| root_logger.setLevel(logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| async def lifespan(app: FastAPI): | |
| # Validar settings críticos al iniciar | |
| if not settings.secret_key: | |
| logger.critical("SECRET_KEY no configurado en .env — la aplicación NO puede iniciar sin una clave secreta") | |
| raise SystemExit(1) | |
| if settings.mp_access_token and "TEST" in settings.mp_access_token: | |
| logger.warning("MercadoPago usando access token de TEST — no procesará pagos reales") | |
| logger.info("🚀 CrowData API iniciando...") | |
| await init_db() | |
| logger.info("✅ Base de datos inicializada") | |
| # Iniciar Celery Beat para tareas programadas (monitoring, cleanup, etc.) | |
| # En producción: celery -A app.tasks.celery_app beat -l info | |
| # En desarrollo: usamos daemon simple | |
| # Iniciar daemon de monitoring en desarrollo (sin Celery) | |
| if settings.environment != "production": | |
| try: | |
| from app.tasks.monitoring import start_monitoring_daemon | |
| monitor_task = asyncio.create_task(start_monitoring_daemon()) | |
| except (ImportError, AttributeError) as e: | |
| logger.warning(f"⚠️ Monitoring daemon no disponible (Celery no instalado o función faltante): {e}") | |
| yield | |
| logger.info("🛑 Cancelando tareas de fondo...") | |
| # Celery workers se gestionan externamente | |
| logger.info("🛑 CrowData API cerrando...") | |
| app = FastAPI( | |
| title="CrowData API", | |
| description=""" | |
| ## API de Consulta de Datos Públicos Argentinos | |
| CrowData consulta múltiples fuentes oficiales argentinas para generar informes completos de personas, empresas, vehículos y propiedades. | |
| ### Fuentes de datos | |
| - **ARCA/AFIP** — Situación fiscal, IVA, Monotributo | |
| - **BCRA** — Situación crediticia, cheques rechazados | |
| - **IGJ** — Sociedades, directivos, sede social | |
| - **Boletín Oficial** — Publicaciones oficiales | |
| - **DNRPA** — Vehículos registrados | |
| - **ANSES** — Aportes previsionales, obra social | |
| - **INPI** — Marcas y patentes comerciales | |
| - **Poder Judicial** — Causas judiciales federales y provinciales | |
| - **Redes Sociales** — Perfiles públicos OSINT | |
| ### Autenticación | |
| Todos los endpoints requieren JWT token. Obtener token via `POST /api/auth/jwt/login`. | |
| ### Rate Limits | |
| - **Free**: 10 requests/min, 5 informes/día | |
| - **Basic**: 60 requests/min, 50 informes/día | |
| - **Pro**: 200 requests/min, ilimitado | |
| """, | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| docs_url="/api/docs", | |
| redoc_url="/api/redoc", | |
| ) | |
| async def global_exception_handler(request: Request, exc: Exception): | |
| """Captura errores no manejados y retorna respuesta segura.""" | |
| logger.error(f"Unhandled exception: {exc}", exc_info=True) | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "detail": f"Error: {type(exc).__name__}: {str(exc)}", | |
| "type": "internal_error", | |
| }, | |
| ) | |
| # CORS — producción: solo dominios permitidos | |
| ALLOWED_ORIGINS = [ | |
| "http://localhost:3000", | |
| "http://localhost:5173", | |
| "http://localhost:8080", | |
| "https://crowdata.ar", | |
| "https://www.crowdata.ar", | |
| "https://crowdata.netlify.app", | |
| "https://tomasdelpico-crowdata-api.hf.space", | |
| "https://yosoyyonosoyotro-crowdata.hf.space", | |
| ] | |
| import os | |
| env_origins = os.getenv("ALLOWED_ORIGINS") | |
| if env_origins: | |
| ALLOWED_ORIGINS.extend([origin.strip() for origin in env_origins.split(",") if origin.strip()]) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=ALLOWED_ORIGINS, | |
| allow_origin_regex=r"^https://(.*\.)?(crowdata\.ar|netlify\.app|hf\.space)$", | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Correlation ID Middleware - must be after CORS but before other middleware | |
| from app.utils.logging_structured import CorrelationMiddleware | |
| app.add_middleware(CorrelationMiddleware) | |
| class SecurityHeadersMiddleware(BaseHTTPMiddleware): | |
| """Agrega headers de seguridad a todas las respuestas.""" | |
| async def dispatch(self, request: Request, call_next): | |
| response = await call_next(request) | |
| response.headers["X-Content-Type-Options"] = "nosniff" | |
| response.headers["X-Frame-Options"] = "DENY" | |
| response.headers["X-XSS-Protection"] = "1; mode=block" | |
| response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" | |
| response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" | |
| # HSTS solo en producción (HTTPS) | |
| if settings.environment == "production": | |
| response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" | |
| return response | |
| app.add_middleware(SecurityHeadersMiddleware) | |
| # Rate Limiting | |
| from app.middleware.rate_limit import RateLimitMiddleware | |
| app.add_middleware(RateLimitMiddleware) | |
| # Login History | |
| from app.middleware.login_history import LoginHistoryMiddleware | |
| app.add_middleware(LoginHistoryMiddleware) | |
| # CSRF Protection (Double-submit cookie) | |
| from app.middleware.csrf import get_csrf_middleware | |
| from app.config import get_settings | |
| settings = get_settings() | |
| app.add_middleware( | |
| get_csrf_middleware( | |
| cookie_secure=not settings.debug, | |
| cookie_samesite="lax", | |
| excluded_paths=["/api/auth/jwt/login", "/api/auth/jwt/logout", "/api/auth/register", "/api/auth/forgot-password", "/api/auth/reset-password"], | |
| ) | |
| ) | |
| from app.reports.router import router as reports_router | |
| from app.reports.vehiculo_router import router as vehiculo_router | |
| from app.payments.router import router as payments_router | |
| from app.admin.router import router as admin_router | |
| # Routers | |
| app.include_router(auth_router, prefix="/api") | |
| app.include_router(reports_router, prefix="/api") | |
| app.include_router(vehiculo_router, prefix="/api") | |
| app.include_router(payments_router, prefix="/api") | |
| app.include_router(admin_router, prefix="/api") | |
| async def health_check(): | |
| return {"status": "ok", "service": "CrowData API", "version": "1.0.0"} | |
| async def root(): | |
| return { | |
| "service": "CrowData API", | |
| "docs": "/api/docs", | |
| "endpoints": { | |
| "auth": "/api/auth", | |
| "reports": "/api/reports", | |
| } | |
| } |