- Removed PATCH /api/users/<name> route (rename_user) - Removed renameModal from index.html - Removed openRenameModal/submitRename from users.js - Removed Rename button from users table - Removed rename docs from README - Added green info box in WebDAV edit modal: set group=www-data, mode=775 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
236 lines
9.9 KiB
JavaScript
236 lines
9.9 KiB
JavaScript
// 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 = `<tr><td colspan="4" style="color:var(--muted)">No users found.</td></tr>`;
|
|
return;
|
|
}
|
|
tb.innerHTML = _cachedUsers.map(u => {
|
|
const nameJson = esc(JSON.stringify(u.name));
|
|
return `<tr>
|
|
<td>${esc(u.name)}</td>
|
|
<td style="color:var(--muted)">${u.uid}</td>
|
|
<td>
|
|
<span class="badge rounded-pill"
|
|
style="${u.smb ? 'background:#1a3a1f; color:var(--green)' : 'background:var(--surf2); color:var(--muted)'}">
|
|
${u.smb ? 'Active' : 'No'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
|
onclick="openPwModal(${nameJson})">Password</button>
|
|
<button class="btn btn-sm btn-danger"
|
|
onclick="confirmDeleteUser(${nameJson})">Delete</button>
|
|
</td>
|
|
</tr>`;
|
|
}).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}".`);
|
|
}
|
|
|
|
|
|
function openAdminSettings() {
|
|
document.getElementById('adminCurrentPw').value = '';
|
|
document.getElementById('adminNewUser').value = '';
|
|
document.getElementById('adminNewPw').value = '';
|
|
document.getElementById('adminNewPwConfirm').value = '';
|
|
new bootstrap.Modal(document.getElementById('adminModal')).show();
|
|
}
|
|
|
|
async function submitAdminSettings() {
|
|
const currentPw = document.getElementById('adminCurrentPw').value;
|
|
const newUser = document.getElementById('adminNewUser').value.trim();
|
|
const newPw = document.getElementById('adminNewPw').value;
|
|
const newPwConf = document.getElementById('adminNewPwConfirm').value;
|
|
|
|
if (!currentPw) return showToast('Current password is required.', 'danger');
|
|
if (newPw && newPw !== newPwConf) return showToast('New passwords do not match.', 'danger');
|
|
if (newPw && newPw.length < 8) return showToast('Password must be at least 8 characters.', 'danger');
|
|
|
|
const res = await apiFetch('/api/admin/credentials', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({currentPassword: currentPw, username: newUser, password: newPw}),
|
|
});
|
|
if (!res) return;
|
|
const data = await res.json();
|
|
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
|
|
bootstrap.Modal.getInstance(document.getElementById('adminModal')).hide();
|
|
showToast('Credentials updated — please log in again.');
|
|
setTimeout(() => { window.location.href = '/login'; }, 1500);
|
|
}
|
|
|
|
// ===== 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 = `<tr><td colspan="4" style="color:var(--muted)">No groups found.</td></tr>`;
|
|
return;
|
|
}
|
|
tb.innerHTML = groups.map(g => {
|
|
const gJson = esc(JSON.stringify(g.name));
|
|
const memberBadges = g.members.length
|
|
? g.members.map(m => `
|
|
<span class="badge me-1 mb-1 d-inline-flex align-items-center gap-1"
|
|
style="background:var(--surf2); color:var(--text); font-weight:400">
|
|
${esc(m)}
|
|
<button onclick="removeMember(${gJson},${esc(JSON.stringify(m))})"
|
|
style="background:none; border:none; color:var(--muted); cursor:pointer; padding:0; line-height:1; font-size:.8rem">×</button>
|
|
</span>`).join('')
|
|
: `<span style="color:var(--muted)">—</span>`;
|
|
|
|
const nonMembers = _cachedUsers.filter(u => !g.members.includes(u.name));
|
|
const addSelect = nonMembers.length
|
|
? `<select id="addMember-${esc(g.name)}" class="form-select form-select-sm d-inline-block me-1" style="width:auto; max-width:130px">
|
|
${nonMembers.map(u => `<option value="${esc(u.name)}">${esc(u.name)}</option>`).join('')}
|
|
</select>
|
|
<button class="btn btn-sm me-2" style="border:1px solid var(--border); color:var(--muted)"
|
|
onclick="addMember(${gJson})">Add</button>`
|
|
: '';
|
|
|
|
return `<tr>
|
|
<td>${esc(g.name)}</td>
|
|
<td style="color:var(--muted)">${g.gid}</td>
|
|
<td>${memberBadges}</td>
|
|
<td>
|
|
${addSelect}
|
|
<button class="btn btn-sm btn-danger"
|
|
onclick="confirmDeleteGroup(${gJson})">Delete</button>
|
|
</td>
|
|
</tr>`;
|
|
}).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();
|
|
}
|