diff --git a/backend/app/user/routes.py b/backend/app/user/routes.py index 1f04384e..fb74ff88 100644 --- a/backend/app/user/routes.py +++ b/backend/app/user/routes.py @@ -7,13 +7,16 @@ UserProfileUpdateRequest, ) from app.user.service import user_service -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Request, status +from utils.limiter import limiter router = APIRouter(prefix="/users", tags=["User"]) @router.get("/me", response_model=UserProfileResponse) +@limiter.limit("20/minute") async def get_current_user_profile( + request: Request, current_user: Dict[str, Any] = Depends(get_current_user), ): user = await user_service.get_user_by_id(current_user["_id"]) @@ -25,7 +28,9 @@ async def get_current_user_profile( @router.patch("/me", response_model=Dict[str, Any]) +@limiter.limit("5/minute") async def update_user_profile( + request: Request, updates: UserProfileUpdateRequest, current_user: Dict[str, Any] = Depends(get_current_user), ): @@ -47,7 +52,10 @@ async def update_user_profile( @router.delete("/me", response_model=DeleteUserResponse) -async def delete_user_account(current_user: Dict[str, Any] = Depends(get_current_user)): +@limiter.limit("2/minute") +async def delete_user_account( + request: Request, current_user: Dict[str, Any] = Depends(get_current_user) +): deleted = await user_service.delete_user(current_user["_id"]) if not deleted: raise HTTPException( diff --git a/backend/main.py b/backend/main.py index 3372ffb8..46dd706a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,11 +10,19 @@ from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from utils.limiter import Limiter, get_remote_address, limiter + +limiter = Limiter(key_func=get_remote_address) +# limiter = Limiter(key_func=get_remote_address) @asynccontextmanager async def lifespan(app: FastAPI): # Startup + app.state.limiter = limiter logger.info("Lifespan: Connecting to MongoDB...") await connect_to_mongo() logger.info("Lifespan: MongoDB connected.") @@ -77,6 +85,9 @@ async def lifespan(app: FastAPI): ) +app.add_middleware(SlowAPIMiddleware) + + # Add a catch-all OPTIONS handler that should work for any path @app.options("/{path:path}") async def options_handler(request: Request, path: str): diff --git a/backend/requirements.txt b/backend/requirements.txt index 14825b14..a1b714f7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,16 +1,16 @@ -fastapi==0.116.1 -uvicorn[standard]==0.34.3 -python-jose[cryptography]==3.5.0 -passlib[bcrypt]==1.7.4 -python-multipart==0.0.20 -pydantic==2.11.7 -pydantic-settings==2.1.0 -pymongo==4.13.1 -motor==3.7.1 -firebase-admin==6.9.0 -python-dotenv==1.0.0 -bcrypt==4.0.1 -email-validator==2.2.0 +fastapi +uvicorn[standard] +python-jose[cryptography] +passlib[bcrypt] +python-multipart +pydantic +pydantic-settings +pymongo +motor +firebase-admin +python-dotenv +bcrypt +email-validator pytest pytest-asyncio httpx @@ -18,3 +18,4 @@ mongomock-motor pytest-env pytest-cov pytest-mock +slowapi diff --git a/backend/utils/limiter.py b/backend/utils/limiter.py new file mode 100644 index 00000000..38404a8a --- /dev/null +++ b/backend/utils/limiter.py @@ -0,0 +1,4 @@ +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address) diff --git a/backend/utils/limiter_helper.py b/backend/utils/limiter_helper.py new file mode 100644 index 00000000..2fbd78bc --- /dev/null +++ b/backend/utils/limiter_helper.py @@ -0,0 +1,11 @@ +from slowapi.util import get_remote_address +from utils.limiter import limiter + + +def limit_all_routes(router, rate: str): + for route in router.routes: + + print(route) + if hasattr(route, "endpoint"): + + route.endpoint = limiter.limit(rate)(route.endpoint) diff --git a/ui-poc/Home.py b/ui-poc/Home.py index d329e645..526e50c4 100644 --- a/ui-poc/Home.py +++ b/ui-poc/Home.py @@ -1,8 +1,9 @@ -from streamlit_cookies_manager import EncryptedCookieManager -import requests -from datetime import datetime import json +from datetime import datetime + +import requests import streamlit as st +from streamlit_cookies_manager import EncryptedCookieManager # Configure the page – must come immediately after importing Streamlit st.set_page_config(