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" },
hvnc: { label: "HVNC", 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) ? `` : ""}
| IP |
Last Active |
Status |
Action |
${rows}
`;
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();