From df46a8da6f1b63c678264e61c0a3d8b09f242290 Mon Sep 17 00:00:00 2001 From: David MENDY Date: Thu, 19 Dec 2024 09:04:29 +0100 Subject: [PATCH] feat: :passport_control: implement rate limit on api requests --- backend/requirements.txt | 1 + backend/src/main.py | 6 ++++ backend/src/router.py | 16 ++++++++- frontend/src/components.d.ts | 1 + frontend/src/main.css | 4 +++ frontend/src/views/ErrorPage.vue | 45 +++++++++++++++++++++---- frontend/src/views/InstructionsPage.vue | 3 +- 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index ef8af00ce..d709383ea 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,6 +11,7 @@ boto3==1.35.81 autodynatrace==2.1.1 PyJWT==2.10.1 cryptography==44.0.0 +slowapi===0.1.9 # ML basegun-ml==2.0.5 numpy<3.0.0 diff --git a/backend/src/main.py b/backend/src/main.py index fdefb8320..b51f5059e 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,10 +1,16 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.util import get_remote_address from .config import HEADERS from .router import router +limiter = Limiter(key_func=get_remote_address) app = FastAPI(docs_url="/api/docs") +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware( CORSMiddleware, diff --git a/backend/src/router.py b/backend/src/router.py index 34a8e6bf0..9fb47f341 100644 --- a/backend/src/router.py +++ b/backend/src/router.py @@ -22,6 +22,8 @@ status, ) from fastapi.responses import PlainTextResponse +from slowapi import Limiter +from slowapi.util import get_remote_address from user_agents import parse from .config import ( @@ -35,21 +37,26 @@ from .utils import get_current_user, send_mail, upload_image router = APIRouter(prefix="/api") +limiter = Limiter(key_func=get_remote_address) @router.get("/", response_class=PlainTextResponse) +@limiter.limit("5/minute") def home(): return "Basegun backend" @router.get("/version", response_class=PlainTextResponse) -def version(): +@limiter.limit("5/minute") +def version(request: Request): return APP_VERSION @router.get("/contact-details") +@limiter.limit("5/minute") async def phone_number( current_user: Annotated[dict, Depends(get_current_user)], + request: Request, ): if current_user.get("idp") != "proxyma": raise HTTPException( @@ -63,6 +70,7 @@ async def phone_number( @router.post("/upload") +@limiter.limit("5/minute") async def imageupload( request: Request, response: Response, @@ -141,6 +149,7 @@ async def imageupload( @router.post("/identification-feedback") +@limiter.limit("5/minute") async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None)): res = await request.json() @@ -155,6 +164,7 @@ async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None @router.post("/tutorial-feedback") +@limiter.limit("5/minute") async def log_tutorial_feedback( request: Request, user_id: Union[str, None] = Cookie(None) ): @@ -178,7 +188,9 @@ async def log_tutorial_feedback( @router.post("/expert-contact") +@limiter.limit("5/minute") async def expert_contact( + request: Request, firstname: Annotated[str, Form()], lastname: Annotated[str, Form()], nigend: Annotated[str, Form()], @@ -219,7 +231,9 @@ async def expert_contact( @router.post("/identification-alarm-gun") +@limiter.limit("5/minute") async def image_alarm_gun( + request: Request, image: UploadFile = File(...), ): try: diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts index 621db027e..561af805b 100644 --- a/frontend/src/components.d.ts +++ b/frontend/src/components.d.ts @@ -12,6 +12,7 @@ declare module 'vue' { DsfrAlert: typeof import('@gouvminint/vue-dsfr')['DsfrAlert'] DsfrButton: typeof import('@gouvminint/vue-dsfr')['DsfrButton'] DsfrCheckbox: typeof import('@gouvminint/vue-dsfr')['DsfrCheckbox'] + DsfrErrorPage: typeof import('@gouvminint/vue-dsfr')['DsfrErrorPage'] DsfrFileUpload: typeof import('@gouvminint/vue-dsfr')['DsfrFileUpload'] DsfrHeader: typeof import('@gouvminint/vue-dsfr')['DsfrHeader'] DsfrInput: typeof import('@gouvminint/vue-dsfr')['DsfrInput'] diff --git a/frontend/src/main.css b/frontend/src/main.css index 0f8226476..4c52a2811 100644 --- a/frontend/src/main.css +++ b/frontend/src/main.css @@ -6,6 +6,10 @@ h2 { font-size: 1.3rem; } + + .error-img { + width: 120%; + } } .text-blue { color: var(--blue-france-sun-113-625); diff --git a/frontend/src/views/ErrorPage.vue b/frontend/src/views/ErrorPage.vue index 8770137d2..f9871eee5 100644 --- a/frontend/src/views/ErrorPage.vue +++ b/frontend/src/views/ErrorPage.vue @@ -1,15 +1,46 @@ + +