0, 'last' => 0]); if (($t['failures'] ?? 0) < AUTH_MAX_FAILURES) { return 0; } $remaining = ($t['last'] ?? 0) + AUTH_LOCK_SECONDS - time(); return max(0, $remaining); } function auth_attempt(string $username, string $password): bool { session_boot(); if (auth_locked_for() > 0) { return false; } $cred = credentials_load(); $ok = hash_equals($cred['username'], $username) && password_verify($password, $cred['password_hash']); if ($ok) { if (is_file(auth_throttle_file())) { @unlink(auth_throttle_file()); } session_regenerate_id(true); $_SESSION['admin'] = true; return true; } $t = json_read(auth_throttle_file(), ['failures' => 0, 'last' => 0]); // A stale lock window restarts the count. if (time() - ($t['last'] ?? 0) > AUTH_LOCK_SECONDS) { $t['failures'] = 0; } $t['failures'] = ($t['failures'] ?? 0) + 1; $t['last'] = time(); json_write(auth_throttle_file(), $t); return false; } function auth_logout(): void { session_boot(); $_SESSION = []; session_destroy(); } /** * Change the admin password: verifies the current one, then atomically * rewrites config/credentials.php. Returns an error message or null on success. */ function auth_change_password(string $current, string $new): ?string { $cred = credentials_load(); if (!password_verify($current, $cred['password_hash'])) { return 'Current password is incorrect.'; } if (strlen($new) < 8) { return 'New password must be at least 8 characters.'; } $cred['password_hash'] = password_hash($new, PASSWORD_DEFAULT); $file = CONFIG_DIR . '/credentials.php'; $php = "