From ee1a4fe70a0d71308e4d05a734a76ac26fb241ca Mon Sep 17 00:00:00 2001 From: Yogesh Date: Tue, 31 Mar 2026 18:54:36 +0530 Subject: [PATCH 1/4] feat: rate-limit /request and /subscribe --- src/lib/utils/ratelimit.ts | 20 ++++++++++++++++ src/middleware.ts | 49 ++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/lib/utils/ratelimit.ts diff --git a/src/lib/utils/ratelimit.ts b/src/lib/utils/ratelimit.ts new file mode 100644 index 00000000..20ff8c46 --- /dev/null +++ b/src/lib/utils/ratelimit.ts @@ -0,0 +1,20 @@ +import { Ratelimit } from "@upstash/ratelimit"; +import { kv } from "@vercel/kv"; + +// Rate limiter for AI Upload (5 requests per 15 minutes) +export const aiUploadRatelimit = new Ratelimit({ + redis: kv, + limiter: Ratelimit.slidingWindow(5, "900 s"), +}); + +// Rate limiter for Paper Requests (5 requests per 15 minutes) +export const paperRequestRatelimit = new Ratelimit({ + redis: kv, + limiter: Ratelimit.slidingWindow(5, "900 s"), +}); + +// Rate limiter for Subscriptions (3 requests per hour) +export const subscribeRatelimit = new Ratelimit({ + redis: kv, + limiter: Ratelimit.slidingWindow(3, "1 h"), +}); diff --git a/src/middleware.ts b/src/middleware.ts index 76e4e07c..6e5fa642 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,23 +1,48 @@ import { type NextRequest, NextResponse } from "next/server"; -import { Ratelimit } from "@upstash/ratelimit"; -import { kv } from "@vercel/kv"; - -const ratelimit = new Ratelimit({ - redis: kv, - limiter: Ratelimit.slidingWindow(100, "900 s"), -}); +import { + aiUploadRatelimit, + paperRequestRatelimit, + subscribeRatelimit, +} from "./lib/utils/ratelimit"; export const config = { - matcher: "/api/upload", + matcher: ["/api/upload", "/api/request", "/api/subscribe"], }; export default async function middleware(request: NextRequest) { const ip = request.ip ?? "127.0.0.1"; - const { success } = await ratelimit.limit(ip); - return success - ? NextResponse.next() - : NextResponse.json( + const { pathname } = request.nextUrl; + + if (pathname === "/api/upload") { + const { success } = await aiUploadRatelimit.limit(ip); + if (!success) { + return NextResponse.json( { message: "You can upload a maximum of 5 papers every 15 minutes" }, { status: 429 }, ); + } + } + + if (pathname === "/api/request") { + const { success } = await paperRequestRatelimit.limit(ip); + if (!success) { + return NextResponse.json( + { message: "You can submit a maximum of 5 requests every 15 minutes" }, + { status: 429 }, + ); + } + } + + if (pathname === "/api/subscribe") { + const { success } = await subscribeRatelimit.limit(ip); + if (!success) { + return NextResponse.json( + { message: "Maximum of 3 newsletter subscriptions per hour" }, + { status: 429 }, + ); + } + } + + return NextResponse.next(); } + From 168895b7528a799ff761e9481ee2f67aa9b68df4 Mon Sep 17 00:00:00 2001 From: Yogesh Date: Tue, 31 Mar 2026 19:37:35 +0530 Subject: [PATCH 2/4] refactor: Remove KV dependancy, migrate to upstash --- .env.example | 8 ++++---- package.json | 1 - pnpm-lock.yaml | 11 ----------- src/lib/utils/ratelimit.ts | 8 ++++---- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index e535ff56..2a6e557a 100644 --- a/.env.example +++ b/.env.example @@ -23,10 +23,10 @@ GOOGLE_CLOUD_BUCKET="" # Your Google Cloud Storage Bucket n GOOGLE_APPLICATION_CREDENTIALS_JSON="" # The content of the JSON file you download when creating a service account key # Vercel KV -KV_URL="" # The URL of your Vercel KV instance -KV_REST_API_URL="" # The REST API URL of your Vercel KV instance -KV_REST_API_TOKEN="" # The REST API token for your Vercel KV instance -KV_REST_API_READ_ONLY_TOKEN="" # The read-only REST API token for your Vercel KV instance +# KV_URL="" # The URL of your Vercel KV instance +# KV_REST_API_URL="" # The REST API URL of your Vercel KV instance +# KV_REST_API_TOKEN="" # The REST API token for your Vercel KV instance +# KV_REST_API_READ_ONLY_TOKEN="" # The read-only REST API token for your Vercel KV instance # Upstash_Redis UPSTASH_REDIS_REST_URL="" # REST URL of your Upstash Redis database diff --git a/package.json b/package.json index 22069a89..ed875c45 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@ungap/with-resolvers": "^0.1.0", "@upstash/ratelimit": "^2.0.5", "@upstash/redis": "^1.33.0", - "@vercel/kv": "^3.0.0", "axios": "^1.8.4", "canvas": "^3.2.0", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 240913bb..6a7f3cdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: '@upstash/redis': specifier: ^1.33.0 version: 1.33.0 - '@vercel/kv': - specifier: ^3.0.0 - version: 3.0.0 axios: specifier: ^1.8.4 version: 1.12.2 @@ -1258,10 +1255,6 @@ packages: '@upstash/redis@1.35.7': resolution: {integrity: sha512-bdCdKhke+kYUjcLLuGWSeQw7OLuWIx3eyKksyToLBAlGIMX9qiII0ptp8E0y7VFE1yuBxBd/3kSzJ8774Q4g+A==} - '@vercel/kv@3.0.0': - resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==} - engines: {node: '>=14.6'} - '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -4816,10 +4809,6 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@vercel/kv@3.0.0': - dependencies: - '@upstash/redis': 1.35.7 - '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 diff --git a/src/lib/utils/ratelimit.ts b/src/lib/utils/ratelimit.ts index 20ff8c46..55542a9e 100644 --- a/src/lib/utils/ratelimit.ts +++ b/src/lib/utils/ratelimit.ts @@ -1,20 +1,20 @@ import { Ratelimit } from "@upstash/ratelimit"; -import { kv } from "@vercel/kv"; +import { redis } from "./redis"; // Rate limiter for AI Upload (5 requests per 15 minutes) export const aiUploadRatelimit = new Ratelimit({ - redis: kv, + redis: redis, limiter: Ratelimit.slidingWindow(5, "900 s"), }); // Rate limiter for Paper Requests (5 requests per 15 minutes) export const paperRequestRatelimit = new Ratelimit({ - redis: kv, + redis: redis, limiter: Ratelimit.slidingWindow(5, "900 s"), }); // Rate limiter for Subscriptions (3 requests per hour) export const subscribeRatelimit = new Ratelimit({ - redis: kv, + redis: redis, limiter: Ratelimit.slidingWindow(3, "1 h"), }); From 5679afaf165f5fd7857cfa8c22315e03aeae8b70 Mon Sep 17 00:00:00 2001 From: Yogesh Date: Tue, 31 Mar 2026 20:21:54 +0530 Subject: [PATCH 3/4] fix: images.domains deprecated migrate to remotePatterns --- next.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index dbd9a43b..4a9e2741 100644 --- a/next.config.js +++ b/next.config.js @@ -8,7 +8,12 @@ await import("./src/env.js"); const config = { swcMinify: false, images: { - domains: ["storage.googleapis.com"], + remotePatterns: [ + { + protocol: "https", + hostname: "storage.googleapis.com", + }, + ], }, async headers() { return [ From e39cfd87c3bb8e390f8a9189a26f0444f8808b90 Mon Sep 17 00:00:00 2001 From: Yogesh Date: Tue, 31 Mar 2026 20:26:07 +0530 Subject: [PATCH 4/4] fix: useEffect double render use isSelected directly, this causes a double render on every parent state change. --- src/components/Card.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index b67540fb..e476147d 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import { type IPaper } from "@/interface"; import Image from "next/image"; import { Eye, Download, Check } from "lucide-react"; @@ -24,22 +23,12 @@ interface CardProps { } const Card = ({ paper, onSelect, isSelected }: CardProps) => { - const [checked, setChecked] = useState(isSelected); - - useEffect(() => { - setChecked(isSelected); - }, [isSelected]); - const handleDownload = async (paper: IPaper) => { await downloadFile(getSecureUrl(paper.file_url), generateFileName(paper)); }; const handleCheckboxChange = () => { - setChecked((prev) => { - const newChecked = !prev; - onSelect(paper, newChecked); - return newChecked; - }); + onSelect(paper, !isSelected); }; const paperLink = `/paper/${paper._id}`; @@ -48,7 +37,7 @@ const Card = ({ paper, onSelect, isSelected }: CardProps) => {
@@ -100,7 +89,7 @@ const Card = ({ paper, onSelect, isSelected }: CardProps) => {