Skip to content

Commit 595c7d2

Browse files
authored
feat: introduced rate-limit (#20)
1 parent 4289439 commit 595c7d2

6 files changed

Lines changed: 854 additions & 15 deletions

File tree

app/api/evaluator/register/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dbConnect from "@/lib/db";
44
import Evaluator from "@/models/Evaluator";
55
import User from "@/models/User";
66
import { getAuth } from "@/lib/firebase-admin";
7+
import { checkRateLimit } from "@/lib/rate-limit";
78

89
export const dynamic = 'force-dynamic';
910

@@ -32,6 +33,11 @@ function createErrorResponse(message: string, code: string, status: number) {
3233
*/
3334
export async function POST(request: NextRequest) {
3435
try {
36+
const ip = request.headers.get("x-forwarded-for") || "unknown";
37+
if (!checkRateLimit(ip, 5, 60 * 1000)) {
38+
return createErrorResponse("Too many requests. Please try again later.", "RATE_LIMIT_EXCEEDED", 429);
39+
}
40+
3541
const authHeader = request.headers.get('authorization');
3642
if (!authHeader || !authHeader.startsWith('Bearer ')) {
3743
return createErrorResponse("Authentication required", "AUTH_REQUIRED", 401);

app/api/registration/route.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from "path";
1010
import os from "os";
1111
import dbConnect from "@/lib/db";
1212
import User, { IUser } from "@/models/User";
13+
import { checkRateLimit } from "@/lib/rate-limit";
1314

1415
// Utility functions for format validation
1516
const validateEmail = (email: string) =>
@@ -205,6 +206,18 @@ const getOrCreateBatchDocument = async () => {
205206

206207
export async function POST(request: Request) {
207208
try {
209+
// Basic IP Rate limiting (5 requests per minute)
210+
const ip = request.headers.get("x-forwarded-for") || "unknown";
211+
if (!checkRateLimit(ip, 5, 60 * 1000)) {
212+
return NextResponse.json(
213+
{
214+
message: "Too many requests. Please try again later.",
215+
error: "Rate limit exceeded",
216+
},
217+
{ status: 429 }
218+
);
219+
}
220+
208221
const REGISTRATION_DEADLINE = new Date("2026-07-19T10:00:00+05:30");
209222
if (new Date() > REGISTRATION_DEADLINE) {
210223
return NextResponse.json(
@@ -361,11 +374,12 @@ export async function POST(request: Request) {
361374
);
362375
}
363376

364-
// Validate password
365-
if (password.length < 6) {
377+
// Validate password (min 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char)
378+
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/;
379+
if (!passwordRegex.test(password)) {
366380
return NextResponse.json(
367381
{
368-
message: "Password must be at least 6 characters long.",
382+
message: "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
369383
error: "Weak password",
370384
},
371385
{ status: 400 },

app/evaluator/register/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export default function EvaluatorRegisterPage() {
4949
return;
5050
}
5151

52+
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/;
53+
if (!passwordRegex.test(formData.password)) {
54+
setError("Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.");
55+
setIsSubmitting(false);
56+
return;
57+
}
58+
5259
if (!formData.evaluatorCode.trim()) {
5360
setError("Evaluator code is required");
5461
setIsSubmitting(false);

lib/rate-limit.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
type RateLimitInfo = {
2+
count: number;
3+
lastReset: number;
4+
};
5+
6+
const rateLimits = new Map<string, RateLimitInfo>();
7+
8+
// Cleans up the Map every 5 minutes to prevent memory leaks
9+
setInterval(() => {
10+
const now = Date.now();
11+
for (const [ip, info] of rateLimits.entries()) {
12+
if (now - info.lastReset > 5 * 60 * 1000) {
13+
rateLimits.delete(ip);
14+
}
15+
}
16+
}, 5 * 60 * 1000);
17+
18+
export function checkRateLimit(ip: string, limit: number, windowMs: number): boolean {
19+
const now = Date.now();
20+
const info = rateLimits.get(ip) || { count: 0, lastReset: now };
21+
22+
if (now - info.lastReset > windowMs) {
23+
info.count = 1;
24+
info.lastReset = now;
25+
rateLimits.set(ip, info);
26+
return true;
27+
}
28+
29+
if (info.count >= limit) {
30+
return false;
31+
}
32+
33+
info.count++;
34+
rateLimits.set(ip, info);
35+
return true;
36+
}

0 commit comments

Comments
 (0)