diff --git a/addUser.php b/addUser.php index 6d3cd5d..e42ee65 100644 --- a/addUser.php +++ b/addUser.php @@ -7,7 +7,7 @@ $usersFile = USERS_DIR . USERS_FILE; // Determine if we are in setup mode: // - Query parameter setup=1 is passed // - And users.txt is either missing or empty -$isSetup = (isset($_GET['setup']) && $_GET['setup'] == '1'); +$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1'); if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '')) { // Allow initial admin creation without session checks. $setupMode = true; @@ -16,7 +16,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) // In non-setup mode, check CSRF token and require admin privileges. $headers = array_change_key_case(getallheaders(), CASE_LOWER); $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - if ($receivedToken !== $_SESSION['csrf_token']) { + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { echo json_encode(["error" => "Invalid CSRF token"]); http_response_code(403); exit; @@ -30,7 +30,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) } } -// Get input data from JSON +// Get input data from JSON. $data = json_decode(file_get_contents("php://input"), true); $newUsername = trim($data["username"] ?? ""); $newPassword = trim($data["password"] ?? ""); @@ -42,7 +42,7 @@ if ($setupMode) { $isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user. } -// Validate input +// Validate input. if (!$newUsername || !$newPassword) { echo json_encode(["error" => "Username and password required"]); exit; @@ -54,12 +54,12 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) { exit; } -// Ensure users.txt exists +// Ensure users.txt exists. if (!file_exists($usersFile)) { file_put_contents($usersFile, ''); } -// Check if username already exists +// Check if username already exists. $existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($existingUsers as $line) { list($storedUser, $storedHash, $storedRole) = explode(':', trim($line)); @@ -69,10 +69,10 @@ foreach ($existingUsers as $line) { } } -// Hash the password +// Hash the password. $hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); -// Prepare new user line +// Prepare new user line. $newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL; // In setup mode, overwrite users.txt; otherwise, append to it. diff --git a/auth.js b/auth.js index 8ef252e..5e1b75b 100644 --- a/auth.js +++ b/auth.js @@ -37,10 +37,8 @@ function initAuth() { restoreBtn.classList.add("btn", "btn-warning"); // Use a material icon. restoreBtn.innerHTML = 'restore_from_trash'; - const headerButtons = document.querySelector(".header-buttons"); if (headerButtons) { - // Insert after the third child if available. if (headerButtons.children.length >= 5) { headerButtons.insertBefore(restoreBtn, headerButtons.children[5]); } else { @@ -54,20 +52,17 @@ function initAuth() { const removeUserBtn = document.getElementById("removeUserBtn"); if (addUserBtn) addUserBtn.style.display = "none"; if (removeUserBtn) removeUserBtn.style.display = "none"; - // If not admin, hide the restore button. const restoreBtn = document.getElementById("restoreFilesBtn"); if (restoreBtn) { restoreBtn.style.display = "none"; } } - // Set items-per-page. const selectElem = document.querySelector(".form-control.bottom-select"); if (selectElem) { const stored = localStorage.getItem("itemsPerPage") || "10"; selectElem.value = stored; } } else { - // Do not show a toast message repeatedly during initial check. toggleVisibility("loginForm", true); toggleVisibility("mainOperations", false); toggleVisibility("uploadFileForm", false); @@ -78,7 +73,7 @@ function initAuth() { console.error("Error checking authentication:", error); }); - // Attach login event listener once. + // Attach login event listener. const authForm = document.getElementById("authForm"); if (authForm) { authForm.addEventListener("submit", function (event) { @@ -94,7 +89,19 @@ function initAuth() { sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); window.location.reload(); } else { - showToast("Login failed: " + (data.error || "Unknown error")); + if (data.error && data.error.includes("Too many failed login attempts")) { + showToast(data.error); + const loginButton = authForm.querySelector("button[type='submit']"); + if (loginButton) { + loginButton.disabled = true; + setTimeout(() => { + loginButton.disabled = false; + showToast("You can now try logging in again."); + }, 30 * 60 * 1000); + } + } else { + showToast("Login failed: " + (data.error || "Unknown error")); + } } }) .catch(error => console.error("❌ Error logging in:", error)); @@ -119,8 +126,10 @@ function initAuth() { }); document.getElementById("saveUserBtn").addEventListener("click", function () { const newUsername = document.getElementById("newUsername").value.trim(); - const newPassword = document.getElementById("newPassword").value.trim(); + // Use the new ID for the add user modal's password field. + const newPassword = document.getElementById("addUserPassword").value.trim(); const isAdmin = document.getElementById("isAdmin").checked; + console.log("newUsername:", newUsername, "newPassword:", newPassword); if (!newUsername || !newPassword) { showToast("Username and password are required!"); return; @@ -143,7 +152,7 @@ function initAuth() { if (data.success) { showToast("User added successfully!"); closeAddUserModal(); - checkAuthentication(false); // Re-check without showing toast + checkAuthentication(false); } else { showToast("Error: " + (data.error || "Could not add user")); } @@ -195,18 +204,18 @@ function initAuth() { }); document.getElementById("changePasswordBtn").addEventListener("click", function() { - // Show the modal. + // Show the Change Password modal. document.getElementById("changePasswordModal").style.display = "block"; }); document.getElementById("closeChangePasswordModal").addEventListener("click", function() { - // Hide the modal. + // Hide the Change Password modal. document.getElementById("changePasswordModal").style.display = "none"; }); document.getElementById("saveNewPasswordBtn").addEventListener("click", function() { const oldPassword = document.getElementById("oldPassword").value.trim(); - const newPassword = document.getElementById("newPassword").value.trim(); + const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field const confirmPassword = document.getElementById("confirmPassword").value.trim(); if (!oldPassword || !newPassword || !confirmPassword) { @@ -236,7 +245,7 @@ function initAuth() { .then(result => { if (result.success) { showToast(result.success); - // Optionally clear form fields and close modal. + // Clear form fields and close modal. document.getElementById("oldPassword").value = ""; document.getElementById("newPassword").value = ""; document.getElementById("confirmPassword").value = ""; @@ -253,7 +262,6 @@ function initAuth() { } function checkAuthentication(showLoginToast = true) { - // Optionally pass a flag so we don't show a toast every time. return sendRequest("checkAuth.php") .then(data => { if (data.setup) { @@ -303,7 +311,7 @@ document.addEventListener("DOMContentLoaded", function () { function resetUserForm() { document.getElementById("newUsername").value = ""; - document.getElementById("newPassword").value = ""; + document.getElementById("addUserPassword").value = ""; // Updated for add user modal } function closeAddUserModal() { diff --git a/auth.php b/auth.php index bb771d0..cd2199b 100644 --- a/auth.php +++ b/auth.php @@ -4,7 +4,45 @@ header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; -// Function to authenticate user +// --- Brute Force Protection Settings --- +$maxAttempts = 5; +$lockoutTime = 30 * 60; // 30 minutes in seconds +$attemptsFile = USERS_DIR . 'failed_logins.json'; // JSON file for tracking failed login attempts +$failedLogFile = USERS_DIR . 'failed_login.log'; // Plain text log for fail2ban + +// Load failed attempts data from file. +function loadFailedAttempts($file) { + if (file_exists($file)) { + $data = json_decode(file_get_contents($file), true); + if (is_array($data)) { + return $data; + } + } + return []; +} + +// Save failed attempts data to file. +function saveFailedAttempts($file, $data) { + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); +} + +// Get current IP address. +$ip = $_SERVER['REMOTE_ADDR']; +$currentTime = time(); + +// Load failed attempts. +$failedAttempts = loadFailedAttempts($attemptsFile); + +// Check if this IP is currently locked out. +if (isset($failedAttempts[$ip])) { + $attemptData = $failedAttempts[$ip]; + if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { + echo json_encode(["error" => "Too many failed login attempts. Please try again later."]); + exit; + } +} + +// --- Authentication Function --- function authenticate($username, $password) { global $usersFile; @@ -23,7 +61,7 @@ function authenticate($username, $password) return false; } -// Get JSON input +// Get JSON input. $data = json_decode(file_get_contents("php://input"), true); $username = trim($data["username"] ?? ""); $password = trim($data["password"] ?? ""); @@ -40,10 +78,15 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { exit; } -// Authenticate user +// Attempt to authenticate the user. $userRole = authenticate($username, $password); if ($userRole !== false) { - // Regenerate session ID to mitigate session fixation attacks + // On successful login, reset failed attempts for this IP. + if (isset($failedAttempts[$ip])) { + unset($failedAttempts[$ip]); + saveFailedAttempts($attemptsFile, $failedAttempts); + } + // Regenerate session ID to mitigate session fixation attacks. session_regenerate_id(true); $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; @@ -51,5 +94,19 @@ if ($userRole !== false) { echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]); } else { + // On failed login, update failed attempts. + if (isset($failedAttempts[$ip])) { + $failedAttempts[$ip]['count']++; + $failedAttempts[$ip]['last_attempt'] = $currentTime; + } else { + $failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime]; + } + saveFailedAttempts($attemptsFile, $failedAttempts); + + // Log the failed attempt to the plain text log for fail2ban. + $logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL; + file_put_contents($failedLogFile, $logLine, FILE_APPEND); + echo json_encode(["error" => "Invalid credentials"]); } +?> \ No newline at end of file diff --git a/index.html b/index.html index 66809a1..2869804 100644 --- a/index.html +++ b/index.html @@ -332,8 +332,8 @@