mirror of
https://github.com/SrIzan10/echospace.git
synced 2026-06-06 00:56:54 +00:00
feat: implement ratelimit
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] }) {
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
className={`w-8 h-8 aspect-square rounded-[50%] flex items-center justify-center ${
|
||||
index === currentStep ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
@@ -61,9 +61,7 @@ export default function LandingStepper({ steps }: { steps: Step[] }) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2">
|
||||
{steps[currentStep].html}
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2">{steps[currentStep].html}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function ProjectSettings(project: Project) {
|
||||
<CardHeader>
|
||||
<CardTitle>Rate limiting</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your API rate limits. Not implemented but you can change it
|
||||
Manage your API rate limits.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
4
src/lib/db/redis.ts
Normal file
4
src/lib/db/redis.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL!);
|
||||
export default redis;
|
||||
31
src/lib/ratelimit.ts
Normal file
31
src/lib/ratelimit.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
62
yarn.lock
62
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"
|
||||
|
||||
Reference in New Issue
Block a user