diff --git a/CHANGELOG.md b/CHANGELOG.md index 9743f3c..c83f84d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,67 @@ # Changelog +## Changes 12/10/2025 (v2.5.2) + +release(v2.5.2): new user management hub & relocated shared upload limits + +**Admin panel – User Management** + +- Added a new **“Manage users” hub** in the Users section, replacing the old separate “Add user / Remove user / User permissions” buttons. +- Introduced an inline **Add user dropdown card** anchored directly under the “Add user” button: + - Opens as a small card right under the button. + - Creates the user via `/api/addUser.php` and auto-selects them in the dropdown on success. + - Closes the card after a successful create. +- Added an inline **User Permissions (flags) row** for the selected user: + - Toggles `readOnly`, `disableUpload`, `canShare`, and `bypassOwnership`. + - Changes are saved immediately via `updateUserPermissions.php` and cached in `__userFlagsCacheHub`. + - Admin users are detected and treated as full-access (toggles disabled with a note). + +**User creation & password resets** + +- Improved `/api/addUser.php` responses: + - Returns proper HTTP status codes (e.g. **422** for validation failures). + - Normalized JSON shape to `{ ok: false, error: "…" }` for errors and `{ ok: true, data: … }` for success. + - Enforces **minimum 6-character passwords** for new users; invalid usernames and short passwords surface as clear error messages. +- Updated the admin “Add user” form in the hub to: + - Use `fetch` directly so 4xx responses (like 422) are correctly parsed. + - Show a toast (or `alert` fallback) on both success and failure, including backend validation messages. +- Added `UserModel::adminResetPassword()` to reset a user’s password from the admin hub without the old password, preserving TOTP/extra fields in the users file. +- Added new endpoint `public/api/admin/changeUserPassword.php`: + - Admin-only, with CSRF header check. + - Resets a user’s password via `adminResetPassword()` and returns consistent JSON. + +**Shared links & upload limits** + +- Reworked **shared upload size limit** UI: + - Removed the “Shared upload limits” block from the **Upload** section. + - Moved the **Shared Max Upload Size (bytes)** input into the **Shared links** section, above the links table. + - Renamed the section label to **“Manage Shared Links & Upload Size Limit”** and added a new `manage_shared_links_size` i18n key. +- Updated the Upload section label to **“Antivirus”** / “Antivirus upload scanning” since that section now focuses purely on AV configuration. + +**Admin UI & Pro integrations** + +- Updated the **Users** tab toolbar: + - Replaced old “Add user / Remove user / User permissions” buttons with: + - **Manage users** (opens the new hub). + - **Folder Access** (per-folder ACLs). + - **User Groups** (Pro). + - **Client Portals** (Pro). +- Wired **Client Portals** to open the new hub for user management: + - “Manage users…” button now triggers the global `adminOpenUserHub` instead of the old Add-User modal. + - “Folder access…” and “User groups…” buttons now link to `adminOpenFolderAccess` and the Pro groups modal respectively. +- Increased `#adminPanelModal` base width slightly (60% → 64%) for a bit more breathing room in the new layouts. +- Slight visual polish: + - Thicker `admin-divider` and OIDC debug snapshot border. + - **Admin subsection titles** bumped to 1.15rem. + - Toggle “ON” state uses `--filr-accent-400` for a slightly softer accent. + +**Miscellaneous** + +- Bumped the **upload card/modal z-index** so the upload UI always sits cleanly above other overlays. +- Added styling for `#adminUserHubModal .modal-content` in both light and dark mode so it matches existing admin panel modals. + +--- + ## Changes 12/9/2025 (v2.5.1) release(v2.5.1): upgrade vendor libs and enhance OIDC + admin UX diff --git a/public/api/admin/changeUserPassword.php b/public/api/admin/changeUserPassword.php new file mode 100644 index 0000000..b31ed97 --- /dev/null +++ b/public/api/admin/changeUserPassword.php @@ -0,0 +1,10 @@ +adminChangeUserPassword(); \ No newline at end of file diff --git a/public/css/styles.css b/public/css/styles.css index 2cfd434..3e1883d 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -6,6 +6,7 @@ img.logo{width:50px; height:50px; display:block;} #userPanelModal .modal-content, #adminPanelModal .modal-content, #userPermissionsModal .modal-content, +#adminUserHubModal .modal-content, #userFlagsModal .modal-content, #userGroupsModal .modal-content, #groupAclModal .modal-content, @@ -2066,7 +2067,7 @@ body.dark-mode #deleteSelectedBtn,body.dark-mode #deleteAllBtn,body.dark-mode #d body.dark-mode .folder-strip-container.folder-strip-mobile{background:var(--fr-surface-dark-2)!important;border:1px solid var(--fr-border-dark)!important} body.dark-mode #customToast{background:#212121!important;border:1px solid var(--fr-border-dark)!important;box-shadow:0 8px 20px rgba(0,0,0,.9)!important} body.dark-mode #fileSummary{color:var(--fr-muted-dark)!important} -body.dark-mode #createMenu,body.dark-mode .user-dropdown .user-menu,body.dark-mode #fileContextMenu,body.dark-mode #folderContextMenu,body.dark-mode #folderManagerContextMenu,body.dark-mode #adminPanelModal .modal-content,body.dark-mode #userPermissionsModal .modal-content,body.dark-mode #userFlagsModal .modal-content,body.dark-mode #userGroupsModal .modal-content,body.dark-mode #userPanelModal .modal-content,body.dark-mode #groupAclModal .modal-content,body.dark-mode .editor-modal,body.dark-mode #filePreviewModal .modal-content,body.dark-mode #loginForm,body.dark-mode .editor-header,#clientPortalsModal .modal-content{background:var(--fr-surface-dark)!important;border:1px solid var(--fr-border-dark)!important;color:#f1f1f1!important;border-radius:12px!important;box-shadow:0 8px 24px rgba(0,0,0,.9)!important} +body.dark-mode #createMenu,body.dark-mode .user-dropdown .user-menu,body.dark-mode #fileContextMenu,body.dark-mode #folderContextMenu,body.dark-mode #folderManagerContextMenu,body.dark-mode #adminPanelModal .modal-content,body.dark-mode #userPermissionsModal .modal-content,body.dark-mode #userPermissionsModal .modal-content,body.dark-mode #userFlagsModal .modal-content,body.dark-mode #userGroupsModal .modal-content,body.dark-mode #userPanelModal .modal-content,body.dark-mode #groupAclModal .modal-content,body.dark-mode .editor-modal,body.dark-mode #filePreviewModal .modal-content,body.dark-mode #loginForm,body.dark-mode .editor-header,#clientPortalsModal .modal-content{background:var(--fr-surface-dark)!important;border:1px solid var(--fr-border-dark)!important;color:#f1f1f1!important;border-radius:12px!important;box-shadow:0 8px 24px rgba(0,0,0,.9)!important} body.dark-mode .user-dropdown .user-menu,body.dark-mode #createMenu,body.dark-mode #fileContextMenu,body.dark-mode #folderContextMenu,body.dark-mode #folderManagerContextMenu{background-clip:padding-box} body:not(.dark-mode){background:var(--fr-bg-light)!important;color:#111!important;background-image:none!important} body:not(.dark-mode) #fileListContainer,body:not(.dark-mode) #uploadCard,body:not(.dark-mode) #folderManagementCard,body:not(.dark-mode) .card,body:not(.dark-mode) .admin-panel-content{background:var(--fr-surface-light)!important;border-color:var(--fr-border-light)!important;box-shadow:0 3px 8px rgba(0,0,0,.04)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important} @@ -2084,7 +2085,7 @@ body:not(.dark-mode) #deleteSelectedBtn,body:not(.dark-mode) #deleteAllBtn,body: body:not(.dark-mode) .folder-strip-container.folder-strip-mobile{background:#f1f1f1!important;border:1px solid var(--fr-border-light)!important} body:not(.dark-mode) #customToast{background:#212121!important;color:#fff!important;border:1px solid #000!important;box-shadow:0 8px 18px rgba(0,0,0,.45)!important} body:not(.dark-mode) #fileSummary{color:var(--fr-muted-light)!important} -body:not(.dark-mode) #createMenu,body:not(.dark-mode) .user-dropdown .user-menu,body:not(.dark-mode) #fileContextMenu,body:not(.dark-mode) #folderContextMenu,body:not(.dark-mode) #folderManagerContextMenu,body:not(.dark-mode) #adminPanelModal .modal-content,body:not(.dark-mode) #userPermissionsModal .modal-content,body:not(.dark-mode) #userFlagsModal .modal-content,body:not(.dark-mode) #userGroupsModal .modal-content,body:not(.dark-mode) #userPanelModal .modal-content,body:not(.dark-mode) #groupAclModal .modal-content,body:not(.dark-mode) .editor-modal,body:not(.dark-mode) #filePreviewModal .modal-content,body:not(.dark-mode) #loginForm,body:not(.dark-mode) .editor-header,body:not(.dark-mode) #clientPortalsModal .modal-content{background:var(--fr-surface-light)!important;border:1px solid var(--fr-border-light)!important;color:#111!important;border-radius:12px!important;box-shadow:0 4px 12px rgba(0,0,0,.12)!important} +body:not(.dark-mode) #createMenu,body:not(.dark-mode) .user-dropdown .user-menu,body:not(.dark-mode) #fileContextMenu,body:not(.dark-mode) #folderContextMenu,body:not(.dark-mode) #folderManagerContextMenu,body:not(.dark-mode) #adminPanelModal .modal-content,body:not(.dark-mode) #userPermissionsModal .modal-content,body:not(.dark-mode) #userPermissionsModal .modal-content,body:not(.dark-mode) #userFlagsModal .modal-content,body:not(.dark-mode) #userGroupsModal .modal-content,body:not(.dark-mode) #userPanelModal .modal-content,body:not(.dark-mode) #groupAclModal .modal-content,body:not(.dark-mode) .editor-modal,body:not(.dark-mode) #filePreviewModal .modal-content,body:not(.dark-mode) #loginForm,body:not(.dark-mode) .editor-header,body:not(.dark-mode) #clientPortalsModal .modal-content{background:var(--fr-surface-light)!important;border:1px solid var(--fr-border-light)!important;color:#111!important;border-radius:12px!important;box-shadow:0 4px 12px rgba(0,0,0,.12)!important} #searchIcon{display:inline-flex;align-items:center;justify-content:center;width:38px;height:36px;padding:0;border-radius:999px 0 0 999px;border:1px solid rgba(0,0,0,.18);border-right:none;background:#fff;cursor:pointer;box-shadow:none;transform:none} #searchIcon .material-icons{font-size:20px;line-height:1;color:#555} #searchIcon:hover{background:#f5f5f5} @@ -2251,7 +2252,7 @@ body:not(.dark-mode) .header-zoom-controls .btn-icon.zoom-btn .material-icons{ /* Modal sizing */ #adminPanelModal .modal-content { max-width: 1100px; - width: 60% !important; + width: 64% !important; background: #fff !important; color: #000 !important; border: 1px solid #ccc !important; @@ -2964,8 +2965,9 @@ body.dark-mode .oidc-debug-snapshot { /* admin section separators */ .admin-divider { border: 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); - margin: 10px 0; + border-top: 2px solid rgba(0, 0, 0, 0.08); +margin-top: 20px; +margin-bottom: 20px; } .dark-mode .admin-divider { @@ -2975,7 +2977,7 @@ body.dark-mode .oidc-debug-snapshot { /* OIDC debug JSON snapshot */ #oidcContent .oidc-debug-snapshot { background: rgba(0, 0, 0, 0.03); - border: 1px solid rgba(0, 0, 0, 0.08); + border: 2px solid rgba(0, 0, 0, 0.08); border-radius: 4px; } @@ -3028,8 +3030,8 @@ body.dark-mode .oidc-debug-snapshot { /* ON state */ #adminPanelModal .form-check.fr-toggle .fr-toggle-input:checked { - background-color: var(--filr-accent-500, #0d6efd); - border-color: var(--filr-accent-500, #0d6efd); + background-color: var(--filr-accent-400, #0d6efd); + border-color: var(--filr-accent-400, #0d6efd); } #adminPanelModal .form-check.fr-toggle .fr-toggle-input:checked::before { @@ -3062,7 +3064,7 @@ body.dark-mode .oidc-debug-snapshot { } #adminPanelModal .admin-subsection-title { font-weight: 600; - font-size: 0.95rem; + font-size: 1.15rem; margin-top: 4px; margin-bottom: 6px; } \ No newline at end of file diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index 45de1c6..622ff61 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -15,8 +15,8 @@ if (!window.currentOIDCConfig || typeof window.currentOIDCConfig !== 'object') { async function loadVirusDetectionLog() { const tableBody = document.getElementById('virusLogTableBody'); - const emptyEl = document.getElementById('virusLogEmpty'); - const wrapper = document.getElementById('virusLogWrapper'); + const emptyEl = document.getElementById('virusLogEmpty'); + const wrapper = document.getElementById('virusLogWrapper'); if (!wrapper || !tableBody || !emptyEl) return; @@ -128,7 +128,7 @@ async function downloadVirusLogCsv() { } const blob = await res.blob(); - const url = URL.createObjectURL(blob); + const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -165,7 +165,7 @@ function initVirusLogUI({ isPro }) { return; } - const refreshBtn = uploadScope.querySelector('#virusLogRefreshBtn'); + const refreshBtn = uploadScope.querySelector('#virusLogRefreshBtn'); const downloadBtn = uploadScope.querySelector('#virusLogDownloadCsvBtn'); if (refreshBtn && !refreshBtn.__wired) { @@ -467,12 +467,12 @@ function wireOidcTestButton(scope = document) { } const parts = []; -const authEndpoint = data.authorization_endpoint || data.authorizationUrl; -const userinfoEndpoint = data.userinfo_endpoint || data.userinfoUrl; + const authEndpoint = data.authorization_endpoint || data.authorizationUrl; + const userinfoEndpoint = data.userinfo_endpoint || data.userinfoUrl; -if (data.issuer) parts.push('issuer: ' + data.issuer); -if (authEndpoint) parts.push('auth: ' + authEndpoint); -if (userinfoEndpoint) parts.push('userinfo: ' + userinfoEndpoint); + if (data.issuer) parts.push('issuer: ' + data.issuer); + if (authEndpoint) parts.push('auth: ' + authEndpoint); + if (userinfoEndpoint) parts.push('userinfo: ' + userinfoEndpoint); const summary = parts.length ? 'OK – ' + parts.join(' • ') @@ -530,13 +530,13 @@ function wireClamavTestButton(scope = document) { return; } - const cmd = data.command || 'clamscan'; - const engine = data.engine || ''; + const cmd = data.command || 'clamscan'; + const engine = data.engine || ''; const details = data.details || ''; const parts = []; parts.push(`OK – ${cmd} is reachable`); - if (engine) parts.push(engine); + if (engine) parts.push(engine); if (details) parts.push(details); statusEl.textContent = parts.join(' • '); @@ -557,7 +557,7 @@ function initVirusLogSection({ isPro }) { if (!uploadScope) return; const wrapper = uploadScope.querySelector('#virusLogWrapper'); - const shell = uploadScope.querySelector('#virusLogTableShell'); + const shell = uploadScope.querySelector('#virusLogTableShell'); if (!wrapper || !shell) return; // Let us overlay a Pro banner on top of the table @@ -1009,7 +1009,7 @@ export function initProBundleInstaller() { statusEl.className = 'small text-success'; // Clear file input so repeat installs feel "fresh" - try { fileInput.value = ''; } catch (_) {} + try { fileInput.value = ''; } catch (_) { } // Keep existing behavior: refresh any admin config in the header, etc. if (typeof loadAdminConfigFunc === 'function') { @@ -1028,16 +1028,713 @@ export function initProBundleInstaller() { } } +let __userFlagsCacheHub = null; +let __userMetaCache = {}; // username -> { isAdmin } + +async function getUserFlagsCacheForHub() { + if (!__userFlagsCacheHub) { + __userFlagsCacheHub = await fetchAllUserFlags(); + } + return __userFlagsCacheHub; +} + +function updateUserMetaCache(list) { + __userMetaCache = {}; + (list || []).forEach(u => { + if (!u || !u.username) return; + __userMetaCache[u.username] = { + isAdmin: !!(u.isAdmin || u.admin || u.role === "1" || u.username.toLowerCase() === "admin") + }; + }); +} + +async function renderUserHubFlagsForSelected(modal) { + const flagsHost = modal.querySelector('#adminUserHubFlagsRow'); + const selectEl = modal.querySelector('#adminUserHubSelect'); + if (!flagsHost || !selectEl) return; + + const username = (selectEl.value || "").trim(); + if (!username) { + flagsHost.innerHTML = ` +
+ ${tf("select_user_for_flags", "Select a user above to view account-level switches.")} +
+ `; + return; + } + + const flagsCache = await getUserFlagsCacheForHub(); + const flags = flagsCache[username] || {}; + const meta = __userMetaCache[username] || {}; + const isAdmin = !!meta.isAdmin; + + const disabledAttr = isAdmin ? 'disabled data-admin="1" title="Admin: full access"' : ''; + const adminNote = isAdmin + ? `(${tf("admin_full_access", "Admin: full access")})` + : ''; + + flagsHost.innerHTML = ` +
+ + + + + + + + + + + + + + + + + + + +
${t("user")}${t("read_only")}${t("disable_upload")}${t("can_share")}${t("bypass_ownership")}
${escapeHTML(username)}${adminNote} +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + ${isAdmin + ? tf("admin_flags_info", "Admins already have full access. These switches are disabled.") + : tf("user_flags_inline_help", "Changes here are saved immediately for this user.")} + + `; + + // Admin row is read-only + if (isAdmin) return; + + const row = flagsHost.querySelector('tr[data-username]'); + if (!row) return; + const checkboxes = row.querySelectorAll('input[type="checkbox"][data-flag]'); + + const getFlagsFromRow = () => { + const get = (k) => { + const el = row.querySelector(`input[data-flag="${k}"]`); + return !!(el && el.checked); + }; + return { + username, + readOnly: get("readOnly"), + disableUpload: get("disableUpload"), + canShare: get("canShare"), + bypassOwnership: get("bypassOwnership") + }; + }; + + const saveFlags = async () => { + const permissions = [getFlagsFromRow()]; + try { + const res = await sendRequest( + "/api/updateUserPermissions.php", + "PUT", + { permissions }, + { "X-CSRF-Token": window.csrfToken } + ); + + if (!res || res.success === false) { + const msg = (res && (res.error || res.message)) || tf("error_updating_permissions", "Error updating permissions"); + showToast(msg, "error"); + return; + } + + // keep local cache in sync + const flagsCache = await getUserFlagsCacheForHub(); + flagsCache[username] = permissions[0]; + showToast(tf("user_permissions_updated_successfully", "User permissions updated successfully")); + } catch (err) { + console.error("save inline flags error", err); + showToast(tf("error_updating_permissions", "Error updating permissions"), "error"); + } + }; + + checkboxes.forEach(cb => { + cb.addEventListener("change", () => { + saveFlags(); + }); + }); +} + +export function openAdminUserHubModal() { + const isDark = document.body.classList.contains("dark-mode"); + const overlayBg = isDark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; + const contentBg = isDark ? "var(--fr-surface-dark)" : "#fff"; + const contentFg = isDark ? "#e0e0e0" : "#000"; + const borderCol = isDark ? "var(--fr-border-dark)" : "#ccc"; + + // Local helper so we ALWAYS see something (toast or alert) + const safeToast = (msg, type) => { + try { + if (typeof showToast === "function") { + showToast(msg, 7000); + } else { + alert(msg); + } + } catch (e) { + console.error("showToast failed, falling back to alert", e); + alert(msg); + } + }; + + let modal = document.getElementById("adminUserHubModal"); + if (!modal) { + modal = document.createElement("div"); + modal.id = "adminUserHubModal"; + modal.style.cssText = ` + position:fixed; inset:0; + background:${overlayBg}; + display:flex; align-items:center; justify-content:center; + z-index:9999; + `; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + const closeBtn = modal.querySelector("#closeAdminUserHub"); + if (closeBtn) { + closeBtn.addEventListener("click", () => { + modal.style.display = "none"; + }); + } + + // ESC closes modal + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && modal.style.display === "flex") { + modal.style.display = "none"; + } + }); + + const selectEl = modal.querySelector("#adminUserHubSelect"); + const refreshBtn = modal.querySelector("#adminUserHubRefresh"); + const addForm = modal.querySelector("#adminUserHubAddForm"); + const addBtn = modal.querySelector("#adminUserHubAddBtn"); + const addCard = modal.querySelector("#adminUserHubAddCard"); + const delBtn = modal.querySelector("#adminUserHubDeleteBtn"); + const pwBtn = modal.querySelector("#adminUserHubSavePassword"); + + const newUserInput = modal.querySelector("#adminUserHubNewUsername"); + const newPassInput = modal.querySelector("#adminUserHubAddPassword"); + const newAdminInput = modal.querySelector("#adminUserHubIsAdmin"); + + const resetNewPwInput = modal.querySelector("#adminUserHubNewPassword"); + const resetConfPwInput = modal.querySelector("#adminUserHubConfirmPassword"); + + const getSelectedUser = () => { + return (selectEl && selectEl.value) ? selectEl.value.trim() : ""; + }; + + if (refreshBtn && selectEl) { + refreshBtn.addEventListener("click", async () => { + await populateAdminUserHubSelect(selectEl); + await renderUserHubFlagsForSelected(modal); + }); + } + + // "Add user" button toggles the dropdown card under the button + if (addBtn && addCard && newUserInput) { + addCard.style.display = "none"; + + addBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const isHidden = + addCard.style.display === "none" || addCard.style.display === ""; + addCard.style.display = isHidden ? "block" : "none"; + if (isHidden) { + newUserInput.focus(); + } + }); + + // Clicking outside of the addCard closes it + document.addEventListener("click", (e) => { + if (!modal.contains(e.target)) return; + if ( + addCard.style.display === "block" && + !addCard.contains(e.target) && + !addBtn.contains(e.target) + ) { + addCard.style.display = "none"; + } + }); + } + + // Inline "Add user" form WITH backend error -> toast (handles 422) + if (addForm && newUserInput && newPassInput && newAdminInput && selectEl) { + addForm.addEventListener("submit", async (e) => { + e.preventDefault(); + + const username = newUserInput.value.trim(); + const password = newPassInput.value.trim(); + const isAdmin = !!newAdminInput.checked; + + if (!username || !password) { + safeToast("Username and password are required!", "error"); + return; + } + + try { + const resp = await fetch("/api/addUser.php", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken || "" + }, + body: JSON.stringify({ username, password, isAdmin }) + }); + + let data = null; + try { + data = await resp.json(); + } catch { + // non-JSON or empty; leave as null + } + + const isError = + !resp.ok || + !data || + (data.ok === false) || + (data.success === false); + + if (isError) { + const msg = + (data && (data.error || data.message)) || + (resp.status === 422 + ? "Could not create user. Please check username/password." + : "Error: Could not add user"); + + console.error("Add user failed –", resp.status, data); + safeToast(msg, "error"); + return; + } + + // success + safeToast("User added successfully!"); + newUserInput.value = ""; + newPassInput.value = ""; + newAdminInput.checked = false; + + // hide dropdown after successful create + if (addCard) { + addCard.style.display = "none"; + } + + await populateAdminUserHubSelect(selectEl); + selectEl.value = username; + + if (typeof __userFlagsCacheHub !== "undefined") { + __userFlagsCacheHub = null; + } + await renderUserHubFlagsForSelected(modal); + } catch (err) { + console.error("Add user error", err); + const msg = + err && err.message + ? err.message + : "Network error while creating user."; + safeToast(msg, "error"); + } + }); + } + + // Delete user + if (delBtn && selectEl) { + delBtn.addEventListener("click", async () => { + const username = getSelectedUser(); + if (!username) { + safeToast("Please select a user first.", "error"); + return; + } + + const current = (localStorage.getItem("username") || "").trim(); + if (current && current === username) { + safeToast( + "You cannot delete the account you are currently logged in as.", + "error" + ); + return; + } + + const ok = await showCustomConfirmModal( + `Are you sure you want to delete user "${username}"?` + ); + if (!ok) return; + + try { + const res = await sendRequest( + "/api/removeUser.php", + "POST", + { username }, + { "X-CSRF-Token": window.csrfToken || "" } + ); + + if (!res || res.success === false) { + const msg = + (res && (res.error || res.message)) || + "Error: Could not remove user"; + safeToast(msg, "error"); + return; + } + + safeToast("User removed successfully!"); + if (typeof __userFlagsCacheHub !== "undefined") { + __userFlagsCacheHub = null; + } + await populateAdminUserHubSelect(selectEl); + await renderUserHubFlagsForSelected(modal); + } catch (err) { + console.error(err); + const msg = + err && err.message + ? err.message + : "Error: Could not remove user"; + safeToast(msg, "error"); + } + }); + } + + // Reset password for selected user (admin) + if (pwBtn && resetNewPwInput && resetConfPwInput && selectEl) { + pwBtn.addEventListener("click", async () => { + if (window.__FR_DEMO__) { + safeToast("Password changes are disabled on the public demo."); + return; + } + + const username = getSelectedUser(); + if (!username) { + safeToast("Please select a user first.", "error"); + return; + } + + const newPw = resetNewPwInput.value.trim(); + const conf = resetConfPwInput.value.trim(); + + if (!newPw || !conf) { + safeToast("Please fill in both password fields.", "error"); + return; + } + if (newPw !== conf) { + safeToast("New passwords do not match.", "error"); + return; + } + + try { + const res = await sendRequest( + "/api/admin/changeUserPassword.php", + "POST", + { username, newPassword: newPw }, + { "X-CSRF-Token": window.csrfToken || "" } + ); + + // Handle both legacy {success:false} and new {ok:false,error:...} + if (!res || res.success === false || res.ok === false) { + const msg = + (res && (res.error || res.message)) || + "Error changing password. Password must be at least 6 characters."; + safeToast(msg, "error"); + return; + } + + safeToast("Password updated successfully."); + resetNewPwInput.value = ""; + resetConfPwInput.value = ""; + } catch (err) { + // If sendRequest throws on non-2xx, e.g. 422, surface backend JSON error + console.error("Change password failed –", err.status, err.data || err); + + const msg = + (err && + err.data && + (err.data.error || err.data.message)) || + (err && err.message) || + "Error changing password. Password must be at least 6 characters."; + + safeToast(msg, "error"); + } + }); + } + + // When user selection changes, refresh inline flags row + if (selectEl) { + selectEl.addEventListener("change", () => { + renderUserHubFlagsForSelected(modal); + }); + } + + // Expose for later calls to re-populate + modal.__populate = async () => { + const sel = modal.querySelector("#adminUserHubSelect"); + if (sel) { + await populateAdminUserHubSelect(sel); + if (typeof __userFlagsCacheHub !== "undefined") { + __userFlagsCacheHub = null; + } + await renderUserHubFlagsForSelected(modal); + } + }; + } else { + // Update colors/theme if already exists + modal.style.background = overlayBg; + const content = modal.querySelector(".modal-content"); + if (content) { + content.style.background = contentBg; + content.style.color = contentFg; + content.style.border = `1px solid ${borderCol}`; + } + } + + modal.style.display = "flex"; + if (modal.__populate) { + modal.__populate(); + } +} + function loadShareLinksSection() { - const container = document.getElementById("shareLinksContent"); + const container = + document.getElementById("shareLinksList") || + document.getElementById("shareLinksContent"); if (!container) return; + container.textContent = t("loading") + "..."; function fetchMeta(fileName) { return fetch(`/api/admin/readMetadata.php?file=${encodeURIComponent(fileName)}`, { credentials: "include" }) - .then(resp => resp.ok ? resp.json() : {}) + .then(resp => (resp.ok ? resp.json() : {})) .catch(() => ({})); } @@ -1091,12 +1788,12 @@ function loadShareLinksSection() { const endpoint = isFolder ? "/api/folder/deleteShareFolderLink.php" : "/api/file/deleteShareLink.php"; - + const csrfToken = (document.querySelector('meta[name="csrf-token"]')?.content || window.csrfToken || ""); - + const body = new URLSearchParams({ token }); - + fetch(endpoint, { method: "POST", credentials: "include", @@ -1151,7 +1848,7 @@ export function openAdminPanel() { if (config.oidc && typeof config.oidc === 'object') { Object.assign(window.currentOIDCConfig, config.oidc); } - + if (config.globalOtpauthUrl) { window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; } @@ -1164,14 +1861,14 @@ export function openAdminPanel() { const proEmail = proInfo.email || ''; const proVersion = proInfo.version || 'not installed'; const proLicense = proInfo.license || ''; - // New: richer license metadata from FR_PRO_INFO / backend - const proPlan = proInfo.plan || ''; // e.g. "early_supporter_1x", "personal_yearly" - const proExpiresAt = proInfo.expiresAt || ''; // ISO timestamp string or "" - const proMaxMajor = ( - typeof proInfo.maxMajor === 'number' - ? proInfo.maxMajor - : (proInfo.maxMajor ? Number(proInfo.maxMajor) : null) - ); + // New: richer license metadata from FR_PRO_INFO / backend + const proPlan = proInfo.plan || ''; // e.g. "early_supporter_1x", "personal_yearly" + const proExpiresAt = proInfo.expiresAt || ''; // ISO timestamp string or "" + const proMaxMajor = ( + typeof proInfo.maxMajor === 'number' + ? proInfo.maxMajor + : (proInfo.maxMajor ? Number(proInfo.maxMajor) : null) + ); const brandingCfg = config.branding || {}; const brandingCustomLogoUrl = brandingCfg.customLogoUrl || ""; const brandingHeaderBgLight = brandingCfg.headerBgLight || ""; @@ -1208,9 +1905,9 @@ export function openAdminPanel() { { id: "headerSettings", label: tf("header_footer_settings", "Header & Footer settings") }, { id: "loginOptions", label: t("login_webdav") }, { id: "onlyoffice", label: "ONLYOFFICE" }, - { id: "upload", label: tf("upload_limits_and_antivirus", "Upload limits & antivirus") }, + { id: "upload", label: tf("antivirus_settings", "Antivirus") }, { id: "oidc", label: t("oidc_configuration") + " & TOTP" }, - { id: "shareLinks", label: t("manage_shared_links") }, + { id: "shareLinks", label: t("manage_shared_links_size") }, { id: "storage", label: "Storage / Disk Usage" }, { id: "pro", label: "FileRise Pro" }, { id: "sponsor", label: (typeof tf === 'function' ? tf("sponsor_donations", "Sponsor / Donations") : "Sponsor / Donations") } @@ -1251,122 +1948,70 @@ export function openAdminPanel() { }); document.getElementById("userManagementContent").innerHTML = ` -
- - - - - - - - - ${isPro - ? ` +
+ ${!isPro ? 'Pro' : ''}
- ` - : ` -
- - Pro -
- ` - } - - ${isPro - ? ` +
+ ${!isPro ? 'Pro' : ''}
- ` - : ` -
- - Pro -
- ` - }
- - Use the core tools to manage users, permissions and per-folder access. - User Groups and Client Portals are only available in FileRise Pro. + + Manage users, passwords and account-level flags from “Manage users”. + Use “Folder Access” for per-folder ACLs. User Groups and Client Portals are available in FileRise Pro. `; - 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); - - // Pro-only stubs for future features - const regBtn = document.getElementById("adminOpenUserRegistration"); - const groupsBtn = document.getElementById("adminOpenUserGroups"); - const clientBtn = document.getElementById("adminOpenClientPortal"); - - if (regBtn) { - regBtn.addEventListener("click", () => { - if (!isPro) { - showToast("User registration is a FileRise Pro feature. Visit filerise.net to purchase a license."); - window.open("https://filerise.net", "_blank", "noopener"); - return; - } - // Placeholder for future Pro UI: - showToast("User registration management is coming soon in FileRise Pro."); + // Wiring for the 4 buttons + const userHubBtn = document.getElementById("adminOpenUserHub"); + if (userHubBtn) { + userHubBtn.addEventListener("click", () => { + openAdminUserHubModal(); }); } + const folderAccessBtn = document.getElementById("adminOpenFolderAccess"); + if (folderAccessBtn) { + folderAccessBtn.addEventListener("click", () => { + openUserPermissionsModal(); + }); + } + + const groupsBtn = document.getElementById("adminOpenUserGroups"); if (groupsBtn) { groupsBtn.addEventListener("click", () => { if (!isPro) { @@ -1378,6 +2023,7 @@ export function openAdminPanel() { }); } + const clientBtn = document.getElementById("adminOpenClientPortal"); if (clientBtn) { clientBtn.addEventListener("click", () => { if (!isPro) { @@ -1385,7 +2031,6 @@ export function openAdminPanel() { window.open("https://filerise.net", "_blank", "noopener"); return; } - openClientPortalsModal(); }); } @@ -1410,8 +2055,8 @@ export function openAdminPanel() { ${isPro - ? 'Upload a logo image or paste a local path.' - : 'Requires FileRise Pro to enable custom header branding.'} + ? 'Upload a logo image or paste a local path.' + : 'Requires FileRise Pro to enable custom header branding.'}
@@ -1419,7 +2064,7 @@ export function openAdminPanel() { type="text" id="brandingCustomLogoUrl" class="form-control" - placeholder="/uploads/profile_pics/logo.png" + placeholder="/assets/logo.png" value="${isPro ? (brandingCustomLogoUrl.replace(/"/g, '"')) : ''}" ${!isPro ? 'disabled data-disabled-reason="pro"' : ''} /> @@ -1475,8 +2120,8 @@ export function openAdminPanel() {
${isPro - ? 'If left empty, FileRise uses its default blue and dark header colors.' - : 'Requires FileRise Pro to enable custom color branding.'} + ? 'If left empty, FileRise uses its default blue and dark header colors.' + : 'Requires FileRise Pro to enable custom color branding.'} @@ -1492,8 +2137,8 @@ export function openAdminPanel() { ${isPro - ? 'Shown at the bottom of every page. You can include simple HTML like links.' - : 'Requires FileRise Pro to customize footer text.'} + ? 'Shown at the bottom of every page. You can include simple HTML like links.' + : 'Requires FileRise Pro to customize footer text.'}