diff --git a/package.json b/package.json index 792555c..74979be 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "arctic": "^2.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", + "ioredis": "^5.4.1", + "lru-cache": "^11.0.2", "lucia": "^3.1.1", "lucide-react": "^0.368.0", "next": "^14.2.3", diff --git a/src/app/api/feedback/[projectId]/route.ts b/src/app/api/feedback/[projectId]/route.ts index e36693e..26a5bb9 100644 --- a/src/app/api/feedback/[projectId]/route.ts +++ b/src/app/api/feedback/[projectId]/route.ts @@ -1,4 +1,5 @@ import prisma from '@/lib/db'; +import ratelimit from '@/lib/ratelimit'; export const dynamic = 'force-dynamic'; @@ -14,18 +15,33 @@ export async function POST(request: Request, { params }: { params: { projectId: return Response.json({ success: false, error: 'Project not found' }, { status: 404 }); } + // TODO: optimize to query redis instead of prisma first. + const ip = request.headers.get('x-forwarded-for') || request.headers.get('cf-connecting-ip'); + const queryRL = await ratelimit( + ip!, + queryProject.id, + queryProject.rateLimitReq, + queryProject.rateLimitTime + ); + if (queryRL.exceeded) { + return Response.json( + { + success: false, + error: `Rate limit exceeded. Try again in ${queryRL.reset} seconds.`, + }, + { status: 429, headers: { 'Retry-After': queryRL.reset.toString() } } + ); + } + // Convert customKeys to regular array and add message const customKeys = [...queryProject.customData, 'message']; const bodyKeys = Object.keys(body); - console.log(bodyKeys); // Find missing required keys (keys that should be in body but aren't) const keysLeft = customKeys.filter((key) => !bodyKeys.includes(key)); - console.log(keysLeft); // Find invalid keys (keys in body that aren't allowed) const invalidKeys = bodyKeys.filter((key) => !customKeys.includes(key)); - console.log(invalidKeys); if (keysLeft.length || invalidKeys.length) { return Response.json( diff --git a/src/components/app/LandingStepper/LandingStepper.tsx b/src/components/app/LandingStepper/LandingStepper.tsx index ad53538..cc52b9a 100644 --- a/src/components/app/LandingStepper/LandingStepper.tsx +++ b/src/components/app/LandingStepper/LandingStepper.tsx @@ -1,4 +1,4 @@ -"use client" +'use client'; import { type ReactNode, useState } from 'react'; import { Button } from '@/components/ui/button'; @@ -39,7 +39,7 @@ export default function LandingStepper({ steps }: { steps: Step[] }) { }`} >
@@ -61,9 +61,7 @@ export default function LandingStepper({ steps }: { steps: Step[] }) {
-
- {steps[currentStep].html} -
+
{steps[currentStep].html}
); diff --git a/src/components/app/ProjectSettings/ProjectSettings.tsx b/src/components/app/ProjectSettings/ProjectSettings.tsx index 075aa67..1a762fc 100644 --- a/src/components/app/ProjectSettings/ProjectSettings.tsx +++ b/src/components/app/ProjectSettings/ProjectSettings.tsx @@ -157,7 +157,7 @@ export default function ProjectSettings(project: Project) { Rate limiting - Manage your API rate limits. Not implemented but you can change it + Manage your API rate limits. @@ -166,14 +166,14 @@ export default function ProjectSettings(project: Project) { { name: 'requests', label: 'Requests', - placeholder: project.rateLimitReq.toString(), + placeholder: 'The amount of requests you want to limit', value: project.rateLimitReq.toString(), type: 'number', }, { name: 'duration', label: 'Duration', - placeholder: project.rateLimitTime.toString(), + placeholder: 'In seconds', value: project.rateLimitTime.toString(), type: 'number', }, diff --git a/src/lib/db/redis.ts b/src/lib/db/redis.ts new file mode 100644 index 0000000..3fa16fe --- /dev/null +++ b/src/lib/db/redis.ts @@ -0,0 +1,4 @@ +import { Redis } from 'ioredis'; + +const redis = new Redis(process.env.REDIS_URL!); +export default redis; \ No newline at end of file diff --git a/src/lib/ratelimit.ts b/src/lib/ratelimit.ts new file mode 100644 index 0000000..fd76d92 --- /dev/null +++ b/src/lib/ratelimit.ts @@ -0,0 +1,31 @@ +import redis from "./db/redis"; + +// TODO: the ratelimit function doesn't look right with the times. may revisit. +export default async function ratelimit(ip: string, projectId: string, limit: number, duration: number) { + const key = `ratelimit:${ip}:${projectId}`; + + // use multi to save on requests + const multi = redis.multi(); + multi.exists(key); + multi.incr(key); + + const result = await multi.exec(); + if (!result) { + throw new Error("Failed to execute multi command"); + } + + const exists = result[0][1] as number; + const current = result[1][1] as number; + + if (exists === 0) { + await redis.expire(key, Math.floor(duration)); + } + + const ttl = await redis.ttl(key); + + return { + exceeded: current > limit, + remaining: Math.max(0, limit - current), + reset: ttl, + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 057227a..aebda33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -365,6 +365,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1740,6 +1745,11 @@ clsx@^2.1.0, clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + code-block-writer@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770" @@ -1908,6 +1918,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -2735,6 +2750,21 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.1.tgz#1c56b70b759f01465913887375ed809134296f40" + integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -3114,6 +3144,16 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -3154,6 +3194,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.1.tgz#e8d901141f22937968e45a6533d52824070151e4" integrity sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA== +lru-cache@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39" + integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -3774,6 +3819,18 @@ recast@^0.23.2: tiny-invariant "^1.3.3" tslib "^2.0.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -4008,6 +4065,11 @@ source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + stdin-discarder@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21"