// 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'}
Password
Delete
`;
}).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
? `
${nonMembers.map(u => `${esc(u.name)} `).join('')}
Add `
: '';
return `
${esc(g.name)}
${g.gid}
${memberBadges}
${addSelect}
Delete
`;
}).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();
}