Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement rate limit #195

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ CRON_SECRET=
# mainnet or sepolia
# Note: Not everything is supported on sepolia
# Default: mainnet
NEXT_PUBLIC_NETWORK=mainnet
NEXT_PUBLIC_NETWORK=mainnet
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Rate limiting configuration
RATE_LIMIT_REQUESTS=50
RATE_LIMIT_WINDOW="60 s"
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"format:check": "prettier --check \"**/*.{ts,tsx,json}\"",
"format:fix": "prettier --write \"**/*.{ts,tsx,json}\"",
"prepare": "husky",
"postinstall": "prisma generate"
"postinstall": "prisma generate",
"test:ratelimit": "ts-node --project tsconfig.server.json src/scripts/test-rate-limit.ts"
},
"files": [
"CHANGELOG.md",
Expand All @@ -34,6 +35,8 @@
"@tanstack/query-core": "5.28.0",
"@types/mixpanel-browser": "2.49.0",
"@types/mustache": "4.2.5",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.3",
"@vercel/analytics": "1.2.2",
"@vercel/speed-insights": "1.0.12",
"axios": "1.6.7",
Expand Down Expand Up @@ -86,6 +89,7 @@
"prettier": "3.3.3",
"prisma": "5.18.0",
"tailwindcss": "3.3.0",
"ts-node": "^10.9.2",
"typescript": "5"
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/price/[name]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextResponse } from 'next/server';

export const revalidate = 300; // 5 mins
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

// only meant for backend calls
async function initRedis() {
Expand Down
1 change: 1 addition & 0 deletions src/app/api/raffle/luckyWinner/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic'; // static by default, unless reading the request
export const runtime = 'nodejs';

export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
Expand Down Expand Up @@ -50,7 +51,7 @@
userId: true,
},
});
if (totalExistingWinners.length == raffleParticipants.length) {

Check warning on line 54 in src/app/api/raffle/luckyWinner/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

Expected '===' and instead saw '=='
return NextResponse.json({
success: false,
message: 'No new participants found',
Expand Down Expand Up @@ -111,7 +112,7 @@
}

// Check if we were able to select enough winners
if (luckyWinners.length == 0) {

Check warning on line 115 in src/app/api/raffle/luckyWinner/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

Expected '===' and instead saw '=='
return NextResponse.json({
success: false,
message: 'No winner found',
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/raffle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { db } from '@/db';
import { getStrategies } from '@/store/strategies.atoms';
import { standariseAddress } from '@/utils';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function POST(req: Request) {
const { address, type } = await req.json();

Expand Down
3 changes: 3 additions & 0 deletions src/app/api/referral/createUser/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { NextResponse } from 'next/server';
import { db } from '@/db';
import { standariseAddress } from '@/utils';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

function isSixDigitAlphanumeric(str: string) {
const regex = /^[a-zA-Z0-9]{6}$/;
return regex.test(str);
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/stats/[address]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { standariseAddress } from '@/utils';
import { NextResponse } from 'next/server';

export const revalidate = 0;
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(_req: Request, context: any) {
const { params } = context;
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { getStrategies } from '@/store/strategies.atoms';
import { NextResponse } from 'next/server';

export const revalidate = 1800;
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(_req: Request) {
const strategies = getStrategies();
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/strategies/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import { STRKFarmStrategyAPIResult } from '@/store/strkfarm.atoms';

export const revalidate = 3600; // 1 hr
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

const allPoolsAtom = atom<PoolInfo[]>((get) => {
const pools: PoolInfo[] = [];
Expand All @@ -25,7 +27,7 @@
const hasRequiredPools = minProtocolsRequired.every((p) => {
if (!allPools) return false;
return allPools.some(
(pool) => pool.protocol.name === p && pool.type == PoolType.Lending,

Check warning on line 30 in src/app/api/strategies/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

Expected '===' and instead saw '=='
);
});
const MAX_RETRIES = 120;
Expand Down Expand Up @@ -90,7 +92,7 @@
};
}

export async function GET(req: Request) {

Check warning on line 95 in src/app/api/strategies/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

'req' is defined but never used. Allowed unused args must match /^_/u
const allPools = await getPools(MY_STORE);
const strategies = getStrategies();

Expand Down
2 changes: 2 additions & 0 deletions src/app/api/tnc/getUser/[address]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';

import { db } from '@/db';
import { standariseAddress } from '@/utils';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(req: Request, context: any) {
const { params } = context;
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/tnc/signUser/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import Mixpanel from 'mixpanel';
const mixpanel = Mixpanel.init('118f29da6a372f0ccb6f541079cad56b');

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function POST(req: Request) {
const { address, signature } = await req.json();

Expand Down Expand Up @@ -94,7 +97,7 @@

if (!isValid) {
try {
const cls = await provider.getClassAt(address, 'pending');

Check warning on line 100 in src/app/api/tnc/signUser/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

'cls' is assigned a value but never used. Allowed unused vars must match /^_/u
// means account is deployed
return NextResponse.json({
success: false,
Expand Down Expand Up @@ -178,7 +181,7 @@
}),
});
console.debug('verifyMessageHash resp', resp);
if (Number(resp[0]) == 0) {

Check warning on line 184 in src/app/api/tnc/signUser/route.ts

View workflow job for this annotation

GitHub Actions / Performs linting, formatting on the application

Expected '===' and instead saw '=='
throw new Error('Invalid signature');
}
return true;
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/users/ognft/[address]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { standariseAddress } from '../../../../../utils';
import OGNFTUsersJson from '../../../../../../public/og_nft_eligible_users.json';

export const revalidate = 3600;
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(req: Request, context: any) {
try {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

export default redis;
56 changes: 56 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import redis from './lib/redis';

export const config = {
matcher: ['/api/:path*'],
};

const RATE_LIMIT_REQUESTS = parseInt(
process.env.RATE_LIMIT_REQUESTS || '20',
10,
);
const RATE_LIMIT_WINDOW = process.env.RATE_LIMIT_WINDOW || '10 s';

const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(
RATE_LIMIT_REQUESTS,
RATE_LIMIT_WINDOW as `${number} s`,
),
analytics: true,
prefix: '@upstash/ratelimit',
});

export async function middleware(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') || '127.0.0.1';
const identifier = ip;

let success, limit, remaining, reset;
try {
const result = await ratelimit.limit(identifier);
success = result.success;
limit = result.limit;
remaining = result.remaining;
reset = result.reset;
} catch (error) {
console.log(error);
return NextResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 },
);
}

const response = success
? NextResponse.next()
: NextResponse.json(
{ message: 'Rate limit exceeded', limit, remaining, reset },
{ status: 429 },
);

response.headers.set('X-RateLimit-Limit', limit.toString());
response.headers.set('X-RateLimit-Remaining', remaining.toString());
response.headers.set('X-RateLimit-Reset', reset.toString());

return response;
}
30 changes: 30 additions & 0 deletions src/scripts/test-rate-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
async function testRateLimit(url: string, attempts: number) {
console.log(`Testing rate limit for ${url}`);
for (let i = 0; i < attempts; i++) {
const response = await fetch(url);
const remaining = response.headers.get('X-RateLimit-Remaining');
console.log(
`Attempt ${i + 1}: Status ${response.status}, Remaining: ${remaining}`,
);
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait 100ms between requests
}
console.log('\n');
}

async function runTests() {
const baseUrl = 'http://localhost:3000/api';
await testRateLimit(`${baseUrl}/price`, 25);
await testRateLimit(`${baseUrl}/raffle`, 25);
await testRateLimit(`${baseUrl}/raffle/luckyWinner`, 25);
await testRateLimit(`${baseUrl}/referral/createUser`, 25);
await testRateLimit(`${baseUrl}/stats`, 25);
await testRateLimit(`${baseUrl}/stats/[address]`, 25);
await testRateLimit(`${baseUrl}/strategies`, 25);
await testRateLimit(`${baseUrl}/tnc/getUser`, 25);
await testRateLimit(`${baseUrl}/tnc/getUser/[address]`, 25);
await testRateLimit(`${baseUrl}/tnc/signUser`, 25);
await testRateLimit(`${baseUrl}/users/ognft`, 25);
await testRateLimit(`${baseUrl}/users/ognft/[address]`, 25);
}

runTests().catch(console.error);
5 changes: 5 additions & 0 deletions src/types/redis.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'lib/redis' {
import { Redis } from '@upstash/redis';
const redis: Redis;
export default redis;
}
11 changes: 11 additions & 0 deletions tsconfig.server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["scripts/**/*.ts"]
}
Loading
Loading