export class RateLimiter {
private limits: Record<string, number> = {
admin: 1000, // 1000 requests per minute for admins
user: 100, // 100 requests per minute for regular users
anonymous: 20 // 20 requests per minute for anonymous users
}
private windowMs = 60 * 1000 // 1 minute window
private requestCounts: Map<string, { count: number, resetTime: number }> = new Map()
// Check if a request should be rate limited
public checkLimit(key: string, role: string = 'anonymous'): { limited: boolean, limit: number, remaining: number, resetTime: number } {
const now = Date.now()
const limit = this.limits[role] || this.limits.anonymous
// Clean up expired entries first
this.cleanUp(now)
// Get or create entry for this key
let entry = this.requestCounts.get(key)
if (!entry) {
entry = { count: 0, resetTime: now + this.windowMs }
this.requestCounts.set(key, entry)
}
// If reset time has passed, create a new entry
if (now > entry.resetTime) {
entry.count = 0
entry.resetTime = now + this.windowMs
}
// Increment count
entry.count += 1
// Check if limit exceeded
const limited = entry.count > limit
const remaining = Math.max(0, limit - entry.count)
return { limited, limit, remaining, resetTime: entry.resetTime }
}
// Clean up expired entries to prevent memory leaks
private cleanUp(now: number): void {
for (const [key, entry] of this.requestCounts.entries()) {
if (now > entry.resetTime) {
this.requestCounts.delete(key)
}
}
}
}
// Middleware implementation
export const rateLimitMiddleware: MiddlewareHandler = async ({ request, locals }, next) => {
// Skip rate limiting for non-API routes
if (!request.url.includes('/api/ai/')) {
return await next()
}
// Get IP address or user ID for rate limiting key
const session = locals.session
const userId = session?.user?.id
const clientIP = request.headers.get('X-Forwarded-For') || 'unknown'
const key = userId || clientIP
const role = session?.user?.role || 'anonymous'
// Skip rate limiting for admins if desired
if (role === 'admin' && BYPASS_RATE_LIMIT_FOR_ADMINS) {
return await next()
}
// Check rate limit
const limiter = getLimiter()
const { limited, limit, remaining, resetTime } = limiter.checkLimit(key, role)
if (limited) {
// Calculate retry after in seconds
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000)
return new Response(JSON.stringify({
error: 'Too many requests',
message: 'Rate limit exceeded',
retryAfter
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': retryAfter.toString(),
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': Math.ceil(resetTime / 1000).toString()
}
})
}
// Proceed with the request
const response = await next()
// Add rate limit headers to the response
response.headers.set('X-RateLimit-Limit', limit.toString())
response.headers.set('X-RateLimit-Remaining', remaining.toString())
response.headers.set('X-RateLimit-Reset', Math.ceil(resetTime / 1000).toString())
return response
}