From 5b561d844543787a1477d9335a6c026fcd045a2e Mon Sep 17 00:00:00 2001 From: anushkamittal1 Date: Thu, 31 Jul 2025 19:21:51 +0530 Subject: [PATCH 1/2] Add rate limiting with user and endpoint-specific limits --- backend/app/user/routes.py | 12 +++++++++--- backend/main.py | 16 ++++++++++++++++ backend/requirements.txt | 27 ++++++++++++++------------- backend/utils/limiter.py | 4 ++++ backend/utils/limiter_helper.py | 10 ++++++++++ 5 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 backend/utils/limiter.py create mode 100644 backend/utils/limiter_helper.py diff --git a/backend/app/user/routes.py b/backend/app/user/routes.py index 1f04384e..a512f319 100644 --- a/backend/app/user/routes.py +++ b/backend/app/user/routes.py @@ -1,4 +1,7 @@ from typing import Any, Dict +from fastapi import Request +from utils.limiter import limiter + from app.auth.security import get_current_user from app.user.schemas import ( @@ -13,7 +16,8 @@ @router.get("/me", response_model=UserProfileResponse) -async def get_current_user_profile( +@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 +29,8 @@ async def get_current_user_profile( @router.patch("/me", response_model=Dict[str, Any]) -async def update_user_profile( +@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,8 @@ 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..4ebcf49d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,11 +10,24 @@ from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response +from slowapi.middleware import SlowAPIMiddleware +from slowapi.errors import RateLimitExceeded +from slowapi import _rate_limit_exceeded_handler +from utils.limiter import limiter +from utils.limiter import get_remote_address +from utils.limiter import 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 +90,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..da7e9585 --- /dev/null +++ b/backend/utils/limiter_helper.py @@ -0,0 +1,10 @@ +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) From 1a49e712c37a0b7a00e1901212722a16eabcd902 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:56:06 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/app/user/routes.py | 20 +++++++++++--------- backend/main.py | 11 +++-------- backend/utils/limiter_helper.py | 5 +++-- ui-poc/Home.py | 7 ++++--- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/backend/app/user/routes.py b/backend/app/user/routes.py index a512f319..fb74ff88 100644 --- a/backend/app/user/routes.py +++ b/backend/app/user/routes.py @@ -1,7 +1,4 @@ from typing import Any, Dict -from fastapi import Request -from utils.limiter import limiter - from app.auth.security import get_current_user from app.user.schemas import ( @@ -10,14 +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, +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"]) @@ -29,8 +28,9 @@ async def get_current_user_profile(request: Request, @router.patch("/me", response_model=Dict[str, Any]) -@limiter.limit("5/minute") -async def update_user_profile(request: Request, +@limiter.limit("5/minute") +async def update_user_profile( + request: Request, updates: UserProfileUpdateRequest, current_user: Dict[str, Any] = Depends(get_current_user), ): @@ -52,8 +52,10 @@ async def update_user_profile(request: Request, @router.delete("/me", response_model=DeleteUserResponse) -@limiter.limit("2/minute") -async def delete_user_account(request: Request,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 4ebcf49d..46dd706a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,20 +10,15 @@ from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response -from slowapi.middleware import SlowAPIMiddleware -from slowapi.errors import RateLimitExceeded from slowapi import _rate_limit_exceeded_handler -from utils.limiter import limiter -from utils.limiter import get_remote_address -from utils.limiter import Limiter +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 diff --git a/backend/utils/limiter_helper.py b/backend/utils/limiter_helper.py index da7e9585..2fbd78bc 100644 --- a/backend/utils/limiter_helper.py +++ b/backend/utils/limiter_helper.py @@ -1,10 +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(