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",
        }
    }