diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..0ff2b76 --- /dev/null +++ b/.env.local @@ -0,0 +1,3 @@ +AUTH_SECRET="super-secret-dummy-key-for-local-dev" +NEXTAUTH_SECRET="super-secret-dummy-key-for-local-dev" +GITHUB_APP_URL="https://github.com/apps/dummy-secureflow" diff --git a/package-lock.json b/package-lock.json index 366f239..20ab7d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,8 @@ "@radix-ui/react-tabs": "^1.1.16", "@radix-ui/react-toast": "^1.2.18", "@radix-ui/react-tooltip": "^1.2.11", + "@upstash/ratelimit": "^2.0.8", + "@upstash/redis": "^1.38.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", @@ -7440,6 +7442,39 @@ "win32" ] }, + "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.8", + "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.8.tgz", + "integrity": "sha512-YSTMBJ1YIxsoPkUMX/P4DDks/xV5YYCswWMamU8ZIfK9ly6ppjRnVOyBhMDXBmzjODm4UQKcxsJPvaeFAijp5w==", + "license": "MIT", + "dependencies": { + "@upstash/core-analytics": "^0.0.10" + }, + "peerDependencies": { + "@upstash/redis": "^1.34.3" + } + }, + "node_modules/@upstash/redis": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.0.tgz", + "integrity": "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "license": "BSD-2-Clause" @@ -17234,6 +17269,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", diff --git a/package.json b/package.json index 40745ed..0a0dc2c 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "@radix-ui/react-tabs": "^1.1.16", "@radix-ui/react-toast": "^1.2.18", "@radix-ui/react-tooltip": "^1.2.11", + "@upstash/ratelimit": "^2.0.8", + "@upstash/redis": "^1.38.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", diff --git a/src/lib/ratelimit.ts b/src/lib/ratelimit.ts new file mode 100644 index 0000000..0842c12 --- /dev/null +++ b/src/lib/ratelimit.ts @@ -0,0 +1,9 @@ +import { Ratelimit } from "@upstash/ratelimit"; +import { Redis } from "@upstash/redis"; + +export const ratelimit = process.env.UPSTASH_REDIS_REST_URL + ? new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(20, "60 s"), + }) + : null; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..ac02ae2 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { ratelimit } from '@/lib/ratelimit'; + +export async function middleware(request: NextRequest) { + if (request.nextUrl.pathname.startsWith('/api/og') && ratelimit) { + const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'; + const { success } = await ratelimit.limit(ip); + + if (!success) { + return new NextResponse('Too Many Requests', { status: 429 }); + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/api/og/:path*'], +};