From 1ca8d864337bc8e8548bcb6d8a938955120e9bb3 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 | 22 ++++++++- docker-compose.ci.yml | 2 + frontend/src/components.d.ts | 1 + frontend/src/main.css | 4 ++ frontend/src/router/index.ts | 17 +++---- frontend/src/views/ErrorPage.vue | 66 ++++++++++++++++++++++--- frontend/src/views/InstructionsPage.vue | 3 +- frontend/src/views/PageNotFound.vue | 11 ----- 10 files changed, 101 insertions(+), 32 deletions(-) delete mode 100644 frontend/src/views/PageNotFound.vue diff --git a/backend/requirements.txt b/backend/requirements.txt index 0da93a05..0c4b85e7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,6 +11,7 @@ boto3==1.35.92 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 fdefb832..b51f5059 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 34a8e6bf..f6b5ebd4 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 ( @@ -34,22 +36,31 @@ ) from .utils import get_current_user, send_mail, upload_image +DISABLE_RATE_LIMITS = os.environ.get("DISABLE_RATE_LIMITS", "false").lower() == "true" + router = APIRouter(prefix="/api") +limiter = Limiter( + key_func=get_remote_address if not DISABLE_RATE_LIMITS else lambda: None +) @router.get("/", response_class=PlainTextResponse) -def home(): +@limiter.limit("60/minute") +def home(request: Request): return "Basegun backend" @router.get("/version", response_class=PlainTextResponse) -def version(): +@limiter.limit("60/minute") +def version(request: Request): return APP_VERSION @router.get("/contact-details") +@limiter.limit("60/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 +74,7 @@ async def phone_number( @router.post("/upload") +@limiter.limit("60/minute") async def imageupload( request: Request, response: Response, @@ -141,6 +153,7 @@ async def imageupload( @router.post("/identification-feedback") +@limiter.limit("60/minute") async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None)): res = await request.json() @@ -155,6 +168,7 @@ async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None @router.post("/tutorial-feedback") +@limiter.limit("60/minute") async def log_tutorial_feedback( request: Request, user_id: Union[str, None] = Cookie(None) ): @@ -178,7 +192,9 @@ async def log_tutorial_feedback( @router.post("/expert-contact") +@limiter.limit("60/minute") async def expert_contact( + request: Request, firstname: Annotated[str, Form()], lastname: Annotated[str, Form()], nigend: Annotated[str, Form()], @@ -219,7 +235,9 @@ async def expert_contact( @router.post("/identification-alarm-gun") +@limiter.limit("60/minute") async def image_alarm_gun( + request: Request, image: UploadFile = File(...), ): try: diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index c06d8291..179d17f3 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,3 +1,5 @@ services: backend: image: ${IMAGE_TAG} + environment: + - DISABLE_RATE_LIMITS=true diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts index 621db027..561af805 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 0f822647..4c52a281 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/router/index.ts b/frontend/src/router/index.ts index 94d62a2d..ce47a6ba 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -16,7 +16,6 @@ const HomePage = () => import("@/views/HomePage.vue"); const StartPage = () => import("@/views/StartPage.vue"); const InstructionsPage = () => import("@/views/InstructionsPage.vue"); const ErrorPage = () => import("@/views/ErrorPage.vue"); -const PageNotFound = () => import("@/views/PageNotFound.vue"); const AboutPage = () => import("@/views/AboutPage.vue"); const LegalPage = () => import("@/views/LegalPage.vue"); const ContactPage = () => import("@/views/ContactPage.vue"); @@ -173,6 +172,7 @@ const routes: RouteRecordRaw[] = [ }, { path: "/erreur", + alias: "/:pathMach(.*)*", name: "ErrorPage", component: ErrorPage, meta: { @@ -190,15 +190,6 @@ const routes: RouteRecordRaw[] = [ title: "Accessibilité", }, }, - { - path: "/:pathMach(.*)*", - name: "PageNotFound", - component: PageNotFound, - meta: { - wholeLogo: true, - title: "Page non trouvée", - }, - }, { path: "/guide-contact", name: "ExpertContact", @@ -269,7 +260,11 @@ const router = createRouter({ router.beforeEach((to, from, next) => { console.log(to); document.title = "Basegun | " + to.meta.title; - next(); + if (to.name === "ErrorPage" && !to.query.status) { + next({ ...to, query: { ...to.query, status: 404 } }); + } else { + next(); + } }); export default router; diff --git a/frontend/src/views/ErrorPage.vue b/frontend/src/views/ErrorPage.vue index 8770137d..d24f845e 100644 --- a/frontend/src/views/ErrorPage.vue +++ b/frontend/src/views/ErrorPage.vue @@ -1,15 +1,67 @@ + +