// server/middleware/rate-limit.ts /** * Rate limiting middleware for auth endpoints * * Prevents brute force attacks on login/registration * * Limits: * - /api/auth/login: 5 attempts per 15 minutes per IP * - /api/auth/register: 3 attempts per hour per IP */ interface RateLimitEntry { count: number resetAt: number } // In-memory rate limit store (use Redis in production!) const rateLimitStore = new Map() // Clean up expired entries every 5 minutes setInterval( () => { const now = Date.now() for (const [key, entry] of rateLimitStore.entries()) { if (entry.resetAt < now) { rateLimitStore.delete(key) } } }, 5 * 60 * 1000 ) export default defineEventHandler((event) => { const path = event.path // Only apply to auth endpoints if (!path.startsWith('/api/auth/')) { return } // Get client IP const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown' // Define rate limits per endpoint const limits: Record = { '/api/auth/login': { maxAttempts: 5, windowMs: 15 * 60 * 1000 }, // 5 per 15min '/api/auth/register': { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, // 3 per hour } const limit = limits[path] if (!limit) { return // No rate limit for this endpoint } // Check rate limit const key = `${ip}:${path}` const now = Date.now() const entry = rateLimitStore.get(key) if (!entry || entry.resetAt < now) { // First attempt or window expired - reset counter rateLimitStore.set(key, { count: 1, resetAt: now + limit.windowMs, }) return } // Increment counter entry.count++ if (entry.count > limit.maxAttempts) { // Rate limit exceeded const retryAfter = Math.ceil((entry.resetAt - now) / 1000) setResponseStatus(event, 429) setResponseHeader(event, 'Retry-After', retryAfter.toString()) throw createError({ statusCode: 429, statusMessage: 'Too many requests', data: { retryAfter, message: `Too many attempts. Please try again in ${retryAfter} seconds.`, }, }) } })