// 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'); }; // Password validation function export const validatePassword = (password: string): { isValid: boolean; errors: string[] } => { const errors: string[] = []; if (!password || typeof password !== 'string') { errors.push('Password is required'); return { isValid: false, errors }; } if (password.length < 8) { errors.push('Password must be at least 8 characters long'); } if (password.length > 128) { errors.push('Password must not exceed 128 characters'); } if (!/[A-Z]/.test(password)) { errors.push('Password must contain at least one uppercase letter'); } if (!/[a-z]/.test(password)) { errors.push('Password must contain at least one lowercase letter'); } if (!/[0-9]/.test(password)) { errors.push('Password must contain at least one number'); } // Optional: require special characters // if (!/[^A-Za-z0-9]/.test(password)) { // errors.push('Password must contain at least one special character'); // } // Check for common weak patterns const commonPatterns = [ /(.)\1{2,}/i, // Three or more consecutive identical characters /123456|654321|abcdef|qwerty|password|admin|login/i, // Common weak passwords ]; for (const pattern of commonPatterns) { if (pattern.test(password)) { errors.push('Password contains common patterns that make it weak'); break; } } return { isValid: errors.length === 0, errors }; }; // Initialize cleanup interval (runs every 5 minutes) if (typeof setInterval !== 'undefined') { setInterval(cleanupOldEntries, 5 * 60 * 1000); }