From 1b1a0eb6ffc65925c39d39882a8b81e54e601ccc Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 25 May 2026 18:51:58 +0200 Subject: [PATCH] feat: add rate limiting, CORS, queue loop in lifespan --- src/backend/api/dependencies.py | 11 ++++++++- src/backend/api/routes/health.py | 19 ++++++++++++++- src/backend/api/routes/requests.py | 8 ++++--- src/backend/main.py | 38 ++++++++++++++++++++++++++++-- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/backend/api/dependencies.py b/src/backend/api/dependencies.py index f412284..86901b0 100644 --- a/src/backend/api/dependencies.py +++ b/src/backend/api/dependencies.py @@ -1,8 +1,17 @@ -from fastapi import Header, HTTPException +from fastapi import Header, HTTPException, Request +from slowapi import Limiter +from slowapi.util import get_remote_address from src.backend.config import settings +def _get_api_key(request: Request) -> str: + return request.headers.get("X-Api-Key", get_remote_address(request)) + + +limiter = Limiter(key_func=_get_api_key) + + async def verify_api_key(x_api_key: str | None = Header(default=None)): if x_api_key is None or x_api_key != settings.api_key: raise HTTPException(status_code=403, detail="API key non valida") diff --git a/src/backend/api/routes/health.py b/src/backend/api/routes/health.py index 57b127b..d688a9d 100644 --- a/src/backend/api/routes/health.py +++ b/src/backend/api/routes/health.py @@ -1,11 +1,28 @@ from fastapi import APIRouter, Depends from src.backend.api.dependencies import verify_api_key +from src.backend.db.database import get_db from src.backend.models.api import ok +from src.backend.services.worker_client import WorkerClient router = APIRouter(dependencies=[Depends(verify_api_key)]) @router.get("/health", summary="Health check", tags=["system"]) async def health(): - return ok({"status": "running"}) + db_ok = True + try: + db = await get_db() + await db.execute("SELECT 1") + await db.close() + except Exception: + db_ok = False + + worker = WorkerClient() + worker_ok = await worker.health() + + return ok({ + "status": "running", + "database": db_ok, + "worker": worker_ok, + }) diff --git a/src/backend/api/routes/requests.py b/src/backend/api/routes/requests.py index 394734a..132006e 100644 --- a/src/backend/api/routes/requests.py +++ b/src/backend/api/routes/requests.py @@ -1,7 +1,8 @@ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, Request as FastAPIRequest from starlette.responses import JSONResponse -from src.backend.api.dependencies import verify_api_key +from src.backend.api.dependencies import verify_api_key, limiter +from src.backend.config import settings from src.backend.db.database import ( get_db, create_request, @@ -27,7 +28,8 @@ async def _get_db(): @router.post("", summary="Create request") -async def create(body: RequestCreate, db=Depends(_get_db)): +@limiter.limit(lambda: f"{settings.rate_limit_per_minute}/minute;{settings.rate_limit_per_hour}/hour") +async def create(request: FastAPIRequest, body: RequestCreate, db=Depends(_get_db)): req = await create_request( db, prompt=body.prompt, diff --git a/src/backend/main.py b/src/backend/main.py index b721819..c96c624 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -1,21 +1,55 @@ +import asyncio +import logging from contextlib import asynccontextmanager from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from src.backend.api.dependencies import limiter from src.backend.api.routes.health import router as health_router -from src.backend.api.routes.topics import router as topics_router from src.backend.api.routes.requests import router as requests_router from src.backend.api.routes.sessions import router as sessions_router +from src.backend.api.routes.topics import router as topics_router from src.backend.db.database import init_db +from src.backend.services.queue import queue_loop +from src.backend.services.worker_client import WorkerClient + +logging.basicConfig(level=logging.INFO) @asynccontextmanager async def lifespan(app: FastAPI): await init_db() + worker = WorkerClient() + task = asyncio.create_task(queue_loop(worker)) yield + task.cancel() + try: + await task + except asyncio.CancelledError: + pass -app = FastAPI(title="OpusAgent", version="0.1.0", lifespan=lifespan) +app = FastAPI( + title="OpusAgent", + version="0.1.0", + docs_url="/doc", + redoc_url="/redoc", + openapi_url="/openapi.json", + lifespan=lifespan, +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) app.include_router(health_router, prefix="/api") app.include_router(topics_router, prefix="/api")