From 3f1007b1b325c57132eef5230cb257247514d52c Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 30 Mar 2025 14:44:53 -0400 Subject: [PATCH] New User Panel with TOTP & change password. Admin Panel added Global OTPAuth URL --- README.md | 22 +- auth.js | 886 ++++++++++++++++++++++++++++---------------- auth.php | 50 ++- changePassword.php | 22 +- checkAuth.php | 19 +- fileManager.js | 2 +- getConfig.php | 10 +- index.html | 5 +- styles.css | 11 + totp_setup.php | 152 ++++++++ updateConfig.php | 6 +- updateUserPanel.php | 82 ++++ upload.js | 2 +- 13 files changed, 918 insertions(+), 351 deletions(-) create mode 100644 totp_setup.php create mode 100644 updateUserPanel.php diff --git a/README.md b/README.md index 51d909e..17e048e 100644 --- a/README.md +++ b/README.md @@ -137,20 +137,26 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy - **Seamless Interaction:** - Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position. -### 🔒 Admin Panel & OpenID Connect (OIDC) Integration +# 🔒 Admin Panel, TOTP & OpenID Connect (OIDC) Integration - **Flexible Authentication:** - - Supports multiple authentication methods including Form-based Login, Basic Auth, and OpenID Connect (OIDC). Allow disable of only two login options. + - Supports multiple authentication methods including Form-based Login, Basic Auth, OpenID Connect (OIDC), and TOTP-based Two-Factor Authentication. + - Ensures continuous secure access by allowing administrators to disable only two of the available login options at any time. - **Secure OIDC Authentication:** - - Integrates seamlessly with OIDC providers (e.g., Keycloak, Okta). - - Admin-configurable OIDC settings, including Provider URL, Client ID, Client Secret, and Redirect URI. - - All sensitive configurations are securely stored in an encrypted JSON file. + - Seamlessly integrates with OIDC providers (e.g., Keycloak, Okta). + - Provides admin-configurable OIDC settings—including Provider URL, Client ID, Client Secret, and Redirect URI. + - Stores all sensitive configurations in an encrypted JSON file. + +- **TOTP Two-Factor Authentication:** + - Enhances security by integrating Time-based One-Time Password (TOTP) functionality. + - The new User Panel automatically displays the TOTP setup modal when users enable TOTP, presenting a QR code for easy configuration in authenticator apps. + - Administrators can customize a global OTPAuth URL template for consistent TOTP provisioning across accounts. - **Dynamic Admin Panel:** - - Intuitive Admin Panel with Material Icons for quick recognition and access. - - Allows administrators to easily manage authentication settings, user management, and login methods. - - Real-time validation prevents disabling all authentication methods simultaneously, ensuring continuous secure access. + - Features an intuitive interface with Material Icons for quick recognition and access. + - Allows administrators to manage authentication settings, user management, and login methods in real time. + - Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously. --- diff --git a/auth.js b/auth.js index d05a730..8d01e51 100644 --- a/auth.js +++ b/auth.js @@ -8,37 +8,34 @@ const currentOIDCConfig = { providerUrl: "https://your-oidc-provider.com", clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", - redirectUri: "https://yourdomain.com/auth.php?oidc=callback" + redirectUri: "https://yourdomain.com/auth.php?oidc=callback", + // Global OTPAuth URL; default applied if not set. + globalOtpauthUrl: "" }; +/* ----------------- Utility Functions ----------------- */ function updateItemsPerPageSelect() { const selectElem = document.querySelector(".form-control.bottom-select"); if (selectElem) { - const stored = localStorage.getItem("itemsPerPage") || "10"; - selectElem.value = stored; + selectElem.value = localStorage.getItem("itemsPerPage") || "10"; } } function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) { const authForm = document.getElementById("authForm"); - if (authForm) { - authForm.style.display = disableFormLogin ? "none" : "block"; - } + if (authForm) authForm.style.display = disableFormLogin ? "none" : "block"; const basicAuthLink = document.querySelector("a[href='login_basic.php']"); - if (basicAuthLink) { - basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block"; - } + if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block"; const oidcLoginBtn = document.getElementById("oidcLoginBtn"); - if (oidcLoginBtn) { - oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block"; - } + if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block"; } function updateLoginOptionsUIFromStorage() { - const disableFormLogin = localStorage.getItem("disableFormLogin") === "true"; - const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true"; - const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true"; - updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }); + updateLoginOptionsUI({ + disableFormLogin: localStorage.getItem("disableFormLogin") === "true", + disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true", + disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true" + }); } function loadAdminConfigFunc() { @@ -48,16 +45,22 @@ function loadAdminConfigFunc() { localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin); localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth); localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); + localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/FileRise?issuer=FileRise"); updateLoginOptionsUIFromStorage(); }) .catch(() => { localStorage.setItem("disableFormLogin", "false"); localStorage.setItem("disableBasicAuth", "false"); localStorage.setItem("disableOIDCLogin", "false"); + localStorage.setItem("globalOtpauthUrl", "otpauth://totp/FileRise?issuer=FileRise"); updateLoginOptionsUIFromStorage(); }); } +function insertAfter(newNode, referenceNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); +} + function updateAuthenticatedUI(data) { toggleVisibility("loginForm", false); toggleVisibility("mainOperations", true); @@ -68,40 +71,41 @@ function updateAuthenticatedUI(data) { attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn"); document.querySelector(".header-buttons").style.visibility = "visible"; + // Update TOTP state from the server response. + if (typeof data.totp_enabled !== "undefined") { + localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); + } + + const headerButtons = document.querySelector(".header-buttons"); + const firstButton = headerButtons.firstElementChild; // first button in container + + // Admin controls: restore and admin panel buttons are shown only for admins. if (data.isAdmin) { + // Create restore button. let restoreBtn = document.getElementById("restoreFilesBtn"); if (!restoreBtn) { restoreBtn = document.createElement("button"); restoreBtn.id = "restoreFilesBtn"; restoreBtn.classList.add("btn", "btn-warning"); restoreBtn.innerHTML = 'restore_from_trash'; - const headerButtons = document.querySelector(".header-buttons"); - if (headerButtons) { - if (headerButtons.children.length >= 3) { - headerButtons.insertBefore(restoreBtn, headerButtons.children[3]); - } else { - headerButtons.appendChild(restoreBtn); - } + // Insert restoreBtn right after the first button. + if (firstButton) { + insertAfter(restoreBtn, firstButton); + } else { + headerButtons.appendChild(restoreBtn); } } restoreBtn.style.display = "block"; - + + // Create admin panel button. let adminPanelBtn = document.getElementById("adminPanelBtn"); if (!adminPanelBtn) { adminPanelBtn = document.createElement("button"); adminPanelBtn.id = "adminPanelBtn"; adminPanelBtn.classList.add("btn", "btn-info"); - // Use material icon for the admin panel button. adminPanelBtn.innerHTML = 'admin_panel_settings'; - const headerButtons = document.querySelector(".header-buttons"); - if (headerButtons) { - // Insert the adminPanelBtn immediately after the restoreBtn. - if (restoreBtn.nextSibling) { - headerButtons.insertBefore(adminPanelBtn, restoreBtn.nextSibling); - } else { - headerButtons.appendChild(adminPanelBtn); - } - } + // Insert adminPanelBtn right after the restore button. + insertAfter(adminPanelBtn, restoreBtn); adminPanelBtn.addEventListener("click", openAdminPanel); } else { adminPanelBtn.style.display = "block"; @@ -112,6 +116,33 @@ function updateAuthenticatedUI(data) { const adminPanelBtn = document.getElementById("adminPanelBtn"); if (adminPanelBtn) adminPanelBtn.style.display = "none"; } + + // User panel button: Always visible for authenticated users. + let userPanelBtn = document.getElementById("userPanelBtn"); + if (!userPanelBtn) { + userPanelBtn = document.createElement("button"); + userPanelBtn.id = "userPanelBtn"; + userPanelBtn.classList.add("btn", "btn-user"); + userPanelBtn.innerHTML = 'account_circle'; + + // Try to insert the user panel button right after the admin panel button if it exists. + let adminPanelBtn = document.getElementById("adminPanelBtn"); + if (adminPanelBtn) { + insertAfter(userPanelBtn, adminPanelBtn); + } else { + // If no admin panel button exists, insert right after the first button in headerButtons. + const firstButton = headerButtons.firstElementChild; + if (firstButton) { + insertAfter(userPanelBtn, firstButton); + } else { + headerButtons.appendChild(userPanelBtn); + } + } + userPanelBtn.addEventListener("click", openUserPanel); + } else { + userPanelBtn.style.display = "block"; + } + updateItemsPerPageSelect(); updateLoginOptionsUIFromStorage(); } @@ -131,6 +162,10 @@ function checkAuthentication(showLoginToast = true) { } window.setupMode = false; if (data.authenticated) { + // Update localStorage for TOTP state if provided by the server. + if (typeof data.totp_enabled !== "undefined") { + localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); + } updateAuthenticatedUI(data); return data; } else { @@ -146,208 +181,254 @@ function checkAuthentication(showLoginToast = true) { .catch(() => false); } -function initAuth() { - checkAuthentication(false); - loadAdminConfigFunc(); - - const authForm = document.getElementById("authForm"); - if (authForm) { - authForm.addEventListener("submit", function (event) { - event.preventDefault(); - const rememberMe = document.getElementById("rememberMeCheckbox") - ? document.getElementById("rememberMeCheckbox").checked - : false; - const formData = { - username: document.getElementById("loginUsername").value.trim(), - password: document.getElementById("loginPassword").value.trim(), - remember_me: rememberMe - }; - sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken }) - .then(data => { - if (data.success) { - sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); - window.location.reload(); - } else { - 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(() => {}); +/* ----------------- TOTP Login Modal ----------------- */ +let lastLoginData = null; // For auto-submission +function submitLogin(data) { + lastLoginData = data; + sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) + .then(response => { + if (response.success) { + sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!"); + window.location.reload(); + } else if (response.totp_required) { + openTOTPLoginModal(); + } else if (response.error && response.error.includes("Too many failed login attempts")) { + showToast(response.error); + const loginButton = document.getElementById("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: " + (response.error || "Unknown error")); + } + }) + .catch(() => { + showToast("Login failed: Unknown error"); }); - } - - document.getElementById("logoutBtn").addEventListener("click", function () { - fetch("logout.php", { - method: "POST", - credentials: "include", - headers: { "X-CSRF-Token": window.csrfToken } - }) - .then(() => window.location.reload(true)) - .catch(() => {}); - }); - - const oidcLoginBtn = document.getElementById("oidcLoginBtn"); - if (oidcLoginBtn) { - oidcLoginBtn.addEventListener("click", function () { - window.location.href = "auth.php?oidc"; - }); - } - - document.getElementById("addUserBtn").addEventListener("click", function () { - resetUserForm(); - toggleVisibility("addUserModal", true); - document.getElementById("newUsername").focus(); - }); - document.getElementById("saveUserBtn").addEventListener("click", function () { - const newUsername = document.getElementById("newUsername").value.trim(); - const newPassword = document.getElementById("addUserPassword").value.trim(); - const isAdmin = document.getElementById("isAdmin").checked; - if (!newUsername || !newPassword) { - showToast("Username and password are required!"); - return; - } - let url = "addUser.php"; - if (window.setupMode) { - url += "?setup=1"; - } - fetch(url, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("User added successfully!"); - closeAddUserModal(); - checkAuthentication(false); - } else { - showToast("Error: " + (data.error || "Could not add user")); - } - }) - .catch(() => {}); - }); - document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal); - - document.getElementById("removeUserBtn").addEventListener("click", function () { - loadUserList(); - toggleVisibility("removeUserModal", true); - }); - document.getElementById("deleteUserBtn").addEventListener("click", async function () { - const selectElem = document.getElementById("removeUsernameSelect"); - const usernameToRemove = selectElem.value; - if (!usernameToRemove) { - showToast("Please select a user to remove."); - return; - } - const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); - if (!confirmed) return; - fetch("removeUser.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ username: usernameToRemove }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("User removed successfully!"); - closeRemoveUserModal(); - loadUserList(); - } else { - showToast("Error: " + (data.error || "Could not remove user")); - } - }) - .catch(() => {}); - }); - document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal); - - document.getElementById("changePasswordBtn").addEventListener("click", function () { - document.getElementById("changePasswordModal").style.display = "block"; - document.getElementById("oldPassword").focus(); - }); - document.getElementById("closeChangePasswordModal").addEventListener("click", function () { - 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 confirmPassword = document.getElementById("confirmPassword").value.trim(); - if (!oldPassword || !newPassword || !confirmPassword) { - showToast("Please fill in all fields."); - return; - } - if (newPassword !== confirmPassword) { - showToast("New passwords do not match."); - return; - } - const data = { oldPassword, newPassword, confirmPassword }; - fetch("changePassword.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify(data) - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast(result.success); - document.getElementById("oldPassword").value = ""; - document.getElementById("newPassword").value = ""; - document.getElementById("confirmPassword").value = ""; - document.getElementById("changePasswordModal").style.display = "none"; - } else { - showToast("Error: " + (result.error || "Could not change password.")); - } - }) - .catch(() => { - showToast("Error changing password."); - }); - }); } -function loadOIDCConfig() { - return fetch("getConfig.php", { credentials: "include" }) - .then(response => response.json()) - .then(config => { - if (config.oidc) { - Object.assign(currentOIDCConfig, config.oidc); +function openTOTPLoginModal() { + let totpLoginModal = document.getElementById("totpLoginModal"); + const isDarkMode = document.body.classList.contains("dark-mode"); + const modalBg = isDarkMode ? "#2c2c2c" : "#fff"; + const textColor = isDarkMode ? "#e0e0e0" : "#000"; + + if (!totpLoginModal) { + totpLoginModal = document.createElement("div"); + totpLoginModal.id = "totpLoginModal"; + totpLoginModal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0,0,0,0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 3200; + `; + totpLoginModal.innerHTML = ` +
+ × +

Enter TOTP Code

+ +
+ `; + document.body.appendChild(totpLoginModal); + document.getElementById("closeTOTPLoginModal").addEventListener("click", () => { + totpLoginModal.style.display = "none"; + }); + const totpInput = document.getElementById("totpLoginInput"); + document.getElementById("totpLoginInput").focus(); + totpInput.addEventListener("input", function () { + if (this.value.trim().length === 6 && lastLoginData) { + lastLoginData.totp_code = this.value.trim(); + totpLoginModal.style.display = "none"; + submitLogin(lastLoginData); } - return currentOIDCConfig; - }) - .catch(() => currentOIDCConfig); + }); + } else { + totpLoginModal.style.display = "flex"; + // Update colors in case dark mode changed. + const modalContent = totpLoginModal.firstElementChild; + modalContent.style.background = modalBg; + modalContent.style.color = textColor; + } +} + +/* ----------------- User Panel Modal ----------------- */ +function openUserPanel() { + let userPanelModal = document.getElementById("userPanelModal"); + const isDarkMode = document.body.classList.contains("dark-mode"); + const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; + // Added transform and transition none to prevent scaling on click. + const modalContentStyles = ` + background: ${isDarkMode ? "#2c2c2c" : "#fff"}; + color: ${isDarkMode ? "#e0e0e0" : "#000"}; + padding: 20px; + max-width: 600px; + width: 90%; + border-radius: 8px; + position: relative; + overflow-y: auto; + max-height: 90vh; + border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"}; + transform: none; + transition: none; + `; + if (!userPanelModal) { + userPanelModal = document.createElement("div"); + userPanelModal.id = "userPanelModal"; + userPanelModal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: ${overlayBackground}; + display: flex; + justify-content: center; + align-items: center; + z-index: 3000; + `; + userPanelModal.innerHTML = ` + + `; + document.body.appendChild(userPanelModal); + document.getElementById("closeUserPanel").addEventListener("click", () => { + userPanelModal.style.display = "none"; + }); + // Bind the "Change Password" button to open the changePasswordModal. + document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => { + document.getElementById("changePasswordModal").style.display = "block"; + }); + // Initialize TOTP checkbox state and TOTP configuration button. + const totpCheckbox = document.getElementById("userTOTPEnabled"); + // Initialize checkbox based on stored setting. + totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true"; + + totpCheckbox.addEventListener("change", function () { + // Save the new state. + localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false"); + + const enabled = this.checked; + fetch("updateUserPanel.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ totp_enabled: enabled }) + }) + .then(r => r.json()) + .then(result => { + if (!result.success) { + showToast("Error updating TOTP setting: " + result.error); + } else if (enabled) { + // Automatically open the TOTP modal when TOTP is enabled. + openTOTPModal(); + } + }) + .catch(() => { showToast("Error updating TOTP setting."); }); + }); + } else { + // Update colors in case dark mode changed. + userPanelModal.style.backgroundColor = overlayBackground; + const modalContent = userPanelModal.querySelector(".modal-content"); + modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff"; + modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000"; + modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc"; + } + userPanelModal.style.display = "flex"; +} + +/* ----------------- TOTP Setup Modal ----------------- */ +function openTOTPModal() { + let totpModal = document.getElementById("totpModal"); + const isDarkMode = document.body.classList.contains("dark-mode"); + const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; + const modalContentStyles = ` + background: ${isDarkMode ? "#2c2c2c" : "#fff"}; + color: ${isDarkMode ? "#e0e0e0" : "#000"}; + padding: 20px; + max-width: 400px; + width: 90%; + border-radius: 8px; + position: relative; + `; + if (!totpModal) { + totpModal = document.createElement("div"); + totpModal.id = "totpModal"; + totpModal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: ${overlayBackground}; + display: flex; + justify-content: center; + align-items: center; + z-index: 3100; + `; + totpModal.innerHTML = ` + + `; + document.body.appendChild(totpModal); + document.getElementById("closeTOTPModal").addEventListener("click", closeTOTPModal); + const totpInput = document.getElementById("totpSetupInput"); + totpInput.addEventListener("input", function () { + if (this.value.trim().length === 6 && lastLoginData) { + lastLoginData.totp_code = this.value.trim(); + totpModal.style.display = "none"; + submitLogin(lastLoginData); + } + }); + } else { + totpModal.style.display = "flex"; + totpModal.style.backgroundColor = overlayBackground; + const modalContent = totpModal.querySelector(".modal-content"); + modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff"; + modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000"; + } +} + +function closeTOTPModal() { + const totpModal = document.getElementById("totpModal"); + if (totpModal) totpModal.style.display = "none"; } function openAdminPanel() { fetch("getConfig.php", { credentials: "include" }) .then(response => response.json()) .then(config => { - if (config.oidc) { - Object.assign(currentOIDCConfig, config.oidc); - } + if (config.oidc) Object.assign(currentOIDCConfig, config.oidc); + if (config.globalOtpauthUrl) currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; const isDarkMode = document.body.classList.contains("dark-mode"); - const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)"; + const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; const modalContentStyles = ` background: ${isDarkMode ? "#2c2c2c" : "#fff"}; color: ${isDarkMode ? "#e0e0e0" : "#000"}; @@ -361,6 +442,7 @@ function openAdminPanel() { border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"}; `; let adminModal = document.getElementById("adminPanelModal"); + if (!adminModal) { adminModal = document.createElement("div"); adminModal.id = "adminPanelModal"; @@ -377,80 +459,85 @@ function openAdminPanel() { z-index: 3000; `; adminModal.innerHTML = ` - -`; + + `; document.body.appendChild(adminModal); + document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel); - adminModal.addEventListener("click", function (e) { - if (e.target === adminModal) { - closeAdminPanel(); - } + adminModal.addEventListener("click", (e) => { + if (e.target === adminModal) closeAdminPanel(); }); document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel); - document.getElementById("adminOpenAddUser").addEventListener("click", function () { + document.getElementById("adminOpenAddUser").addEventListener("click", () => { toggleVisibility("addUserModal", true); document.getElementById("newUsername").focus(); }); - document.getElementById("adminOpenRemoveUser").addEventListener("click", function () { + document.getElementById("adminOpenRemoveUser").addEventListener("click", () => { loadUserList(); toggleVisibility("removeUserModal", true); }); - document.getElementById("saveAdminSettings").addEventListener("click", function () { + document.getElementById("saveAdminSettings").addEventListener("click", () => { const disableFormLoginCheckbox = document.getElementById("disableFormLogin"); const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth"); const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin"); - const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox] - .filter(cb => cb.checked).length; + const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length; if (totalDisabled === 3) { showToast("At least one login method must remain enabled."); disableOIDCLoginCheckbox.checked = false; @@ -471,11 +558,13 @@ function openAdminPanel() { const disableFormLogin = disableFormLoginCheckbox.checked; const disableBasicAuth = disableBasicAuthCheckbox.checked; const disableOIDCLogin = disableOIDCLoginCheckbox.checked; + const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim(); sendRequest("updateConfig.php", "POST", { oidc: newOIDCConfig, disableFormLogin, disableBasicAuth, - disableOIDCLogin + disableOIDCLogin, + globalOtpauthUrl }, { "X-CSRF-Token": window.csrfToken }) .then(response => { if (response.success) { @@ -489,34 +578,29 @@ function openAdminPanel() { showToast("Error updating settings: " + (response.error || "Unknown error")); } }) - .catch(() => {}); + .catch(() => { }); }); + // Enforce that at least one login method remains enabled. const disableFormLoginCheckbox = document.getElementById("disableFormLogin"); const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth"); const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin"); function enforceLoginOptionConstraint(changedCheckbox) { - const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox] - .filter(cb => cb.checked).length; + const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length; if (changedCheckbox.checked && totalDisabled === 3) { showToast("At least one login method must remain enabled."); changedCheckbox.checked = false; } } - disableFormLoginCheckbox.addEventListener("change", function () { - enforceLoginOptionConstraint(this); - }); - disableBasicAuthCheckbox.addEventListener("change", function () { - enforceLoginOptionConstraint(this); - }); - disableOIDCLoginCheckbox.addEventListener("change", function () { - enforceLoginOptionConstraint(this); - }); - document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true"; - document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true"; - document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true"; + disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); + disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); + disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); + + // UPDATE checkboxes using fetched configuration: + document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; + document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; + document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; } else { - const isDarkMode = document.body.classList.contains("dark-mode"); - const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)"; + // If the modal already exists, update its styles and values. adminModal.style.backgroundColor = overlayBackground; const modalContent = adminModal.querySelector(".modal-content"); if (modalContent) { @@ -528,19 +612,36 @@ function openAdminPanel() { document.getElementById("oidcClientId").value = currentOIDCConfig.clientId; document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret; document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri; - document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true"; - document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true"; - document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true"; + document.getElementById("globalOtpauthUrl").value = currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise'; + + // UPDATE checkboxes using fetched configuration: + document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; + document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; + document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; + adminModal.style.display = "flex"; } }) .catch(() => { + // In case of error, fallback to localStorage values let adminModal = document.getElementById("adminPanelModal"); if (adminModal) { + adminModal.style.backgroundColor = "rgba(0,0,0,0.5)"; + const modalContent = adminModal.querySelector(".modal-content"); + if (modalContent) { + modalContent.style.background = "#fff"; + modalContent.style.color = "#000"; + modalContent.style.border = "1px solid #ccc"; + } document.getElementById("oidcProviderUrl").value = currentOIDCConfig.providerUrl; document.getElementById("oidcClientId").value = currentOIDCConfig.clientId; document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret; document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri; + document.getElementById("globalOtpauthUrl").value = currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise'; + + document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true"; + document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true"; + document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true"; adminModal.style.display = "flex"; } else { openAdminPanel(); @@ -550,27 +651,15 @@ function openAdminPanel() { function closeAdminPanel() { const adminModal = document.getElementById("adminPanelModal"); - if (adminModal) { - adminModal.style.display = "none"; - } + if (adminModal) adminModal.style.display = "none"; } +/* ----------------- Other Helpers ----------------- */ window.changeItemsPerPage = function (value) { localStorage.setItem("itemsPerPage", value); - const folder = window.currentFolder || "root"; - if (typeof renderFileTable === "function") { - renderFileTable(folder); - } + if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root"); }; -document.addEventListener("DOMContentLoaded", function () { - updateItemsPerPageSelect(); - const disableFormLogin = localStorage.getItem("disableFormLogin") === "true"; - const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true"; - const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true"; - updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }); -}); - function resetUserForm() { document.getElementById("newUsername").value = ""; document.getElementById("addUserPassword").value = ""; @@ -604,7 +693,158 @@ function loadUserList() { closeRemoveUserModal(); } }) - .catch(() => {}); + .catch(() => { }); } +/* ----------------- Initialization ----------------- */ +function initAuth() { + checkAuthentication(false); + loadAdminConfigFunc(); + const authForm = document.getElementById("authForm"); + if (authForm) { + authForm.addEventListener("submit", function (event) { + event.preventDefault(); + const rememberMe = document.getElementById("rememberMeCheckbox") + ? document.getElementById("rememberMeCheckbox").checked + : false; + const formData = { + username: document.getElementById("loginUsername").value.trim(), + password: document.getElementById("loginPassword").value.trim(), + remember_me: rememberMe + }; + submitLogin(formData); + }); + } + document.getElementById("logoutBtn").addEventListener("click", function () { + fetch("logout.php", { + method: "POST", + credentials: "include", + headers: { "X-CSRF-Token": window.csrfToken } + }).then(() => window.location.reload(true)).catch(() => { }); + }); + const oidcLoginBtn = document.getElementById("oidcLoginBtn"); + if (oidcLoginBtn) { + oidcLoginBtn.addEventListener("click", function () { + window.location.href = "auth.php?oidc"; + }); + } + document.getElementById("addUserBtn").addEventListener("click", function () { + resetUserForm(); + toggleVisibility("addUserModal", true); + document.getElementById("newUsername").focus(); + }); + document.getElementById("saveUserBtn").addEventListener("click", function () { + const newUsername = document.getElementById("newUsername").value.trim(); + const newPassword = document.getElementById("addUserPassword").value.trim(); + const isAdmin = document.getElementById("isAdmin").checked; + if (!newUsername || !newPassword) { + showToast("Username and password are required!"); + return; + } + let url = "addUser.php"; + if (window.setupMode) url += "?setup=1"; + fetch(url, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, + body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("User added successfully!"); + closeAddUserModal(); + checkAuthentication(false); + } else { + showToast("Error: " + (data.error || "Could not add user")); + } + }) + .catch(() => { }); + }); + document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal); + + document.getElementById("removeUserBtn").addEventListener("click", function () { + loadUserList(); + toggleVisibility("removeUserModal", true); + }); + document.getElementById("deleteUserBtn").addEventListener("click", async function () { + const selectElem = document.getElementById("removeUsernameSelect"); + const usernameToRemove = selectElem.value; + if (!usernameToRemove) { + showToast("Please select a user to remove."); + return; + } + const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); + if (!confirmed) return; + fetch("removeUser.php", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, + body: JSON.stringify({ username: usernameToRemove }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("User removed successfully!"); + closeRemoveUserModal(); + loadUserList(); + } else { + showToast("Error: " + (data.error || "Could not remove user")); + } + }) + .catch(() => { }); + }); + document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal); + // Change password bindings. + document.getElementById("changePasswordBtn").addEventListener("click", function () { + document.getElementById("changePasswordModal").style.display = "block"; + document.getElementById("oldPassword").focus(); + }); + document.getElementById("closeChangePasswordModal").addEventListener("click", function () { + 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 confirmPassword = document.getElementById("confirmPassword").value.trim(); + if (!oldPassword || !newPassword || !confirmPassword) { + showToast("Please fill in all fields."); + return; + } + if (newPassword !== confirmPassword) { + showToast("New passwords do not match."); + return; + } + const data = { oldPassword, newPassword, confirmPassword }; + fetch("changePassword.php", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + if (result.success) { + showToast(result.success); + document.getElementById("oldPassword").value = ""; + document.getElementById("newPassword").value = ""; + document.getElementById("confirmPassword").value = ""; + document.getElementById("changePasswordModal").style.display = "none"; + } else { + showToast("Error: " + (result.error || "Could not change password.")); + } + }) + .catch(() => { showToast("Error changing password."); }); + }); +} + +document.addEventListener("DOMContentLoaded", function () { + updateItemsPerPageSelect(); + updateLoginOptionsUI({ + disableFormLogin: localStorage.getItem("disableFormLogin") === "true", + disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true", + disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true" + }); +}); + export { initAuth, checkAuthentication }; \ No newline at end of file diff --git a/auth.php b/auth.php index 834a22a..ada5929 100644 --- a/auth.php +++ b/auth.php @@ -104,16 +104,31 @@ if (isset($failedAttempts[$ip])) { } } +/* + * Updated authenticate() function: + * It reads each line from users.txt. + * It expects records in the format: + * username:hashed_password:role[:encrypted_totp_secret] + * If a fourth field is present and non-empty, it decrypts it to obtain the TOTP secret. + */ function authenticate($username, $password) { - global $usersFile; + global $usersFile, $encryptionKey; if (!file_exists($usersFile)) { return false; } $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { - list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3); - if ($username === $storedUser && password_verify($password, $storedPass)) { - return $storedRole; + $parts = explode(':', trim($line)); + if (count($parts) < 3) continue; // Skip invalid lines. + if ($username === $parts[0] && password_verify($password, $parts[1])) { + $result = ['role' => $parts[2]]; + // If there's a fourth field, decrypt it to get the TOTP secret. + if (isset($parts[3]) && !empty($parts[3])) { + $result['totp_secret'] = decryptData($parts[3], $encryptionKey); + } else { + $result['totp_secret'] = null; + } + return $result; } } return false; @@ -134,8 +149,27 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { exit(); } -$userRole = authenticate($username, $password); -if ($userRole !== false) { +$user = authenticate($username, $password); +if ($user !== false) { + // Only require TOTP if the user's TOTP secret is set. + if (!empty($user['totp_secret'])) { + if (empty($data['totp_code'])) { + echo json_encode([ + "totp_required" => true, + "message" => "TOTP code required" + ]); + exit(); + } else { + $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); + $providedCode = trim($data['totp_code']); + if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) { + echo json_encode(["error" => "Invalid TOTP code"]); + exit(); + } + } + } + // --- End TOTP Integration --- + if (isset($failedAttempts[$ip])) { unset($failedAttempts[$ip]); saveFailedAttempts($attemptsFile, $failedAttempts); @@ -143,7 +177,7 @@ if ($userRole !== false) { session_regenerate_id(true); $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = ($userRole === "1"); + $_SESSION["isAdmin"] = ($user['role'] === "1"); if ($rememberMe) { $token = bin2hex(random_bytes(32)); @@ -160,7 +194,7 @@ if ($userRole !== false) { $persistentTokens[$token] = [ "username" => $username, "expiry" => $expiry, - "isAdmin" => ($userRole === "1") + "isAdmin" => ($user['role'] === "1") ]; $encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey); file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX); diff --git a/changePassword.php b/changePassword.php index f894f4c..0834d46 100644 --- a/changePassword.php +++ b/changePassword.php @@ -54,7 +54,19 @@ $userFound = false; $newLines = []; foreach ($lines as $line) { - list($storedUser, $storedHash, $storedRole) = explode(':', trim($line)); + $parts = explode(':', trim($line)); + // Expect at least 3 parts: username, hashed password, and role. + if (count($parts) < 3) { + // Skip invalid lines. + $newLines[] = $line; + continue; + } + $storedUser = $parts[0]; + $storedHash = $parts[1]; + $storedRole = $parts[2]; + // Preserve TOTP secret if it exists. + $totpSecret = (count($parts) >= 4) ? $parts[3] : ""; + if ($storedUser === $username) { $userFound = true; // Verify the old password. @@ -64,8 +76,12 @@ foreach ($lines as $line) { } // Hash the new password. $newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); - // Rebuild the line with the new hash. - $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole; + // Rebuild the line with the new hash and preserve TOTP secret if present. + if ($totpSecret !== "") { + $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret; + } else { + $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole; + } } else { $newLines[] = $line; } diff --git a/checkAuth.php b/checkAuth.php index 3c1e105..683d427 100644 --- a/checkAuth.php +++ b/checkAuth.php @@ -15,8 +15,25 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { exit; } +$totp_enabled = false; +$username = $_SESSION['username'] ?? ''; +if ($username) { + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $parts = explode(":", trim($line)); + // Assuming first field is username and fourth (if exists) is the TOTP secret. + if ($parts[0] === $username) { + if (isset($parts[3]) && trim($parts[3]) !== "") { + $totp_enabled = true; + } + break; + } + } +} + echo json_encode([ "authenticated" => true, - "isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false + "isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false, + "totp_enabled" => $totp_enabled ]); ?> \ No newline at end of file diff --git a/fileManager.js b/fileManager.js index bd7ccb6..0e4b516 100644 --- a/fileManager.js +++ b/fileManager.js @@ -258,7 +258,7 @@ function previewFile(fileUrl, fileName) { embed.style.height = "80vh"; embed.style.border = "none"; container.appendChild(embed); - } else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) { + } else if (/\.(mp4|webm|mov)$/i.test(fileName)) { const video = document.createElement("video"); video.src = fileUrl; video.controls = true; diff --git a/getConfig.php b/getConfig.php index a10a439..11818ca 100644 --- a/getConfig.php +++ b/getConfig.php @@ -11,7 +11,12 @@ if (file_exists($configFile)) { echo json_encode(['error' => 'Failed to decrypt configuration.']); exit; } - echo $decryptedContent; + // Decode the configuration and ensure globalOtpauthUrl is set + $config = json_decode($decryptedContent, true); + if (!isset($config['globalOtpauthUrl'])) { + $config['globalOtpauthUrl'] = ""; + } + echo json_encode($config); } else { echo json_encode([ 'oidc' => [ @@ -24,7 +29,8 @@ if (file_exists($configFile)) { 'disableFormLogin' => false, 'disableBasicAuth' => false, 'disableOIDCLogin' => false - ] + ], + 'globalOtpauthUrl' => "" ]); } ?> \ No newline at end of file diff --git a/index.html b/index.html index fdccbe1..d408427 100644 --- a/index.html +++ b/index.html @@ -99,7 +99,7 @@ - - - + diff --git a/styles.css b/styles.css index 4b1ec85..93ebe0e 100644 --- a/styles.css +++ b/styles.css @@ -2047,4 +2047,15 @@ body.dark-mode .admin-panel-content textarea { body.dark-mode .admin-panel-content label { color: #e0e0e0; +} + +#openChangePasswordModalBtn { + width: auto; + padding: 5px 10px; + font-size: 14px; + margin-right: 300px; +} + +#changePasswordModal { + z-index: 9999; } \ No newline at end of file diff --git a/totp_setup.php b/totp_setup.php new file mode 100644 index 0000000..346ccdb --- /dev/null +++ b/totp_setup.php @@ -0,0 +1,152 @@ += 4) { + $parts[3] = $encryptedSecret; + } else { + $parts[] = $encryptedSecret; + } + $newLines[] = implode(':', $parts); + } else { + $newLines[] = $line; + } + } + file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); +} + +/** + * Retrieves the current user's TOTP secret from users.txt (if present). + * + * @param string $username + * @return string|null The decrypted TOTP secret or null if not found. + */ +function getUserTOTPSecret($username) { + global $usersFile, $encryptionKey; + if (!file_exists($usersFile)) { + return null; + } + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + return decryptData($parts[3], $encryptionKey); + } + } + return null; +} + +/** + * Retrieves the global OTPAuth URL from admin configuration. + * + * @return string Global OTPAuth URL template or an empty string if not set. + */ +function getGlobalOtpauthUrl() { + global $encryptionKey; + $adminConfigFile = USERS_DIR . 'adminConfig.json'; + if (file_exists($adminConfigFile)) { + $encryptedContent = file_get_contents($adminConfigFile); + $decryptedContent = decryptData($encryptedContent, $encryptionKey); + if ($decryptedContent !== false) { + $config = json_decode($decryptedContent, true); + if (isset($config['globalOtpauthUrl']) && !empty($config['globalOtpauthUrl'])) { + return $config['globalOtpauthUrl']; + } + } + } + return ""; +} + +$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); + +// Retrieve the current TOTP secret for the user. +$totpSecret = getUserTOTPSecret($username); +if (!$totpSecret) { + // If no TOTP secret exists, generate a new one. + $totpSecret = $tfa->createSecret(); + $encryptedSecret = encryptData($totpSecret, $encryptionKey); + updateUserTOTPSecret($username, $encryptedSecret); +} + +// Determine the otpauth URL to use. +// If a global OTPAuth URL template is defined, replace placeholders {label} and {secret}. +// Otherwise, use the default method. +$globalOtpauthUrl = getGlobalOtpauthUrl(); +if (!empty($globalOtpauthUrl)) { + $label = "FileRise:" . $username; + $otpauthUrl = str_replace( + ["{label}", "{secret}"], + [urlencode($label), $totpSecret], + $globalOtpauthUrl + ); +} else { + $label = urlencode("FileRise:" . $username); + $issuer = urlencode("FileRise"); + $otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}"; +} + +// Build the QR code using Endroid QR Code. +$result = Builder::create() + ->writer(new PngWriter()) + ->data($otpauthUrl) + ->errorCorrectionLevel(new ErrorCorrectionLevelHigh()) + ->build(); + +header('Content-Type: ' . $result->getMimeType()); +echo $result->getString(); +?> \ No newline at end of file diff --git a/updateConfig.php b/updateConfig.php index f8ec09c..564ff38 100644 --- a/updateConfig.php +++ b/updateConfig.php @@ -51,6 +51,9 @@ $disableFormLogin = isset($data['disableFormLogin']) ? filter_var($data['disable $disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false; $disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN) : false; +// Retrieve the global OTPAuth URL (new field). If not provided, default to an empty string. +$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : ""; + // Prepare configuration array. $configUpdate = [ 'oidc' => [ @@ -63,7 +66,8 @@ $configUpdate = [ 'disableFormLogin' => $disableFormLogin, 'disableBasicAuth' => $disableBasicAuth, 'disableOIDCLogin' => $disableOIDCLogin, - ] + ], + 'globalOtpauthUrl' => $globalOtpauthUrl ]; // Define the configuration file path. diff --git a/updateUserPanel.php b/updateUserPanel.php new file mode 100644 index 0000000..baed453 --- /dev/null +++ b/updateUserPanel.php @@ -0,0 +1,82 @@ + "Unauthorized"]); + exit; +} + +// Verify the CSRF token from headers. +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$csrfToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; +if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; +} + +$data = json_decode(file_get_contents("php://input"), true); +if (!is_array($data)) { + http_response_code(400); + echo json_encode(["error" => "Invalid input"]); + exit; +} + +$username = $_SESSION['username'] ?? ''; +if (!$username) { + http_response_code(400); + echo json_encode(["error" => "No username in session"]); + exit; +} + +$totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false; +$usersFile = USERS_DIR . USERS_FILE; + +/** + * Clears the TOTP secret for a given user by removing or emptying the fourth field. + * + * @param string $username + */ +function disableUserTOTP($username) { + global $usersFile; + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $newLines = []; + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + // If the line doesn't have at least three parts, leave it alone. + if (count($parts) < 3) { + $newLines[] = $line; + continue; + } + if ($parts[0] === $username) { + // If a fourth field exists, clear it; otherwise, append an empty field. + if (count($parts) >= 4) { + $parts[3] = ""; + } else { + $parts[] = ""; + } + $newLines[] = implode(':', $parts); + } else { + $newLines[] = $line; + } + } + file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); +} + +// If TOTP is disabled, clear the user's TOTP secret. +if (!$totp_enabled) { + disableUserTOTP($username); + echo json_encode(["success" => "User panel updated: TOTP disabled"]); + exit; +} else { + // If TOTP is enabled, do not change the stored secret. + echo json_encode(["success" => "User panel updated: TOTP remains enabled"]); + exit; +} +?> \ No newline at end of file diff --git a/upload.js b/upload.js index e65a709..bdc78db 100644 --- a/upload.js +++ b/upload.js @@ -407,7 +407,7 @@ function initResumableUpload() { resumableInstance = new Resumable({ target: "upload.php", query: { folder: window.currentFolder || "root", upload_token: window.csrfToken }, - chunkSize: 3 * 1024 * 1024, // 3 MB chunks + chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks simultaneousUploads: 3, testChunks: false, throttleProgressCallbacks: 1,