diff --git a/backend/admin_api/package.json b/backend/admin_api/package.json index 390e9596e..a07e33e9b 100644 --- a/backend/admin_api/package.json +++ b/backend/admin_api/package.json @@ -23,6 +23,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "express-rate-limit": "^8.2.1", "he": "^1.2.0", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", @@ -38,6 +39,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^5.0.6", + "@types/express-rate-limit": "^6.0.2", "@types/he": "^1.2.0", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.7", diff --git a/backend/admin_api/src/middleware/rate_limit.ts b/backend/admin_api/src/middleware/rate_limit.ts new file mode 100644 index 000000000..5ed6db971 --- /dev/null +++ b/backend/admin_api/src/middleware/rate_limit.ts @@ -0,0 +1,24 @@ +import type { OkoApiErrorResponse } from "@oko-wallet/oko-types/api_response"; +import rateLimit from "express-rate-limit"; + +export interface RateLimitMiddlewareOption { + windowSeconds: number; + maxRequests: number; +} + +export function rateLimitMiddleware(option: RateLimitMiddlewareOption) { + const message: OkoApiErrorResponse = { + success: false, + code: "RATE_LIMIT_EXCEEDED", + msg: `Too many requests, please try again after ${option.windowSeconds} seconds`, + }; + + return rateLimit({ + windowMs: option.windowSeconds * 1000, + max: option.maxRequests, + message, + statusCode: 429, + standardHeaders: true, + legacyHeaders: false, + }); +} diff --git a/backend/admin_api/src/routes/index.ts b/backend/admin_api/src/routes/index.ts index 231602478..782727649 100644 --- a/backend/admin_api/src/routes/index.ts +++ b/backend/admin_api/src/routes/index.ts @@ -53,6 +53,7 @@ import { import { customerLogoUploadMiddleware } from "@oko-wallet-admin-api/middleware/multer"; import { adminAuthMiddleware } from "@oko-wallet-admin-api/middleware/auth"; +import { rateLimitMiddleware } from "@oko-wallet-admin-api/middleware/rate_limit"; import { typeformWebhookMiddleware } from "../middleware/typeform_webhook"; import { create_customer } from "./create_customer"; import { get_customer_list } from "./get_customer_list"; @@ -413,7 +414,11 @@ export function makeOkoAdminRouter() { }, }, }); - router.post("/user/login", user_login); + router.post( + "/user/login", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), + user_login, + ); registry.registerPath({ method: "post", diff --git a/backend/ct_dashboard_api/src/routes/customer_auth.ts b/backend/ct_dashboard_api/src/routes/customer_auth.ts index 4d67fe0e8..8c0815b35 100644 --- a/backend/ct_dashboard_api/src/routes/customer_auth.ts +++ b/backend/ct_dashboard_api/src/routes/customer_auth.ts @@ -527,6 +527,7 @@ export function setCustomerAuthRoutes(router: Router) { }); router.post( "/customer/auth/send-code", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals; @@ -630,6 +631,7 @@ export function setCustomerAuthRoutes(router: Router) { }); router.post( "/customer/auth/verify-login", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals as any; @@ -820,6 +822,7 @@ export function setCustomerAuthRoutes(router: Router) { }); router.post( "/customer/auth/signin", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals as any; @@ -1010,6 +1013,7 @@ export function setCustomerAuthRoutes(router: Router) { }); router.post( "/customer/auth/change-password", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), customerJwtMiddleware, async ( req: CustomerAuthenticatedRequest, diff --git a/backend/social_login_api/src/routes/social_login.ts b/backend/social_login_api/src/routes/social_login.ts index eeb228c18..1a08ea0f1 100644 --- a/backend/social_login_api/src/routes/social_login.ts +++ b/backend/social_login_api/src/routes/social_login.ts @@ -19,6 +19,7 @@ import { X_CLIENT_ID, X_SOCIAL_LOGIN_TOKEN_URL, } from "@oko-wallet-social-login-api/constants/x"; +import { rateLimitMiddleware } from "@oko-wallet-social-login-api/middleware/rate_limit"; export function setSocialLoginRoutes(router: Router) { registry.registerPath({ @@ -66,6 +67,7 @@ export function setSocialLoginRoutes(router: Router) { }); router.post( "/x/get-token", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async ( req: Request, res: Response>, @@ -159,6 +161,7 @@ export function setSocialLoginRoutes(router: Router) { }); router.get( "/x/verify-user", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async ( req: Request, res: Response>, diff --git a/backend/user_dashboard_api/src/routes/user_auth.ts b/backend/user_dashboard_api/src/routes/user_auth.ts index b29de8db9..ae5e76ecb 100644 --- a/backend/user_dashboard_api/src/routes/user_auth.ts +++ b/backend/user_dashboard_api/src/routes/user_auth.ts @@ -42,6 +42,7 @@ import { customerJwtMiddleware, type CustomerAuthenticatedRequest, } from "@oko-wallet-usrd-api/middleware/auth"; +import { rateLimitMiddleware } from "@oko-wallet-usrd-api/middleware/rate_limit"; export function setUserAuthRoutes(router: Router) { registry.registerPath({ @@ -98,6 +99,7 @@ export function setUserAuthRoutes(router: Router) { }); router.post( "/customer/auth/send-code", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals; @@ -201,6 +203,7 @@ export function setUserAuthRoutes(router: Router) { }); router.post( "/customer/auth/verify-login", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals as any; @@ -391,6 +394,7 @@ export function setUserAuthRoutes(router: Router) { }); router.post( "/customer/auth/signin", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), async (req, res: Response>) => { try { const state = req.app.locals as any; @@ -581,6 +585,7 @@ export function setUserAuthRoutes(router: Router) { }); router.post( "/customer/auth/change-password", + rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }), customerJwtMiddleware, async ( req: CustomerAuthenticatedRequest, diff --git a/yarn.lock b/yarn.lock index 81f43c5a0..873fc31a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10095,6 +10095,7 @@ __metadata: "@oko-wallet/stdlib-js": "npm:0.0.2-rc.41" "@types/cors": "npm:^2.8.17" "@types/express": "npm:^5.0.6" + "@types/express-rate-limit": "npm:^6.0.2" "@types/he": "npm:^1.2.0" "@types/jest": "npm:^29.5.14" "@types/jsonwebtoken": "npm:^9.0.7" @@ -10108,6 +10109,7 @@ __metadata: cors: "npm:^2.8.5" dotenv: "npm:^16.4.5" express: "npm:^4.21.1" + express-rate-limit: "npm:^8.2.1" he: "npm:^1.2.0" helmet: "npm:^8.0.0" jest: "npm:^30.1.3" @@ -25625,7 +25627,7 @@ __metadata: languageName: node linkType: hard -"express-rate-limit@npm:*, express-rate-limit@npm:8.2.1": +"express-rate-limit@npm:*, express-rate-limit@npm:8.2.1, express-rate-limit@npm:^8.2.1": version: 8.2.1 resolution: "express-rate-limit@npm:8.2.1" dependencies: