Admin Panel Refactor & Enhancements

This commit is contained in:
Ryan
2025-05-04 00:23:46 -04:00
committed by GitHub
parent 4d329e046f
commit 56f34ba362
3 changed files with 305 additions and 248 deletions

View File

@@ -1,5 +1,54 @@
# Changelog # Changelog
## Changes 5/3/2025 v1.3.0
**Admin Panel Refactor & Enhancements**
### Moved from `authModals.js` to `adminPanel.js`
- Extracted all admin-related UI and logic out of `authModals.js`
- Created a standalone `adminPanel.js` module
- Initialized `openAdminPanel()` and `closeAdminPanel()` exports
### Responsive, Collapsible Sections
- Injected new CSS via JS (`adminPanelStyles`)
- Default modal width: 50%
- Small-screen override (`@media (max-width: 600px)`) to 90% width
- Introduced `.section-header` / `.section-content` pattern
- Click header to expand/collapse its content
- Animated arrow via Material Icons
- Indented and padded expanded content
### “Manage Shared Links” Feature
- Added new **Manage Shared Links** section to Admin Panel
- Endpoint **GET** `/api/admin/readMetadata.php?file=…`
- Reads `share_folder_links.json` & `share_links.json` under `META_DIR`
- Endpoint **POST**
- `/api/folder/deleteShareFolderLink.php`
- `/api/file/deleteShareLink.php`
- `loadShareLinksSection()` AJAX loader
- Displays folder & file shares, expiry dates, upload-allowed, and 🔒 if password-protected
- “🗑️” delete buttons refresh the list on success
### Dark-Mode & Theming Fixes
- Dark-mode CSS overrides for:
- Modal border
- `.btn-primary`, `.btn-secondary`
- `.form-control` backgrounds & placeholders
- Section headers & icons
- Close button restyled to use shared **.editor-close-btn** look
### API and Controller changes
- Updated all endpoints to use correct controller casing
- Renamed controller files to PascalCase (e.g. `adminController.php` to `AdminController.php`, `fileController.php` to `FileController.php`, `folderController.php` to `FolderController.php`)
- Adjusted endpoint paths to match controller filenames
---
## Changes 4/30/2025 v1.2.8 ## Changes 4/30/2025 v1.2.8
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions. - **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.

View File

@@ -7,11 +7,11 @@ const version = "v1.3.0";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
// ————— Inject updated styles ————— // ————— Inject updated styles —————
(function(){ (function () {
if (document.getElementById('adminPanelStyles')) return; if (document.getElementById('adminPanelStyles')) return;
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'adminPanelStyles'; style.id = 'adminPanelStyles';
style.textContent = ` style.textContent = `
/* Modal sizing */ /* Modal sizing */
#adminPanelModal .modal-content { #adminPanelModal .modal-content {
max-width: 1100px; max-width: 1100px;
@@ -112,24 +112,24 @@ const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;
margin-top:15px; margin-top:15px;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
})(); })();
// ———————————————————————————————————— // ————————————————————————————————————
let originalAdminConfig = {}; let originalAdminConfig = {};
function captureInitialAdminConfig() { function captureInitialAdminConfig() {
originalAdminConfig = { originalAdminConfig = {
headerTitle: document.getElementById("headerTitle").value.trim(), headerTitle: document.getElementById("headerTitle").value.trim(),
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(), oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
oidcClientId: document.getElementById("oidcClientId").value.trim(), oidcClientId: document.getElementById("oidcClientId").value.trim(),
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(), oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(), oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
disableFormLogin: document.getElementById("disableFormLogin").checked, disableFormLogin: document.getElementById("disableFormLogin").checked,
disableBasicAuth: document.getElementById("disableBasicAuth").checked, disableBasicAuth: document.getElementById("disableBasicAuth").checked,
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked, disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
enableWebDAV: document.getElementById("enableWebDAV").checked, enableWebDAV: document.getElementById("enableWebDAV").checked,
sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(), sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(),
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim() globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
}; };
} }
function hasUnsavedChanges() { function hasUnsavedChanges() {
@@ -143,18 +143,18 @@ function hasUnsavedChanges() {
document.getElementById("disableFormLogin").checked !== o.disableFormLogin || document.getElementById("disableFormLogin").checked !== o.disableFormLogin ||
document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth || document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth ||
document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin || document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin ||
document.getElementById("enableWebDAV").checked !== o.enableWebDAV || document.getElementById("enableWebDAV").checked !== o.enableWebDAV ||
document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize || document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize ||
document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl
); );
} }
function showCustomConfirmModal(message) { function showCustomConfirmModal(message) {
return new Promise(resolve => { return new Promise(resolve => {
const modal = document.getElementById("customConfirmModal"); const modal = document.getElementById("customConfirmModal");
const msg = document.getElementById("confirmMessage"); const msg = document.getElementById("confirmMessage");
const yes = document.getElementById("confirmYesBtn"); const yes = document.getElementById("confirmYesBtn");
const no = document.getElementById("confirmNoBtn"); const no = document.getElementById("confirmNoBtn");
msg.textContent = message; msg.textContent = message;
modal.style.display = "block"; modal.style.display = "block";
function clean() { function clean() {
@@ -163,7 +163,7 @@ function showCustomConfirmModal(message) {
no.removeEventListener("click", onNo); no.removeEventListener("click", onNo);
} }
function onYes() { clean(); resolve(true); } function onYes() { clean(); resolve(true); }
function onNo() { clean(); resolve(false); } function onNo() { clean(); resolve(false); }
yes.addEventListener("click", onYes); yes.addEventListener("click", onYes);
no.addEventListener("click", onNo); no.addEventListener("click", onNo);
}); });
@@ -181,51 +181,57 @@ function toggleSection(id) {
} }
function loadShareLinksSection() { function loadShareLinksSection() {
const container = document.getElementById("shareLinksContent"); const container = document.getElementById("shareLinksContent");
container.textContent = t("loading") + "..."; container.textContent = t("loading") + "...";
Promise.all([ // Helper to fetch a metadata file or return {} on any error
fetch("/api/admin/readMetadata.php?file=share_folder_links.json", { credentials: "include" }) const fetchMeta = file =>
.then(r => r.ok ? r.json() : Promise.reject(`Folder fetch ${r.status}`)), fetch(`/api/admin/readMetadata.php?file=${file}`, { credentials: "include" })
fetch("/api/admin/readMetadata.php?file=share_links.json", { credentials: "include" }) .then(r => r.ok ? r.json() : {}) // non-2xx → treat as empty
.then(r => r.ok ? r.json() : Promise.reject(`File fetch ${r.status}`)) .catch(() => ({}));
])
Promise.all([
fetchMeta("share_folder_links.json"),
fetchMeta("share_links.json")
])
.then(([folders, files]) => { .then(([folders, files]) => {
// If nothing at all, show a friendly message
if (Object.keys(folders).length === 0 && Object.keys(files).length === 0) {
container.textContent = t("no_shared_links_available");
return;
}
let html = `<h5>${t("folder_shares")}</h5><ul>`; let html = `<h5>${t("folder_shares")}</h5><ul>`;
Object.entries(folders).forEach(([token, o]) => { Object.entries(folders).forEach(([token, o]) => {
const lock = o.password ? `🔒 ` : ""; const lock = o.password ? `🔒 ` : "";
html += ` html += `
<li> <li>
${lock} ${lock}<strong>${o.folder}</strong>
<strong>${o.folder}</strong> <small>(${new Date(o.expires * 1000).toLocaleString()})</small>
<small>(${new Date(o.expires * 1000).toLocaleString()})</small> <button type="button"
<button type="button" data-key="${token}"
data-key="${token}" data-type="folder"
data-type="folder" class="btn btn-sm btn-link delete-share">🗑️</button>
class="btn btn-sm btn-link delete-share">🗑️</button> </li>`;
</li>`;
}); });
html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`; html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`;
Object.entries(files).forEach(([token, o]) => { Object.entries(files).forEach(([token, o]) => {
const lock = o.password ? `🔒 ` : ""; const lock = o.password ? `🔒 ` : "";
html += ` html += `
<li> <li>
${lock} ${lock}<strong>${o.folder}/${o.file}</strong>
<strong>${o.folder}/${o.file}</strong> <small>(${new Date(o.expires * 1000).toLocaleString()})</small>
<small>(${new Date(o.expires * 1000).toLocaleString()})</small> <button type="button"
<button type="button" data-key="${token}"
data-key="${token}" data-type="file"
data-type="file" class="btn btn-sm btn-link delete-share">🗑️</button>
class="btn btn-sm btn-link delete-share">🗑️</button> </li>`;
</li>`;
}); });
html += `</ul>`; html += `</ul>`;
container.innerHTML = html; container.innerHTML = html;
// wire up delete buttons // wire up delete buttons
container.querySelectorAll(".delete-share").forEach(btn => { container.querySelectorAll(".delete-share").forEach(btn => {
btn.addEventListener("click", evt => { btn.addEventListener("click", evt => {
@@ -235,29 +241,29 @@ function loadShareLinksSection() {
const endpoint = isFolder const endpoint = isFolder
? "/api/folder/deleteShareFolderLink.php" ? "/api/folder/deleteShareFolderLink.php"
: "/api/file/deleteShareLink.php"; : "/api/file/deleteShareLink.php";
fetch(endpoint, { fetch(endpoint, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ token }) body: new URLSearchParams({ token })
}) })
.then(res => { .then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); return res.json();
}) })
.then(json => { .then(json => {
if (json.success) { if (json.success) {
showToast(t("share_deleted_successfully")); showToast(t("share_deleted_successfully"));
loadShareLinksSection(); loadShareLinksSection();
} else { } else {
showToast(t("error_deleting_share") + ": " + (json.error||""), "error"); showToast(t("error_deleting_share") + ": " + (json.error || ""), "error");
} }
}) })
.catch(err => { .catch(err => {
console.error("Delete error:", err); console.error("Delete error:", err);
showToast(t("error_deleting_share"), "error"); showToast(t("error_deleting_share"), "error");
}); });
}); });
}); });
}) })
@@ -265,36 +271,36 @@ function loadShareLinksSection() {
console.error("loadShareLinksSection error:", err); console.error("loadShareLinksSection error:", err);
container.textContent = t("error_loading_share_links"); container.textContent = t("error_loading_share_links");
}); });
} }
export function openAdminPanel() { export function openAdminPanel() {
fetch("/api/admin/getConfig.php",{credentials:"include"}) fetch("/api/admin/getConfig.php", { credentials: "include" })
.then(r=>r.json()) .then(r => r.json())
.then(config=>{ .then(config => {
// apply header title + globals // apply header title + globals
if(config.header_title){ if (config.header_title) {
document.querySelector(".header-title h1").textContent = config.header_title; document.querySelector(".header-title h1").textContent = config.header_title;
window.headerTitle = config.header_title; window.headerTitle = config.header_title;
} }
if(config.oidc) Object.assign(window.currentOIDCConfig,config.oidc); if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
if(config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
const dark = document.body.classList.contains("dark-mode"); const dark = document.body.classList.contains("dark-mode");
const bg = dark?"rgba(0,0,0,0.7)":"rgba(0,0,0,0.3)"; const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
const inner= ` const inner = `
background:${dark?"#2c2c2c":"#fff"}; background:${dark ? "#2c2c2c" : "#fff"};
color:${dark?"#e0e0e0":"#000"}; color:${dark ? "#e0e0e0" : "#000"};
padding:20px; max-width:1100px; width:50%; padding:20px; max-width:1100px; width:50%;
border-radius:8px; position:relative; border-radius:8px; position:relative;
max-height:90vh; overflow:auto; max-height:90vh; overflow:auto;
border:1px solid ${dark?"#555":"#ccc"}; border:1px solid ${dark ? "#555" : "#ccc"};
`; `;
let mdl = document.getElementById("adminPanelModal"); let mdl = document.getElementById("adminPanelModal");
if(!mdl){ if (!mdl) {
mdl = document.createElement("div"); mdl = document.createElement("div");
mdl.id="adminPanelModal"; mdl.id = "adminPanelModal";
mdl.style.cssText = ` mdl.style.cssText = `
position:fixed; top:0; left:0; position:fixed; top:0; left:0;
width:100vw; height:100vh; width:100vw; height:100vh;
@@ -310,14 +316,14 @@ export function openAdminPanel() {
<!-- each section: header + content --> <!-- each section: header + content -->
${[ ${[
{ id:"userManagement", label:t("user_management") }, { id: "userManagement", label: t("user_management") },
{ id:"headerSettings", label:t("header_settings") }, { id: "headerSettings", label: t("header_settings") },
{ id:"loginOptions", label:t("login_options") }, { id: "loginOptions", label: t("login_options") },
{ id:"webdav", label:"WebDAV Access" }, { id: "webdav", label: "WebDAV Access" },
{ id:"upload", label:t("shared_max_upload_size_bytes") }, { id: "upload", label: t("shared_max_upload_size_bytes_title") },
{ id:"oidc", label:t("oidc_configuration") + " & TOTP" }, { id: "oidc", label: t("oidc_configuration") + " & TOTP" },
{ id:"shareLinks", label:t("manage_shared_links") } { id: "shareLinks", label: t("manage_shared_links") }
].map(sec=>` ].map(sec => `
<div id="${sec.id}Header" class="section-header collapsed"> <div id="${sec.id}Header" class="section-header collapsed">
${sec.label} <i class="material-icons">expand_more</i> ${sec.label} <i class="material-icons">expand_more</i>
</div> </div>
@@ -340,10 +346,10 @@ export function openAdminPanel() {
.addEventListener("click", closeAdminPanel); .addEventListener("click", closeAdminPanel);
// Section toggles // Section toggles
["userManagement","headerSettings","loginOptions","webdav","upload","oidc","shareLinks"] ["userManagement", "headerSettings", "loginOptions", "webdav", "upload", "oidc", "shareLinks"]
.forEach(id=>{ .forEach(id => {
document.getElementById(id+"Header") document.getElementById(id + "Header")
.addEventListener("click", ()=>toggleSection(id)); .addEventListener("click", () => toggleSection(id));
}); });
// Populate each sections CONTENT: // Populate each sections CONTENT:
@@ -354,14 +360,14 @@ export function openAdminPanel() {
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button> <button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
`; `;
document.getElementById("adminOpenAddUser") document.getElementById("adminOpenAddUser")
.addEventListener("click",()=>{ .addEventListener("click", () => {
toggleVisibility("addUserModal",true); toggleVisibility("addUserModal", true);
document.getElementById("newUsername").focus(); document.getElementById("newUsername").focus();
}); });
document.getElementById("adminOpenRemoveUser") document.getElementById("adminOpenRemoveUser")
.addEventListener("click",()=>{ .addEventListener("click", () => {
if(typeof window.loadUserList==="function") window.loadUserList(); if (typeof window.loadUserList === "function") window.loadUserList();
toggleVisibility("removeUserModal",true); toggleVisibility("removeUserModal", true);
}); });
document.getElementById("adminOpenUserPermissions") document.getElementById("adminOpenUserPermissions")
.addEventListener("click", openUserPermissionsModal); .addEventListener("click", openUserPermissionsModal);
@@ -401,21 +407,21 @@ export function openAdminPanel() {
<div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div> <div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div>
<div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div> <div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div>
<div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div> <div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div>
<div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl||'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div> <div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div>
`; `;
// — Share Links — // — Share Links —
document.getElementById("shareLinksContent").textContent = t("loading")+"…"; document.getElementById("shareLinksContent").textContent = t("loading") + "…";
// — Save handler & constraints — // — Save handler & constraints —
document.getElementById("saveAdminSettings") document.getElementById("saveAdminSettings")
.addEventListener("click", handleSave); .addEventListener("click", handleSave);
["disableFormLogin","disableBasicAuth","disableOIDCLogin"].forEach(id=>{ ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
document.getElementById(id) document.getElementById(id)
.addEventListener("change", e=>{ .addEventListener("change", e => {
const chk = ["disableFormLogin","disableBasicAuth","disableOIDCLogin"] const chk = ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"]
.filter(i=>document.getElementById(i).checked).length; .filter(i => document.getElementById(i).checked).length;
if(chk===3){ if (chk === 3) {
showToast(t("at_least_one_login_method")); showToast(t("at_least_one_login_method"));
e.target.checked = false; e.target.checked = false;
} }
@@ -423,85 +429,85 @@ export function openAdminPanel() {
}); });
// Initialize inputs from config + capture // Initialize inputs from config + capture
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin===true; document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth===true; document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin===true; document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
document.getElementById("enableWebDAV").checked = config.enableWebDAV===true; document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize||""; document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
captureInitialAdminConfig(); captureInitialAdminConfig();
} else { } else {
// modal already exists → just refresh values & re-show // modal already exists → just refresh values & re-show
mdl.style.display = "flex"; mdl.style.display = "flex";
// update dark/light as above... // update dark/light as above...
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin===true; document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth===true; document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin===true; document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
document.getElementById("enableWebDAV").checked = config.enableWebDAV===true; document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize||""; document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl; document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId; document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret; document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri; document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl||''; document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || '';
captureInitialAdminConfig(); captureInitialAdminConfig();
} }
}) })
.catch(()=>{/* if even fetching fails, open empty panel */}); .catch(() => {/* if even fetching fails, open empty panel */ });
} }
function handleSave(){ function handleSave() {
const dFL = document.getElementById("disableFormLogin").checked; const dFL = document.getElementById("disableFormLogin").checked;
const dBA = document.getElementById("disableBasicAuth").checked; const dBA = document.getElementById("disableBasicAuth").checked;
const dOIDC = document.getElementById("disableOIDCLogin").checked; const dOIDC = document.getElementById("disableOIDCLogin").checked;
const eWD = document.getElementById("enableWebDAV").checked; const eWD = document.getElementById("enableWebDAV").checked;
const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value,10)||0; const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value, 10) || 0;
const nHT = document.getElementById("headerTitle").value.trim(); const nHT = document.getElementById("headerTitle").value.trim();
const nOIDC = { const nOIDC = {
providerUrl : document.getElementById("oidcProviderUrl").value.trim(), providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
clientId : document.getElementById("oidcClientId").value.trim(), clientId: document.getElementById("oidcClientId").value.trim(),
clientSecret: document.getElementById("oidcClientSecret").value.trim(), clientSecret: document.getElementById("oidcClientSecret").value.trim(),
redirectUri : document.getElementById("oidcRedirectUri").value.trim() redirectUri: document.getElementById("oidcRedirectUri").value.trim()
}; };
const gURL = document.getElementById("globalOtpauthUrl").value.trim(); const gURL = document.getElementById("globalOtpauthUrl").value.trim();
if([dFL,dBA,dOIDC].filter(x=>x).length===3){ if ([dFL, dBA, dOIDC].filter(x => x).length === 3) {
showToast(t("at_least_one_login_method")); showToast(t("at_least_one_login_method"));
return; return;
} }
sendRequest("/api/admin/updateConfig.php","POST",{ sendRequest("/api/admin/updateConfig.php", "POST", {
header_title:nHT, oidc:nOIDC, header_title: nHT, oidc: nOIDC,
disableFormLogin:dFL, disableBasicAuth:dBA, disableOIDCLogin:dOIDC, disableFormLogin: dFL, disableBasicAuth: dBA, disableOIDCLogin: dOIDC,
enableWebDAV:eWD, sharedMaxUploadSize:sMax, globalOtpauthUrl:gURL enableWebDAV: eWD, sharedMaxUploadSize: sMax, globalOtpauthUrl: gURL
},{ }, {
"X-CSRF-Token":window.csrfToken "X-CSRF-Token": window.csrfToken
}).then(res=>{ }).then(res => {
if(res.success){ if (res.success) {
showToast(t("settings_updated_successfully"),"success"); showToast(t("settings_updated_successfully"), "success");
captureInitialAdminConfig(); captureInitialAdminConfig();
closeAdminPanel(); closeAdminPanel();
loadAdminConfigFunc(); loadAdminConfigFunc();
} else { } else {
showToast(t("error_updating_settings")+": "+(res.error||t("unknown_error")),"error"); showToast(t("error_updating_settings") + ": " + (res.error || t("unknown_error")), "error");
} }
}).catch(()=>{/*noop*/}); }).catch(() => {/*noop*/ });
} }
export async function closeAdminPanel() { export async function closeAdminPanel() {
if(hasUnsavedChanges()){ if (hasUnsavedChanges()) {
const ok = await showCustomConfirmModal(t("unsaved_changes_confirm")); const ok = await showCustomConfirmModal(t("unsaved_changes_confirm"));
if(!ok) return; if (!ok) return;
} }
document.getElementById("adminPanelModal").style.display="none"; document.getElementById("adminPanelModal").style.display = "none";
} }
// --- New: User Permissions Modal --- // --- New: User Permissions Modal ---
export function openUserPermissionsModal() { export function openUserPermissionsModal() {
let userPermissionsModal = document.getElementById("userPermissionsModal"); let userPermissionsModal = document.getElementById("userPermissionsModal");
const isDarkMode = document.body.classList.contains("dark-mode"); const isDarkMode = document.body.classList.contains("dark-mode");
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
const modalContentStyles = ` const modalContentStyles = `
background: ${isDarkMode ? "#2c2c2c" : "#fff"}; background: ${isDarkMode ? "#2c2c2c" : "#fff"};
color: ${isDarkMode ? "#e0e0e0" : "#000"}; color: ${isDarkMode ? "#e0e0e0" : "#000"};
padding: 20px; padding: 20px;
@@ -510,11 +516,11 @@ export function openUserPermissionsModal() {
border-radius: 8px; border-radius: 8px;
position: relative; position: relative;
`; `;
if (!userPermissionsModal) { if (!userPermissionsModal) {
userPermissionsModal = document.createElement("div"); userPermissionsModal = document.createElement("div");
userPermissionsModal.id = "userPermissionsModal"; userPermissionsModal.id = "userPermissionsModal";
userPermissionsModal.style.cssText = ` userPermissionsModal.style.cssText = `
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -526,7 +532,7 @@ export function openUserPermissionsModal() {
align-items: center; align-items: center;
z-index: 3500; z-index: 3500;
`; `;
userPermissionsModal.innerHTML = ` userPermissionsModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}"> <div class="modal-content" style="${modalContentStyles}">
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span> <span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>${t("user_permissions")}</h3> <h3>${t("user_permissions")}</h3>
@@ -539,92 +545,92 @@ export function openUserPermissionsModal() {
</div> </div>
</div> </div>
`; `;
document.body.appendChild(userPermissionsModal); document.body.appendChild(userPermissionsModal);
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => { document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
userPermissionsModal.style.display = "none"; userPermissionsModal.style.display = "none";
}); });
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => { document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
userPermissionsModal.style.display = "none"; userPermissionsModal.style.display = "none";
}); });
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => { document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
// Collect permissions data from each user row. // Collect permissions data from each user row.
const rows = userPermissionsModal.querySelectorAll(".user-permission-row"); const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
const permissionsData = []; const permissionsData = [];
rows.forEach(row => { rows.forEach(row => {
const username = row.getAttribute("data-username"); const username = row.getAttribute("data-username");
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']"); const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']"); const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']"); const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
permissionsData.push({ permissionsData.push({
username, username,
folderOnly: folderOnlyCheckbox.checked, folderOnly: folderOnlyCheckbox.checked,
readOnly: readOnlyCheckbox.checked, readOnly: readOnlyCheckbox.checked,
disableUpload: disableUploadCheckbox.checked disableUpload: disableUploadCheckbox.checked
});
}); });
// Send the permissionsData to the server.
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast(t("user_permissions_updated_successfully"));
userPermissionsModal.style.display = "none";
} else {
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => {
showToast(t("error_updating_permissions"));
});
}); });
} else { // Send the permissionsData to the server.
userPermissionsModal.style.display = "flex"; sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
} .then(response => {
// Load the list of users into the modal. if (response.success) {
loadUserPermissionsList(); showToast(t("user_permissions_updated_successfully"));
userPermissionsModal.style.display = "none";
} else {
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => {
showToast(t("error_updating_permissions"));
});
});
} else {
userPermissionsModal.style.display = "flex";
} }
// Load the list of users into the modal.
function loadUserPermissionsList() { loadUserPermissionsList();
const listContainer = document.getElementById("userPermissionsList"); }
if (!listContainer) return;
listContainer.innerHTML = ""; function loadUserPermissionsList() {
const listContainer = document.getElementById("userPermissionsList");
// First, fetch the current permissions from the server. if (!listContainer) return;
fetch("/api/getUserPermissions.php", { credentials: "include" }) listContainer.innerHTML = "";
.then(response => response.json())
.then(permissionsData => { // First, fetch the current permissions from the server.
// Then, fetch the list of users. fetch("/api/getUserPermissions.php", { credentials: "include" })
return fetch("/api/getUsers.php", { credentials: "include" }) .then(response => response.json())
.then(response => response.json()) .then(permissionsData => {
.then(usersData => { // Then, fetch the list of users.
const users = Array.isArray(usersData) ? usersData : (usersData.users || []); return fetch("/api/getUsers.php", { credentials: "include" })
if (users.length === 0) { .then(response => response.json())
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>"; .then(usersData => {
return; const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
} if (users.length === 0) {
users.forEach(user => { listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
// Skip admin users. return;
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return; }
users.forEach(user => {
// Use stored permissions if available; otherwise fall back to defaults. // Skip admin users.
const defaultPerm = { if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
folderOnly: false,
readOnly: false, // Use stored permissions if available; otherwise fall back to defaults.
disableUpload: false, const defaultPerm = {
}; folderOnly: false,
readOnly: false,
// Normalize the username key to match server storage (e.g., lowercase) disableUpload: false,
const usernameKey = user.username.toLowerCase(); };
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData)) // Normalize the username key to match server storage (e.g., lowercase)
? permissionsData[usernameKey] const usernameKey = user.username.toLowerCase();
: defaultPerm;
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
// Create a row for the user. ? permissionsData[usernameKey]
const row = document.createElement("div"); : defaultPerm;
row.classList.add("user-permission-row");
row.setAttribute("data-username", user.username); // Create a row for the user.
row.style.padding = "10px 0"; const row = document.createElement("div");
row.innerHTML = ` 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="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
<div style="display: flex; flex-direction: column; gap: 5px;"> <div style="display: flex; flex-direction: column; gap: 5px;">
<label style="display: flex; align-items: center; gap: 5px;"> <label style="display: flex; align-items: center; gap: 5px;">
@@ -642,11 +648,11 @@ export function openUserPermissionsModal() {
</div> </div>
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;"> <hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
`; `;
listContainer.appendChild(row); listContainer.appendChild(row);
});
}); });
}) });
.catch(() => { })
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>"; .catch(() => {
}); listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
} });
}

View File

@@ -184,6 +184,7 @@ const translations = {
// Admin Panel // Admin Panel
"header_settings": "Header Settings", "header_settings": "Header Settings",
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)", "shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads", "max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
"manage_shared_links": "Manage Shared Links", "manage_shared_links": "Manage Shared Links",
@@ -194,6 +195,7 @@ const translations = {
"share_deleted_successfully": "Share deleted successfully", "share_deleted_successfully": "Share deleted successfully",
"error_deleting_share": "Error deleting share", "error_deleting_share": "Error deleting share",
"password_protected": "Password protected", "password_protected": "Password protected",
"no_shared_links_available": "No shared links available",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS: // NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS: