diff --git a/backend/app/api/v1/test_rate_limit.py b/backend/app/api/v1/test_rate_limit.py new file mode 100644 index 0000000..f3f4851 --- /dev/null +++ b/backend/app/api/v1/test_rate_limit.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Request, Depends +from fastapi_ratelimiter import rate_limiter + +router = APIRouter(prefix="/test", tags=["test"]) + +@router.get("/unlimited") +async def unlimited(): + """Unlimited endpoint for testing.""" + return {"message": "This is an unlimited endpoint"} + +@router.get("/limited") +@rate_limiter.limit("5/minute") +async def limited(request: Request): + """Limited endpoint (5 requests per minute).""" + return { + "message": "This is a rate-limited endpoint (5 requests per minute)", + "remaining": request.state.rate_limit_remaining, + "reset": request.state.rate_limit_reset + } + +@router.get("/user-limited") +@rate_limiter.limit("10/minute", key_func=lambda r: f"user:{r.client.host}") +async def user_limited(request: Request): + """User-specific rate limited endpoint (10 requests per minute per user).""" + return { + "message": "This is a user-specific rate-limited endpoint (10 requests per minute per user)", + "remaining": request.state.rate_limit_remaining, + "reset": request.state.rate_limit_reset + } diff --git a/backend/app/core/rate_limiter.py b/backend/app/core/rate_limiter.py new file mode 100644 index 0000000..4480170 --- /dev/null +++ b/backend/app/core/rate_limiter.py @@ -0,0 +1,55 @@ +import os +from typing import Optional +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse +from fastapi_ratelimiter import RedisDependencyMarker, RedisSettings, RateLimiter +from fastapi_ratelimiter.ratelimiting import RateLimitExceeded + +# Configure Redis settings +redis_settings = RedisSettings( + host=os.getenv("REDIS_HOST", "localhost"), + port=int(os.getenv("REDIS_PORT", 6379)), + db=int(os.getenv("REDIS_DB", 0)), + password=os.getenv("REDIS_PASSWORD"), + ssl=os.getenv("REDIS_SSL", "false").lower() == "true", +) + +# Initialize the rate limiter +rate_limiter = RateLimiter( + redis_settings=redis_settings, + default_limits=["1000 per day", "100 per hour"], + default_limits_per_method=True, + default_limits_exempt_when=lambda request: request.method == "OPTIONS", +) + +async def rate_limit_exception_handler(request: Request, exc: RateLimitExceeded): + """Handle rate limit exceeded exceptions with a JSON response.""" + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={ + "detail": f"Rate limit exceeded. Try again in {exc.retry_after} seconds.", + "retry_after": exc.retry_after, + }, + headers={"Retry-After": str(exc.retry_after)}, + ) + +def get_user_identifier(request: Request) -> str: + """Extract user identifier from request for rate limiting.""" + # Try to get user ID from JWT or session + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + token = auth_header.split(" ")[1] + return f"user:{token[:8]}" # Simplified token-based identifier + + # Fall back to IP-based rate limiting + client_ip = request.client.host if request.client else "unknown" + return f"ip:{client_ip}" + +def setup_rate_limiter(app: FastAPI): + """Set up rate limiting for the FastAPI application.""" + # Add rate limiter middleware + app.add_middleware(rate_limiter.middleware) + app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler) + + # Add rate limiter to app state for easy access in routes + app.state.rate_limiter = rate_limiter \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index a4daf1c..1ab1ac7 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -41,12 +41,27 @@ services: timeout: 10s retries: 5 start_period: 10s + redis: + image: redis:7-alpine + container_name: redis + ports: + - '6379:6379' + volumes: + - redis_data:/data + command: redis-server --requirepass ${REDIS_PASSWORD:-your_secure_password} --appendonly yes + restart: always + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 30s + retries: 3 + falkordb: image: falkordb/falkordb:latest container_name: falkordb ports: - - '6379:6379' # Redis protocol - - '3000:3000' # Web UI + - '6380:6379' # Redis protocol + - '3001:3000' # Web UI volumes: - falkordb_data:/data restart: unless-stopped @@ -60,4 +75,5 @@ services: volumes: weaviate_data: rabbitmq_data: - falkordb_data: \ No newline at end of file + falkordb_data: + redis_data: \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index b7ad80a..eefe61d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,8 +4,9 @@ from contextlib import asynccontextmanager import uvicorn -from fastapi import FastAPI, Response +from fastapi import FastAPI, Response, Request from fastapi.middleware.cors import CORSMiddleware +from app.core.rate_limiter import setup_rate_limiter from app.api.router import api_router from app.core.config import settings @@ -102,7 +103,10 @@ async def lifespan(app: FastAPI): await app_instance.stop_background_tasks() -api = FastAPI(title="Devr.AI API", version="1.0", lifespan=lifespan) +api = FastAPI(title="Devr.AI API", version="1.0", lifespan=lifespan, docs_url="/docs", redoc_url="/redoc") + +# Setup rate limiting +setup_rate_limiter(api) # Configure CORS api.add_middleware( diff --git a/backend/minimal-requirements.txt b/backend/minimal-requirements.txt new file mode 100644 index 0000000..9fe2f62 --- /dev/null +++ b/backend/minimal-requirements.txt @@ -0,0 +1,15 @@ +fastapi==0.109.2 +uvicorn[standard]==0.27.1 +python-dotenv==1.0.1 +pydantic==2.7.1 +python-multipart==0.0.9 +httpx==0.27.0 +websockets==12.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.9 +sqlalchemy==2.0.28 +psycopg2-binary==2.9.9 +python-jose[cryptography]==3.3.0 +python-multipart==0.0.9 +python-dotenv==1.0.1 diff --git a/backend/requirements.txt b/backend/requirements.txt index 5982753..71ea70c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -230,3 +230,7 @@ xxhash==3.5.0 yarl==1.20.1 zipp==3.21.0 zstandard==0.23.0 + +# Rate limiting +fastapi-ratelimiter==0.4.0 +redis==5.0.1 diff --git a/backend/test_server.py b/backend/test_server.py new file mode 100644 index 0000000..22dbef5 --- /dev/null +++ b/backend/test_server.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +import uvicorn + +app = FastAPI() + +@app.get("/") +async def read_root(): + return {"message": "Devr.AI Backend is running!"} + +if __name__ == "__main__": + uvicorn.run("test_server:app", host="0.0.0.0", port=8000, reload=True)