feat: edit user groups + email via delete+recreate

This commit is contained in:
Claus Lohmar 2026-07-11 22:55:08 +01:00
parent 359a0dccb6
commit 6cf084b931
3 changed files with 95 additions and 3 deletions

View file

@ -1,5 +1,11 @@
# Changelog
## 0.1.0.0043 — 2026-07-11
### Added
- Edit user button in Access tab — admin can change email and groups (delete + recreate approach)
- Edit user modal with email, groups fields, and new password display
## 0.1.0.0039 — 2026-07-11
### Changed

View file

@ -1 +1 @@
0.1.0.0039
0.1.0.0043

90
main.go
View file

@ -1316,6 +1316,8 @@ const adminHTML = `<!DOCTYPE html>
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 0.9rem; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; border: none; text-decoration: none; transition: all .12s; }
.btn-primary { background: #1a1a2e; color: #fff; }
.btn-primary:hover { background: #2d3748; }
.btn-edit { background: #fff; color: #3182ce; border: 1px solid #bee3f8; }
.btn-edit:hover { background: #ebf8ff; }
.btn-danger { background: #fff; color: #e53e3e; border: 1px solid #fed7d7; }
.btn-danger:hover { background: #fff5f5; }
.btn-ghost { background: transparent; color: #718096; border: 1px solid #e2e8f0; }
@ -1459,6 +1461,30 @@ const adminHTML = `<!DOCTYPE html>
</div>
</div>
<!-- Edit User Modal -->
<div id="edit-user-modal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;">
<div style="background:#fff;border-radius:12px;padding:2rem;width:500px;max-width:90%;margin:5vh auto;">
<h3 style="margin-bottom:1.5rem;">Edit User</h3>
<p id="edit-username-display" style="font-weight:600;margin-bottom:1rem;"></p>
<form id="edit-user-form" onsubmit="return saveEditUser(event)">
<input type="hidden" name="edit_username" id="edit-username">
<div class="field"><label>Email</label><input type="email" name="edit_email" id="edit-email" required></div>
<div class="field"><label>Groups</label>
<div class="checkbox-group">
<label style="font-size:0.9rem;display:flex;align-items:center;gap:0.3rem;"><input type="checkbox" name="edit_groups" value="users" checked> User (access to all apps)</label>
<label style="font-size:0.9rem;display:flex;align-items:center;gap:0.3rem;"><input type="checkbox" name="edit_groups" value="admins"> Admin (access to config panel)</label>
</div>
</div>
<p style="color:#718096;font-size:0.82rem;margin:0.5rem 0;">The user will receive a new generated password. Share it with them.</p>
<div style="display:flex;gap:0.75rem;margin-top:1.5rem;">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-ghost" onclick="closeEditUserModal()">Cancel</button>
</div>
</form>
<div id="edit-result" style="display:none;margin-top:1rem;"></div>
</div>
</div>
<!-- Security -->
<div id="page-security" class="page hidden">
<div class="page-header"><h2>Security</h2><p>Authentication policies, tokens, and session configuration.</p></div>
@ -1527,8 +1553,9 @@ const adminHTML = `<!DOCTYPE html>
tbody.innerHTML = users.map(u => {
const groups = (u.groups||[]).map(g => '<span class="badge">' + esc(g) + '</span>').join(' ');
const status = u.disabled ? '<span style="color:#e53e3e;font-weight:500;">Disabled</span>' : '<span style="color:#38a169;font-weight:500;">Active</span>';
const del = u.username === 'master' ? '<button class="btn btn-danger btn-sm" disabled title="Cannot delete master">Delete</button>' : '<button class="btn btn-danger btn-sm" onclick="deleteUser(\'' + u.username + '\')">Delete</button>';
return '<tr><td style="padding:12px 16px;border-top:1px solid #edf2f7;"><strong>' + esc(u.username) + '</strong></td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + esc(u.display_name||'') + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + esc(u.email||'') + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + groups + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + status + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + del + '</td></tr>';
const editBtn = '<button class="btn btn-edit btn-sm" onclick="editUser(\'' + u.username + '\',\'' + esc(u.email||'') + '\',\'' + (u.groups||[]).join(',') + '\')">Edit</button>';
const delBtn = u.username === 'master' ? '<button class="btn btn-danger btn-sm" disabled title="Cannot delete master">Delete</button>' : '<button class="btn btn-danger btn-sm" onclick="deleteUser(\'' + u.username + '\')">Delete</button>';
return '<tr><td style="padding:12px 16px;border-top:1px solid #edf2f7;"><strong>' + esc(u.username) + '</strong></td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + esc(u.display_name||'') + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + esc(u.email||'') + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + groups + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + status + '</td><td style="padding:12px 16px;border-top:1px solid #edf2f7;">' + editBtn + ' ' + delBtn + '</td></tr>';
}).join('');
}
@ -1560,6 +1587,65 @@ const adminHTML = `<!DOCTYPE html>
else alert('Failed to delete user');
}
function editUser(username, email, groups) {
document.getElementById('edit-username').value = username;
document.getElementById('edit-username-display').textContent = 'Editing: ' + username;
document.getElementById('edit-email').value = email;
const groupList = groups.split(',');
document.querySelectorAll('#edit-user-form input[name="edit_groups"]').forEach(cb => {
cb.checked = groupList.includes(cb.value);
});
document.getElementById('edit-result').style.display = 'none';
document.getElementById('edit-user-modal').style.display = 'block';
}
function closeEditUserModal() {
document.getElementById('edit-user-modal').style.display = 'none';
}
async function saveEditUser(event) {
event.preventDefault();
const username = document.getElementById('edit-username').value;
const email = document.getElementById('edit-email').value;
const groups = [];
document.querySelectorAll('#edit-user-form input[name="edit_groups"]:checked').forEach(cb => {
groups.push(cb.value);
});
// 1. Delete user
const delResp = await fetch('/api/users/' + username, { method: 'DELETE' });
if (!delResp.ok) { alert('Failed to delete user for re-creation'); return; }
// 2. Recreate with new data
const body = JSON.stringify({
users: [{
username: username,
display_name: username,
email: email,
groups: groups
}]
});
const createResp = await fetch('/api/users/bulk', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: body
});
const result = await createResp.json();
const resultDiv = document.getElementById('edit-result');
resultDiv.style.display = 'block';
if (createResp.ok && result.users && result.users[0]) {
const pwd = result.users[0].placeholder_password || '(unchanged)';
resultDiv.innerHTML = '<div style="padding:0.75rem 1rem;background:#f0fff4;border:1px solid #c6f6d5;border-radius:8px;color:#276749;font-size:0.88rem;"> User updated.<br>New password: <code style="background:#edf2f7;padding:0.15rem 0.4rem;border-radius:4px;font-size:0.82rem;">' + pwd + '</code><br>Share this with the user.</div>';
closeEditUserModal();
loadUsers();
} else {
resultDiv.innerHTML = '<div style="padding:0.75rem 1rem;background:#fff5f5;border:1px solid #fed7d7;border-radius:8px;color:#c53030;font-size:0.88rem;"> Failed to recreate user: ' + JSON.stringify(result) + '</div>';
}
return false;
}
function showCreateModal() { document.getElementById('createModal').style.display = 'block'; }
function closeCreateModal() { document.getElementById('createModal').style.display = 'none'; }