← back to Wine Finder Next

lib/rateLimit.ts

88 lines

// Rate Limiting Middleware
import { NextRequest } from 'next/server';

interface RateLimitConfig {
  windowMs: number;
  maxRequests: number;
}

const rateLimitStore = new Map<string, { count: number; resetAt: number }>();

// Different rate limits for different endpoints
const RATE_LIMITS: Record<string, RateLimitConfig> = {
  '/api/membership/votes/': { windowMs: 60000, maxRequests: 10 }, // 10 votes per minute
  '/api/membership/marketplace/': { windowMs: 60000, maxRequests: 20 }, // 20 market actions per minute
  '/api/membership/bottles': { windowMs: 60000, maxRequests: 100 }, // 100 reads per minute
  default: { windowMs: 60000, maxRequests: 60 } // 60 requests per minute default
};

export function getRateLimitConfig(path: string): RateLimitConfig {
  for (const [key, config] of Object.entries(RATE_LIMITS)) {
    if (path.startsWith(key)) {
      return config;
    }
  }
  return RATE_LIMITS.default;
}

export function checkRateLimit(
  identifier: string,
  config: RateLimitConfig
): { success: boolean; limit: number; remaining: number; reset: number } {
  const now = Date.now();
  const key = `${identifier}`;
  const record = rateLimitStore.get(key);

  // Clean up expired entries periodically
  if (Math.random() < 0.01) {
    cleanupExpiredEntries();
  }

  if (!record || now > record.resetAt) {
    const resetAt = now + config.windowMs;
    rateLimitStore.set(key, { count: 1, resetAt });
    return {
      success: true,
      limit: config.maxRequests,
      remaining: config.maxRequests - 1,
      reset: resetAt
    };
  }

  if (record.count >= config.maxRequests) {
    return {
      success: false,
      limit: config.maxRequests,
      remaining: 0,
      reset: record.resetAt
    };
  }

  record.count++;
  return {
    success: true,
    limit: config.maxRequests,
    remaining: config.maxRequests - record.count,
    reset: record.resetAt
  };
}

function cleanupExpiredEntries(): void {
  const now = Date.now();
  for (const [key, record] of rateLimitStore.entries()) {
    if (now > record.resetAt) {
      rateLimitStore.delete(key);
    }
  }
}

export function getClientIdentifier(request: NextRequest): string {
  // Use multiple identifiers for better accuracy
  const forwarded = request.headers.get('x-forwarded-for');
  const realIp = request.headers.get('x-real-ip');
  const ip = forwarded?.split(',')[0] || realIp || 'unknown';

  // In production, also consider user session/auth token
  return ip;
}