diff --git a/backend/app/main.py b/backend/app/main.py index 7616acc..f733e54 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,15 +1,19 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from contextlib import asynccontextmanager import logging import os +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded + from .config import get_settings from .database import init_db, SessionLocal from .routes import router from .seed import seed_database from .auth import create_default_admin +from .rate_limit import limiter settings = get_settings() logging.basicConfig(level=logging.INFO) @@ -55,6 +59,10 @@ app = FastAPI( lifespan=lifespan, ) +# Set up rate limiting +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + # Include API routes app.include_router(router) diff --git a/backend/app/rate_limit.py b/backend/app/rate_limit.py new file mode 100644 index 0000000..80184d1 --- /dev/null +++ b/backend/app/rate_limit.py @@ -0,0 +1,16 @@ +"""Rate limiting configuration for Orchard API. + +Uses slowapi for rate limiting with IP-based keys. +""" + +import os +from slowapi import Limiter +from slowapi.util import get_remote_address + +# Rate limiter - uses IP address as key +limiter = Limiter(key_func=get_remote_address) + +# Rate limit strings - configurable via environment for testing +# Default: 5 login attempts per minute per IP +# In tests: set ORCHARD_LOGIN_RATE_LIMIT to a high value like "1000/minute" +LOGIN_RATE_LIMIT = os.environ.get("ORCHARD_LOGIN_RATE_LIMIT", "5/minute") diff --git a/backend/app/routes.py b/backend/app/routes.py index 9fd1aa7..8935e21 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -374,9 +374,11 @@ from .auth import ( check_project_access, AuthorizationService, ) +from .rate_limit import limiter, LOGIN_RATE_LIMIT @router.post("/api/v1/auth/login", response_model=LoginResponse) +@limiter.limit(LOGIN_RATE_LIMIT) def login( login_request: LoginRequest, request: Request, diff --git a/backend/requirements.txt b/backend/requirements.txt index bcc4060..604f19c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,6 +10,7 @@ pydantic-settings==2.1.0 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 bcrypt==4.0.1 +slowapi==0.1.9 # Test dependencies pytest>=7.4.0 diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 4706417..543a943 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -24,6 +24,8 @@ services: - ORCHARD_S3_USE_PATH_STYLE=true - ORCHARD_REDIS_HOST=redis - ORCHARD_REDIS_PORT=6379 + # Higher rate limit for local development/testing + - ORCHARD_LOGIN_RATE_LIMIT=1000/minute depends_on: postgres: condition: service_healthy