Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions backend/app/api/v1/test_rate_limit.py
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions backend/app/core/rate_limiter.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 19 additions & 3 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment on lines +44 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix Redis password default to avoid auth mismatch and weak default

The new Redis service wiring and decoupling Falkordb ports look good, but this line is risky:

command: redis-server --requirepass ${REDIS_PASSWORD:-your_secure_password} --appendonly yes

If REDIS_PASSWORD isn’t set, Redis will require your_secure_password while app.core.rate_limiter.redis_settings will try connecting with no password, causing auth failures and leaving an extremely weak default secret in any environment that forgets to set it.

Consider instead requiring the env var explicitly:

-    command: redis-server --requirepass ${REDIS_PASSWORD:-your_secure_password} --appendonly yes
+    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes

and ensure REDIS_PASSWORD is always set via .env / secrets for both Redis and the backend. Also double‑check that REDIS_HOST for the app is set to the service name redis so connections work correctly in this compose network.

Also applies to: 63-64, 78-79

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
Expand All @@ -60,4 +75,5 @@ services:
volumes:
weaviate_data:
rabbitmq_data:
falkordb_data:
falkordb_data:
redis_data:
8 changes: 6 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions backend/minimal-requirements.txt
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions backend/test_server.py
Original file line number Diff line number Diff line change
@@ -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)