88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
import { Redis } from '@upstash/redis'
|
|
import { headers } from 'next/headers'
|
|
|
|
export interface RateLimitConfig {
|
|
interval: number // in seconds
|
|
limit: number
|
|
}
|
|
|
|
export interface RateLimitResult {
|
|
success: boolean
|
|
limit: number
|
|
remaining: number
|
|
reset: number // timestamp in seconds
|
|
}
|
|
|
|
export class RateLimit {
|
|
private redis: Redis
|
|
private prefix: string
|
|
|
|
constructor() {
|
|
this.redis = Redis.fromEnv()
|
|
this.prefix = 'ratelimit'
|
|
}
|
|
|
|
private async getIP(): Promise<string> {
|
|
try {
|
|
const headersList = await headers()
|
|
const forwardedFor = headersList.get('x-forwarded-for')
|
|
const realIP = headersList.get('x-real-ip')
|
|
return forwardedFor?.split(',')[0] || realIP || 'development'
|
|
} catch (error) {
|
|
console.warn('Failed to get IP address, using fallback')
|
|
return 'development'
|
|
}
|
|
}
|
|
|
|
private getKey(identifier: string): string {
|
|
return `${this.prefix}:${identifier}`
|
|
}
|
|
|
|
async check(config: RateLimitConfig): Promise<RateLimitResult> {
|
|
try {
|
|
const ip = await this.getIP()
|
|
const key = this.getKey(ip)
|
|
const now = Math.floor(Date.now() / 1000)
|
|
const windowStart = now - (now % config.interval)
|
|
const windowKey = `${key}:${windowStart}`
|
|
|
|
const pipeline = this.redis.pipeline()
|
|
pipeline.incr(windowKey)
|
|
pipeline.expire(windowKey, config.interval)
|
|
|
|
const [count] = await pipeline.exec()
|
|
const remaining = Math.max(0, config.limit - (count as number))
|
|
|
|
return {
|
|
success: (count as number) <= config.limit,
|
|
limit: config.limit,
|
|
remaining,
|
|
reset: windowStart + config.interval
|
|
}
|
|
} catch (error) {
|
|
console.warn('Redis unavailable, allowing request (rate limiting disabled):', error)
|
|
// Fallback: allow the request when Redis is unavailable
|
|
// TODO: Fix Redis connection or create new instance
|
|
return {
|
|
success: true,
|
|
limit: config.limit,
|
|
remaining: config.limit - 1,
|
|
reset: Math.floor(Date.now() / 1000) + config.interval
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default configuration
|
|
const DEFAULT_CONFIG: RateLimitConfig = {
|
|
interval: 60 * 60, // 1 hour
|
|
limit: 5 // 5 requests per hour
|
|
}
|
|
|
|
// Helper function for easy usage
|
|
export async function rateLimit(
|
|
config: RateLimitConfig = DEFAULT_CONFIG
|
|
): Promise<RateLimitResult> {
|
|
const limiter = new RateLimit()
|
|
return limiter.check(config)
|
|
}
|