import { t } from './i18n.js'; import { loadAdminConfigFunc } from './auth.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; const version = "v1.3.12"; const adminTitle = `${t("admin_panel")} ${version}`; // ————— Inject updated styles ————— (function () { if (document.getElementById('adminPanelStyles')) return; const style = document.createElement('style'); style.id = 'adminPanelStyles'; style.textContent = ` /* Modal sizing */ #adminPanelModal .modal-content { max-width: 1100px; width: 50%; } /* Small phones: 90% width */ @media (max-width: 900px) { #adminPanelModal .modal-content { width: 90% !important; max-width: none !important; } } /* Dark-mode fixes */ body.dark-mode #adminPanelModal .modal-content { border-color: #555 !important; } /* enforce light‐mode styling */ #adminPanelModal .modal-content { max-width: 1100px; width: 50%; background: #fff !important; color: #000 !important; border: 1px solid #ccc !important; } /* enforce dark‐mode styling */ body.dark-mode #adminPanelModal .modal-content { background: #2c2c2c !important; color: #e0e0e0 !important; border-color: #555 !important; } /* form controls in dark */ body.dark-mode .form-control { background-color: #333; border-color: #555; color: #eee; } body.dark-mode .form-control::placeholder { color: #888; } /* Section headers */ .section-header { background: #f5f5f5; padding: 10px 15px; cursor: pointer; border-radius: 4px; font-weight: bold; display: flex; align-items: center; justify-content: space-between; margin-top: 16px; } .section-header:first-of-type { margin-top: 0; } .section-header.collapsed .material-icons { transform: rotate(-90deg); } .section-header .material-icons { transition: transform .3s; color: #444; } body.dark-mode .section-header { background: #3a3a3a; color: #eee; } body.dark-mode .section-header .material-icons { color: #ccc; } /* Hidden by default */ .section-content { display: none; margin-left: 20px; margin-top: 8px; margin-bottom: 8px; } /* Close button */ #adminPanelModal .editor-close-btn { position: absolute; top:10px; right:10px; display:flex; align-items:center; justify-content:center; font-size:20px; font-weight:bold; cursor:pointer; z-index:1000; width:32px; height:32px; border-radius:50%; text-align:center; line-height:30px; color:#ff4d4d; background:rgba(255,255,255,0.9); border:2px solid transparent; transition:all .3s; } #adminPanelModal .editor-close-btn:hover { color:white; background:#ff4d4d; box-shadow:0 0 6px rgba(255,77,77,.8); transform:scale(1.05); } body.dark-mode #adminPanelModal .editor-close-btn { background:rgba(0,0,0,0.6); color:#ff4d4d; } /* Action-row */ .action-row { display:flex; justify-content:space-between; margin-top:15px; } `; document.head.appendChild(style); })(); // ———————————————————————————————————— let originalAdminConfig = {}; function captureInitialAdminConfig() { originalAdminConfig = { headerTitle: document.getElementById("headerTitle").value.trim(), oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(), oidcClientId: document.getElementById("oidcClientId").value.trim(), oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(), oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(), disableFormLogin: document.getElementById("disableFormLogin").checked, disableBasicAuth: document.getElementById("disableBasicAuth").checked, disableOIDCLogin: document.getElementById("disableOIDCLogin").checked, enableWebDAV: document.getElementById("enableWebDAV").checked, sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(), globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim() }; } function hasUnsavedChanges() { const o = originalAdminConfig; return ( document.getElementById("headerTitle").value.trim() !== o.headerTitle || document.getElementById("oidcProviderUrl").value.trim() !== o.oidcProviderUrl || document.getElementById("oidcClientId").value.trim() !== o.oidcClientId || document.getElementById("oidcClientSecret").value.trim() !== o.oidcClientSecret || document.getElementById("oidcRedirectUri").value.trim() !== o.oidcRedirectUri || document.getElementById("disableFormLogin").checked !== o.disableFormLogin || document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth || document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin || document.getElementById("enableWebDAV").checked !== o.enableWebDAV || document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize || document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl ); } function showCustomConfirmModal(message) { return new Promise(resolve => { const modal = document.getElementById("customConfirmModal"); const msg = document.getElementById("confirmMessage"); const yes = document.getElementById("confirmYesBtn"); const no = document.getElementById("confirmNoBtn"); msg.textContent = message; modal.style.display = "block"; function clean() { modal.style.display = "none"; yes.removeEventListener("click", onYes); no.removeEventListener("click", onNo); } function onYes() { clean(); resolve(true); } function onNo() { clean(); resolve(false); } yes.addEventListener("click", onYes); no.addEventListener("click", onNo); }); } function toggleSection(id) { const hdr = document.getElementById(id + "Header"); const cnt = document.getElementById(id + "Content"); const isCollapsedNow = hdr.classList.toggle("collapsed"); // collapsed class present => hide; absent => show cnt.style.display = isCollapsedNow ? "none" : "block"; if (!isCollapsedNow && id === "shareLinks") { loadShareLinksSection(); } } function loadShareLinksSection() { const container = document.getElementById("shareLinksContent"); container.textContent = t("loading") + "..."; // helper: fetch one metadata file, but never throw — // on non-2xx (including 404) or network error, resolve to {} function fetchMeta(fileName) { return fetch(`/api/admin/readMetadata.php?file=${encodeURIComponent(fileName)}`, { credentials: "include" }) .then(resp => { if (!resp.ok) { // 404 or any other non-OK → treat as empty return {}; } return resp.json(); }) .catch(() => { // network failure, parse error, etc → also empty return {}; }); } Promise.all([ fetchMeta("share_folder_links.json"), fetchMeta("share_links.json") ]) .then(([folders, files]) => { // if *both* are empty, show "no shared links" const hasAny = Object.keys(folders).length || Object.keys(files).length; if (!hasAny) { container.innerHTML = `

${t("no_shared_links_available")}

`; return; } let html = `
${t("folder_shares")}
${t("file_shares")}
`; container.innerHTML = html; // wire up delete buttons container.querySelectorAll(".delete-share").forEach(btn => { btn.addEventListener("click", evt => { evt.preventDefault(); const token = btn.dataset.key; const isFolder = btn.dataset.type === "folder"; const endpoint = isFolder ? "/api/folder/deleteShareFolderLink.php" : "/api/file/deleteShareLink.php"; fetch(endpoint, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token }) }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(json => { if (json.success) { showToast(t("share_deleted_successfully")); loadShareLinksSection(); } else { showToast(t("error_deleting_share") + ": " + (json.error || ""), "error"); } }) .catch(err => { console.error("Delete error:", err); showToast(t("error_deleting_share"), "error"); }); }); }); }) .catch(err => { console.error("loadShareLinksSection error:", err); container.textContent = t("error_loading_share_links"); }); } export function openAdminPanel() { fetch("/api/admin/getConfig.php", { credentials: "include" }) .then(r => r.json()) .then(config => { // apply header title + globals if (config.header_title) { document.querySelector(".header-title h1").textContent = config.header_title; window.headerTitle = config.header_title; } if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc); if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; const dark = document.body.classList.contains("dark-mode"); const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; const inner = ` background:${dark ? "#2c2c2c" : "#fff"}; color:${dark ? "#e0e0e0" : "#000"}; padding:20px; max-width:1100px; width:50%; border-radius:8px; position:relative; max-height:90vh; overflow:auto; border:1px solid ${dark ? "#555" : "#ccc"}; `; let mdl = document.getElementById("adminPanelModal"); if (!mdl) { mdl = document.createElement("div"); mdl.id = "adminPanelModal"; mdl.style.cssText = ` position:fixed; top:0; left:0; width:100vw; height:100vh; background:${bg}; display:flex; justify-content:center; align-items:center; z-index:3000; `; mdl.innerHTML = ` `; document.body.appendChild(mdl); // Bind close & cancel document.getElementById("closeAdminPanel") .addEventListener("click", closeAdminPanel); document.getElementById("cancelAdminSettings") .addEventListener("click", closeAdminPanel); // Section toggles ["userManagement", "headerSettings", "loginOptions", "webdav", "upload", "oidc", "shareLinks"] .forEach(id => { document.getElementById(id + "Header") .addEventListener("click", () => toggleSection(id)); }); // Populate each section’s CONTENT: // — User Mgmt — document.getElementById("userManagementContent").innerHTML = ` `; 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); }); document.getElementById("adminOpenUserPermissions") .addEventListener("click", openUserPermissionsModal); // — Header Settings — document.getElementById("headerSettingsContent").innerHTML = `
`; // — Login Options — document.getElementById("loginOptionsContent").innerHTML = `
`; // — WebDAV — document.getElementById("webdavContent").innerHTML = `
`; // — Upload — document.getElementById("uploadContent").innerHTML = `
${t("max_bytes_shared_uploads_note")}
`; // — OIDC & TOTP — document.getElementById("oidcContent").innerHTML = `
Note: OIDC credentials (Client ID/Secret) will show blank here after saving, but remain unchanged until you explicitly edit and save them.
`; // — Share Links — document.getElementById("shareLinksContent").textContent = t("loading") + "…"; // — Save handler & constraints — document.getElementById("saveAdminSettings") .addEventListener("click", handleSave); ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => { document.getElementById(id) .addEventListener("change", e => { const chk = ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"] .filter(i => document.getElementById(i).checked).length; if (chk === 3) { showToast(t("at_least_one_login_method")); e.target.checked = false; } }); }); // If authBypass is checked, clear the other three document.getElementById("authBypass").addEventListener("change", e => { if (e.target.checked) { ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"] .forEach(i => document.getElementById(i).checked = false); } }); // Initialize inputs from config + capture document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; document.getElementById("authBypass").checked = !!config.loginOptions.authBypass; document.getElementById("authHeaderName").value = config.loginOptions.authHeaderName || "X-Remote-User"; document.getElementById("enableWebDAV").checked = config.enableWebDAV === true; document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || ""; captureInitialAdminConfig(); } else { // modal already exists → just refresh values & re-show mdl.style.display = "flex"; // update dark/light as above... document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; document.getElementById("authBypass").checked = !!config.loginOptions.authBypass; document.getElementById("authHeaderName").value = config.loginOptions.authHeaderName || "X-Remote-User"; document.getElementById("enableWebDAV").checked = config.enableWebDAV === true; document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || ""; 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 || ''; captureInitialAdminConfig(); } }) .catch(() => {/* if even fetching fails, open empty panel */ }); } function handleSave() { const dFL = document.getElementById("disableFormLogin").checked; const dBA = document.getElementById("disableBasicAuth").checked; const dOIDC = document.getElementById("disableOIDCLogin").checked; const aBypass= document.getElementById("authBypass").checked; const aHeader= document.getElementById("authHeaderName").value.trim() || "X-Remote-User"; const eWD = document.getElementById("enableWebDAV").checked; const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value, 10) || 0; const nHT = document.getElementById("headerTitle").value.trim(); const nOIDC = { 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 gURL = document.getElementById("globalOtpauthUrl").value.trim(); if ([dFL, dBA, dOIDC].filter(x => x).length === 3) { showToast(t("at_least_one_login_method")); return; } sendRequest("/api/admin/updateConfig.php", "POST", { header_title: nHT, oidc: nOIDC, loginOptions: { disableFormLogin: dFL, disableBasicAuth: dBA, disableOIDCLogin: dOIDC, authBypass: aBypass, authHeaderName: aHeader }, enableWebDAV: eWD, sharedMaxUploadSize: sMax, globalOtpauthUrl: gURL }, { "X-CSRF-Token": window.csrfToken }) .then(res => { if (res.success) { showToast(t("settings_updated_successfully"), "success"); captureInitialAdminConfig(); closeAdminPanel(); loadAdminConfigFunc(); } else { showToast(t("error_updating_settings") + ": " + (res.error || t("unknown_error")), "error"); } }).catch(() => {/*noop*/}); } export async function closeAdminPanel() { if (hasUnsavedChanges()) { const ok = await showCustomConfirmModal(t("unsaved_changes_confirm")); if (!ok) return; } document.getElementById("adminPanelModal").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 = ` `; 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("/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 { 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("/api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { // Then, fetch the list of users. return fetch("/api/getUsers.php", { credentials: "include" }) .then(response => response.json()) .then(usersData => { const users = Array.isArray(usersData) ? usersData : (usersData.users || []); if (users.length === 0) { listContainer.innerHTML = "

" + t("no_users_found") + "

"; 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 defaults. const defaultPerm = { folderOnly: false, readOnly: false, disableUpload: false, }; // Normalize the username key to match server storage (e.g., lowercase) const usernameKey = user.username.toLowerCase(); const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData)) ? permissionsData[usernameKey] : 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 = `
${user.username}

`; listContainer.appendChild(row); }); }); }) .catch(() => { listContainer.innerHTML = "

" + t("error_loading_users") + "

"; }); }