fix: grouped pathnames for rate limit
This commit is contained in:
parent
701f038971
commit
78320fdd56
6 changed files with 16 additions and 12 deletions
|
|
@ -14,7 +14,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const rateLimit = new RateLimit(request, 10);
|
const rateLimit = new RateLimit(request, 30, "/api/mii/delete");
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
// todo: rate limit by mii
|
const rateLimit = new RateLimit(request, 1); // no grouped pathname; edit each mii 1 time a minute
|
||||||
const rateLimit = new RateLimit(request, 3);
|
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const rateLimit = new RateLimit(request, 100);
|
const rateLimit = new RateLimit(request, 100, "/api/mii/like");
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const searchParamsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const rateLimit = new RateLimit(request, 200);
|
const rateLimit = new RateLimit(request, 200, "/mii/image");
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { idSchema } from "@/lib/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const rateLimit = new RateLimit(request, 16);
|
const rateLimit = new RateLimit(request, 16, "/profile/picture");
|
||||||
const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ interface RateLimitData {
|
||||||
export class RateLimit {
|
export class RateLimit {
|
||||||
private request: NextRequest;
|
private request: NextRequest;
|
||||||
private maxRequests: number;
|
private maxRequests: number;
|
||||||
|
private pathname: string; // instead of using the request's pathname, use this custom one to group all routes together
|
||||||
private data: RateLimitData;
|
private data: RateLimitData;
|
||||||
|
|
||||||
constructor(request: NextRequest, maxRequests: number) {
|
constructor(request: NextRequest, maxRequests: number, pathname?: string) {
|
||||||
this.request = request;
|
this.request = request;
|
||||||
this.maxRequests = maxRequests;
|
this.maxRequests = maxRequests;
|
||||||
|
this.pathname = pathname ? pathname : this.request.nextUrl.pathname;
|
||||||
this.data = {
|
this.data = {
|
||||||
success: true,
|
success: true,
|
||||||
limit: maxRequests,
|
limit: maxRequests,
|
||||||
|
|
@ -31,8 +33,7 @@ export class RateLimit {
|
||||||
|
|
||||||
// Check and update rate limit
|
// Check and update rate limit
|
||||||
async check(identifier: string): Promise<RateLimitData> {
|
async check(identifier: string): Promise<RateLimitData> {
|
||||||
const pathname = this.request.nextUrl.pathname;
|
const key = `ratelimit:${this.pathname}:${identifier}`;
|
||||||
const key = `ratelimit:${pathname}:${identifier}`;
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const seconds = Math.floor(now / 1000);
|
const seconds = Math.floor(now / 1000);
|
||||||
|
|
@ -46,8 +47,12 @@ export class RateLimit {
|
||||||
tx.expireat(key, expireAt);
|
tx.expireat(key, expireAt);
|
||||||
|
|
||||||
// Execute transaction and get the count
|
// Execute transaction and get the count
|
||||||
const [count] = (await tx.exec().then((results) => results?.map((res) => res[1]))) as [number];
|
const results = await tx.exec();
|
||||||
|
if (!results) {
|
||||||
|
throw new Error("Redis transaction failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = results[0][1] as number;
|
||||||
const success = count <= this.maxRequests;
|
const success = count <= this.maxRequests;
|
||||||
const remaining = Math.max(0, this.maxRequests - count);
|
const remaining = Math.max(0, this.maxRequests - count);
|
||||||
|
|
||||||
|
|
@ -55,7 +60,7 @@ export class RateLimit {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Rate limit check failed", error);
|
console.error("Rate limit check failed", error);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
limit: this.maxRequests,
|
limit: this.maxRequests,
|
||||||
remaining: this.maxRequests,
|
remaining: this.maxRequests,
|
||||||
expires: expireAt,
|
expires: expireAt,
|
||||||
|
|
@ -84,7 +89,7 @@ export class RateLimit {
|
||||||
async handle(): Promise<NextResponse<object | unknown> | undefined> {
|
async handle(): Promise<NextResponse<object | unknown> | undefined> {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const ip = this.request.headers.get("CF-Connecting-IP") || this.request.headers.get("X-Forwarded-For")?.split(",")[0];
|
const ip = this.request.headers.get("CF-Connecting-IP") || this.request.headers.get("X-Forwarded-For")?.split(",")[0];
|
||||||
const identifier = (session ? session.user.id : ip) ?? "null";
|
const identifier = (session ? session.user.id : ip) ?? "anonymous";
|
||||||
|
|
||||||
this.data = await this.check(identifier);
|
this.data = await this.check(identifier);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue