// Cached user list — used by groups table to populate "add member" selects let _cachedUsers = []; async function loadUsers() { const res = await apiFetch('/api/users'); if (!res) return; _cachedUsers = await res.json(); const tb = document.getElementById('usersTableBody'); if (!_cachedUsers.length) { tb.innerHTML = `No users found.`; return; } tb.innerHTML = _cachedUsers.map(u => { const nameJson = esc(JSON.stringify(u.name)); return ` ${esc(u.name)} ${u.uid} ${u.smb ? 'Active' : 'No'} `; }).join(''); } async function createUser() { const name = document.getElementById('newUsername').value.trim(); const password = document.getElementById('newPassword').value; if (!name || !password) return showToast('Username and password are required.', 'danger'); const res = await apiFetch('/api/users', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, password}), }); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`User "${name}" created.`); document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; loadUsers(); loadGroups(); } function confirmDeleteUser(name) { document.getElementById('deleteModalMsg').textContent = `Are you sure you want to delete user "${name}"? This removes both the system account and SMB access.`; const modal = new bootstrap.Modal(document.getElementById('deleteModal')); const btn = document.getElementById('deleteModalConfirm'); const handler = async () => { btn.removeEventListener('click', handler); modal.hide(); const res = await apiFetch(`/api/users/${encodeURIComponent(name)}`, { method: 'DELETE' }); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`User "${name}" deleted.`); loadUsers(); loadGroups(); }; btn.addEventListener('click', handler); modal.show(); } function openPwModal(name) { document.getElementById('pwUsername').value = name; document.getElementById('pwNew').value = ''; document.getElementById('pwConfirm').value = ''; new bootstrap.Modal(document.getElementById('pwModal')).show(); } async function submitPassword() { const name = document.getElementById('pwUsername').value; const pw = document.getElementById('pwNew').value; const pwConfirm = document.getElementById('pwConfirm').value; if (pw.length < 8) return showToast('Password must be at least 8 characters.', 'danger'); if (pw !== pwConfirm) return showToast('Passwords do not match.', 'danger'); const res = await apiFetch(`/api/users/${encodeURIComponent(name)}/password`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({password: pw}), }); if (!res) return; const data = await res.json(); bootstrap.Modal.getInstance(document.getElementById('pwModal')).hide(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`Password updated for "${name}".`); } // ===== GROUPS ===== async function loadGroups() { const res = await apiFetch('/api/groups'); if (!res) return; const groups = await res.json(); const tb = document.getElementById('groupsTableBody'); if (!groups.length) { tb.innerHTML = `No groups found.`; return; } tb.innerHTML = groups.map(g => { const gJson = esc(JSON.stringify(g.name)); const memberBadges = g.members.length ? g.members.map(m => ` ${esc(m)} `).join('') : `—`; const nonMembers = _cachedUsers.filter(u => !g.members.includes(u.name)); const addSelect = nonMembers.length ? ` ` : ''; return ` ${esc(g.name)} ${g.gid} ${memberBadges} ${addSelect} `; }).join(''); } async function createGroup() { const name = document.getElementById('newGroupName').value.trim(); if (!name) return showToast('Group name is required.', 'danger'); const res = await apiFetch('/api/groups', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name}), }); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`Group "${name}" created.`); document.getElementById('newGroupName').value = ''; loadGroups(); } function confirmDeleteGroup(name) { document.getElementById('deleteModalMsg').textContent = `Delete group "${name}"? Users in this group won't be deleted, just removed from it.`; const modal = new bootstrap.Modal(document.getElementById('deleteModal')); const btn = document.getElementById('deleteModalConfirm'); const handler = async () => { btn.removeEventListener('click', handler); modal.hide(); const res = await apiFetch(`/api/groups/${encodeURIComponent(name)}`, { method: 'DELETE' }); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`Group "${name}" deleted.`); loadGroups(); }; btn.addEventListener('click', handler); modal.show(); } async function addMember(groupName) { const sel = document.getElementById(`addMember-${groupName}`); if (!sel) return; const user = sel.value; const res = await apiFetch(`/api/groups/${encodeURIComponent(groupName)}/members`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user}), }); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`${user} added to ${groupName}.`); loadGroups(); } async function removeMember(groupName, userName) { const res = await apiFetch( `/api/groups/${encodeURIComponent(groupName)}/members/${encodeURIComponent(userName)}`, { method: 'DELETE' } ); if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`${userName} removed from ${groupName}.`); loadGroups(); }