fitaiProto/apps/admin/src/lib/rate-limit.ts
2026-03-10 04:14:03 +01:00

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);
}