let currentUser = null; let users = []; const usersTableBody = document.getElementById("users-table-body"); const addUserBtn = document.getElementById("add-user-btn"); 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 = "/"; } } else { window.location.href = "/login.html"; } } catch (err) { console.error("Failed to get current user:", err); window.location.href = "/login.html"; } } 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() { if (users.length === 0) { usersTableBody.innerHTML = ` No users found `; return; } usersTableBody.innerHTML = users .map( (user) => `
${escapeHtml(user.username)} ${user.id === currentUser?.userId ? '(You)' : ""}
${getRoleBadge(user.role)} ${user.role !== "admin" ? ` ${user.can_build ? 'Build' : 'No Build'} ` : ''} ${formatDate(user.created_at)} ${user.last_login ? formatDate(user.last_login) : 'Never'} ${escapeHtml(user.created_by || "System")}
${ user.id !== currentUser?.userId ? ` ` : 'Cannot edit yourself' }
`, ) .join(""); attachActionListeners(); } 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 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; } }; usersTableBody.addEventListener("click", listener); usersTableBody._actionListener = listener; } function formatDate(timestamp) { const date = new Date(timestamp); return date.toLocaleString(); } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } 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); 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()}`; }; 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."); } }; getCurrentUser(); loadUsers();