New User Panel with TOTP & change password. Admin Panel added Global OTPAuth URL
This commit is contained in:
22
README.md
22
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
744
auth.js
744
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 = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
if (headerButtons) {
|
||||
if (headerButtons.children.length >= 3) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[3]);
|
||||
// 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 = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||
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 = '<i class="material-icons" title="User Panel">account_circle</i>';
|
||||
|
||||
// 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,31 +181,20 @@ 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 + "!");
|
||||
/* ----------------- 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 (data.error && data.error.includes("Too many failed login attempts")) {
|
||||
showToast(data.error);
|
||||
const loginButton = authForm.querySelector("button[type='submit']");
|
||||
} 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(() => {
|
||||
@@ -179,173 +203,230 @@ function initAuth() {
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} else {
|
||||
showToast("Login failed: " + (data.error || "Unknown error"));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
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."));
|
||||
showToast("Login failed: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Error changing password.");
|
||||
});
|
||||
showToast("Login failed: Unknown error");
|
||||
});
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div style="background: ${modalBg}; padding: 20px; border-radius: 8px; text-align: center; position: relative; color: ${textColor};">
|
||||
<span id="closeTOTPLoginModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>Enter TOTP Code</h3>
|
||||
<input type="text" id="totpLoginInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
</div>
|
||||
`;
|
||||
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;
|
||||
});
|
||||
} 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 = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Panel</h3>
|
||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="userTOTPEnabled">Enable TOTP:</label>
|
||||
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
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 })
|
||||
})
|
||||
.catch(() => currentOIDCConfig);
|
||||
.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 = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>TOTP Setup</h3>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<br/>
|
||||
</div>
|
||||
`;
|
||||
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 modalContentStyles = `
|
||||
@@ -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";
|
||||
@@ -400,6 +482,13 @@ function openAdminPanel() {
|
||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${currentOIDCConfig.redirectUri}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Global TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="globalOtpauthUrl">Global OTPAuth URL:</label>
|
||||
<input type="text" id="globalOtpauthUrl" class="form-control" value="${currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Login Options</legend>
|
||||
<div class="form-group">
|
||||
@@ -430,27 +519,25 @@ function openAdminPanel() {
|
||||
</div>
|
||||
`;
|
||||
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) {
|
||||
@@ -491,32 +580,27 @@ function openAdminPanel() {
|
||||
})
|
||||
.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 = "";
|
||||
@@ -607,4 +696,155 @@ function loadUserList() {
|
||||
.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 };
|
||||
50
auth.php
50
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);
|
||||
|
||||
@@ -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.
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
?>
|
||||
@@ -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;
|
||||
|
||||
@@ -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' => ""
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -99,7 +99,7 @@
|
||||
<button id="logoutBtn" title="Logout">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
</button>
|
||||
<button id="changePasswordBtn" title="Change Password">
|
||||
<button id="changePasswordBtn" title="Change Password" style="display: none;">
|
||||
<i class="material-icons">vpn_key</i>
|
||||
</button>
|
||||
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
||||
@@ -391,7 +391,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Files -->
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
11
styles.css
11
styles.css
@@ -2048,3 +2048,14 @@ 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;
|
||||
}
|
||||
152
totp_setup.php
Normal file
152
totp_setup.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
// totp_setup.php
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
require 'config.php';
|
||||
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
|
||||
// For debugging purposes, you might enable error reporting temporarily:
|
||||
// ini_set('display_errors', 1);
|
||||
// error_reporting(E_ALL);
|
||||
|
||||
// Start the session and ensure the user is authenticated.
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify CSRF token provided as a GET parameter.
|
||||
if (!isset($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set header to output a PNG image.
|
||||
header("Content-Type: image/png");
|
||||
|
||||
// Define the path to your users.txt file.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
|
||||
/**
|
||||
* Updates the TOTP secret for the given user in users.txt.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $encryptedSecret The encrypted TOTP secret.
|
||||
*/
|
||||
function updateUserTOTPSecret($username, $encryptedSecret) {
|
||||
global $usersFile;
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$newLines = [];
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) < 3) {
|
||||
$newLines[] = $line;
|
||||
continue;
|
||||
}
|
||||
if ($parts[0] === $username) {
|
||||
// If a fourth field exists, update it; otherwise, append it.
|
||||
if (count($parts) >= 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();
|
||||
?>
|
||||
@@ -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.
|
||||
|
||||
82
updateUserPanel.php
Normal file
82
updateUserPanel.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// updateUserPanel.php
|
||||
require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
session_start();
|
||||
|
||||
// Ensure the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "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;
|
||||
}
|
||||
?>
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user