diff --git a/package-lock.json b/package-lock.json index 6b80055..4f388d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", "@uiw/react-md-editor": "^3.11.0", + "@upstash/ratelimit": "^2.0.3", + "@vercel/kv": "^3.0.0", "axios": "^1.7.2", "bech32": "^2.0.0", "chart.js": "^4.4.4", @@ -5065,6 +5067,48 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "license": "ISC" }, + "node_modules/@upstash/core-analytics": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.10.tgz", + "integrity": "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==", + "license": "MIT", + "dependencies": { + "@upstash/redis": "^1.28.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@upstash/ratelimit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.3.tgz", + "integrity": "sha512-BMUpZPZ9IMwrUwohw0HoVAwjBRo5SDb0riAxfCGrLbutuZTPiVagh017Cm3GfhMqwUWLOp0xJQxTCXp812UJVQ==", + "license": "MIT", + "dependencies": { + "@upstash/core-analytics": "^0.0.10" + } + }, + "node_modules/@upstash/redis": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.0.tgz", + "integrity": "sha512-TrXNoJLkysIl8SBc4u9bNnyoFYoILpCcFJcLyWCccb/QSUmaVKdvY0m5diZqc3btExsapcMbaw/s/wh9Sf1pJw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0" + } + }, + "node_modules/@vercel/kv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-3.0.0.tgz", + "integrity": "sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==", + "license": "Apache-2.0", + "dependencies": { + "@upstash/redis": "^1.34.0" + }, + "engines": { + "node": ">=14.6" + } + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", diff --git a/package.json b/package.json index 3600c28..0359cad 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", "@uiw/react-md-editor": "^3.11.0", + "@upstash/ratelimit": "^2.0.3", + "@vercel/kv": "^3.0.0", "axios": "^1.7.2", "bech32": "^2.0.0", "chart.js": "^4.4.4", diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..cc3f264 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,46 @@ +import { NextResponse } from 'next/server'; +import { Ratelimit } from '@upstash/ratelimit'; +import { kv } from '@vercel/kv'; + +const FRONTEND_HOSTNAME = process.env.FRONTEND_HOSTNAME +const FRONTEND_STAGING_HOSTNAME = process.env.FRONTEND_STAGING_HOSTNAME + +const ratelimit = new Ratelimit({ + redis: kv, + // 5 requests from the same IP in 10 seconds + limiter: Ratelimit.slidingWindow(5, '10 s'), +}); + +export const config = { + matcher: ['/api/:path*'], +}; + +export default async function combinedMiddleware(request) { + const ip = request.ip ?? '127.0.0.1'; + const hostname = request.nextUrl.hostname; + const referer = request.headers.get('referer') || ''; + + // Bypass rate limiting and referer check for the deployment IP + if (hostname === FRONTEND_HOSTNAME || hostname === FRONTEND_STAGING_HOSTNAME) { + return NextResponse.next(); + } + + // Bypass referer check for paths following /link + if (request.nextUrl.pathname.startsWith('/.well-known')) { + const { success } = await ratelimit.limit(ip); + return success + ? NextResponse.next() + : NextResponse.redirect(new URL('/blocked', request.url)); + } + + // Apply referer check for all other routes + if (!referer.startsWith(FRONTEND_HOSTNAME) && !referer.startsWith(FRONTEND_STAGING_HOSTNAME)) { + return new NextResponse(JSON.stringify({ error: 'Forbidden' }), { status: 403 }); + } + + // Apply rate limiting for all other routes + const { success } = await ratelimit.limit(ip); + return success + ? NextResponse.next() + : NextResponse.redirect(new URL('/blocked', request.url)); +} \ No newline at end of file