// Simple in-memory rate limiting (for production, consider Redis or database) const loginAttempts = new Map(); const blockedIPs = new Map(); // IP -> blocked until timestamp export const checkRateLimit = (ip: string): { allowed: boolean; attemptsLeft: number } => { const now = Date.now(); const maxAttempts = 5; const windowMs = 15 * 60 * 1000; // 15 minutes const blockDurationMs = 60 * 60 * 1000; // 1 hour // Check if IP is currently blocked const blockedUntil = blockedIPs.get(ip); if (blockedUntil && now < blockedUntil) { return { allowed: false, attemptsLeft: 0 }; } // Remove expired block if (blockedUntil && now >= blockedUntil) { blockedIPs.delete(ip); } // Get current attempts for this IP const attempts = loginAttempts.get(ip); // Clean up old attempts outside the window if (attempts && (now - attempts.lastAttempt) > windowMs) { loginAttempts.delete(ip); return { allowed: true, attemptsLeft: maxAttempts }; } const currentCount = attempts?.count || 0; if (currentCount >= maxAttempts) { // Block the IP blockedIPs.set(ip, now + blockDurationMs); console.warn(`🚨 IP ${ip} blocked due to ${currentCount} failed login attempts`); return { allowed: false, attemptsLeft: 0 }; } return { allowed: true, attemptsLeft: maxAttempts - currentCount }; }; export const recordFailedAttempt = (ip: string): void => { const now = Date.now(); const attempts = loginAttempts.get(ip); if (attempts) { attempts.count += 1; attempts.lastAttempt = now; } else { loginAttempts.set(ip, { count: 1, lastAttempt: now }); } const newCount = loginAttempts.get(ip)?.count || 1; console.warn(`⚠️ Failed login attempt from ${ip} (${newCount}/5)`); }; export const clearFailedAttempts = (ip: string): void => { loginAttempts.delete(ip); blockedIPs.delete(ip); console.log(`✅ Cleared failed attempts for ${ip}`); }; // Input validation export const validateLoginInput = (username: string, password: string): string[] => { const errors: string[] = []; if (!username || typeof username !== 'string') { errors.push('Username is required'); } else if (username.length < 2) { errors.push('Username must be at least 2 characters'); } else if (username.length > 100) { errors.push('Username is too long'); } if (!password || typeof password !== 'string') { errors.push('Password is required'); } else if (password.length < 6) { errors.push('Password must be at least 6 characters'); } else if (password.length > 200) { errors.push('Password is too long'); } // Basic sanitization check const dangerousChars = / { // Try various headers that might contain the real IP const headers = getHeaders(event); return ( headers['x-forwarded-for']?.split(',')[0]?.trim() || headers['x-real-ip'] || headers['x-client-ip'] || headers['cf-connecting-ip'] || // Cloudflare event.node?.req?.connection?.remoteAddress || event.node?.req?.socket?.remoteAddress || 'unknown' ); }; // Clean up old entries periodically (call this from a cron job or similar) export const cleanupOldEntries = (): void => { const now = Date.now(); const windowMs = 15 * 60 * 1000; // 15 minutes // Clean up old login attempts for (const [ip, attempts] of loginAttempts.entries()) { if ((now - attempts.lastAttempt) > windowMs) { loginAttempts.delete(ip); } } // Clean up expired blocks for (const [ip, blockedUntil] of blockedIPs.entries()) { if (now >= blockedUntil) { blockedIPs.delete(ip); } } console.log('🧹 Cleaned up old security entries'); }; // Initialize cleanup interval (runs every 5 minutes) if (typeof setInterval !== 'undefined') { setInterval(cleanupOldEntries, 5 * 60 * 1000); }