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:**
|
- **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.
|
- 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:**
|
- **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:**
|
- **Secure OIDC Authentication:**
|
||||||
- Integrates seamlessly with OIDC providers (e.g., Keycloak, Okta).
|
- Seamlessly integrates with OIDC providers (e.g., Keycloak, Okta).
|
||||||
- Admin-configurable OIDC settings, including Provider URL, Client ID, Client Secret, and Redirect URI.
|
- Provides 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.
|
- 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:**
|
- **Dynamic Admin Panel:**
|
||||||
- Intuitive Admin Panel with Material Icons for quick recognition and access.
|
- Features an intuitive interface with Material Icons for quick recognition and access.
|
||||||
- Allows administrators to easily manage authentication settings, user management, and login methods.
|
- Allows administrators to manage authentication settings, user management, and login methods in real time.
|
||||||
- Real-time validation prevents disabling all authentication methods simultaneously, ensuring continuous secure access.
|
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
752
auth.js
752
auth.js
@@ -8,37 +8,34 @@ const currentOIDCConfig = {
|
|||||||
providerUrl: "https://your-oidc-provider.com",
|
providerUrl: "https://your-oidc-provider.com",
|
||||||
clientId: "YOUR_CLIENT_ID",
|
clientId: "YOUR_CLIENT_ID",
|
||||||
clientSecret: "YOUR_CLIENT_SECRET",
|
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() {
|
function updateItemsPerPageSelect() {
|
||||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||||
if (selectElem) {
|
if (selectElem) {
|
||||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
selectElem.value = localStorage.getItem("itemsPerPage") || "10";
|
||||||
selectElem.value = stored;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||||
const authForm = document.getElementById("authForm");
|
const authForm = document.getElementById("authForm");
|
||||||
if (authForm) {
|
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
||||||
authForm.style.display = disableFormLogin ? "none" : "block";
|
|
||||||
}
|
|
||||||
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
|
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
|
||||||
if (basicAuthLink) {
|
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||||
basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
|
||||||
}
|
|
||||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
if (oidcLoginBtn) {
|
if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
|
||||||
oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLoginOptionsUIFromStorage() {
|
function updateLoginOptionsUIFromStorage() {
|
||||||
const disableFormLogin = localStorage.getItem("disableFormLogin") === "true";
|
updateLoginOptionsUI({
|
||||||
const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true";
|
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||||
const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true";
|
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||||
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAdminConfigFunc() {
|
function loadAdminConfigFunc() {
|
||||||
@@ -48,16 +45,22 @@ function loadAdminConfigFunc() {
|
|||||||
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
|
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
|
||||||
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||||
|
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/FileRise?issuer=FileRise");
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
localStorage.setItem("disableFormLogin", "false");
|
localStorage.setItem("disableFormLogin", "false");
|
||||||
localStorage.setItem("disableBasicAuth", "false");
|
localStorage.setItem("disableBasicAuth", "false");
|
||||||
localStorage.setItem("disableOIDCLogin", "false");
|
localStorage.setItem("disableOIDCLogin", "false");
|
||||||
|
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/FileRise?issuer=FileRise");
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertAfter(newNode, referenceNode) {
|
||||||
|
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthenticatedUI(data) {
|
function updateAuthenticatedUI(data) {
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
toggleVisibility("mainOperations", true);
|
toggleVisibility("mainOperations", true);
|
||||||
@@ -68,40 +71,41 @@ function updateAuthenticatedUI(data) {
|
|||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
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) {
|
if (data.isAdmin) {
|
||||||
|
// Create restore button.
|
||||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (!restoreBtn) {
|
if (!restoreBtn) {
|
||||||
restoreBtn = document.createElement("button");
|
restoreBtn = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
restoreBtn.classList.add("btn", "btn-warning");
|
||||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
// Insert restoreBtn right after the first button.
|
||||||
if (headerButtons) {
|
if (firstButton) {
|
||||||
if (headerButtons.children.length >= 3) {
|
insertAfter(restoreBtn, firstButton);
|
||||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[3]);
|
|
||||||
} else {
|
} else {
|
||||||
headerButtons.appendChild(restoreBtn);
|
headerButtons.appendChild(restoreBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
restoreBtn.style.display = "block";
|
restoreBtn.style.display = "block";
|
||||||
|
|
||||||
|
// Create admin panel button.
|
||||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
if (!adminPanelBtn) {
|
if (!adminPanelBtn) {
|
||||||
adminPanelBtn = document.createElement("button");
|
adminPanelBtn = document.createElement("button");
|
||||||
adminPanelBtn.id = "adminPanelBtn";
|
adminPanelBtn.id = "adminPanelBtn";
|
||||||
adminPanelBtn.classList.add("btn", "btn-info");
|
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>';
|
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
// Insert adminPanelBtn right after the restore button.
|
||||||
if (headerButtons) {
|
insertAfter(adminPanelBtn, restoreBtn);
|
||||||
// Insert the adminPanelBtn immediately after the restoreBtn.
|
|
||||||
if (restoreBtn.nextSibling) {
|
|
||||||
headerButtons.insertBefore(adminPanelBtn, restoreBtn.nextSibling);
|
|
||||||
} else {
|
|
||||||
headerButtons.appendChild(adminPanelBtn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||||
} else {
|
} else {
|
||||||
adminPanelBtn.style.display = "block";
|
adminPanelBtn.style.display = "block";
|
||||||
@@ -112,6 +116,33 @@ function updateAuthenticatedUI(data) {
|
|||||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
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();
|
updateItemsPerPageSelect();
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
}
|
}
|
||||||
@@ -131,6 +162,10 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
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);
|
updateAuthenticatedUI(data);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -146,31 +181,20 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAuth() {
|
/* ----------------- TOTP Login Modal ----------------- */
|
||||||
checkAuthentication(false);
|
let lastLoginData = null; // For auto-submission
|
||||||
loadAdminConfigFunc();
|
function submitLogin(data) {
|
||||||
|
lastLoginData = data;
|
||||||
const authForm = document.getElementById("authForm");
|
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||||
if (authForm) {
|
.then(response => {
|
||||||
authForm.addEventListener("submit", function (event) {
|
if (response.success) {
|
||||||
event.preventDefault();
|
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||||
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();
|
window.location.reload();
|
||||||
} else {
|
} else if (response.totp_required) {
|
||||||
if (data.error && data.error.includes("Too many failed login attempts")) {
|
openTOTPLoginModal();
|
||||||
showToast(data.error);
|
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||||
const loginButton = authForm.querySelector("button[type='submit']");
|
showToast(response.error);
|
||||||
|
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
||||||
if (loginButton) {
|
if (loginButton) {
|
||||||
loginButton.disabled = true;
|
loginButton.disabled = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -179,175 +203,232 @@ function initAuth() {
|
|||||||
}, 30 * 60 * 1000);
|
}, 30 * 60 * 1000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast("Login failed: " + (data.error || "Unknown error"));
|
showToast("Login failed: " + (response.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."));
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
showToast("Error changing password.");
|
showToast("Login failed: Unknown error");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOIDCConfig() {
|
function openTOTPLoginModal() {
|
||||||
return fetch("getConfig.php", { credentials: "include" })
|
let totpLoginModal = document.getElementById("totpLoginModal");
|
||||||
.then(response => response.json())
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
.then(config => {
|
const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
|
||||||
if (config.oidc) {
|
const textColor = isDarkMode ? "#e0e0e0" : "#000";
|
||||||
Object.assign(currentOIDCConfig, config.oidc);
|
|
||||||
|
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() {
|
function openAdminPanel() {
|
||||||
fetch("getConfig.php", { credentials: "include" })
|
fetch("getConfig.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(config => {
|
.then(config => {
|
||||||
if (config.oidc) {
|
if (config.oidc) Object.assign(currentOIDCConfig, config.oidc);
|
||||||
Object.assign(currentOIDCConfig, config.oidc);
|
if (config.globalOtpauthUrl) currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
}
|
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
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 = `
|
const modalContentStyles = `
|
||||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||||
@@ -361,6 +442,7 @@ function openAdminPanel() {
|
|||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||||
`;
|
`;
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
let adminModal = document.getElementById("adminPanelModal");
|
||||||
|
|
||||||
if (!adminModal) {
|
if (!adminModal) {
|
||||||
adminModal = document.createElement("div");
|
adminModal = document.createElement("div");
|
||||||
adminModal.id = "adminPanelModal";
|
adminModal.id = "adminPanelModal";
|
||||||
@@ -400,6 +482,13 @@ function openAdminPanel() {
|
|||||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${currentOIDCConfig.redirectUri}" />
|
<input type="text" id="oidcRedirectUri" class="form-control" value="${currentOIDCConfig.redirectUri}" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>Login Options</legend>
|
<legend>Login Options</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -428,29 +517,27 @@ function openAdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(adminModal);
|
document.body.appendChild(adminModal);
|
||||||
|
|
||||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||||
adminModal.addEventListener("click", function (e) {
|
adminModal.addEventListener("click", (e) => {
|
||||||
if (e.target === adminModal) {
|
if (e.target === adminModal) closeAdminPanel();
|
||||||
closeAdminPanel();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||||
document.getElementById("adminOpenAddUser").addEventListener("click", function () {
|
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", function () {
|
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||||
loadUserList();
|
loadUserList();
|
||||||
toggleVisibility("removeUserModal", true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
document.getElementById("saveAdminSettings").addEventListener("click", function () {
|
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||||
.filter(cb => cb.checked).length;
|
|
||||||
if (totalDisabled === 3) {
|
if (totalDisabled === 3) {
|
||||||
showToast("At least one login method must remain enabled.");
|
showToast("At least one login method must remain enabled.");
|
||||||
disableOIDCLoginCheckbox.checked = false;
|
disableOIDCLoginCheckbox.checked = false;
|
||||||
@@ -471,11 +558,13 @@ function openAdminPanel() {
|
|||||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||||
|
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
sendRequest("updateConfig.php", "POST", {
|
sendRequest("updateConfig.php", "POST", {
|
||||||
oidc: newOIDCConfig,
|
oidc: newOIDCConfig,
|
||||||
disableFormLogin,
|
disableFormLogin,
|
||||||
disableBasicAuth,
|
disableBasicAuth,
|
||||||
disableOIDCLogin
|
disableOIDCLogin,
|
||||||
|
globalOtpauthUrl
|
||||||
}, { "X-CSRF-Token": window.csrfToken })
|
}, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -489,34 +578,29 @@ function openAdminPanel() {
|
|||||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
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 disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||||
.filter(cb => cb.checked).length;
|
|
||||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||||
showToast("At least one login method must remain enabled.");
|
showToast("At least one login method must remain enabled.");
|
||||||
changedCheckbox.checked = false;
|
changedCheckbox.checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disableFormLoginCheckbox.addEventListener("change", function () {
|
disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
enforceLoginOptionConstraint(this);
|
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
});
|
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
disableBasicAuthCheckbox.addEventListener("change", function () {
|
|
||||||
enforceLoginOptionConstraint(this);
|
// UPDATE checkboxes using fetched configuration:
|
||||||
});
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
disableOIDCLoginCheckbox.addEventListener("change", function () {
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
enforceLoginOptionConstraint(this);
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
});
|
|
||||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
|
||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
|
||||||
} else {
|
} else {
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
// If the modal already exists, update its styles and values.
|
||||||
const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)";
|
|
||||||
adminModal.style.backgroundColor = overlayBackground;
|
adminModal.style.backgroundColor = overlayBackground;
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
const modalContent = adminModal.querySelector(".modal-content");
|
||||||
if (modalContent) {
|
if (modalContent) {
|
||||||
@@ -528,19 +612,36 @@ function openAdminPanel() {
|
|||||||
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
||||||
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
||||||
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
|
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
|
||||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
document.getElementById("globalOtpauthUrl").value = currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise';
|
||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
// 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";
|
adminModal.style.display = "flex";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
// In case of error, fallback to localStorage values
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
let adminModal = document.getElementById("adminPanelModal");
|
||||||
if (adminModal) {
|
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("oidcProviderUrl").value = currentOIDCConfig.providerUrl;
|
||||||
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
||||||
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
||||||
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
|
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";
|
adminModal.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
openAdminPanel();
|
openAdminPanel();
|
||||||
@@ -550,27 +651,15 @@ function openAdminPanel() {
|
|||||||
|
|
||||||
function closeAdminPanel() {
|
function closeAdminPanel() {
|
||||||
const adminModal = document.getElementById("adminPanelModal");
|
const adminModal = document.getElementById("adminPanelModal");
|
||||||
if (adminModal) {
|
if (adminModal) adminModal.style.display = "none";
|
||||||
adminModal.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------- Other Helpers ----------------- */
|
||||||
window.changeItemsPerPage = function (value) {
|
window.changeItemsPerPage = function (value) {
|
||||||
localStorage.setItem("itemsPerPage", value);
|
localStorage.setItem("itemsPerPage", value);
|
||||||
const folder = window.currentFolder || "root";
|
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||||
if (typeof renderFileTable === "function") {
|
|
||||||
renderFileTable(folder);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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() {
|
function resetUserForm() {
|
||||||
document.getElementById("newUsername").value = "";
|
document.getElementById("newUsername").value = "";
|
||||||
document.getElementById("addUserPassword").value = "";
|
document.getElementById("addUserPassword").value = "";
|
||||||
@@ -604,7 +693,158 @@ function loadUserList() {
|
|||||||
closeRemoveUserModal();
|
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 };
|
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) {
|
function authenticate($username, $password) {
|
||||||
global $usersFile;
|
global $usersFile, $encryptionKey;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
|
$parts = explode(':', trim($line));
|
||||||
if ($username === $storedUser && password_verify($password, $storedPass)) {
|
if (count($parts) < 3) continue; // Skip invalid lines.
|
||||||
return $storedRole;
|
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;
|
return false;
|
||||||
@@ -134,8 +149,27 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$userRole = authenticate($username, $password);
|
$user = authenticate($username, $password);
|
||||||
if ($userRole !== false) {
|
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])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
unset($failedAttempts[$ip]);
|
unset($failedAttempts[$ip]);
|
||||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||||
@@ -143,7 +177,7 @@ if ($userRole !== false) {
|
|||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_SESSION["username"] = $username;
|
||||||
$_SESSION["isAdmin"] = ($userRole === "1");
|
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
||||||
|
|
||||||
if ($rememberMe) {
|
if ($rememberMe) {
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
@@ -160,7 +194,7 @@ if ($userRole !== false) {
|
|||||||
$persistentTokens[$token] = [
|
$persistentTokens[$token] = [
|
||||||
"username" => $username,
|
"username" => $username,
|
||||||
"expiry" => $expiry,
|
"expiry" => $expiry,
|
||||||
"isAdmin" => ($userRole === "1")
|
"isAdmin" => ($user['role'] === "1")
|
||||||
];
|
];
|
||||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||||
|
|||||||
@@ -54,7 +54,19 @@ $userFound = false;
|
|||||||
$newLines = [];
|
$newLines = [];
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
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) {
|
if ($storedUser === $username) {
|
||||||
$userFound = true;
|
$userFound = true;
|
||||||
// Verify the old password.
|
// Verify the old password.
|
||||||
@@ -64,8 +76,12 @@ foreach ($lines as $line) {
|
|||||||
}
|
}
|
||||||
// Hash the new password.
|
// Hash the new password.
|
||||||
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
$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;
|
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$newLines[] = $line;
|
$newLines[] = $line;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,25 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
exit;
|
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([
|
echo json_encode([
|
||||||
"authenticated" => true,
|
"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.height = "80vh";
|
||||||
embed.style.border = "none";
|
embed.style.border = "none";
|
||||||
container.appendChild(embed);
|
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");
|
const video = document.createElement("video");
|
||||||
video.src = fileUrl;
|
video.src = fileUrl;
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ if (file_exists($configFile)) {
|
|||||||
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
||||||
exit;
|
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 {
|
} else {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
@@ -24,7 +29,8 @@ if (file_exists($configFile)) {
|
|||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => false,
|
||||||
'disableOIDCLogin' => false
|
'disableOIDCLogin' => false
|
||||||
]
|
],
|
||||||
|
'globalOtpauthUrl' => ""
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<button id="logoutBtn" title="Logout">
|
<button id="logoutBtn" title="Logout">
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
</button>
|
</button>
|
||||||
<button id="changePasswordBtn" title="Change Password">
|
<button id="changePasswordBtn" title="Change Password" style="display: none;">
|
||||||
<i class="material-icons">vpn_key</i>
|
<i class="material-icons">vpn_key</i>
|
||||||
</button>
|
</button>
|
||||||
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
||||||
@@ -391,7 +391,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JavaScript Files -->
|
|
||||||
<script type="module" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</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 {
|
body.dark-mode .admin-panel-content label {
|
||||||
color: #e0e0e0;
|
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;
|
$disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false;
|
||||||
$disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], 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.
|
// Prepare configuration array.
|
||||||
$configUpdate = [
|
$configUpdate = [
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
@@ -63,7 +66,8 @@ $configUpdate = [
|
|||||||
'disableFormLogin' => $disableFormLogin,
|
'disableFormLogin' => $disableFormLogin,
|
||||||
'disableBasicAuth' => $disableBasicAuth,
|
'disableBasicAuth' => $disableBasicAuth,
|
||||||
'disableOIDCLogin' => $disableOIDCLogin,
|
'disableOIDCLogin' => $disableOIDCLogin,
|
||||||
]
|
],
|
||||||
|
'globalOtpauthUrl' => $globalOtpauthUrl
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define the configuration file path.
|
// 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({
|
resumableInstance = new Resumable({
|
||||||
target: "upload.php",
|
target: "upload.php",
|
||||||
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
|
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,
|
simultaneousUploads: 3,
|
||||||
testChunks: false,
|
testChunks: false,
|
||||||
throttleProgressCallbacks: 1,
|
throttleProgressCallbacks: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user