let currentUser = null; let users = []; let onlyOwnUsers = localStorage.getItem("adminOnlyOwnUsers") === "1"; import { escapeHtml, formatDate } from "./format.js"; const usersTableBody = document.getElementById("users-table-body"); const addUserBtn = document.getElementById("add-user-btn"); const ownUsersToggle = document.getElementById("own-users-toggle"); const userModal = document.getElementById("user-modal"); const modalTitle = document.getElementById("modal-title"); const userForm = document.getElementById("user-form"); const closeModal = document.getElementById("close-modal"); const cancelBtn = document.getElementById("cancel-btn"); const errorMessage = document.getElementById("error-message"); const errorText = document.getElementById("error-text"); const logoutBtn = document.getElementById("logout-btn"); const currentUserEl = document.getElementById("username-display"); const currentRoleEl = document.getElementById("role-badge"); async function getCurrentUser() { try { const res = await fetch("/api/auth/me"); if (res.ok) { currentUser = await res.json(); currentUserEl.textContent = currentUser.username; const roleBadges = { admin: 'Admin', operator: 'Operator', viewer: 'Viewer', }; if (roleBadges[currentUser.role]) { currentRoleEl.innerHTML = roleBadges[currentUser.role]; } else { currentRoleEl.textContent = currentUser.role || ""; } if (currentUser.role === "admin") { currentRoleEl.classList.add( "bg-purple-900/50", "text-purple-300", "border", "border-purple-800", ); } else if (currentUser.role === "operator") { currentRoleEl.classList.add( "bg-blue-900/50", "text-blue-300", "border", "border-blue-800", ); } else { currentRoleEl.classList.add( "bg-slate-700", "text-slate-300", "border", "border-slate-600", ); } if (currentUser.role === "admin") { document.getElementById("metrics-link")?.classList.remove("hidden"); document.getElementById("scripts-link")?.classList.remove("hidden"); document.getElementById("build-link")?.classList.remove("hidden"); document.getElementById("users-link")?.classList.remove("hidden"); document.getElementById("plugins-link")?.classList.remove("hidden"); document.getElementById("deploy-link")?.classList.remove("hidden"); } else if (currentUser.role === "operator") { document.getElementById("metrics-link")?.classList.remove("hidden"); document.getElementById("scripts-link")?.classList.remove("hidden"); document.getElementById("build-link")?.classList.remove("hidden"); } if (currentUser.canBuild) { document.getElementById("build-link")?.classList.remove("hidden"); } if (currentUser.role !== "admin") { alert("Access denied. Admin role required."); window.location.href = "/"; } renderUsers(); } else { window.location.href = "/"; } } catch (err) { console.error("Failed to get current user:", err); window.location.href = "/"; } } if (logoutBtn && !logoutBtn.dataset.boundLogout) { logoutBtn.dataset.boundLogout = "true"; logoutBtn.addEventListener("click", async () => { if (!confirm("Are you sure you want to logout?")) return; try { const res = await fetch("/api/logout", { method: "POST" }); if (res.ok) { window.location.href = "/"; } else { alert("Logout failed. Please try again."); } } catch (err) { console.error("Logout error:", err); alert("Logout failed. Please try again."); } }); } async function loadUsers() { try { const res = await fetch("/api/users"); if (!res.ok) { throw new Error("Failed to load users"); } const data = await res.json(); users = data.users || []; renderUsers(); } catch (err) { console.error("Load users error:", err); usersTableBody.innerHTML = ` Failed to load users `; } } function renderUsers() { const visibleUsers = getVisibleUsers(); if (visibleUsers.length === 0) { const emptyMessage = onlyOwnUsers && users.length > 0 ? "No users created by you" : "No users found"; usersTableBody.innerHTML = ` ${emptyMessage} `; return; } usersTableBody.innerHTML = visibleUsers .map( (user) => `
${escapeHtml(user.username)} ${user.id === currentUser?.userId ? '(You)' : ""}
${getRoleBadge(user.role)} ${user.role !== "admin" ? ` ${user.can_build ? 'Build' : 'No Build'} ${user.can_upload_files ? 'Upload' : 'No Upload'} ` : ''} ${formatDate(user.created_at)} ${user.last_login ? formatDate(user.last_login) : 'Never'} ${escapeHtml(user.created_by || "System")}
${ user.id !== currentUser?.userId ? ` ${user.role === "operator" ? ` ` : ''} ${user.role !== "admin" ? ` ` : ''} ${user.role !== "admin" ? ` ` : ''} ` : 'Cannot edit yourself' }
`, ) .join(""); attachActionListeners(); } function getVisibleUsers() { if (!onlyOwnUsers || !currentUser) return users; return users.filter((user) => ( user.id === currentUser.userId || user.username === currentUser.username || user.created_by === currentUser.username )); } function getRoleBadge(role) { const badges = { admin: 'Admin', operator: 'Operator', viewer: 'Viewer', }; return badges[role] || escapeHtml(role || ""); } function attachActionListeners() { const oldListener = usersTableBody._actionListener; if (oldListener) { usersTableBody.removeEventListener("click", oldListener); } const listener = (e) => { const toggleBuild = e.target.closest("[data-action='toggle-build']"); if (toggleBuild) { const userId = parseInt(toggleBuild.dataset.userId); const username = toggleBuild.dataset.username; const canBuild = toggleBuild.dataset.canBuild === "1"; toggleBuildPermission(userId, username, canBuild); return; } const toggleUpload = e.target.closest("[data-action='toggle-upload']"); if (toggleUpload) { const userId = parseInt(toggleUpload.dataset.userId); const username = toggleUpload.dataset.username; const canUpload = toggleUpload.dataset.canUpload === "1"; toggleUploadPermission(userId, username, canUpload); return; } const btn = e.target.closest(".user-action-btn"); if (!btn) return; const action = btn.dataset.action; const userId = parseInt(btn.dataset.userId); const username = btn.dataset.username; const role = btn.dataset.role; switch (action) { case "change-password": changePassword(userId, username); break; case "change-role": changeRole(userId, username, role); break; case "delete": deleteUser(userId, username); break; case "client-access": configureClientAccess(userId, username, role); break; case "feature-permissions": configureFeaturePermissions(userId, username); break; case "plugin-access": configurePluginAccess(userId, username); break; case "view-sessions": viewUserSessions(userId, username); break; case "permissions": openUserPermissionsModal(userId, username); break; } }; usersTableBody.addEventListener("click", listener); usersTableBody._actionListener = listener; } function showModal(title) { modalTitle.textContent = title; userModal.classList.remove("hidden"); errorMessage.classList.add("hidden"); userForm.reset(); } function hideModal() { userModal.classList.add("hidden"); userForm.reset(); } function showError(message) { errorText.textContent = message; errorMessage.classList.remove("hidden"); } addUserBtn.addEventListener("click", () => { showModal("Add User"); document.getElementById("password-field").classList.remove("hidden"); document.getElementById("password").required = true; }); closeModal.addEventListener("click", hideModal); cancelBtn.addEventListener("click", hideModal); if (ownUsersToggle) { ownUsersToggle.checked = onlyOwnUsers; ownUsersToggle.addEventListener("change", () => { onlyOwnUsers = ownUsersToggle.checked; localStorage.setItem("adminOnlyOwnUsers", onlyOwnUsers ? "1" : "0"); renderUsers(); }); } userForm.addEventListener("submit", async (e) => { e.preventDefault(); const formData = new FormData(userForm); const username = formData.get("username"); const password = formData.get("password"); const role = formData.get("role"); try { const res = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, role }), }); const data = await res.json(); if (res.ok) { hideModal(); await loadUsers(); } else { showError(data.error || "Failed to create user"); } } catch (err) { console.error("Create user error:", err); showError("Network error. Please try again."); } }); window.changePassword = async function (userId, username) { const password = prompt(`Enter new password for ${username}:`); if (!password) return; if (password.length < 6) { alert("Password must be at least 6 characters"); return; } try { const res = await fetch(`/api/users/${userId}/password`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); const data = await res.json(); if (res.ok) { alert("Password updated successfully"); } else { alert(data.error || "Failed to update password"); } } catch (err) { console.error("Update password error:", err); alert("Network error. Please try again."); } }; window.changeRole = async function (userId, username, currentRole) { const roles = ["viewer", "operator", "admin"]; const roleNames = { viewer: "Viewer", operator: "Operator", admin: "Admin" }; const message = `Select new role for ${username}:\n\n1. Viewer (Read-only)\n2. Operator (Control clients)\n3. Admin (Full access)\n\nCurrent: ${roleNames[currentRole]}`; const choice = prompt(message); if (!choice || !["1", "2", "3"].includes(choice)) return; const newRole = roles[parseInt(choice) - 1]; if (newRole === currentRole) { alert("No change made"); return; } if (!confirm(`Change ${username}'s role to ${roleNames[newRole]}?`)) return; try { const res = await fetch(`/api/users/${userId}/role`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role: newRole }), }); const data = await res.json(); if (res.ok) { alert("Role updated successfully"); await loadUsers(); } else { alert(data.error || "Failed to update role"); } } catch (err) { console.error("Update role error:", err); alert("Network error. Please try again."); } }; window.deleteUser = async function (userId, username) { if ( !confirm( `Are you sure you want to delete user "${username}"? This action cannot be undone.`, ) ) { return; } try { const res = await fetch(`/api/users/${userId}`, { method: "DELETE", }); const data = await res.json(); if (res.ok) { alert("User deleted successfully"); await loadUsers(); } else { alert(data.error || "Failed to delete user"); } } catch (err) { console.error("Delete user error:", err); alert("Network error. Please try again."); } }; window.configureClientAccess = async function (userId, username, role) { const params = new URLSearchParams({ userId: String(userId) }); window.location.href = `/user-client-access?${params.toString()}`; }; const FEATURE_LABELS = { console: { label: "Console", icon: "fa-terminal" }, remote_desktop: { label: "Remote Desktop", icon: "fa-desktop" }, backstage: { label: "backstage", icon: "fa-window-restore" }, webcam: { label: "Webcam", icon: "fa-video" }, file_browser: { label: "File Browser", icon: "fa-folder-open" }, processes: { label: "Processes", icon: "fa-microchip" }, keylogger: { label: "Keylogger", icon: "fa-keyboard" }, voice: { label: "Voice", icon: "fa-microphone" }, disconnect: { label: "Disconnect Client", icon: "fa-plug-circle-xmark" }, reconnect: { label: "Reconnect Client", icon: "fa-arrows-rotate" }, uninstall: { label: "Uninstall Agent", icon: "fa-trash-can" }, client_metadata: { label: "Edit Client Metadata", icon: "fa-pen-to-square" }, }; window.configureFeaturePermissions = async function (userId, username) { try { const res = await fetch(`/api/users/${userId}/feature-permissions`); if (!res.ok) throw new Error("Failed to load feature permissions"); const data = await res.json(); const perms = data.permissions; const features = data.features; let modal = document.getElementById("feature-perms-modal"); if (modal) modal.remove(); modal = document.createElement("div"); modal.id = "feature-perms-modal"; modal.className = "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"; modal.innerHTML = `

Feature Permissions

Manage feature access for ${escapeHtml(username)}. Disabled features will return 403 when accessed.

${features.map(f => { const meta = FEATURE_LABELS[f] || { label: f, icon: "fa-puzzle-piece" }; const checked = perms[f] !== false; return ` `; }).join("")}
`; document.body.appendChild(modal); document.getElementById("close-feature-modal").addEventListener("click", () => modal.remove()); modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); document.getElementById("save-feature-perms").addEventListener("click", async () => { const toggles = modal.querySelectorAll("[data-feature]"); const permissions = {}; toggles.forEach(t => { permissions[t.dataset.feature] = t.checked; }); try { const saveRes = await fetch(`/api/users/${userId}/feature-permissions`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ permissions }), }); if (!saveRes.ok) throw new Error("Failed to save"); modal.remove(); if (window.showToast) window.showToast("Feature permissions updated", "success"); } catch (err) { alert("Failed to save feature permissions"); } }); document.getElementById("reset-feature-perms").addEventListener("click", async () => { if (!confirm("Reset all feature permissions to defaults (all enabled)?")) return; try { const delRes = await fetch(`/api/users/${userId}/feature-permissions`, { method: "DELETE" }); if (!delRes.ok) throw new Error("Failed to reset"); modal.remove(); if (window.showToast) window.showToast("Feature permissions reset", "success"); } catch (err) { alert("Failed to reset feature permissions"); } }); } catch (err) { console.error("Feature permissions error:", err); alert("Failed to load feature permissions"); } }; window.configurePluginAccess = async function (userId, username) { try { const [accessRes, pluginsRes] = await Promise.all([ fetch(`/api/users/${userId}/plugin-access`), fetch("/api/plugins"), ]); if (!accessRes.ok) throw new Error("Failed to load plugin access"); const accessData = await accessRes.json(); const currentScope = accessData.scope || "none"; const allowedIds = new Set((accessData.rules || []).map(r => r.pluginId)); let allPlugins = []; if (pluginsRes.ok) { const pluginsData = await pluginsRes.json(); allPlugins = pluginsData.plugins || []; } let modal = document.getElementById("plugin-access-modal"); if (modal) modal.remove(); modal = document.createElement("div"); modal.id = "plugin-access-modal"; modal.className = "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"; const scopeOptions = [ { value: "none", label: "No Access", desc: "User cannot access any plugins" }, { value: "allowlist", label: "Selected Plugins", desc: "User can only access checked plugins" }, { value: "all", label: "All Plugins", desc: "User can access all plugins" }, ]; modal.innerHTML = `

Plugin Access

Manage plugin access for ${escapeHtml(username)}.

${scopeOptions.find(o => o.value === currentScope)?.desc || ""}

${allPlugins.length === 0 ? '

No plugins installed

' : allPlugins.map(p => ` `).join("")}
`; document.body.appendChild(modal); const scopeSelect = document.getElementById("plugin-scope-select"); const scopeDesc = document.getElementById("plugin-scope-desc"); const listSection = document.getElementById("plugin-list-section"); scopeSelect.addEventListener("change", () => { const val = scopeSelect.value; const opt = scopeOptions.find(o => o.value === val); scopeDesc.textContent = opt?.desc || ""; if (val === "allowlist") { listSection.classList.remove("hidden"); } else { listSection.classList.add("hidden"); } }); document.getElementById("close-plugin-modal").addEventListener("click", () => modal.remove()); document.getElementById("cancel-plugin-access").addEventListener("click", () => modal.remove()); modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); document.getElementById("save-plugin-access").addEventListener("click", async () => { const scope = scopeSelect.value; const pluginIds = []; if (scope === "allowlist") { modal.querySelectorAll("[data-plugin-id]").forEach(cb => { if (cb.checked) pluginIds.push(cb.dataset.pluginId); }); } try { const saveRes = await fetch(`/api/users/${userId}/plugin-access`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope, pluginIds }), }); if (!saveRes.ok) { const err = await saveRes.json().catch(() => ({})); throw new Error(err.error || "Failed to save"); } modal.remove(); if (window.showToast) window.showToast("Plugin access updated", "success"); } catch (err) { alert("Failed to save plugin access: " + err.message); } }); } catch (err) { console.error("Plugin access error:", err); alert("Failed to load plugin access settings"); } }; window.toggleBuildPermission = async function (userId, username, currentCanBuild) { const newVal = !currentCanBuild; if (!confirm(`${newVal ? 'Grant' : 'Revoke'} build permission for ${username}?`)) return; try { const res = await fetch(`/api/users/${userId}/can-build`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ canBuild: newVal }), }); const data = await res.json(); if (res.ok) { await loadUsers(); } else { alert(data.error || "Failed to update build permission"); } } catch (err) { console.error("Toggle build permission error:", err); alert("Network error. Please try again."); } }; window.toggleUploadPermission = async function (userId, username, currentCanUpload) { const newVal = !currentCanUpload; if (!confirm(`${newVal ? 'Grant' : 'Revoke'} file upload permission for ${username}?`)) return; try { const res = await fetch(`/api/users/${userId}/can-upload-files`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ canUploadFiles: newVal }), }); const data = await res.json(); if (res.ok) { await loadUsers(); } else { alert(data.error || "Failed to update upload permission"); } } catch (err) { console.error("Toggle upload permission error:", err); alert("Network error. Please try again."); } }; async function viewUserSessions(userId, username) { try { const res = await fetch(`/api/users/${userId}/sessions`); if (!res.ok) throw new Error("Failed to fetch sessions"); const data = await res.json(); const sessions = Array.isArray(data.sessions) ? data.sessions : []; const existing = document.getElementById("sessions-modal"); if (existing) existing.remove(); const modal = document.createElement("div"); modal.id = "sessions-modal"; modal.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60"; const now = Math.floor(Date.now() / 1000); const rows = sessions.length === 0 ? `No sessions` : sessions.map(s => { const isExpired = s.expiresAt && s.expiresAt < now; const status = s.revoked ? 'Revoked' : isExpired ? 'Expired' : 'Active'; const canRevoke = !s.revoked && !isExpired; const lastAct = s.lastActivity ? formatRelTime(s.lastActivity) : "—"; return ` ${escapeHtml(s.ip || "—")} ${lastAct} ${status} ${canRevoke ? `` : ""} `; }).join(""); modal.innerHTML = `

Sessions — ${escapeHtml(username)}

${sessions.some(s => !s.revoked) ? `` : ""}
${rows}
IP Last Active Status Action
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal || e.target.closest("#close-sessions-modal")) { modal.remove(); } }); modal.querySelectorAll(".admin-revoke-session-btn").forEach(btn => { btn.addEventListener("click", async () => { btn.disabled = true; try { const r = await fetch(`/api/sessions/${encodeURIComponent(btn.dataset.sessionId)}`, { method: "DELETE", }); if (!r.ok) throw new Error("Failed"); modal.remove(); viewUserSessions(userId, username); } catch { alert("Failed to revoke session"); btn.disabled = false; } }); }); const revokeAllBtn = document.getElementById("revoke-all-sessions-btn"); if (revokeAllBtn) { revokeAllBtn.addEventListener("click", async () => { if (!confirm(`Revoke all sessions for ${username}? They will be logged out immediately.`)) return; revokeAllBtn.disabled = true; try { const r = await fetch(`/api/users/${userId}/sessions`, { method: "DELETE" }); if (!r.ok) throw new Error("Failed"); modal.remove(); viewUserSessions(userId, username); } catch { alert("Failed to revoke sessions"); revokeAllBtn.disabled = false; } }); } } catch (err) { console.error("View sessions error:", err); alert("Failed to load sessions"); } } function formatRelTime(epochSeconds) { if (!epochSeconds) return "—"; const diff = Math.floor(Date.now() / 1000) - epochSeconds; if (diff < 60) return "Just now"; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } let allPermissions = []; let allGroups = []; let editingGroupId = null; let editingUserId = null; async function loadPermissionsAndGroups() { try { const [permsRes, groupsRes] = await Promise.all([ fetch("/api/permissions"), fetch("/api/permission-groups"), ]); if (permsRes.ok) { const data = await permsRes.json(); allPermissions = Array.isArray(data.permissions) ? data.permissions : []; } if (groupsRes.ok) { const data = await groupsRes.json(); allGroups = Array.isArray(data.groups) ? data.groups : []; } renderGroupsList(); } catch (err) { console.error("Failed to load permissions/groups:", err); } } function renderGroupsList() { const container = document.getElementById("groups-list"); if (!container) return; if (allGroups.length === 0) { container.innerHTML = `

No groups yet. Click "New Group" to create one.

`; return; } container.innerHTML = allGroups .map( (g) => `
${escapeHtml(g.name)}
${g.description ? `
${escapeHtml(g.description)}
` : ""}
${g.permissions.length} permission${g.permissions.length === 1 ? "" : "s"}: ${g.permissions.map((p) => escapeHtml(p)).join(", ") || "none"}
`, ) .join(""); } document.getElementById("groups-list")?.addEventListener("click", (e) => { const btn = e.target.closest(".group-action"); if (!btn) return; const groupId = Number(btn.dataset.groupId); if (btn.dataset.action === "edit") openGroupModal(groupId); else if (btn.dataset.action === "delete") deleteGroup(groupId, btn.dataset.groupName); }); document.getElementById("new-group-btn")?.addEventListener("click", (e) => { e.preventDefault(); openGroupModal(null); }); function openGroupModal(groupId) { editingGroupId = groupId; const modal = document.getElementById("group-modal"); const title = document.getElementById("group-modal-title"); const nameInput = document.getElementById("group-name"); const descInput = document.getElementById("group-description"); const permsList = document.getElementById("group-permissions-list"); const errorBox = document.getElementById("group-error"); errorBox.classList.add("hidden"); let selected = new Set(); if (groupId) { const g = allGroups.find((x) => x.id === groupId); if (g) { title.textContent = `Edit Group: ${g.name}`; nameInput.value = g.name; descInput.value = g.description || ""; selected = new Set(g.permissions); } } else { title.textContent = "New Group"; nameInput.value = ""; descInput.value = ""; } permsList.innerHTML = allPermissions .map( (p) => ` `, ) .join(""); modal.classList.remove("hidden"); } function closeGroupModal() { document.getElementById("group-modal")?.classList.add("hidden"); editingGroupId = null; } document.getElementById("close-group-modal")?.addEventListener("click", closeGroupModal); document.getElementById("group-cancel-btn")?.addEventListener("click", closeGroupModal); document.getElementById("group-form")?.addEventListener("submit", async (e) => { e.preventDefault(); const name = document.getElementById("group-name").value.trim(); const description = document.getElementById("group-description").value.trim(); const permissions = Array.from( document.querySelectorAll("#group-permissions-list input[type=checkbox]:checked"), ).map((el) => el.dataset.perm); const errorBox = document.getElementById("group-error"); errorBox.classList.add("hidden"); try { const url = editingGroupId ? `/api/permission-groups/${editingGroupId}` : "/api/permission-groups"; const method = editingGroupId ? "PATCH" : "POST"; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, description, permissions }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { errorBox.textContent = data.error || "Failed to save group"; errorBox.classList.remove("hidden"); return; } closeGroupModal(); await loadPermissionsAndGroups(); } catch (err) { console.error(err); errorBox.textContent = "Network error"; errorBox.classList.remove("hidden"); } }); async function deleteGroup(groupId, groupName) { if (!confirm(`Delete group "${groupName}"? Users assigned to this group will lose its permissions.`)) return; try { const res = await fetch(`/api/permission-groups/${groupId}`, { method: "DELETE" }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to delete group"); return; } await loadPermissionsAndGroups(); } catch { alert("Network error"); } } async function openUserPermissionsModal(userId, username) { editingUserId = userId; const modal = document.getElementById("user-perms-modal"); const title = document.getElementById("user-perms-title"); const groupsBox = document.getElementById("user-groups-list"); const extrasBox = document.getElementById("user-extras-list"); const errorBox = document.getElementById("user-perms-error"); title.textContent = `Permissions: ${username}`; errorBox.classList.add("hidden"); groupsBox.innerHTML = `

Loading...

`; extrasBox.innerHTML = `

Loading...

`; modal.classList.remove("hidden"); if (allPermissions.length === 0 || allGroups.length === 0) { await loadPermissionsAndGroups(); } try { const [groupsRes, extrasRes] = await Promise.all([ fetch(`/api/users/${userId}/permission-groups`), fetch(`/api/users/${userId}/extra-permissions`), ]); const groupsData = await groupsRes.json().catch(() => ({})); const extrasData = await extrasRes.json().catch(() => ({})); const assignedGroups = new Set(groupsData.groupIds || []); const assignedExtras = new Set(extrasData.permissions || []); groupsBox.innerHTML = allGroups.length === 0 ? `

No groups defined yet.

` : allGroups .map( (g) => ` `, ) .join(""); extrasBox.innerHTML = allPermissions .map( (p) => ` `, ) .join(""); } catch (err) { console.error(err); errorBox.textContent = "Failed to load current assignments"; errorBox.classList.remove("hidden"); } } function closeUserPermsModal() { document.getElementById("user-perms-modal")?.classList.add("hidden"); editingUserId = null; } document.getElementById("close-user-perms-modal")?.addEventListener("click", closeUserPermsModal); document.getElementById("user-perms-cancel-btn")?.addEventListener("click", closeUserPermsModal); document.getElementById("user-perms-save-btn")?.addEventListener("click", async () => { if (!editingUserId) return; const groupIds = Array.from( document.querySelectorAll("#user-groups-list input[type=checkbox]:checked"), ).map((el) => Number(el.dataset.group)); const permissions = Array.from( document.querySelectorAll("#user-extras-list input[type=checkbox]:checked"), ).map((el) => el.dataset.perm); const errorBox = document.getElementById("user-perms-error"); errorBox.classList.add("hidden"); try { const [gRes, eRes] = await Promise.all([ fetch(`/api/users/${editingUserId}/permission-groups`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ groupIds }), }), fetch(`/api/users/${editingUserId}/extra-permissions`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ permissions }), }), ]); if (!gRes.ok || !eRes.ok) { const data = await (gRes.ok ? eRes : gRes).json().catch(() => ({})); errorBox.textContent = data.error || "Failed to save"; errorBox.classList.remove("hidden"); return; } closeUserPermsModal(); } catch { errorBox.textContent = "Network error"; errorBox.classList.remove("hidden"); } }); getCurrentUser(); loadUsers(); loadPermissionsAndGroups();