70 lines
1.7 KiB
TypeScript
70 lines
1.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
|
|
interface RateLimitEntry {
|
|
count: number
|
|
resetAt: number
|
|
}
|
|
|
|
interface RateLimiterOptions {
|
|
windowMs: number
|
|
maxRequests: number
|
|
message?: string
|
|
}
|
|
|
|
export function createRateLimiter(options: RateLimiterOptions) {
|
|
const store = new Map<string, RateLimitEntry>()
|
|
|
|
// Periodic cleanup every 60s
|
|
const cleanupInterval = setInterval(() => {
|
|
const now = Date.now()
|
|
for (const [key, entry] of store) {
|
|
if (now > entry.resetAt) store.delete(key)
|
|
}
|
|
}, 60_000)
|
|
// Don't prevent process exit
|
|
if (cleanupInterval.unref) cleanupInterval.unref()
|
|
|
|
return function checkRateLimit(request: Request): NextResponse | null {
|
|
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
|
|
const now = Date.now()
|
|
const entry = store.get(ip)
|
|
|
|
if (!entry || now > entry.resetAt) {
|
|
store.set(ip, { count: 1, resetAt: now + options.windowMs })
|
|
return null
|
|
}
|
|
|
|
entry.count++
|
|
if (entry.count > options.maxRequests) {
|
|
return NextResponse.json(
|
|
{ error: options.message || 'Too many requests. Please try again later.' },
|
|
{ status: 429 }
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const loginLimiter = createRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 5,
|
|
message: 'Too many login attempts. Try again in a minute.',
|
|
})
|
|
|
|
export const mutationLimiter = createRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 60,
|
|
})
|
|
|
|
export const readLimiter = createRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 120,
|
|
})
|
|
|
|
export const heavyLimiter = createRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 10,
|
|
message: 'Too many requests for this resource. Please try again later.',
|
|
})
|