User Permissions (User Folder, Read Only, Disable Upload) & more changes
This commit is contained in:
@@ -170,6 +170,10 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- 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.
|
||||
- User Permissions options
|
||||
- Folder Only gives user their own root folder
|
||||
- Read Only makes it so user can only read the files
|
||||
- Disable upload
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
|
||||
495
auth.js
495
auth.js
@@ -2,16 +2,25 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
|
||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import {
|
||||
openTOTPLoginModal,
|
||||
openUserPanel,
|
||||
openTOTPModal,
|
||||
closeTOTPModal,
|
||||
openAdminPanel,
|
||||
closeAdminPanel,
|
||||
setLastLoginData
|
||||
} from './authModals.js';
|
||||
|
||||
// Default OIDC configuration (can be overridden via API in production)
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
providerUrl: "https://your-oidc-provider.com",
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
|
||||
// Global OTPAuth URL; default applied if not set.
|
||||
globalOtpauthUrl: ""
|
||||
};
|
||||
window.currentOIDCConfig = currentOIDCConfig;
|
||||
|
||||
/* ----------------- Utility Functions ----------------- */
|
||||
function updateItemsPerPageSelect() {
|
||||
@@ -71,24 +80,27 @@ 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
|
||||
if (data.username) {
|
||||
localStorage.setItem("username", data.username);
|
||||
}
|
||||
if (typeof data.folderOnly !== "undefined") {
|
||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||
}
|
||||
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
const firstButton = headerButtons.firstElementChild;
|
||||
|
||||
// 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>';
|
||||
// Insert restoreBtn right after the first button.
|
||||
if (firstButton) {
|
||||
insertAfter(restoreBtn, firstButton);
|
||||
} else {
|
||||
@@ -97,14 +109,12 @@ function updateAuthenticatedUI(data) {
|
||||
}
|
||||
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");
|
||||
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||
// Insert adminPanelBtn right after the restore button.
|
||||
insertAfter(adminPanelBtn, restoreBtn);
|
||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||
} else {
|
||||
@@ -117,20 +127,16 @@ function updateAuthenticatedUI(data) {
|
||||
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);
|
||||
@@ -162,7 +168,6 @@ 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");
|
||||
}
|
||||
@@ -181,10 +186,9 @@ function checkAuthentication(showLoginToast = true) {
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
/* ----------------- TOTP Login Modal ----------------- */
|
||||
let lastLoginData = null; // For auto-submission
|
||||
/* ----------------- Authentication Submission ----------------- */
|
||||
function submitLogin(data) {
|
||||
lastLoginData = data;
|
||||
setLastLoginData(data);
|
||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
@@ -210,451 +214,9 @@ function submitLogin(data) {
|
||||
showToast("Login failed: Unknown error");
|
||||
});
|
||||
}
|
||||
window.submitLogin = submitLogin;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
} 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 })
|
||||
})
|
||||
.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.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 = `
|
||||
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"};
|
||||
`;
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
adminModal = document.createElement("div");
|
||||
adminModal.id = "adminPanelModal";
|
||||
adminModal.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;
|
||||
`;
|
||||
adminModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>Admin Panel</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>User Management</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>OIDC Configuration</legend>
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderUrl">OIDC Provider URL:</label>
|
||||
<input type="text" id="oidcProviderUrl" class="form-control" value="${currentOIDCConfig.providerUrl}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">OIDC Client ID:</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" value="${currentOIDCConfig.clientId}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">OIDC Client Secret:</label>
|
||||
<input type="text" id="oidcClientSecret" class="form-control" value="${currentOIDCConfig.clientSecret}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
|
||||
<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">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">Disable Login Form</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">Disable OIDC Login</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
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;
|
||||
if (totalDisabled === 3) {
|
||||
showToast("At least one login method must remain enabled.");
|
||||
disableOIDCLoginCheckbox.checked = false;
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
updateLoginOptionsUI({
|
||||
disableFormLogin: disableFormLoginCheckbox.checked,
|
||||
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
||||
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
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,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("Settings updated successfully.");
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
closeAdminPanel();
|
||||
} else {
|
||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.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;
|
||||
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); });
|
||||
|
||||
// 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 {
|
||||
// If the modal already exists, update its styles and values.
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "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';
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeAdminPanel() {
|
||||
const adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) adminModal.style.display = "none";
|
||||
}
|
||||
|
||||
/* ----------------- Other Helpers ----------------- */
|
||||
/* ----------------- Other Helpers and Initialization ----------------- */
|
||||
window.changeItemsPerPage = function (value) {
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||
@@ -695,8 +257,8 @@ function loadUserList() {
|
||||
})
|
||||
.catch(() => { });
|
||||
}
|
||||
window.loadUserList = loadUserList;
|
||||
|
||||
/* ----------------- Initialization ----------------- */
|
||||
function initAuth() {
|
||||
checkAuthentication(false);
|
||||
loadAdminConfigFunc();
|
||||
@@ -722,12 +284,6 @@ function initAuth() {
|
||||
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);
|
||||
@@ -795,7 +351,6 @@ function initAuth() {
|
||||
.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();
|
||||
|
||||
67
auth.php
67
auth.php
@@ -1,18 +1,36 @@
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- OIDC Authentication Flow ---
|
||||
if (isset($_GET['oidc'])) {
|
||||
/**
|
||||
* Helper: Get the user's role from users.txt.
|
||||
*/
|
||||
function getUserRole($username) {
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* --- OIDC Authentication Flow --- */
|
||||
if (isset($_GET['oidc'])) {
|
||||
// Read and decrypt OIDC configuration from JSON file.
|
||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||
if (file_exists($adminConfigFile)) {
|
||||
$encryptedContent = file_get_contents($adminConfigFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
if ($decryptedContent === false) {
|
||||
echo json_encode(['error' => 'Failed to decrypt admin configuration.']);
|
||||
// Log internal error and return a generic message.
|
||||
error_log("Failed to decrypt admin configuration.");
|
||||
echo json_encode(['error' => 'Internal error.']);
|
||||
exit;
|
||||
}
|
||||
$adminConfig = json_decode($decryptedContent, true);
|
||||
@@ -42,8 +60,6 @@ if (isset($_GET['oidc'])) {
|
||||
);
|
||||
$oidc->setRedirectURL($oidc_redirect_uri);
|
||||
|
||||
// Since PKCE is disabled in Keycloak, we do not set any PKCE parameters.
|
||||
|
||||
if ($_GET['oidc'] === 'callback') {
|
||||
try {
|
||||
$oidc->authenticate();
|
||||
@@ -51,11 +67,16 @@ if (isset($_GET['oidc'])) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = false;
|
||||
// Determine the user role from users.txt.
|
||||
$userRole = getUserRole($username);
|
||||
$_SESSION["isAdmin"] = ($userRole === "1");
|
||||
// *** Use loadUserPermissions() here instead of loadFolderPermission() ***
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||
header("Location: index.html");
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(["error" => "Authentication failed: " . $e->getMessage()]);
|
||||
error_log("OIDC authentication error: " . $e->getMessage());
|
||||
echo json_encode(["error" => "Authentication failed."]);
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
@@ -63,14 +84,15 @@ if (isset($_GET['oidc'])) {
|
||||
$oidc->authenticate();
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(["error" => "Authentication initiation failed: " . $e->getMessage()]);
|
||||
error_log("OIDC initiation error: " . $e->getMessage());
|
||||
echo json_encode(["error" => "Authentication initiation failed."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback: Form-based Authentication ---
|
||||
|
||||
/* --- Fallback: Form-based Authentication --- */
|
||||
// (Form-based branch code remains unchanged. It calls loadUserPermissions() in its basic auth branch.)
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$maxAttempts = 5;
|
||||
$lockoutTime = 30 * 60;
|
||||
@@ -104,13 +126,6 @@ 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, $encryptionKey;
|
||||
if (!file_exists($usersFile)) {
|
||||
@@ -119,10 +134,9 @@ function authenticate($username, $password) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) < 3) continue; // Skip invalid lines.
|
||||
if (count($parts) < 3) continue;
|
||||
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 {
|
||||
@@ -151,7 +165,6 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
|
||||
$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([
|
||||
@@ -168,8 +181,6 @@ if ($user !== false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- End TOTP Integration ---
|
||||
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
unset($failedAttempts[$ip]);
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
@@ -178,6 +189,7 @@ if ($user !== false) {
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||
|
||||
if ($rememberMe) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
@@ -194,14 +206,19 @@ if ($user !== false) {
|
||||
$persistentTokens[$token] = [
|
||||
"username" => $username,
|
||||
"expiry" => $expiry,
|
||||
"isAdmin" => ($user['role'] === "1")
|
||||
"isAdmin" => ($_SESSION["isAdmin"] === true)
|
||||
];
|
||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
|
||||
echo json_encode([
|
||||
"success" => "Login successful",
|
||||
"isAdmin" => $_SESSION["isAdmin"],
|
||||
"folderOnly"=> $_SESSION["folderOnly"],
|
||||
"username" => $_SESSION["username"]
|
||||
]);
|
||||
} else {
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$failedAttempts[$ip]['count']++;
|
||||
|
||||
655
authModals.js
Normal file
655
authModals.js
Normal file
@@ -0,0 +1,655 @@
|
||||
import { showToast, toggleVisibility } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.0.5";
|
||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
let lastLoginData = null;
|
||||
|
||||
export function setLastLoginData(data) {
|
||||
lastLoginData = data;
|
||||
}
|
||||
|
||||
export 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");
|
||||
totpInput.focus();
|
||||
totpInput.addEventListener("input", function () {
|
||||
if (this.value.trim().length === 6 && lastLoginData) {
|
||||
lastLoginData.totp_code = this.value.trim();
|
||||
totpLoginModal.style.display = "none";
|
||||
if (typeof window.submitLogin === "function") {
|
||||
window.submitLogin(lastLoginData);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
totpLoginModal.style.display = "flex";
|
||||
const modalContent = totpLoginModal.firstElementChild;
|
||||
modalContent.style.background = modalBg;
|
||||
modalContent.style.color = textColor;
|
||||
}
|
||||
}
|
||||
|
||||
export function openUserPanel() {
|
||||
const username = localStorage.getItem("username") || "User";
|
||||
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)";
|
||||
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 (${username})</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";
|
||||
});
|
||||
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
});
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||
totpCheckbox.addEventListener("change", function () {
|
||||
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
|
||||
const enabled = this.checked;
|
||||
fetch("updateUserPanel.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_enabled: enabled })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error updating TOTP setting: " + result.error);
|
||||
} else if (enabled) {
|
||||
openTOTPModal();
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error updating TOTP setting."); });
|
||||
});
|
||||
} else {
|
||||
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";
|
||||
}
|
||||
|
||||
export 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/>
|
||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
<br/><br/>
|
||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpModal);
|
||||
// Bind the X button to call closeTOTPModal with disable=true
|
||||
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
||||
closeTOTPModal(true);
|
||||
});
|
||||
|
||||
// Add event listener for TOTP confirmation
|
||||
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
||||
const code = document.getElementById("totpConfirmInput").value.trim();
|
||||
if (code.length !== 6) {
|
||||
showToast("Please enter a valid 6-digit code.");
|
||||
return;
|
||||
}
|
||||
// Call the endpoint to verify the TOTP code
|
||||
fetch("totp_verify.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_code: code })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast("TOTP successfully enabled.");
|
||||
// On success, close the modal without disabling
|
||||
closeTOTPModal(false);
|
||||
} else {
|
||||
showToast("TOTP verification failed: " + (result.error || "Invalid code."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error verifying TOTP code."); });
|
||||
});
|
||||
} 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";
|
||||
}
|
||||
}
|
||||
|
||||
// Updated closeTOTPModal function with a disable parameter
|
||||
export function closeTOTPModal(disable = true) {
|
||||
const totpModal = document.getElementById("totpModal");
|
||||
if (totpModal) totpModal.style.display = "none";
|
||||
|
||||
if (disable) {
|
||||
// Uncheck the Enable TOTP checkbox
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
if (totpCheckbox) {
|
||||
totpCheckbox.checked = false;
|
||||
localStorage.setItem("userTOTPEnabled", "false");
|
||||
}
|
||||
// Call endpoint to remove the TOTP secret from the user's record
|
||||
fetch("totp_disable.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error disabling TOTP setting: " + result.error);
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error disabling TOTP setting."); });
|
||||
}
|
||||
}
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.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 = `
|
||||
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"};
|
||||
`;
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
adminModal = document.createElement("div");
|
||||
adminModal.id = "adminPanelModal";
|
||||
adminModal.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;
|
||||
`;
|
||||
// Added a version number next to "Admin Panel"
|
||||
adminModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>
|
||||
<h3>${adminTitle}</h3>
|
||||
</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>User Management</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>OIDC Configuration</legend>
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderUrl">OIDC Provider URL:</label>
|
||||
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">OIDC Client ID:</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">OIDC Client Secret:</label>
|
||||
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
|
||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.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="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Login Options</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">Disable Login Form</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">Disable OIDC Login</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||
if (typeof window.loadUserList === "function") {
|
||||
window.loadUserList();
|
||||
}
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
// New event binding for the User Permissions button:
|
||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||
openUserPermissionsModal();
|
||||
});
|
||||
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;
|
||||
if (totalDisabled === 3) {
|
||||
showToast("At least one login method must remain enabled.");
|
||||
disableOIDCLoginCheckbox.checked = false;
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({
|
||||
disableFormLogin: disableFormLoginCheckbox.checked,
|
||||
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
||||
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
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,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("Settings updated successfully.");
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
}
|
||||
closeAdminPanel();
|
||||
} else {
|
||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
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;
|
||||
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 = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
} else {
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise';
|
||||
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(() => {
|
||||
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 = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function closeAdminPanel() {
|
||||
const adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) adminModal.style.display = "none";
|
||||
}
|
||||
|
||||
// --- New: User Permissions Modal ---
|
||||
|
||||
export function openUserPermissionsModal() {
|
||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||
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: 500px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
if (!userPermissionsModal) {
|
||||
userPermissionsModal = document.createElement("div");
|
||||
userPermissionsModal.id = "userPermissionsModal";
|
||||
userPermissionsModal.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: 3500;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Permissions</h3>
|
||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will be loaded here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">Save Permissions</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPermissionsModal);
|
||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||
// Collect permissions data from each user row.
|
||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||
const permissionsData = [];
|
||||
rows.forEach(row => {
|
||||
const username = row.getAttribute("data-username");
|
||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||
permissionsData.push({
|
||||
username,
|
||||
folderOnly: folderOnlyCheckbox.checked,
|
||||
readOnly: readOnlyCheckbox.checked,
|
||||
disableUpload: disableUploadCheckbox.checked
|
||||
});
|
||||
});
|
||||
// Send the permissionsData to the server.
|
||||
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("User permissions updated successfully.");
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast("Error updating permissions: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Error updating permissions.");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
userPermissionsModal.style.display = "flex";
|
||||
}
|
||||
// Load the list of users into the modal.
|
||||
loadUserPermissionsList();
|
||||
}
|
||||
|
||||
function loadUserPermissionsList() {
|
||||
const listContainer = document.getElementById("userPermissionsList");
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
// First, fetch the current permissions from the server.
|
||||
fetch("getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(usersData => {
|
||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||
if (users.length === 0) {
|
||||
listContainer.innerHTML = "<p>No users found.</p>";
|
||||
return;
|
||||
}
|
||||
users.forEach(user => {
|
||||
// Skip admin users.
|
||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to localStorage defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: localStorage.getItem("folderOnly") === "true",
|
||||
readOnly: localStorage.getItem("readOnly") === "true",
|
||||
disableUpload: localStorage.getItem("disableUpload") === "true"
|
||||
};
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && permissionsData[user.username]) || defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("user-permission-row");
|
||||
row.setAttribute("data-username", user.username);
|
||||
row.style.padding = "10px 0";
|
||||
row.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||
User Folder Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
Read Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
Disable Upload
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>Error loading users.</p>";
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
<?php
|
||||
// changePassword.php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Make sure the user is logged in.
|
||||
session_start();
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Check if users.txt is empty or doesn't exist
|
||||
// Check if users.txt is empty or doesn't exist.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||
// Return JSON indicating setup mode
|
||||
// In production, you might log that the system is in setup mode.
|
||||
error_log("checkAuth: users file not found or empty; entering setup mode.");
|
||||
echo json_encode(["setup" => true]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check session authentication.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["authenticated" => false]);
|
||||
exit;
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a user's role from users.txt.
|
||||
* Returns the role as a string (e.g. "1") or null if not found.
|
||||
*/
|
||||
function getUserRole($username) {
|
||||
global $usersFile;
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if TOTP is enabled by checking users.txt.
|
||||
$totp_enabled = false;
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
@@ -31,9 +52,19 @@ if ($username) {
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
// Use getUserRole() to determine admin status.
|
||||
// We cast the role to an integer so that "1" (string) is treated as true.
|
||||
$userRole = getUserRole($username);
|
||||
$isAdmin = ((int)$userRole === 1);
|
||||
|
||||
// Build and return the JSON response.
|
||||
$response = [
|
||||
"authenticated" => true,
|
||||
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false,
|
||||
"totp_enabled" => $totp_enabled
|
||||
]);
|
||||
"isAdmin" => $isAdmin,
|
||||
"totp_enabled" => $totp_enabled,
|
||||
"username" => $username,
|
||||
"folderOnly" => isset($_SESSION["folderOnly"]) ? $_SESSION["folderOnly"] : false
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
52
config.php
52
config.php
@@ -21,7 +21,8 @@ date_default_timezone_set(TIMEZONE);
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string Base64-encoded string containing IV and ciphertext.
|
||||
*/
|
||||
function encryptData($data, $encryptionKey) {
|
||||
function encryptData($data, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||
@@ -36,7 +37,8 @@ function encryptData($data, $encryptionKey) {
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string|false The decrypted plaintext or false on failure.
|
||||
*/
|
||||
function decryptData($encryptedData, $encryptionKey) {
|
||||
function decryptData($encryptedData, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$data = base64_decode($encryptedData);
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
@@ -51,6 +53,40 @@ if (!$encryptionKey) {
|
||||
die('Encryption key for persistent tokens is not set.');
|
||||
}
|
||||
|
||||
function loadUserPermissions($username)
|
||||
{
|
||||
global $encryptionKey; // Ensure $encryptionKey is available
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
|
||||
// Try to decrypt the content.
|
||||
$decryptedContent = decryptData($content, $encryptionKey);
|
||||
if ($decryptedContent !== false) {
|
||||
$permissions = json_decode($decryptedContent, true);
|
||||
} else {
|
||||
$permissions = json_decode($content, true);
|
||||
}
|
||||
|
||||
if (!is_array($permissions)) {
|
||||
} else {
|
||||
}
|
||||
|
||||
if (is_array($permissions) && array_key_exists($username, $permissions)) {
|
||||
$result = $permissions[$username];
|
||||
if (empty($result)) {
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
error_log("loadUserPermissions: Permissions file not found: $permissionsFile");
|
||||
}
|
||||
return false; // Return false if no permissions found.
|
||||
}
|
||||
|
||||
// Determine whether HTTPS is used.
|
||||
$envSecure = getenv('SECURE');
|
||||
if ($envSecure !== false) {
|
||||
@@ -67,9 +103,12 @@ $cookieParams = [
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
session_set_cookie_params($cookieParams);
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_cookie_params($cookieParams);
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
@@ -92,6 +131,8 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
||||
$_SESSION["folderOnly"] = loadFolderPermission($tokenData["username"]);
|
||||
} else {
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
@@ -111,4 +152,3 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||
}
|
||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||
?>
|
||||
@@ -18,6 +18,17 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to copy files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
!$data ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
@@ -23,6 +23,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
@@ -19,6 +19,20 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define $username first.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
|
||||
// Now load the user's permissions.
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
|
||||
// Check if the user is read-only.
|
||||
if ($username) {
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to delete files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Setup Trash Folder & Metadata ---
|
||||
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!file_exists($trashDir)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
@@ -23,6 +23,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to delete folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to extract zip files"]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Read and decode the JSON input.
|
||||
$rawData = file_get_contents("php://input");
|
||||
|
||||
@@ -305,7 +305,13 @@ if (localStorage.getItem('globalTags')) {
|
||||
// New function to load global tags from the server's persistent JSON.
|
||||
export function loadGlobalTags() {
|
||||
fetch("metadata/createdTags.json", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// If the file doesn't exist, assume there are no global tags.
|
||||
return [];
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
window.globalTags = data;
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
|
||||
244
folderManager.js
244
folderManager.js
@@ -3,9 +3,9 @@
|
||||
import { loadFileList } from './fileManager.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
|
||||
// ----------------------
|
||||
// Helper Functions (Data/State)
|
||||
// ----------------------
|
||||
/* ----------------------
|
||||
Helper Functions (Data/State)
|
||||
----------------------*/
|
||||
|
||||
// Formats a folder name for display (e.g. adding indentations).
|
||||
export function formatFolderName(folder) {
|
||||
@@ -26,7 +26,6 @@ export function formatFolderName(folder) {
|
||||
function buildFolderTree(folders) {
|
||||
const tree = {};
|
||||
folders.forEach(folderPath => {
|
||||
// Ensure folderPath is a string
|
||||
if (typeof folderPath !== "string") return;
|
||||
const parts = folderPath.split('/');
|
||||
let current = tree;
|
||||
@@ -40,9 +39,9 @@ function buildFolderTree(folders) {
|
||||
return tree;
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Folder Tree State (Save/Load)
|
||||
// ----------------------
|
||||
/* ----------------------
|
||||
Folder Tree State (Save/Load)
|
||||
----------------------*/
|
||||
function loadFolderTreeState() {
|
||||
const state = localStorage.getItem("folderTreeState");
|
||||
return state ? JSON.parse(state) : {};
|
||||
@@ -59,58 +58,41 @@ function getParentFolder(folder) {
|
||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Breadcrumb Functions
|
||||
// ----------------------
|
||||
// Render breadcrumb for a normalized folder path.
|
||||
/* ----------------------
|
||||
Breadcrumb Functions
|
||||
----------------------*/
|
||||
function renderBreadcrumb(normalizedFolder) {
|
||||
if (normalizedFolder === "root") {
|
||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||
}
|
||||
if (!normalizedFolder || normalizedFolder === "") return "";
|
||||
const parts = normalizedFolder.split("/");
|
||||
let breadcrumbItems = [];
|
||||
// Always start with "Root".
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="root">Root</span>`);
|
||||
let cumulative = "";
|
||||
parts.forEach((part, index) => {
|
||||
cumulative = index === 0 ? part : cumulative + "/" + part;
|
||||
// Use the first segment as the root.
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${parts[0]}">${escapeHTML(parts[0])}</span>`);
|
||||
let cumulative = parts[0];
|
||||
parts.slice(1).forEach(part => {
|
||||
cumulative += "/" + part;
|
||||
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
||||
});
|
||||
return breadcrumbItems.join('');
|
||||
}
|
||||
|
||||
// Bind click and drag-and-drop events to breadcrumb links.
|
||||
function bindBreadcrumbEvents() {
|
||||
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbLinks.forEach(link => {
|
||||
// Click event for navigation.
|
||||
link.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
let folder = this.getAttribute("data-folder");
|
||||
window.currentFolder = folder;
|
||||
localStorage.setItem("lastOpenedFolder", folder);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (folder === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
|
||||
}
|
||||
// Expand the folder tree to ensure the target is visible.
|
||||
expandTreePath(folder);
|
||||
// Update folder tree selection.
|
||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||
if (targetOption) {
|
||||
targetOption.classList.add("selected");
|
||||
}
|
||||
// Load the file list.
|
||||
if (targetOption) targetOption.classList.add("selected");
|
||||
loadFileList(folder);
|
||||
// Re-bind breadcrumb events to ensure all links remain active.
|
||||
bindBreadcrumbEvents();
|
||||
});
|
||||
|
||||
// Drag-and-drop events.
|
||||
link.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.add("drop-hover");
|
||||
@@ -161,19 +143,56 @@ function bindBreadcrumbEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// DOM Building Functions for Folder Tree
|
||||
// ----------------------
|
||||
/* ----------------------
|
||||
Check Current User's Folder-Only Permission
|
||||
----------------------*/
|
||||
// This function uses localStorage values (set during login) to determine if the current user is restricted.
|
||||
// If folderOnly is "true", then the personal folder (i.e. username) is forced as the effective root.
|
||||
function checkUserFolderPermission() {
|
||||
const username = localStorage.getItem("username");
|
||||
console.log("checkUserFolderPermission: username =", username);
|
||||
if (!username) {
|
||||
console.warn("No username in localStorage; skipping getUserPermissions fetch.");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (localStorage.getItem("folderOnly") === "true") {
|
||||
window.userFolderOnly = true;
|
||||
console.log("checkUserFolderPermission: using localStorage.folderOnly = true");
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return fetch("getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
console.log("checkUserFolderPermission: permissionsData =", permissionsData);
|
||||
if (permissionsData && permissionsData[username] && permissionsData[username].folderOnly) {
|
||||
window.userFolderOnly = true;
|
||||
localStorage.setItem("folderOnly", "true");
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
return true;
|
||||
} else {
|
||||
window.userFolderOnly = false;
|
||||
localStorage.setItem("folderOnly", "false");
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error fetching user permissions:", err);
|
||||
window.userFolderOnly = false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively builds HTML for the folder tree as nested <ul> elements.
|
||||
/* ----------------------
|
||||
DOM Building Functions for Folder Tree
|
||||
----------------------*/
|
||||
function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
||||
const state = loadFolderTreeState();
|
||||
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
||||
for (const folder in tree) {
|
||||
// Skip the trash folder (case-insensitive)
|
||||
if (folder.toLowerCase() === "trash") {
|
||||
continue;
|
||||
}
|
||||
if (folder.toLowerCase() === "trash") continue;
|
||||
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
||||
const hasChildren = Object.keys(tree[folder]).length > 0;
|
||||
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
||||
@@ -194,7 +213,6 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
||||
return html;
|
||||
}
|
||||
|
||||
// Expands the folder tree along a given normalized path.
|
||||
function expandTreePath(path) {
|
||||
const parts = path.split("/");
|
||||
let cumulative = "";
|
||||
@@ -219,9 +237,9 @@ function expandTreePath(path) {
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Drag & Drop Support for Folder Tree Nodes
|
||||
// ----------------------
|
||||
/* ----------------------
|
||||
Drag & Drop Support for Folder Tree Nodes
|
||||
----------------------*/
|
||||
function folderDragOverHandler(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.add("drop-hover");
|
||||
@@ -272,32 +290,66 @@ function folderDropHandler(event) {
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Main Folder Tree Rendering and Event Binding
|
||||
// ----------------------
|
||||
/* ----------------------
|
||||
Main Folder Tree Rendering and Event Binding
|
||||
----------------------*/
|
||||
export async function loadFolderTree(selectedFolder) {
|
||||
try {
|
||||
const response = await fetch('getFolderList.php');
|
||||
// Check if the user has folder-only permission.
|
||||
await checkUserFolderPermission();
|
||||
|
||||
// Determine effective root folder.
|
||||
const username = localStorage.getItem("username") || "root";
|
||||
let effectiveRoot = "root";
|
||||
let effectiveLabel = "(Root)";
|
||||
if (window.userFolderOnly) {
|
||||
effectiveRoot = username; // Use the username as the personal root.
|
||||
effectiveLabel = `(Root)`;
|
||||
// Force override of any saved folder.
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
|
||||
// Build fetch URL.
|
||||
let fetchUrl = 'getFolderList.php';
|
||||
if (window.userFolderOnly) {
|
||||
fetchUrl += '?restricted=1';
|
||||
}
|
||||
console.log("Fetching folder list from:", fetchUrl);
|
||||
|
||||
// Fetch folder list from the server.
|
||||
const response = await fetch(fetchUrl);
|
||||
if (response.status === 401) {
|
||||
console.error("Unauthorized: Please log in to view folders.");
|
||||
showToast("Session expired. Please log in again.");
|
||||
window.location.href = "logout.php";
|
||||
return;
|
||||
}
|
||||
let folders = await response.json();
|
||||
|
||||
// If returned items are objects (with a "folder" property), extract folder paths.
|
||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
||||
folders = folders.map(item => item.folder);
|
||||
}
|
||||
// Filter out duplicate "root" entries if present.
|
||||
folders = folders.filter(folder => folder !== "root");
|
||||
|
||||
if (!Array.isArray(folders)) {
|
||||
console.error("Folder list response is not an array:", folders);
|
||||
return;
|
||||
let folderData = await response.json();
|
||||
console.log("Folder data received:", folderData);
|
||||
let folders = [];
|
||||
if (Array.isArray(folderData) && folderData.length && typeof folderData[0] === "object" && folderData[0].folder) {
|
||||
folders = folderData.map(item => item.folder);
|
||||
} else if (Array.isArray(folderData)) {
|
||||
folders = folderData;
|
||||
}
|
||||
|
||||
// Remove any global "root" entry.
|
||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||
|
||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||
// Force current folder to be the effective root.
|
||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||
window.currentFolder = effectiveRoot;
|
||||
}
|
||||
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
// Render the folder tree.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (!container) {
|
||||
console.error("Folder tree container not found.");
|
||||
@@ -305,8 +357,8 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||
</div>`;
|
||||
if (folders.length > 0) {
|
||||
const tree = buildFolderTree(folders);
|
||||
@@ -314,48 +366,34 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach drag-and-drop event listeners to folder nodes.
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
|
||||
// Determine current folder (normalized).
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
// Update file list title using breadcrumb.
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (window.currentFolder === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
}
|
||||
// Bind breadcrumb events (click and drag/drop).
|
||||
bindBreadcrumbEvents();
|
||||
|
||||
// Load file list.
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
// Expand tree to current folder.
|
||||
const folderState = loadFolderTreeState();
|
||||
if (window.currentFolder !== "root" && folderState[window.currentFolder] !== "none") {
|
||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||
expandTreePath(window.currentFolder);
|
||||
}
|
||||
|
||||
// Highlight current folder in folder tree.
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
// Event binding for folder selection in folder tree.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -365,18 +403,12 @@ export async function loadFolderTree(selectedFolder) {
|
||||
window.currentFolder = selected;
|
||||
localStorage.setItem("lastOpenedFolder", selected);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (selected === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
||||
}
|
||||
// Re-bind breadcrumb events so the new breadcrumb is clickable.
|
||||
bindBreadcrumbEvents();
|
||||
loadFileList(selected);
|
||||
});
|
||||
});
|
||||
|
||||
// Event binding for toggling folders.
|
||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||
if (rootToggle) {
|
||||
rootToggle.addEventListener("click", function (e) {
|
||||
@@ -388,12 +420,12 @@ export async function loadFolderTree(selectedFolder) {
|
||||
nestedUl.classList.remove("collapsed");
|
||||
nestedUl.classList.add("expanded");
|
||||
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
state["root"] = "block";
|
||||
state[effectiveRoot] = "block";
|
||||
} else {
|
||||
nestedUl.classList.remove("expanded");
|
||||
nestedUl.classList.add("collapsed");
|
||||
this.textContent = "[+]";
|
||||
state["root"] = "none";
|
||||
state[effectiveRoot] = "none";
|
||||
}
|
||||
saveFolderTreeState(state);
|
||||
}
|
||||
@@ -433,12 +465,10 @@ export function loadFolderList(selectedFolder) {
|
||||
loadFolderTree(selectedFolder);
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Folder Management (Rename, Delete, Create)
|
||||
// ----------------------
|
||||
|
||||
/* ----------------------
|
||||
Folder Management (Rename, Delete, Create)
|
||||
----------------------*/
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||
|
||||
function openRenameFolderModal() {
|
||||
@@ -450,7 +480,6 @@ function openRenameFolderModal() {
|
||||
const parts = selectedFolder.split("/");
|
||||
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
|
||||
document.getElementById("renameFolderModal").style.display = "block";
|
||||
// Focus the input field after a short delay to ensure modal is visible.
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("newRenameFolderName");
|
||||
input.focus();
|
||||
@@ -601,8 +630,6 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
||||
});
|
||||
|
||||
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
|
||||
|
||||
// Function to display the custom context menu at (x, y) with given menu items.
|
||||
function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("folderManagerContextMenu");
|
||||
if (!menu) {
|
||||
@@ -614,8 +641,6 @@ function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
menu.style.zIndex = "9999";
|
||||
document.body.appendChild(menu);
|
||||
}
|
||||
|
||||
// Set styles based on dark mode.
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
menu.style.backgroundColor = "#2c2c2c";
|
||||
menu.style.border = "1px solid #555";
|
||||
@@ -625,8 +650,6 @@ function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
menu.style.border = "1px solid #ccc";
|
||||
menu.style.color = "#000";
|
||||
}
|
||||
|
||||
// Clear previous items.
|
||||
menu.innerHTML = "";
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = document.createElement("div");
|
||||
@@ -661,24 +684,16 @@ function hideFolderManagerContextMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu handler for folder tree and breadcrumb items.
|
||||
function folderManagerContextMenuHandler(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get the closest folder element (either from the tree or breadcrumb).
|
||||
const target = e.target.closest(".folder-option, .breadcrumb-link");
|
||||
if (!target) return;
|
||||
|
||||
const folder = target.getAttribute("data-folder");
|
||||
if (!folder) return;
|
||||
|
||||
// Update current folder and highlight the selected element.
|
||||
window.currentFolder = folder;
|
||||
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
|
||||
target.classList.add("selected");
|
||||
|
||||
// Build context menu items.
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Create Folder",
|
||||
@@ -696,20 +711,15 @@ function folderManagerContextMenuHandler(e) {
|
||||
action: () => { openDeleteFolderModal(); }
|
||||
}
|
||||
];
|
||||
|
||||
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
||||
}
|
||||
|
||||
// Bind contextmenu events to folder tree and breadcrumb elements.
|
||||
function bindFolderManagerContextMenu() {
|
||||
// Bind context menu to folder tree container.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (container) {
|
||||
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||
}
|
||||
|
||||
// Bind context menu to breadcrumb links.
|
||||
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbNodes.forEach(node => {
|
||||
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
@@ -717,31 +727,23 @@ function bindFolderManagerContextMenu() {
|
||||
});
|
||||
}
|
||||
|
||||
// Hide context menu when clicking elsewhere.
|
||||
document.addEventListener("click", function () {
|
||||
hideFolderManagerContextMenu();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
|
||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
||||
// Ensure a folder is selected and it isn't the root folder.
|
||||
if (window.currentFolder && window.currentFolder !== "root") {
|
||||
// Prevent default (avoid navigating back on macOS).
|
||||
e.preventDefault();
|
||||
// Call your existing folder delete function.
|
||||
openDeleteFolderModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Call this binding function after rendering the folder tree and breadcrumbs.
|
||||
bindFolderManagerContextMenu();
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$configFile = USERS_DIR . 'adminConfig.json';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
|
||||
47
getUserPermissions.php
Normal file
47
getUserPermissions.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||
$permissionsArray = [];
|
||||
|
||||
// Load permissions file if it exists.
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
// Attempt to decrypt the content.
|
||||
$decryptedContent = decryptData($content, $encryptionKey);
|
||||
if ($decryptedContent === false) {
|
||||
// If decryption fails, assume the file is plain JSON.
|
||||
$permissionsArray = json_decode($content, true);
|
||||
} else {
|
||||
$permissionsArray = json_decode($decryptedContent, true);
|
||||
}
|
||||
if (!is_array($permissionsArray)) {
|
||||
$permissionsArray = [];
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is an admin, return all permissions.
|
||||
if (isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true) {
|
||||
echo json_encode($permissionsArray);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Otherwise, return only the current user's permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
foreach ($permissionsArray as $storedUsername => $data) {
|
||||
if (strcasecmp($storedUsername, $username) === 0) {
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// If no permissions are found for the current user, return an empty object.
|
||||
echo json_encode(new stdClass());
|
||||
?>
|
||||
13
getUsers.php
13
getUsers.php
@@ -1,24 +1,31 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$users = [];
|
||||
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) >= 3) {
|
||||
// Optionally, validate username format:
|
||||
// Validate username format:
|
||||
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
|
||||
$users[] = ["username" => $parts[0]];
|
||||
$users[] = [
|
||||
"username" => $parts[0],
|
||||
"role" => trim($parts[2])
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($users);
|
||||
?>
|
||||
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
session_start();
|
||||
require_once 'config.php';
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
|
||||
|
||||
// Reuse the same authentication function
|
||||
function authenticate($username, $password)
|
||||
{
|
||||
global $usersFile;
|
||||
if (!file_exists($usersFile)) {
|
||||
error_log("authenticate(): users file not found");
|
||||
return false;
|
||||
}
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
@@ -18,9 +18,50 @@ function authenticate($username, $password)
|
||||
return $storedRole; // Return the user's role
|
||||
}
|
||||
}
|
||||
error_log("authenticate(): authentication failed for '$username'");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define helper function to get a user's role from users.txt
|
||||
function getUserRole($username) {
|
||||
global $usersFile;
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add the loadFolderPermission function here:
|
||||
function loadFolderPermission($username) {
|
||||
global $encryptionKey;
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
// Try to decrypt the content.
|
||||
$decryptedContent = decryptData($content, $encryptionKey);
|
||||
if ($decryptedContent !== false) {
|
||||
$permissions = json_decode($decryptedContent, true);
|
||||
} else {
|
||||
$permissions = json_decode($content, true);
|
||||
}
|
||||
if (is_array($permissions)) {
|
||||
// Use case-insensitive comparison.
|
||||
foreach ($permissions as $storedUsername => $data) {
|
||||
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
|
||||
return (bool)$data['folderOnly'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // Default if not set.
|
||||
}
|
||||
|
||||
// Check if the user has sent HTTP Basic auth credentials.
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||
@@ -40,15 +81,18 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
}
|
||||
|
||||
// Attempt authentication
|
||||
$userRole = authenticate($username, $password);
|
||||
if ($userRole !== false) {
|
||||
// Successful login
|
||||
$roleFromAuth = authenticate($username, $password);
|
||||
if ($roleFromAuth !== false) {
|
||||
// Use getUserRole() to determine the user's role from the file
|
||||
$actualRole = getUserRole($username);
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = ($userRole === "1"); // Assuming "1" means admin
|
||||
$_SESSION["isAdmin"] = ($actualRole === "1");
|
||||
// Set the folderOnly flag based on userPermissions.json.
|
||||
$_SESSION["folderOnly"] = loadFolderPermission($username);
|
||||
|
||||
// Redirect to the main page
|
||||
// Redirect to the main page (or output JSON for testing)
|
||||
header("Location: index.html");
|
||||
exit;
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
|
||||
// Retrieve headers and check CSRF token.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
|
||||
@@ -20,6 +20,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to move files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
@@ -72,5 +72,17 @@ if (!$userFound) {
|
||||
|
||||
// Write the updated list back to users.txt
|
||||
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
|
||||
|
||||
// Also update the userPermissions.json file
|
||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||
if (file_exists($permissionsFile)) {
|
||||
$permissionsJson = file_get_contents($permissionsFile);
|
||||
$permissionsArray = json_decode($permissionsJson, true);
|
||||
if (is_array($permissionsArray) && isset($permissionsArray[$usernameToRemove])) {
|
||||
unset($permissionsArray[$usernameToRemove]);
|
||||
file_put_contents($permissionsFile, json_encode($permissionsArray, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "User removed successfully"]);
|
||||
?>
|
||||
@@ -21,6 +21,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to rename files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
@@ -27,6 +27,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to rename folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
10
saveFile.php
10
saveFile.php
@@ -18,6 +18,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
|
||||
@@ -1053,7 +1053,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
||||
|
||||
#customToast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
@@ -1068,7 +1068,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
||||
}
|
||||
|
||||
#customToast.show {
|
||||
opacity: 1;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
"csrf_token" => $_SESSION['csrf_token'],
|
||||
|
||||
74
totp_disable.php
Normal file
74
totp_disable.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// disableTOTP.php
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'config.php';
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Not authenticated"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify CSRF token from request headers.
|
||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (empty($username)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Username not found in session"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the TOTP secret for the given user in users.txt.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
function removeUserTOTPSecret($username) {
|
||||
global $encryptionKey;
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile)) {
|
||||
return false;
|
||||
}
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$modified = false;
|
||||
$newLines = [];
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) < 3) {
|
||||
$newLines[] = $line;
|
||||
continue;
|
||||
}
|
||||
if ($parts[0] === $username) {
|
||||
// Remove the TOTP secret by setting it to an empty string.
|
||||
if (count($parts) >= 4) {
|
||||
$parts[3] = "";
|
||||
}
|
||||
$modified = true;
|
||||
$newLines[] = implode(":", $parts);
|
||||
} else {
|
||||
$newLines[] = $line;
|
||||
}
|
||||
}
|
||||
if ($modified) {
|
||||
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
||||
}
|
||||
return $modified;
|
||||
}
|
||||
|
||||
if (removeUserTOTPSecret($username)) {
|
||||
echo json_encode(["success" => true, "message" => "TOTP disabled successfully."]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Failed to disable TOTP."]);
|
||||
}
|
||||
?>
|
||||
@@ -2,7 +2,7 @@
|
||||
// totp_setup.php
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
@@ -12,10 +12,6 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
// 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;
|
||||
|
||||
84
totp_verify.php
Normal file
84
totp_verify.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
// verifyTOTPSetup.php
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'config.php';
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Not authenticated"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify CSRF token from request headers.
|
||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure Content-Type is JSON.
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Read and decode the JSON request body.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!isset($input['totp_code']) || strlen(trim($input['totp_code'])) !== 6 || !ctype_digit(trim($input['totp_code']))) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "A valid 6-digit TOTP code is required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$totpCode = trim($input['totp_code']);
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (empty($username)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Username not found in session"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current user's TOTP secret from users.txt.
|
||||
*
|
||||
* @param string $username
|
||||
* @return string|null The decrypted TOTP secret or null if not found.
|
||||
*/
|
||||
function getUserTOTPSecret($username) {
|
||||
global $encryptionKey;
|
||||
// Define the path to your users file.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
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));
|
||||
// Assuming format: username:hashedPassword:role:encryptedTOTPSecret
|
||||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
||||
return decryptData($parts[3], $encryptionKey);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retrieve the user's TOTP secret.
|
||||
$totpSecret = getUserTOTPSecret($username);
|
||||
if (!$totpSecret) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "TOTP secret not found. Please try setting up TOTP again."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify the provided TOTP code.
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
if (!$tfa->verifyCode($totpSecret, $totpCode)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid TOTP code."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If needed, you could update a flag or store the confirmation in the user record here.
|
||||
|
||||
// Return a successful response.
|
||||
echo json_encode(["success" => true, "message" => "TOTP successfully verified."]);
|
||||
?>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Verify that the user is authenticated and is an admin.
|
||||
@@ -76,10 +76,24 @@ $configFile = USERS_DIR . 'adminConfig.json';
|
||||
// Convert and encrypt configuration.
|
||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||
$encryptedContent = encryptData($plainTextConfig, $encryptionKey);
|
||||
|
||||
// Attempt to write the new configuration.
|
||||
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
||||
// Log the error.
|
||||
error_log("updateConfig.php: Initial write failed, attempting to delete the old configuration file.");
|
||||
|
||||
// Delete the old file.
|
||||
if (file_exists($configFile)) {
|
||||
unlink($configFile);
|
||||
}
|
||||
|
||||
// Try writing again.
|
||||
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
||||
error_log("updateConfig.php: Failed to write configuration even after deletion.");
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to update configuration.']);
|
||||
echo json_encode(['error' => 'Failed to update configuration even after cleanup.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => 'Configuration updated successfully.']);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<?php
|
||||
// updateUserPanel.php
|
||||
require 'config.php';
|
||||
require_once '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);
|
||||
|
||||
71
updateUserPermissions.php
Normal file
71
updateUserPermissions.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Only admins should update user permissions.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Read the POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!isset($input['permissions']) || !is_array($input['permissions'])) {
|
||||
echo json_encode(["error" => "Invalid input"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$permissions = $input['permissions'];
|
||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||
|
||||
// Load existing permissions if available and decrypt.
|
||||
if (file_exists($permissionsFile)) {
|
||||
$encryptedContent = file_get_contents($permissionsFile);
|
||||
$json = decryptData($encryptedContent, $encryptionKey);
|
||||
$existingPermissions = json_decode($json, true);
|
||||
if (!is_array($existingPermissions)) {
|
||||
$existingPermissions = [];
|
||||
}
|
||||
} else {
|
||||
$existingPermissions = [];
|
||||
}
|
||||
|
||||
// Loop through each permission update.
|
||||
foreach ($permissions as $perm) {
|
||||
// Ensure username is provided.
|
||||
if (!isset($perm['username'])) continue;
|
||||
$username = $perm['username'];
|
||||
// Skip updating permissions for admin users.
|
||||
if (strtolower($username) === "admin") continue;
|
||||
|
||||
// Update permissions: default any missing value to false.
|
||||
$existingPermissions[$username] = [
|
||||
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
||||
'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false,
|
||||
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
||||
];
|
||||
}
|
||||
|
||||
// Convert the permissions array to JSON.
|
||||
$plainText = json_encode($existingPermissions, JSON_PRETTY_PRINT);
|
||||
// Encrypt the JSON data.
|
||||
$encryptedData = encryptData($plainText, $encryptionKey);
|
||||
// Save encrypted permissions back to the JSON file.
|
||||
$result = file_put_contents($permissionsFile, $encryptedData);
|
||||
if ($result === false) {
|
||||
echo json_encode(["error" => "Failed to save user permissions."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "User permissions updated successfully."]);
|
||||
?>
|
||||
@@ -18,6 +18,15 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) {
|
||||
echo json_encode(["error" => "Disabled upload users are not allowed to upload."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle test chunk requests.
|
||||
|
||||
Reference in New Issue
Block a user