-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
146 lines (128 loc) · 6.79 KB
/
Copy pathmain.py
File metadata and controls
146 lines (128 loc) · 6.79 KB
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
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import IntegrityError
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from src.database import engine, get_db
from sqlalchemy.orm import Session
from src.models import db_models
from src.core.security import require_client_key, require_admin_key
import time
import logging
import math
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("sentinel_api")
from src.routes.analyze import router as analyze_router
from src.routes.messages import router as messages_router
from src.routes.messages_crud import router as messages_crud_router
from src.routes.hot_terms import router as hot_terms_router
from src.routes.scraper import router as scraper_router
from src.routes.admin import router as admin_router
from src.routes.feedback import router as feedback_router
from src.routes.network import router as network_router
from src.routes.evidence import router as evidence_router
db_models.Base.metadata.create_all(bind=engine)
# 120 req/min por IP como límite global; el middleware lo aplica a todas las rutas.
limiter = Limiter(key_func=get_remote_address, default_limits=["120/minute"])
app = FastAPI(
title="SENTINEL API",
description="Detección de riesgo digital para menores",
version="0.1.0"
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
logger.info(f"{request.method} {request.url.path} - {response.status_code} - {process_time:.4f}s")
response.headers["X-Process-Time"] = str(process_time)
return response
TRANSLATIONS = {
"Field required": "es obligatorio",
"String should have at least 1 character": "no puede estar vacío",
"String should have at most 5000 characters": "no puede superar los 5000 caracteres",
"Input should be greater than 0": "debe ser mayor a 0",
"Input should be greater than or equal to 0": "debe ser mayor o igual a 0",
"Input should be less than or equal to 100": "debe ser menor o igual a 100",
"Value error, El campo no puede ser solo espacios en blanco": "no puede ser solo espacios en blanco",
"Input should be a valid integer, unable to parse string as an integer": "debe ser un número entero",
"Input should be a valid string": "debe ser texto",
"Input should be a valid boolean": "debe ser verdadero o falso",
"Input should be a valid list": "debe ser una lista",
"Input should be a valid number": "debe ser un número",
"Extra inputs are not permitted": "no es un campo permitido",
"Value is not a valid enumeration member": "tiene un valor no permitido",
"Input should be a valid UUID": "debe ser un UUID válido",
"String should match pattern": "tiene un formato inválido",
"Input should be a valid integer, got a number with a fractional part": "debe ser un número entero, sin decimales",
}
def error_response(status_code: int, details):
return JSONResponse(
status_code=status_code,
content={
"success": False,
"status_code": status_code,
"details": details,
},
)
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
messages = []
for err in exc.errors():
field = " -> ".join(str(loc) for loc in err["loc"] if loc != "body")
field_label = f"El campo '{field}'" if field else "El cuerpo de la solicitud"
msg = TRANSLATIONS.get(err["msg"], err["msg"])
messages.append(f"{field_label} {msg}")
return error_response(422, messages)
@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
return error_response(409, "El recurso que intentas crear ya existe.")
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
return error_response(500, "Ocurrió un error interno. Intenta de nuevo más tarde.")
# Client routes (require client key, rate limit 60/minute)
# We can't apply rate limits directly to include_router without slowapi router setup easily,
# but we can apply the security dependency. For slowapi, it is usually applied via decorators on routes.
# To keep it simple as requested by the plan, we will add the dependencies to the routers.
app.include_router(analyze_router, prefix="/api/v1", dependencies=[Depends(require_client_key)])
app.include_router(messages_router, prefix="/api/v1/messages", dependencies=[Depends(require_client_key)])
app.include_router(feedback_router, prefix="/api/v1/feedback", dependencies=[Depends(require_client_key)])
app.include_router(evidence_router, prefix="/api/v1/evidence")
# Admin routes (require admin key)
app.include_router(messages_crud_router, prefix="/api/v1/admin/messages", dependencies=[Depends(require_admin_key)])
app.include_router(scraper_router, prefix="/api/v1/admin/scrape", dependencies=[Depends(require_admin_key)])
app.include_router(admin_router, prefix="/admin", dependencies=[Depends(require_admin_key)])
app.include_router(network_router, prefix="/api/v1/network")
# hot_terms has both GET (client) and POST (admin) so it needs granular protection in the router itself
app.include_router(hot_terms_router, prefix="/api/v1/hot-terms")
# Static files for the Playground
app.mount("/public", StaticFiles(directory="public"), name="public")
@app.get("/playground", include_in_schema=False)
async def serve_playground():
return FileResponse("public/playground.html")
@app.get("/health")
def health_check(db: Session = Depends(get_db)):
from src.models.db_models import ScraperRun, Message
last_run = db.query(ScraperRun).filter(ScraperRun.status == "success").order_by(ScraperRun.started_at.desc()).first()
now = int(time.time())
scraper_status = "ok"
if not last_run or last_run.finished_at is None or (now - last_run.finished_at > 48 * 3600):
scraper_status = "stale"
# Check retention policy (max message age)
oldest_msg = db.query(Message).order_by(Message.timestamp.asc()).first()
oldest_message_age_days = 0
if oldest_msg:
oldest_message_age_days = math.floor((now - oldest_msg.timestamp) / (24 * 3600))
return {
"status": "ok",
"service": "SENTINEL",
"scraper": scraper_status,
"oldest_message_age_days": oldest_message_age_days
}