| import { getRedis } from "@/lib/redis"; |
| import { NextResponse } from "next/server"; |
| import { NextRequest } from "next/server"; |
|
|
| interface RateLimitConfig { |
| limit: number; |
| windowSeconds: number; |
| } |
|
|
| |
| |
| |
| |
| export const RATE_LIMIT_CONFIG = { |
| |
| general: { limit: 100, windowSeconds: 60 }, |
| api_default: { limit: 100, windowSeconds: 60 }, |
|
|
| |
| email: { limit: 50, windowSeconds: 86400 }, |
| email_send: { limit: 10, windowSeconds: 60 }, |
| email_batch: { limit: 100, windowSeconds: 3600 }, |
|
|
| |
| scraping: { limit: 10, windowSeconds: 60 }, |
| scraping_start: { limit: 5, windowSeconds: 300 }, |
| scraping_search: { limit: 20, windowSeconds: 60 }, |
|
|
| |
| auth: { limit: 5, windowSeconds: 60 }, |
| auth_login: { limit: 5, windowSeconds: 60 }, |
| auth_signup: { limit: 3, windowSeconds: 300 }, |
|
|
| |
| workflow_create: { limit: 50, windowSeconds: 3600 }, |
| workflow_execute: { limit: 100, windowSeconds: 60 }, |
| workflow_update: { limit: 50, windowSeconds: 60 }, |
|
|
| |
| api_search: { limit: 30, windowSeconds: 60 }, |
| api_export: { limit: 10, windowSeconds: 3600 }, |
| api_upload: { limit: 20, windowSeconds: 3600 }, |
| } as const; |
|
|
| export type RateLimitContext = keyof typeof RATE_LIMIT_CONFIG; |
|
|
| export class RateLimiter { |
| |
| |
| |
| |
| |
| |
| static async check(key: string, config: RateLimitConfig) { |
| const redis = getRedis(); |
|
|
| if (!redis) { |
| console.warn("Redis not available, skipping rate limit check"); |
| return { success: true, remaining: 1, reset: 0 }; |
| } |
|
|
| const { limit, windowSeconds } = config; |
| const now = Date.now(); |
| const windowStart = now - windowSeconds * 1000; |
|
|
| const pipeline = redis.pipeline(); |
|
|
| |
| pipeline.zremrangebyscore(key, 0, windowStart); |
|
|
| |
| pipeline.zcard(key); |
|
|
| |
| pipeline.zadd(key, now, `${now}-${Math.random()}`); |
|
|
| |
| pipeline.expire(key, windowSeconds); |
|
|
| const results = await pipeline.exec(); |
|
|
| |
| |
| const requestCount = results?.[1]?.[1] as number; |
|
|
| const remaining = Math.max(0, limit - requestCount); |
| const success = requestCount < limit; |
|
|
| return { |
| success, |
| remaining, |
| reset: Math.floor((now + windowSeconds * 1000) / 1000), |
| }; |
| } |
|
|
| |
| |
| |
| static async cleanup(key: string) { |
| const redis = getRedis(); |
|
|
| if (redis) { |
| await redis.del(key); |
| } |
| } |
| } |
|
|
| |
| |
| |
| export async function rateLimit(request: Request, context: string = "general") { |
| const redis = getRedis(); |
|
|
| if (!redis) return null; |
|
|
| const ip = request.headers.get("x-forwarded-for") || "unknown"; |
| const key = `rate_limit:${context}:${ip}`; |
|
|
| |
| const config = |
| RATE_LIMIT_CONFIG[context as RateLimitContext] || RATE_LIMIT_CONFIG.general; |
|
|
| const result = await RateLimiter.check(key, config); |
|
|
| if (!result.success) { |
| return NextResponse.json( |
| { error: "Too many requests, please try again later." }, |
| { |
| status: 429, |
| headers: { |
| "Retry-After": String(result.reset - Math.floor(Date.now() / 1000)), |
| "X-RateLimit-Limit": String(config.limit), |
| "X-RateLimit-Remaining": String(result.remaining), |
| "X-RateLimit-Reset": String(result.reset), |
| }, |
| } |
| ); |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| export async function checkRateLimit( |
| request: NextRequest | Request, |
| context: RateLimitContext = "general" |
| ): Promise<{ |
| limited: boolean; |
| remaining: number; |
| reset: number; |
| response?: NextResponse; |
| }> { |
| const redis = getRedis(); |
|
|
| if (!redis) { |
| return { limited: false, remaining: 999, reset: 0 }; |
| } |
|
|
| const ip = request.headers.get("x-forwarded-for") || "unknown"; |
| const key = `rate_limit:${context}:${ip}`; |
| const config = RATE_LIMIT_CONFIG[context] || RATE_LIMIT_CONFIG.general; |
|
|
| const result = await RateLimiter.check(key, config); |
|
|
| if (!result.success) { |
| return { |
| limited: true, |
| remaining: result.remaining, |
| reset: result.reset, |
| response: NextResponse.json( |
| { |
| success: false, |
| error: "Rate limit exceeded", |
| code: "RATE_LIMIT_EXCEEDED", |
| retryAfter: result.reset - Math.floor(Date.now() / 1000), |
| }, |
| { |
| status: 429, |
| headers: { |
| "Retry-After": String(result.reset - Math.floor(Date.now() / 1000)), |
| "X-RateLimit-Limit": String(config.limit), |
| "X-RateLimit-Remaining": String(result.remaining), |
| "X-RateLimit-Reset": String(result.reset), |
| }, |
| } |
| ), |
| }; |
| } |
|
|
| return { |
| limited: false, |
| remaining: result.remaining, |
| reset: result.reset, |
| }; |
| } |
|
|
| export async function getRemainingEmails(userId: string): Promise<number> { |
| |
| const key = `email_limit:${userId}`; |
| const config = RATE_LIMIT_CONFIG.email; |
| const redis = getRedis(); |
|
|
| if (!redis) return 50; |
|
|
| const result = await RateLimiter.check(key, config); |
| return result.remaining; |
| } |
|
|
| export async function checkEmailRateLimit(userId: string): Promise<boolean> { |
| const remaining = await getRemainingEmails(userId); |
| return remaining > 0; |
| } |
|
|