Spaces:
Paused
Paused
File size: 7,703 Bytes
83bdb4a c581b0d 83bdb4a c581b0d 83bdb4a e374786 83bdb4a e374786 83bdb4a e374786 83bdb4a e374786 83bdb4a | 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 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 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",
}
} |