prepare( "SELECT COUNT(*) FROM login_attempts WHERE $col = ? AND attempted_at > (NOW() - INTERVAL " . LOGIN_WINDOW_MIN . " MINUTE)"); $stmt->execute([$value]); return (int)$stmt->fetchColumn(); } function record_login_failure(PDO $pdo, string $ip, string $user): void { $pdo->prepare("INSERT INTO login_attempts (ip, username) VALUES (?,?)") ->execute([$ip, substr($user, 0, 50)]); // Opportunistic cleanup so the table cannot grow without bound. $pdo->query("DELETE FROM login_attempts WHERE attempted_at < (NOW() - INTERVAL 1 DAY)"); } $error = ''; $ip = substr($_SERVER['REMOTE_ADDR'] ?? 'unknown', 0, 45); if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_csrf(); $user = trim($_POST['username'] ?? ''); $pass = $_POST['password'] ?? ''; $pdo = get_pdo(); if (login_failures($pdo, 'ip', $ip) >= LOGIN_MAX_PER_IP) { $error = 'Too many failed attempts. Try again in ' . LOGIN_WINDOW_MIN . ' minutes.'; } elseif ($user === '' || $pass === '') { $error = 'Please enter username and password.'; } else { $stmt = $pdo->prepare("SELECT password_hash FROM admin_users WHERE username = ?"); $stmt->execute([$user]); $row = $stmt->fetch(); if ($row && password_verify($pass, $row['password_hash'])) { session_regenerate_id(true); $_SESSION['admin_logged_in'] = true; $_SESSION['admin_user'] = $user; // A fresh token for the new session id. unset($_SESSION['csrf_token']); // Clear this client's failure history. $pdo->prepare("DELETE FROM login_attempts WHERE ip = ?")->execute([$ip]); header('Location: index.php'); exit; } record_login_failure($pdo, $ip, $user); $error = 'Invalid username or password.'; // Slow the next guess down in proportion to how many came before it. sleep(min(LOGIN_MAX_DELAY, 1 + login_failures($pdo, 'username', $user))); } } ?>