Skip to content

Commit 02ceb95

Browse files
authored
Merge pull request #77 from indrasuthar07/rate-limiting
feat: rate limiting on auth
2 parents 7063440 + f944122 commit 02ceb95

File tree

7 files changed

+107
-19
lines changed

7 files changed

+107
-19
lines changed

backend/package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"cors": "^2.8.5",
1818
"dotenv": "^17.2.3",
1919
"express": "^5.1.0",
20+
"express-rate-limit": "^8.2.1",
2021
"jsonwebtoken": "^9.0.2",
2122
"mongodb": "^6.20.0",
2223
"mongoose": "^8.19.4",

backend/src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import path from "path";
1414
dotenv.config();
1515
const app = express();
1616

17+
if (process.env.TRUST_PROXY === "true") {
18+
app.set("trust proxy", true);
19+
}
1720
// Required for __dirname in ES modules / TS
1821
// (TS compiles to CJS so this works fine)
1922
const __dirnameLocal = path.resolve();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import rateLimit from "express-rate-limit";
2+
import type { Request, Response } from "express";
3+
import logger from "../utils/logger.js";
4+
5+
export const authRateLimiter = rateLimit({
6+
windowMs: 15 * 60 * 1000,
7+
max: 5,
8+
standardHeaders: true,
9+
legacyHeaders: false,
10+
handler: (req: Request, res: Response) => {
11+
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
12+
logger.warn(
13+
`Rate limit exceeded for IP: ${clientIP} on ${req.method} ${req.originalUrl}`
14+
);
15+
16+
const retryAfter = req.rateLimit?.resetTime
17+
? Math.round((req.rateLimit.resetTime - Date.now()) / 1000)
18+
: 900;
19+
20+
res.status(429).json({
21+
success: false,
22+
message: "Too many authentication attempts. Please try again after 15 minutes.",
23+
retryAfter,
24+
});
25+
},
26+
keyGenerator: (req: Request) => {
27+
return req.ip || req.socket.remoteAddress || "unknown";
28+
},
29+
});
30+

backend/src/routes/authRoutes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import passport from "passport";
1010
import { Session } from "../models/sessionModel.js";
1111
import { protect } from "../middleware/authMiddleware.js";
12+
import { authRateLimiter } from "../middleware/rateLimiter.js";
1213
import { generateToken, generateRefreshToken } from "../utils/generateToken.js";
1314
import jwt from "jsonwebtoken";
1415
import dotenv from "dotenv";
@@ -20,9 +21,8 @@ dotenv.config();
2021
const router = express.Router();
2122
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173";
2223

23-
// REST endpoints
24-
router.post("/signup", registerUser);
25-
router.post("/signin", loginUser);
24+
router.post("/signup", authRateLimiter, registerUser);
25+
router.post("/signin", authRateLimiter, loginUser);
2626
router.post("/logout", logoutUser);
2727
router.get("/refresh", handleRefreshToken);
2828
router.get("/me", protect, getUserProfile);

frontend/src/pages/Signin.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,23 @@ const SignIn = () => {
5454
reset();
5555
navigate("/"); // redirect to home/dashboard
5656
} catch (error: any) {
57-
toast({
58-
title: "Error",
59-
description:
60-
error.response?.data?.message || "Login failed. Please try again.",
61-
variant: "destructive",
62-
});
57+
// Handle rate limiting errors
58+
if (error.response?.status === 429) {
59+
const retryAfter = error.response?.data?.retryAfter || 15;
60+
const minutes = Math.ceil(retryAfter / 60);
61+
toast({
62+
title: "Too Many Attempts",
63+
description: `Too many login attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`,
64+
variant: "destructive",
65+
});
66+
} else {
67+
toast({
68+
title: "Error",
69+
description:
70+
error.response?.data?.message || "Login failed. Please try again.",
71+
variant: "destructive",
72+
});
73+
}
6374
} finally {
6475
setIsLoading(false);
6576
}

frontend/src/pages/Signup.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,30 @@ const SignUp = () => {
8383
reset();
8484
setTimeout(() => navigate("/signin"), 1500);
8585
} catch (error: any) {
86-
const msg =
87-
error.response?.data?.message ||
88-
"Registration failed. Please try again.";
89-
toast({
90-
title: "Error",
91-
description: msg,
92-
variant: "destructive",
93-
});
94-
setServerMessage(msg);
95-
setIsError(true);
86+
// Handle rate limiting errors
87+
if (error.response?.status === 429) {
88+
const retryAfter = error.response?.data?.retryAfter || 15;
89+
const minutes = Math.ceil(retryAfter / 60);
90+
const msg = `Too many registration attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`;
91+
toast({
92+
title: "Too Many Attempts",
93+
description: msg,
94+
variant: "destructive",
95+
});
96+
setServerMessage(msg);
97+
setIsError(true);
98+
} else {
99+
const msg =
100+
error.response?.data?.message ||
101+
"Registration failed. Please try again.";
102+
toast({
103+
title: "Error",
104+
description: msg,
105+
variant: "destructive",
106+
});
107+
setServerMessage(msg);
108+
setIsError(true);
109+
}
96110
} finally {
97111
setIsLoading(false);
98112
}

0 commit comments

Comments
 (0)