Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 0 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 3 additions & 14 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -24,22 +23,12 @@ interface CardProps {
}

const Card = ({ paper, onSelect, isSelected }: CardProps) => {
const [checked, setChecked] = useState<boolean>(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}`;
Expand All @@ -48,7 +37,7 @@ const Card = ({ paper, onSelect, isSelected }: CardProps) => {
<div
className={cn(
"overflow-hidden rounded-sm border-2 border-[#734DFF] bg-[#FFFFFF] font-play transition-all duration-150 hover:bg-[#EFEAFF] dark:border-[#36266D] dark:bg-[#171720] hover:dark:bg-[#262635]",
checked && "bg-white",
isSelected && "bg-white",
)}
>
<Link href={paperLink} target="_blank" rel="noopener noreferrer">
Expand Down Expand Up @@ -100,7 +89,7 @@ const Card = ({ paper, onSelect, isSelected }: CardProps) => {
<div className="flex items-center justify-between gap-2 px-4 pb-4 font-play">
<div className="flex items-center gap-2">
<input
checked={checked}
checked={isSelected}
onChange={handleCheckboxChange}
className="h-5 w-5 accent-[#7480FF]"
type="checkbox"
Expand Down
20 changes: 20 additions & 0 deletions src/lib/utils/ratelimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "./redis";

// Rate limiter for AI Upload (5 requests per 15 minutes)
export const aiUploadRatelimit = new Ratelimit({
redis: redis,
limiter: Ratelimit.slidingWindow(5, "900 s"),
});

// Rate limiter for Paper Requests (5 requests per 15 minutes)
export const paperRequestRatelimit = new Ratelimit({
redis: redis,
limiter: Ratelimit.slidingWindow(5, "900 s"),
});

// Rate limiter for Subscriptions (3 requests per hour)
export const subscribeRatelimit = new Ratelimit({
redis: redis,
limiter: Ratelimit.slidingWindow(3, "1 h"),
});
49 changes: 37 additions & 12 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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();
}

Loading