// In-memory rate limiter for API endpoints // Tracks request counts per IP within sliding windows interface RateLimitEntry { count: number resetAt: number } const stores = new Map>() interface RateLimitConfig { /** Unique identifier for this limiter (e.g. 'login', 'register') */ id: string /** Maximum requests allowed within the window */ max: number /** Window duration in seconds */ windowSeconds: number } interface RateLimitResult { success: boolean remaining: number resetAt: number } function getStore(id: string): Map { if (!stores.has(id)) { stores.set(id, new Map()) } return stores.get(id)! } // Periodic cleanup of expired entries (every 5 minutes) setInterval(() => { const now = Date.now() for (const [, store] of stores) { for (const [key, entry] of store) { if (now > entry.resetAt) { store.delete(key) } } } }, 5 * 60 * 1000) export function rateLimit(config: RateLimitConfig) { const store = getStore(config.id) return { check(ip: string): RateLimitResult { const now = Date.now() const key = ip const entry = store.get(key) // No entry or expired → fresh window if (!entry || now > entry.resetAt) { store.set(key, { count: 1, resetAt: now + config.windowSeconds * 1000, }) return { success: true, remaining: config.max - 1, resetAt: now + config.windowSeconds * 1000 } } // Within window entry.count++ if (entry.count > config.max) { return { success: false, remaining: 0, resetAt: entry.resetAt } } return { success: true, remaining: config.max - entry.count, resetAt: entry.resetAt } }, } } // Pre-configured limiters for different endpoints export const loginLimiter = rateLimit({ id: 'login', max: 10, windowSeconds: 60 * 5 }) // 10 attempts per 5 min export const registerLimiter = rateLimit({ id: 'register', max: 3, windowSeconds: 60 * 60 }) // 3 per hour export const forgotPasswordLimiter = rateLimit({ id: 'forgot-pw', max: 3, windowSeconds: 60 * 15 }) // 3 per 15 min export const resendVerificationLimiter = rateLimit({ id: 'resend-verify', max: 3, windowSeconds: 60 * 15 }) export const contactLimiter = rateLimit({ id: 'contact', max: 5, windowSeconds: 60 * 60 }) // 5 per hour export const deleteAccountLimiter = rateLimit({ id: 'delete-acct', max: 3, windowSeconds: 60 * 15 }) export const resetPasswordLimiter = rateLimit({ id: 'reset-pw', max: 5, windowSeconds: 60 * 15 }) /** Extract client IP from request headers */ export function getClientIp(req: Request): string { const forwarded = req.headers.get('x-forwarded-for') if (forwarded) { return forwarded.split(',')[0].trim() } const realIp = req.headers.get('x-real-ip') if (realIp) return realIp return '127.0.0.1' } /** Helper: create a 429 response with retry-after header */ export function rateLimitResponse(resetAt: number) { const retryAfter = Math.ceil((resetAt - Date.now()) / 1000) const minutes = Math.ceil(retryAfter / 60) return new Response( JSON.stringify({ error: `Zu viele Versuche. Bitte warten Sie ${minutes > 1 ? `${minutes} Minuten` : `${retryAfter} Sekunden`} und versuchen es erneut.`, retryAfter, }), { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(retryAfter), }, } ) }