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 = `
+
+
×
+
User Panel
+
+
+
+ `;
+ 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 = `
+
+
×
+
TOTP Setup
+
Scan this QR code with your authenticator app:
+
})
+
+
+ `;
+ 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 @@
-