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"