122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
|
|
interface RateLimitStore {
|
|
[key: string]: {
|
|
count: number;
|
|
resetTime: number;
|
|
};
|
|
}
|
|
|
|
const store: RateLimitStore = {};
|
|
|
|
interface RateLimitConfig {
|
|
interval: number; // Time window in milliseconds
|
|
maxRequests: number; // Maximum requests in time window
|
|
}
|
|
|
|
/**
|
|
* Simple in-memory rate limiter
|
|
* For production, use Redis or a dedicated rate limiting service
|
|
*
|
|
* @param identifier - Unique identifier (e.g., IP address, user ID)
|
|
* @param config - Rate limit configuration
|
|
* @returns NextResponse error if rate limited, undefined if allowed
|
|
*
|
|
* @example
|
|
* const rateLimitError = rateLimit(userId, { interval: 60000, maxRequests: 5 });
|
|
* if (rateLimitError) return rateLimitError;
|
|
*/
|
|
export function rateLimit(
|
|
identifier: string,
|
|
config: RateLimitConfig,
|
|
): NextResponse | undefined {
|
|
const now = Date.now();
|
|
const key = identifier;
|
|
|
|
// Initialize or reset if window expired
|
|
if (!store[key] || now > store[key].resetTime) {
|
|
store[key] = {
|
|
count: 1,
|
|
resetTime: now + config.interval,
|
|
};
|
|
return undefined;
|
|
}
|
|
|
|
// Increment count
|
|
store[key].count++;
|
|
|
|
// Check if limit exceeded
|
|
if (store[key].count > config.maxRequests) {
|
|
const retryAfter = Math.ceil((store[key].resetTime - now) / 1000);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: "Too many requests",
|
|
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
retryAfter,
|
|
},
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
"Retry-After": retryAfter.toString(),
|
|
"X-RateLimit-Limit": config.maxRequests.toString(),
|
|
"X-RateLimit-Remaining": "0",
|
|
"X-RateLimit-Reset": store[key].resetTime.toString(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Rate limit configurations for different endpoints
|
|
*/
|
|
export const RATE_LIMITS = {
|
|
// Authentication endpoints - stricter limits
|
|
login: { interval: 60000, maxRequests: 5 }, // 5 attempts per minute
|
|
register: { interval: 3600000, maxRequests: 3 }, // 3 attempts per hour
|
|
passwordReset: { interval: 3600000, maxRequests: 3 }, // 3 attempts per hour
|
|
|
|
// API endpoints - more lenient
|
|
api: { interval: 60000, maxRequests: 100 }, // 100 requests per minute
|
|
apiStrict: { interval: 60000, maxRequests: 10 }, // 10 requests per minute
|
|
} as const;
|
|
|
|
/**
|
|
* Get client identifier for rate limiting
|
|
* Uses IP address as fallback if user is not authenticated
|
|
*
|
|
* @param request - NextRequest object
|
|
* @param userId - Optional authenticated user ID
|
|
* @returns Identifier for rate limiting
|
|
*/
|
|
export function getClientIdentifier(request: Request, userId?: string): string {
|
|
if (userId) return `user:${userId}`;
|
|
|
|
// Try to get IP from headers (works with most reverse proxies)
|
|
const forwarded = request.headers.get("x-forwarded-for");
|
|
const ip =
|
|
forwarded?.split(",")[0] ?? request.headers.get("x-real-ip") ?? "unknown";
|
|
|
|
return `ip:${ip}`;
|
|
}
|
|
|
|
/**
|
|
* Cleanup expired rate limit entries (call periodically)
|
|
*/
|
|
export function cleanupRateLimitStore(): void {
|
|
const now = Date.now();
|
|
for (const key in store) {
|
|
if (store[key].resetTime < now) {
|
|
delete store[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cleanup every 10 minutes
|
|
if (typeof setInterval !== "undefined") {
|
|
setInterval(cleanupRateLimitStore, 10 * 60 * 1000);
|
|
}
|