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__) @asynccontextmanager 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", ) @app.exception_handler(Exception) 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") @app.get("/api/health", tags=["system"]) async def health_check(): return {"status": "ok", "service": "CrowData API", "version": "1.0.0"} @app.get("/api", tags=["system"]) async def root(): return { "service": "CrowData API", "docs": "/api/docs", "endpoints": { "auth": "/api/auth", "reports": "/api/reports", } }