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.
|
- 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.
|
- 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.
|
- 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
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|||||||
493
auth.js
493
auth.js
@@ -2,16 +2,25 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
|
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
|
||||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||||
import { loadFolderTree } from './folderManager.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 = {
|
const currentOIDCConfig = {
|
||||||
providerUrl: "https://your-oidc-provider.com",
|
providerUrl: "https://your-oidc-provider.com",
|
||||||
clientId: "YOUR_CLIENT_ID",
|
clientId: "YOUR_CLIENT_ID",
|
||||||
clientSecret: "YOUR_CLIENT_SECRET",
|
clientSecret: "YOUR_CLIENT_SECRET",
|
||||||
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
|
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
|
||||||
// Global OTPAuth URL; default applied if not set.
|
|
||||||
globalOtpauthUrl: ""
|
globalOtpauthUrl: ""
|
||||||
};
|
};
|
||||||
|
window.currentOIDCConfig = currentOIDCConfig;
|
||||||
|
|
||||||
/* ----------------- Utility Functions ----------------- */
|
/* ----------------- Utility Functions ----------------- */
|
||||||
function updateItemsPerPageSelect() {
|
function updateItemsPerPageSelect() {
|
||||||
@@ -71,24 +80,27 @@ function updateAuthenticatedUI(data) {
|
|||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
|
|
||||||
// Update TOTP state from the server response.
|
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 headerButtons = document.querySelector(".header-buttons");
|
||||||
const firstButton = headerButtons.firstElementChild; // first button in container
|
const firstButton = headerButtons.firstElementChild;
|
||||||
|
|
||||||
// Admin controls: restore and admin panel buttons are shown only for admins.
|
|
||||||
if (data.isAdmin) {
|
if (data.isAdmin) {
|
||||||
// Create restore button.
|
|
||||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (!restoreBtn) {
|
if (!restoreBtn) {
|
||||||
restoreBtn = document.createElement("button");
|
restoreBtn = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
restoreBtn.classList.add("btn", "btn-warning");
|
||||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||||
// Insert restoreBtn right after the first button.
|
|
||||||
if (firstButton) {
|
if (firstButton) {
|
||||||
insertAfter(restoreBtn, firstButton);
|
insertAfter(restoreBtn, firstButton);
|
||||||
} else {
|
} else {
|
||||||
@@ -97,14 +109,12 @@ function updateAuthenticatedUI(data) {
|
|||||||
}
|
}
|
||||||
restoreBtn.style.display = "block";
|
restoreBtn.style.display = "block";
|
||||||
|
|
||||||
// Create admin panel button.
|
|
||||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
if (!adminPanelBtn) {
|
if (!adminPanelBtn) {
|
||||||
adminPanelBtn = document.createElement("button");
|
adminPanelBtn = document.createElement("button");
|
||||||
adminPanelBtn.id = "adminPanelBtn";
|
adminPanelBtn.id = "adminPanelBtn";
|
||||||
adminPanelBtn.classList.add("btn", "btn-info");
|
adminPanelBtn.classList.add("btn", "btn-info");
|
||||||
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||||
// Insert adminPanelBtn right after the restore button.
|
|
||||||
insertAfter(adminPanelBtn, restoreBtn);
|
insertAfter(adminPanelBtn, restoreBtn);
|
||||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||||
} else {
|
} else {
|
||||||
@@ -117,20 +127,16 @@ function updateAuthenticatedUI(data) {
|
|||||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// User panel button: Always visible for authenticated users.
|
|
||||||
let userPanelBtn = document.getElementById("userPanelBtn");
|
let userPanelBtn = document.getElementById("userPanelBtn");
|
||||||
if (!userPanelBtn) {
|
if (!userPanelBtn) {
|
||||||
userPanelBtn = document.createElement("button");
|
userPanelBtn = document.createElement("button");
|
||||||
userPanelBtn.id = "userPanelBtn";
|
userPanelBtn.id = "userPanelBtn";
|
||||||
userPanelBtn.classList.add("btn", "btn-user");
|
userPanelBtn.classList.add("btn", "btn-user");
|
||||||
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
|
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");
|
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
if (adminPanelBtn) {
|
if (adminPanelBtn) {
|
||||||
insertAfter(userPanelBtn, adminPanelBtn);
|
insertAfter(userPanelBtn, adminPanelBtn);
|
||||||
} else {
|
} else {
|
||||||
// If no admin panel button exists, insert right after the first button in headerButtons.
|
|
||||||
const firstButton = headerButtons.firstElementChild;
|
const firstButton = headerButtons.firstElementChild;
|
||||||
if (firstButton) {
|
if (firstButton) {
|
||||||
insertAfter(userPanelBtn, firstButton);
|
insertAfter(userPanelBtn, firstButton);
|
||||||
@@ -162,7 +168,6 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
// Update localStorage for TOTP state if provided by the server.
|
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
@@ -181,10 +186,9 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------- TOTP Login Modal ----------------- */
|
/* ----------------- Authentication Submission ----------------- */
|
||||||
let lastLoginData = null; // For auto-submission
|
|
||||||
function submitLogin(data) {
|
function submitLogin(data) {
|
||||||
lastLoginData = data;
|
setLastLoginData(data);
|
||||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -210,451 +214,9 @@ function submitLogin(data) {
|
|||||||
showToast("Login failed: Unknown error");
|
showToast("Login failed: Unknown error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.submitLogin = submitLogin;
|
||||||
|
|
||||||
function openTOTPLoginModal() {
|
/* ----------------- Other Helpers and Initialization ----------------- */
|
||||||
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 ----------------- */
|
|
||||||
window.changeItemsPerPage = function (value) {
|
window.changeItemsPerPage = function (value) {
|
||||||
localStorage.setItem("itemsPerPage", value);
|
localStorage.setItem("itemsPerPage", value);
|
||||||
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||||
@@ -695,8 +257,8 @@ function loadUserList() {
|
|||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
|
window.loadUserList = loadUserList;
|
||||||
|
|
||||||
/* ----------------- Initialization ----------------- */
|
|
||||||
function initAuth() {
|
function initAuth() {
|
||||||
checkAuthentication(false);
|
checkAuthentication(false);
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
@@ -722,12 +284,6 @@ function initAuth() {
|
|||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
}).then(() => window.location.reload(true)).catch(() => { });
|
}).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 () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
@@ -795,7 +351,6 @@ function initAuth() {
|
|||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
});
|
});
|
||||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||||
// Change password bindings.
|
|
||||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
document.getElementById("oldPassword").focus();
|
document.getElementById("oldPassword").focus();
|
||||||
|
|||||||
67
auth.php
67
auth.php
@@ -1,18 +1,36 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
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.
|
// Read and decrypt OIDC configuration from JSON file.
|
||||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||||
if (file_exists($adminConfigFile)) {
|
if (file_exists($adminConfigFile)) {
|
||||||
$encryptedContent = file_get_contents($adminConfigFile);
|
$encryptedContent = file_get_contents($adminConfigFile);
|
||||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||||
if ($decryptedContent === false) {
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
$adminConfig = json_decode($decryptedContent, true);
|
$adminConfig = json_decode($decryptedContent, true);
|
||||||
@@ -42,8 +60,6 @@ if (isset($_GET['oidc'])) {
|
|||||||
);
|
);
|
||||||
$oidc->setRedirectURL($oidc_redirect_uri);
|
$oidc->setRedirectURL($oidc_redirect_uri);
|
||||||
|
|
||||||
// Since PKCE is disabled in Keycloak, we do not set any PKCE parameters.
|
|
||||||
|
|
||||||
if ($_GET['oidc'] === 'callback') {
|
if ($_GET['oidc'] === 'callback') {
|
||||||
try {
|
try {
|
||||||
$oidc->authenticate();
|
$oidc->authenticate();
|
||||||
@@ -51,11 +67,16 @@ if (isset($_GET['oidc'])) {
|
|||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_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");
|
header("Location: index.html");
|
||||||
exit();
|
exit();
|
||||||
} catch (Exception $e) {
|
} 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();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -63,14 +84,15 @@ if (isset($_GET['oidc'])) {
|
|||||||
$oidc->authenticate();
|
$oidc->authenticate();
|
||||||
exit();
|
exit();
|
||||||
} catch (Exception $e) {
|
} 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();
|
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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$maxAttempts = 5;
|
$maxAttempts = 5;
|
||||||
$lockoutTime = 30 * 60;
|
$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) {
|
function authenticate($username, $password) {
|
||||||
global $usersFile, $encryptionKey;
|
global $usersFile, $encryptionKey;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
@@ -119,10 +134,9 @@ function authenticate($username, $password) {
|
|||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$parts = explode(':', trim($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])) {
|
if ($username === $parts[0] && password_verify($password, $parts[1])) {
|
||||||
$result = ['role' => $parts[2]];
|
$result = ['role' => $parts[2]];
|
||||||
// If there's a fourth field, decrypt it to get the TOTP secret.
|
|
||||||
if (isset($parts[3]) && !empty($parts[3])) {
|
if (isset($parts[3]) && !empty($parts[3])) {
|
||||||
$result['totp_secret'] = decryptData($parts[3], $encryptionKey);
|
$result['totp_secret'] = decryptData($parts[3], $encryptionKey);
|
||||||
} else {
|
} else {
|
||||||
@@ -151,7 +165,6 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
|||||||
|
|
||||||
$user = authenticate($username, $password);
|
$user = authenticate($username, $password);
|
||||||
if ($user !== false) {
|
if ($user !== false) {
|
||||||
// Only require TOTP if the user's TOTP secret is set.
|
|
||||||
if (!empty($user['totp_secret'])) {
|
if (!empty($user['totp_secret'])) {
|
||||||
if (empty($data['totp_code'])) {
|
if (empty($data['totp_code'])) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -168,8 +181,6 @@ if ($user !== false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- End TOTP Integration ---
|
|
||||||
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
unset($failedAttempts[$ip]);
|
unset($failedAttempts[$ip]);
|
||||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||||
@@ -178,6 +189,7 @@ if ($user !== false) {
|
|||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_SESSION["username"] = $username;
|
||||||
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
||||||
|
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||||
|
|
||||||
if ($rememberMe) {
|
if ($rememberMe) {
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
@@ -194,14 +206,19 @@ if ($user !== false) {
|
|||||||
$persistentTokens[$token] = [
|
$persistentTokens[$token] = [
|
||||||
"username" => $username,
|
"username" => $username,
|
||||||
"expiry" => $expiry,
|
"expiry" => $expiry,
|
||||||
"isAdmin" => ($user['role'] === "1")
|
"isAdmin" => ($_SESSION["isAdmin"] === true)
|
||||||
];
|
];
|
||||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
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 {
|
} else {
|
||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
$failedAttempts[$ip]['count']++;
|
$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
|
<?php
|
||||||
// changePassword.php
|
// changePassword.php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Make sure the user is logged in.
|
|
||||||
session_start();
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
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]);
|
echo json_encode(["setup" => true]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check session authentication.
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
echo json_encode(["authenticated" => false]);
|
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;
|
$totp_enabled = false;
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
if ($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,
|
"authenticated" => true,
|
||||||
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false,
|
"isAdmin" => $isAdmin,
|
||||||
"totp_enabled" => $totp_enabled
|
"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.
|
* @param string $encryptionKey The encryption key.
|
||||||
* @return string Base64-encoded string containing IV and ciphertext.
|
* @return string Base64-encoded string containing IV and ciphertext.
|
||||||
*/
|
*/
|
||||||
function encryptData($data, $encryptionKey) {
|
function encryptData($data, $encryptionKey)
|
||||||
|
{
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
$ivlen = openssl_cipher_iv_length($cipher);
|
$ivlen = openssl_cipher_iv_length($cipher);
|
||||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||||
@@ -36,7 +37,8 @@ function encryptData($data, $encryptionKey) {
|
|||||||
* @param string $encryptionKey The encryption key.
|
* @param string $encryptionKey The encryption key.
|
||||||
* @return string|false The decrypted plaintext or false on failure.
|
* @return string|false The decrypted plaintext or false on failure.
|
||||||
*/
|
*/
|
||||||
function decryptData($encryptedData, $encryptionKey) {
|
function decryptData($encryptedData, $encryptionKey)
|
||||||
|
{
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
$data = base64_decode($encryptedData);
|
$data = base64_decode($encryptedData);
|
||||||
$ivlen = openssl_cipher_iv_length($cipher);
|
$ivlen = openssl_cipher_iv_length($cipher);
|
||||||
@@ -51,6 +53,40 @@ if (!$encryptionKey) {
|
|||||||
die('Encryption key for persistent tokens is not set.');
|
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.
|
// Determine whether HTTPS is used.
|
||||||
$envSecure = getenv('SECURE');
|
$envSecure = getenv('SECURE');
|
||||||
if ($envSecure !== false) {
|
if ($envSecure !== false) {
|
||||||
@@ -67,9 +103,12 @@ $cookieParams = [
|
|||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
];
|
];
|
||||||
session_set_cookie_params($cookieParams);
|
|
||||||
ini_set('session.gc_maxlifetime', 7200);
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_set_cookie_params($cookieParams);
|
||||||
|
ini_set('session.gc_maxlifetime', 7200);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($_SESSION['csrf_token'])) {
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
@@ -92,6 +131,8 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
|||||||
if ($tokenData['expiry'] >= time()) {
|
if ($tokenData['expiry'] >= time()) {
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $tokenData["username"];
|
$_SESSION["username"] = $tokenData["username"];
|
||||||
|
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
||||||
|
$_SESSION["folderOnly"] = loadFolderPermission($tokenData["username"]);
|
||||||
} else {
|
} else {
|
||||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$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";
|
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||||
}
|
}
|
||||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||||
?>
|
|
||||||
@@ -18,6 +18,17 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
exit;
|
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);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
if (
|
if (
|
||||||
!$data ||
|
!$data ||
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated
|
// Ensure user is authenticated
|
||||||
@@ -23,6 +23,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
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
|
// Get the JSON input and decode it
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
exit;
|
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 ---
|
// --- Setup Trash Folder & Metadata ---
|
||||||
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
if (!file_exists($trashDir)) {
|
if (!file_exists($trashDir)) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated
|
// Ensure user is authenticated
|
||||||
@@ -23,6 +23,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
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
|
// Get the JSON input and decode it
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
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.
|
// Read and decode the JSON input.
|
||||||
$rawData = file_get_contents("php://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.
|
// New function to load global tags from the server's persistent JSON.
|
||||||
export function loadGlobalTags() {
|
export function loadGlobalTags() {
|
||||||
fetch("metadata/createdTags.json", { credentials: "include" })
|
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 => {
|
.then(data => {
|
||||||
window.globalTags = data;
|
window.globalTags = data;
|
||||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||||
|
|||||||
298
folderManager.js
298
folderManager.js
@@ -3,9 +3,9 @@
|
|||||||
import { loadFileList } from './fileManager.js';
|
import { loadFileList } from './fileManager.js';
|
||||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.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).
|
// Formats a folder name for display (e.g. adding indentations).
|
||||||
export function formatFolderName(folder) {
|
export function formatFolderName(folder) {
|
||||||
@@ -26,7 +26,6 @@ export function formatFolderName(folder) {
|
|||||||
function buildFolderTree(folders) {
|
function buildFolderTree(folders) {
|
||||||
const tree = {};
|
const tree = {};
|
||||||
folders.forEach(folderPath => {
|
folders.forEach(folderPath => {
|
||||||
// Ensure folderPath is a string
|
|
||||||
if (typeof folderPath !== "string") return;
|
if (typeof folderPath !== "string") return;
|
||||||
const parts = folderPath.split('/');
|
const parts = folderPath.split('/');
|
||||||
let current = tree;
|
let current = tree;
|
||||||
@@ -40,9 +39,9 @@ function buildFolderTree(folders) {
|
|||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
/* ----------------------
|
||||||
// Folder Tree State (Save/Load)
|
Folder Tree State (Save/Load)
|
||||||
// ----------------------
|
----------------------*/
|
||||||
function loadFolderTreeState() {
|
function loadFolderTreeState() {
|
||||||
const state = localStorage.getItem("folderTreeState");
|
const state = localStorage.getItem("folderTreeState");
|
||||||
return state ? JSON.parse(state) : {};
|
return state ? JSON.parse(state) : {};
|
||||||
@@ -59,58 +58,41 @@ function getParentFolder(folder) {
|
|||||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
/* ----------------------
|
||||||
// Breadcrumb Functions
|
Breadcrumb Functions
|
||||||
// ----------------------
|
----------------------*/
|
||||||
// Render breadcrumb for a normalized folder path.
|
|
||||||
function renderBreadcrumb(normalizedFolder) {
|
function renderBreadcrumb(normalizedFolder) {
|
||||||
if (normalizedFolder === "root") {
|
if (!normalizedFolder || normalizedFolder === "") return "";
|
||||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
|
||||||
}
|
|
||||||
const parts = normalizedFolder.split("/");
|
const parts = normalizedFolder.split("/");
|
||||||
let breadcrumbItems = [];
|
let breadcrumbItems = [];
|
||||||
// Always start with "Root".
|
// Use the first segment as the root.
|
||||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="root">Root</span>`);
|
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${parts[0]}">${escapeHTML(parts[0])}</span>`);
|
||||||
let cumulative = "";
|
let cumulative = parts[0];
|
||||||
parts.forEach((part, index) => {
|
parts.slice(1).forEach(part => {
|
||||||
cumulative = index === 0 ? part : cumulative + "/" + part;
|
cumulative += "/" + part;
|
||||||
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
||||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
||||||
});
|
});
|
||||||
return breadcrumbItems.join('');
|
return breadcrumbItems.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind click and drag-and-drop events to breadcrumb links.
|
|
||||||
function bindBreadcrumbEvents() {
|
function bindBreadcrumbEvents() {
|
||||||
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
||||||
breadcrumbLinks.forEach(link => {
|
breadcrumbLinks.forEach(link => {
|
||||||
// Click event for navigation.
|
|
||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let folder = this.getAttribute("data-folder");
|
let folder = this.getAttribute("data-folder");
|
||||||
window.currentFolder = folder;
|
window.currentFolder = folder;
|
||||||
localStorage.setItem("lastOpenedFolder", folder);
|
localStorage.setItem("lastOpenedFolder", folder);
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
if (folder === "root") {
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
|
||||||
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);
|
expandTreePath(folder);
|
||||||
// Update folder tree selection.
|
|
||||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||||
if (targetOption) {
|
if (targetOption) targetOption.classList.add("selected");
|
||||||
targetOption.classList.add("selected");
|
|
||||||
}
|
|
||||||
// Load the file list.
|
|
||||||
loadFileList(folder);
|
loadFileList(folder);
|
||||||
// Re-bind breadcrumb events to ensure all links remain active.
|
|
||||||
bindBreadcrumbEvents();
|
bindBreadcrumbEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drag-and-drop events.
|
|
||||||
link.addEventListener("dragover", function (e) {
|
link.addEventListener("dragover", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.classList.add("drop-hover");
|
this.classList.add("drop-hover");
|
||||||
@@ -144,36 +126,73 @@ function bindBreadcrumbEvents() {
|
|||||||
destination: dropFolder
|
destination: dropFolder
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast(`File(s) moved successfully to ${dropFolder}!`);
|
showToast(`File(s) moved successfully to ${dropFolder}!`);
|
||||||
loadFileList(dragData.sourceFolder);
|
loadFileList(dragData.sourceFolder);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error moving files: " + (data.error || "Unknown error"));
|
showToast("Error moving files: " + (data.error || "Unknown error"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error moving files via drop on breadcrumb:", error);
|
console.error("Error moving files via drop on breadcrumb:", error);
|
||||||
showToast("Error moving files.");
|
showToast("Error moving files.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
/* ----------------------
|
||||||
// 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") {
|
function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
||||||
const state = loadFolderTreeState();
|
const state = loadFolderTreeState();
|
||||||
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
||||||
for (const folder in tree) {
|
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 fullPath = parentPath ? parentPath + "/" + folder : folder;
|
||||||
const hasChildren = Object.keys(tree[folder]).length > 0;
|
const hasChildren = Object.keys(tree[folder]).length > 0;
|
||||||
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
||||||
@@ -194,7 +213,6 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expands the folder tree along a given normalized path.
|
|
||||||
function expandTreePath(path) {
|
function expandTreePath(path) {
|
||||||
const parts = path.split("/");
|
const parts = path.split("/");
|
||||||
let cumulative = "";
|
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) {
|
function folderDragOverHandler(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.currentTarget.classList.add("drop-hover");
|
event.currentTarget.classList.add("drop-hover");
|
||||||
@@ -272,90 +290,110 @@ function folderDropHandler(event) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
/* ----------------------
|
||||||
// Main Folder Tree Rendering and Event Binding
|
Main Folder Tree Rendering and Event Binding
|
||||||
// ----------------------
|
----------------------*/
|
||||||
export async function loadFolderTree(selectedFolder) {
|
export async function loadFolderTree(selectedFolder) {
|
||||||
try {
|
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) {
|
if (response.status === 401) {
|
||||||
console.error("Unauthorized: Please log in to view folders.");
|
console.error("Unauthorized: Please log in to view folders.");
|
||||||
showToast("Session expired. Please log in again.");
|
showToast("Session expired. Please log in again.");
|
||||||
window.location.href = "logout.php";
|
window.location.href = "logout.php";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let folders = await response.json();
|
let folderData = await response.json();
|
||||||
|
console.log("Folder data received:", folderData);
|
||||||
// If returned items are objects (with a "folder" property), extract folder paths.
|
let folders = [];
|
||||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
if (Array.isArray(folderData) && folderData.length && typeof folderData[0] === "object" && folderData[0].folder) {
|
||||||
folders = folders.map(item => item.folder);
|
folders = folderData.map(item => item.folder);
|
||||||
|
} else if (Array.isArray(folderData)) {
|
||||||
|
folders = folderData;
|
||||||
}
|
}
|
||||||
// Filter out duplicate "root" entries if present.
|
|
||||||
folders = folders.filter(folder => folder !== "root");
|
// Remove any global "root" entry.
|
||||||
|
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||||
if (!Array.isArray(folders)) {
|
|
||||||
console.error("Folder list response is not an array:", folders);
|
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||||
return;
|
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");
|
const container = document.getElementById("folderTreeContainer");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error("Folder tree container not found.");
|
console.error("Folder tree container not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div id="rootRow" class="root-row">
|
let html = `<div id="rootRow" class="root-row">
|
||||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (folders.length > 0) {
|
if (folders.length > 0) {
|
||||||
const tree = buildFolderTree(folders);
|
const tree = buildFolderTree(folders);
|
||||||
html += renderFolderTree(tree, "", "block");
|
html += renderFolderTree(tree, "", "block");
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Attach drag-and-drop event listeners to folder nodes.
|
// Attach drag/drop event listeners.
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("dragover", folderDragOverHandler);
|
el.addEventListener("dragover", folderDragOverHandler);
|
||||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||||
el.addEventListener("drop", folderDropHandler);
|
el.addEventListener("drop", folderDropHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine current folder (normalized).
|
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
window.currentFolder = selectedFolder;
|
window.currentFolder = selectedFolder;
|
||||||
} else {
|
|
||||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
|
||||||
}
|
}
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
|
|
||||||
// Update file list title using breadcrumb.
|
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
if (window.currentFolder === "root") {
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
|
||||||
} else {
|
|
||||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
|
||||||
}
|
|
||||||
// Bind breadcrumb events (click and drag/drop).
|
|
||||||
bindBreadcrumbEvents();
|
bindBreadcrumbEvents();
|
||||||
|
|
||||||
// Load file list.
|
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
|
|
||||||
// Expand tree to current folder.
|
|
||||||
const folderState = loadFolderTreeState();
|
const folderState = loadFolderTreeState();
|
||||||
if (window.currentFolder !== "root" && folderState[window.currentFolder] !== "none") {
|
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||||
expandTreePath(window.currentFolder);
|
expandTreePath(window.currentFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight current folder in folder tree.
|
|
||||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||||
if (selectedEl) {
|
if (selectedEl) {
|
||||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||||
selectedEl.classList.add("selected");
|
selectedEl.classList.add("selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event binding for folder selection in folder tree.
|
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("click", function (e) {
|
el.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -365,18 +403,12 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
window.currentFolder = selected;
|
window.currentFolder = selected;
|
||||||
localStorage.setItem("lastOpenedFolder", selected);
|
localStorage.setItem("lastOpenedFolder", selected);
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
if (selected === "root") {
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
||||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
|
||||||
} else {
|
|
||||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
|
||||||
}
|
|
||||||
// Re-bind breadcrumb events so the new breadcrumb is clickable.
|
|
||||||
bindBreadcrumbEvents();
|
bindBreadcrumbEvents();
|
||||||
loadFileList(selected);
|
loadFileList(selected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event binding for toggling folders.
|
|
||||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||||
if (rootToggle) {
|
if (rootToggle) {
|
||||||
rootToggle.addEventListener("click", function (e) {
|
rootToggle.addEventListener("click", function (e) {
|
||||||
@@ -388,18 +420,18 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
nestedUl.classList.remove("collapsed");
|
nestedUl.classList.remove("collapsed");
|
||||||
nestedUl.classList.add("expanded");
|
nestedUl.classList.add("expanded");
|
||||||
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||||
state["root"] = "block";
|
state[effectiveRoot] = "block";
|
||||||
} else {
|
} else {
|
||||||
nestedUl.classList.remove("expanded");
|
nestedUl.classList.remove("expanded");
|
||||||
nestedUl.classList.add("collapsed");
|
nestedUl.classList.add("collapsed");
|
||||||
this.textContent = "[+]";
|
this.textContent = "[+]";
|
||||||
state["root"] = "none";
|
state[effectiveRoot] = "none";
|
||||||
}
|
}
|
||||||
saveFolderTreeState(state);
|
saveFolderTreeState(state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||||
toggle.addEventListener("click", function (e) {
|
toggle.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -422,7 +454,7 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading folder tree:", error);
|
console.error("Error loading folder tree:", error);
|
||||||
}
|
}
|
||||||
@@ -433,12 +465,10 @@ export function loadFolderList(selectedFolder) {
|
|||||||
loadFolderTree(selectedFolder);
|
loadFolderTree(selectedFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
/* ----------------------
|
||||||
// Folder Management (Rename, Delete, Create)
|
Folder Management (Rename, Delete, Create)
|
||||||
// ----------------------
|
----------------------*/
|
||||||
|
|
||||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||||
|
|
||||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||||
|
|
||||||
function openRenameFolderModal() {
|
function openRenameFolderModal() {
|
||||||
@@ -450,7 +480,6 @@ function openRenameFolderModal() {
|
|||||||
const parts = selectedFolder.split("/");
|
const parts = selectedFolder.split("/");
|
||||||
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
|
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
|
||||||
document.getElementById("renameFolderModal").style.display = "block";
|
document.getElementById("renameFolderModal").style.display = "block";
|
||||||
// Focus the input field after a short delay to ensure modal is visible.
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const input = document.getElementById("newRenameFolderName");
|
const input = document.getElementById("newRenameFolderName");
|
||||||
input.focus();
|
input.focus();
|
||||||
@@ -601,8 +630,6 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
|
// ---------- 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) {
|
function showFolderManagerContextMenu(x, y, menuItems) {
|
||||||
let menu = document.getElementById("folderManagerContextMenu");
|
let menu = document.getElementById("folderManagerContextMenu");
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
@@ -614,8 +641,6 @@ function showFolderManagerContextMenu(x, y, menuItems) {
|
|||||||
menu.style.zIndex = "9999";
|
menu.style.zIndex = "9999";
|
||||||
document.body.appendChild(menu);
|
document.body.appendChild(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set styles based on dark mode.
|
|
||||||
if (document.body.classList.contains("dark-mode")) {
|
if (document.body.classList.contains("dark-mode")) {
|
||||||
menu.style.backgroundColor = "#2c2c2c";
|
menu.style.backgroundColor = "#2c2c2c";
|
||||||
menu.style.border = "1px solid #555";
|
menu.style.border = "1px solid #555";
|
||||||
@@ -625,8 +650,6 @@ function showFolderManagerContextMenu(x, y, menuItems) {
|
|||||||
menu.style.border = "1px solid #ccc";
|
menu.style.border = "1px solid #ccc";
|
||||||
menu.style.color = "#000";
|
menu.style.color = "#000";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear previous items.
|
|
||||||
menu.innerHTML = "";
|
menu.innerHTML = "";
|
||||||
menuItems.forEach(item => {
|
menuItems.forEach(item => {
|
||||||
const menuItem = document.createElement("div");
|
const menuItem = document.createElement("div");
|
||||||
@@ -661,24 +684,16 @@ function hideFolderManagerContextMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context menu handler for folder tree and breadcrumb items.
|
|
||||||
function folderManagerContextMenuHandler(e) {
|
function folderManagerContextMenuHandler(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Get the closest folder element (either from the tree or breadcrumb).
|
|
||||||
const target = e.target.closest(".folder-option, .breadcrumb-link");
|
const target = e.target.closest(".folder-option, .breadcrumb-link");
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const folder = target.getAttribute("data-folder");
|
const folder = target.getAttribute("data-folder");
|
||||||
if (!folder) return;
|
if (!folder) return;
|
||||||
|
|
||||||
// Update current folder and highlight the selected element.
|
|
||||||
window.currentFolder = folder;
|
window.currentFolder = folder;
|
||||||
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
|
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
|
||||||
target.classList.add("selected");
|
target.classList.add("selected");
|
||||||
|
|
||||||
// Build context menu items.
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
label: "Create Folder",
|
label: "Create Folder",
|
||||||
@@ -696,20 +711,15 @@ function folderManagerContextMenuHandler(e) {
|
|||||||
action: () => { openDeleteFolderModal(); }
|
action: () => { openDeleteFolderModal(); }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind contextmenu events to folder tree and breadcrumb elements.
|
|
||||||
function bindFolderManagerContextMenu() {
|
function bindFolderManagerContextMenu() {
|
||||||
// Bind context menu to folder tree container.
|
|
||||||
const container = document.getElementById("folderTreeContainer");
|
const container = document.getElementById("folderTreeContainer");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||||
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind context menu to breadcrumb links.
|
|
||||||
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
|
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
|
||||||
breadcrumbNodes.forEach(node => {
|
breadcrumbNodes.forEach(node => {
|
||||||
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||||
@@ -717,31 +727,23 @@ function bindFolderManagerContextMenu() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide context menu when clicking elsewhere.
|
|
||||||
document.addEventListener("click", function () {
|
document.addEventListener("click", function () {
|
||||||
hideFolderManagerContextMenu();
|
hideFolderManagerContextMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.addEventListener("keydown", function (e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
|
||||||
const tag = e.target.tagName.toLowerCase();
|
const tag = e.target.tagName.toLowerCase();
|
||||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
|
|
||||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.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") {
|
if (window.currentFolder && window.currentFolder !== "root") {
|
||||||
// Prevent default (avoid navigating back on macOS).
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Call your existing folder delete function.
|
|
||||||
openDeleteFolderModal();
|
openDeleteFolderModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call this binding function after rendering the folder tree and breadcrumbs.
|
|
||||||
bindFolderManagerContextMenu();
|
bindFolderManagerContextMenu();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$configFile = USERS_DIR . 'adminConfig.json';
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated
|
// 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());
|
||||||
|
?>
|
||||||
15
getUsers.php
15
getUsers.php
@@ -1,24 +1,31 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$users = [];
|
$users = [];
|
||||||
|
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if (count($parts) >= 3) {
|
if (count($parts) >= 3) {
|
||||||
// Optionally, validate username format:
|
// Validate username format:
|
||||||
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
|
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);
|
echo json_encode($users);
|
||||||
?>
|
?>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
session_start();
|
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
|
||||||
|
|
||||||
// Reuse the same authentication function
|
// Reuse the same authentication function
|
||||||
function authenticate($username, $password)
|
function authenticate($username, $password)
|
||||||
{
|
{
|
||||||
global $usersFile;
|
global $usersFile;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
|
error_log("authenticate(): users file not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
@@ -18,9 +18,50 @@ function authenticate($username, $password)
|
|||||||
return $storedRole; // Return the user's role
|
return $storedRole; // Return the user's role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
error_log("authenticate(): authentication failed for '$username'");
|
||||||
return false;
|
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.
|
// Check if the user has sent HTTP Basic auth credentials.
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||||
@@ -40,15 +81,18 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt authentication
|
// Attempt authentication
|
||||||
$userRole = authenticate($username, $password);
|
$roleFromAuth = authenticate($username, $password);
|
||||||
if ($userRole !== false) {
|
if ($roleFromAuth !== false) {
|
||||||
// Successful login
|
// Use getUserRole() to determine the user's role from the file
|
||||||
|
$actualRole = getUserRole($username);
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_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");
|
header("Location: index.html");
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
// Retrieve headers and check CSRF token.
|
// Retrieve headers and check CSRF token.
|
||||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
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);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
@@ -72,5 +72,17 @@ if (!$userFound) {
|
|||||||
|
|
||||||
// Write the updated list back to users.txt
|
// Write the updated list back to users.txt
|
||||||
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
|
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"]);
|
echo json_encode(["success" => "User removed successfully"]);
|
||||||
?>
|
?>
|
||||||
@@ -21,6 +21,16 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
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);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) {
|
if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
@@ -27,6 +27,16 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
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
|
// Get the JSON input and decode it
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$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);
|
http_response_code(401);
|
||||||
exit;
|
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);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
|
|
||||||
|
|||||||
@@ -1053,7 +1053,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
|||||||
|
|
||||||
#customToast {
|
#customToast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: #333;
|
background: #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -1068,7 +1068,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#customToast.show {
|
#customToast.show {
|
||||||
opacity: 1;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrap {
|
.button-wrap {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"csrf_token" => $_SESSION['csrf_token'],
|
"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
|
// totp_setup.php
|
||||||
|
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
use Endroid\QrCode\Builder\Builder;
|
use Endroid\QrCode\Builder\Builder;
|
||||||
use Endroid\QrCode\Writer\PngWriter;
|
use Endroid\QrCode\Writer\PngWriter;
|
||||||
@@ -12,10 +12,6 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
|||||||
// ini_set('display_errors', 1);
|
// ini_set('display_errors', 1);
|
||||||
// error_reporting(E_ALL);
|
// 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) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
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
|
<?php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Verify that the user is authenticated and is an admin.
|
// Verify that the user is authenticated and is an admin.
|
||||||
@@ -76,10 +76,24 @@ $configFile = USERS_DIR . 'adminConfig.json';
|
|||||||
// Convert and encrypt configuration.
|
// Convert and encrypt configuration.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
$encryptedContent = encryptData($plainTextConfig, $encryptionKey);
|
$encryptedContent = encryptData($plainTextConfig, $encryptionKey);
|
||||||
|
|
||||||
|
// Attempt to write the new configuration.
|
||||||
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
||||||
http_response_code(500);
|
// Log the error.
|
||||||
echo json_encode(['error' => 'Failed to update configuration.']);
|
error_log("updateConfig.php: Initial write failed, attempting to delete the old configuration file.");
|
||||||
exit;
|
|
||||||
|
// 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 even after cleanup.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => 'Configuration updated successfully.']);
|
echo json_encode(['success' => 'Configuration updated successfully.']);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
// updateUserPanel.php
|
// updateUserPanel.php
|
||||||
require 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
// Ensure the user is authenticated.
|
// Ensure the user is authenticated.
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(403);
|
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);
|
http_response_code(401);
|
||||||
exit;
|
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.
|
* Handle test chunk requests.
|
||||||
|
|||||||
Reference in New Issue
Block a user