From 7e1ca9829373bdbaec19a5c11a6a212267287e20 Mon Sep 17 00:00:00 2001 From: vishal Date: Mon, 29 Jul 2024 22:01:09 +0530 Subject: [PATCH] implemented ratelimiting on submit --- apps/web/app/api/submission/route.ts | 14 +++++++- apps/web/app/components/ProblemSubmitBar.tsx | 14 +++++++- apps/web/app/lib/rateLimit.ts | 38 -------------------- apps/web/app/lib/redis.ts | 33 +++++++++++++++++ apps/web/package.json | 2 ++ 5 files changed, 61 insertions(+), 40 deletions(-) delete mode 100644 apps/web/app/lib/rateLimit.ts create mode 100644 apps/web/app/lib/redis.ts diff --git a/apps/web/app/api/submission/route.ts b/apps/web/app/api/submission/route.ts index c99d8ff..28fb489 100644 --- a/apps/web/app/api/submission/route.ts +++ b/apps/web/app/api/submission/route.ts @@ -6,6 +6,7 @@ import { LANGUAGE_MAPPING } from "@repo/common/language"; import db from "@/app/db"; import { getServerSession } from "next-auth"; import { authOptions } from "../../lib/auth"; +import { isRequestAllowed } from "@/app/lib/redis"; const JUDGE0_URI = process.env.JUDGE0_URI; @@ -21,7 +22,18 @@ export async function POST(req: NextRequest) { status: 401 } ); - } + }; + + //Check if user is allowed to submit + const allowed = await isRequestAllowed(session.user.id, 60);// 1 minute window for 2 requests + if (!allowed) { + return NextResponse.json( + { + message: "Rate limit exceeded", + status: 429 + } + ); + }; //get and check the submission input diff --git a/apps/web/app/components/ProblemSubmitBar.tsx b/apps/web/app/components/ProblemSubmitBar.tsx index 903f2f8..3d03aea 100644 --- a/apps/web/app/components/ProblemSubmitBar.tsx +++ b/apps/web/app/components/ProblemSubmitBar.tsx @@ -146,7 +146,19 @@ function SubmitProblem({ activeContestId: contestId, token: token, }); - console.log("response from proState : ",response.data); + + if(response.status === 429){ + setStatus(SubmitStatus.FAILED); + toast.error("Try again after sometime"); + return; + }; + + if(response.status != 200){ + setStatus(SubmitStatus.FAILED); + toast.error("Failed to submit"); + return; + }; + pollWithBackoff(response.data.id, 30); } catch (e) { //@ts-ignore diff --git a/apps/web/app/lib/rateLimit.ts b/apps/web/app/lib/rateLimit.ts deleted file mode 100644 index 31a71a5..0000000 --- a/apps/web/app/lib/rateLimit.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Redis from "ioredis"; - -function getRedisClient() { - const redis = new Redis(process.env.REDIS_URL || ""); - return redis; -} - -export async function rateLimit( - userId: string, - limit: number, - duration: number -): Promise { - const key = `rate_limit:${userId}`; - const currentTime = Math.floor(Date.now() / 1000); - - try { - const redis = getRedisClient(); - // Start a Redis transaction - const transaction = redis.multi(); - transaction.zremrangebyscore(key, 0, currentTime - duration); - transaction.zadd(key, currentTime, currentTime); - transaction.zcard(key); - - const results = await transaction.exec(); - - const requestCount = results?.[2]?.[1] as number; // Cast to number explicitly - - if (requestCount > limit) { - return false; // Rate limit exceeded - } - - await redis.expire(key, duration + 1); - return true; - } catch (error) { - console.error("Rate limiting error:", error); - return false; // In case of any error, block the request to be safe - } -} diff --git a/apps/web/app/lib/redis.ts b/apps/web/app/lib/redis.ts new file mode 100644 index 0000000..1f7050b --- /dev/null +++ b/apps/web/app/lib/redis.ts @@ -0,0 +1,33 @@ +import Redis from 'ioredis'; + +const redis = new Redis(process.env.REDIS_URL!); + +redis.on('connect', () => { + console.log('Connected to Redis'); +}); + +redis.on('error', (error) => { + console.error('Redis error:', error); +}); + +export async function isRequestAllowed(userId: string,timeWindow:number): Promise { + const currentTime = Math.floor(Date.now() / 1000); + const key = `rate_limit:${userId}`; + + const maxRequests = 2; + + const timestamps = await redis.lrange(key, 0, -1); + const validTimestamps = timestamps + .map(Number) + .filter(timestamp => timestamp > currentTime - timeWindow); + + if (validTimestamps.length < maxRequests) { + await redis.multi() + .rpush(key, currentTime) + .expire(key, timeWindow) + .exec(); + return true; + } else { + return false; + } +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 0cc140c..0551b8b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "@upstash/ratelimit": "^2.0.1", "axios": "^1.7.2", "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.0", @@ -27,6 +28,7 @@ "react": "^18", "react-dom": "^18", "react-toastify": "^10.0.5", + "redis": "^4.7.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "vconcol": "^1.0.1"