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 = `
+
+
+ ${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 = `
+
+
×
+
+
+ ${tf("manage_users", "Manage users")}
+
+
+ ${tf(
+ "manage_users_help",
+ "Select a user from the list to change their password, delete them, or update account-level flags. Use Add User to create a brand new account."
+ )}
+
+
+
+
+
+ ${t("username")}
+
+
+
+
+
+
+
+ person_add
+
+ ${t("add_user")}
+
+
+
+
+
+ ${tf("create_new_user_title", "Create New User")}
+
+
+
+ ${tf(
+ "create_user_help",
+ "New users are created immediately and appear in the dropdown at the top."
+ )}
+
+
+
+
+
+ person_remove
+
+ ${t("remove_user")}
+
+
+
+
+ ${tf("refresh", "Refresh")}
+
+
+
+
+ ${tf(
+ "user_actions_help_inline",
+ "Delete, change password, and flags apply to the selected user in the dropdown above."
+ )}
+
+
+
+
+
+
+
+ ${tf("change_user_password", "Change user password")}
+
+
+
+
+
+
+
+
+
+ ${t("save")}
+
+
+ ${tf(
+ "change_user_password_help",
+ "Resets the selected user’s password. Does not require their old password (admin-only)."
+ )}
+
+
+
+
+
+
+ ${tf("user_permissions", "User Permissions")}
+
+
+ ${tf(
+ "user_flags_inline_help_long",
+ "Account-level switches (read-only, disable upload, can share, bypass ownership) for the selected user. For per-folder ACLs, use Folder Access."
+ )}
+
+
+
+
+
+ `;
+
+ 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 = `
-
-
-
- person_add
- ${t("add_user")}
+
+
+
+ people
+ ${tf("manage_users", "Manage users")}
-
- person_remove
- ${t("remove_user")}
-
-
-
+
+
folder_shared
${tf("folder_access", "Folder Access")}
-
- tune
- ${tf("user_permissions", "User Permissions")}
-
-
-
- ${isPro
- ? `
+
+ class="btn btn-sm btn-pro-admin"
+ ${!isPro ? "data-pro-locked='1'" : ""}
+ >
groups
User Groups
+ ${!isPro ? 'Pro ' : ''}
- `
- : `
-
-
- groups
- User Groups
-
- Pro
-
- `
- }
-
- ${isPro
- ? `
+
cloud_upload
Client Portals
+ ${!isPro ? 'Pro ' : ''}
- `
- : `
-
-
- cloud_upload
- Client Portals
-
- 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.'}