Replace login-style 'form-group' class (absolute-positioned floating labels) with dashboard-standard 'form-control' class (block labels, proper input styling). Add missing 'modal-title' class to headings and remove incorrect 'form-control' class from individual inputs.
288 lines
13 KiB
JavaScript
288 lines
13 KiB
JavaScript
// User Management Page
|
|
|
|
(function () {
|
|
let usersData = [];
|
|
|
|
window.initUsers = async function () {
|
|
await loadUsers();
|
|
setupEventListeners();
|
|
};
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
usersData = await window.api.get('/users');
|
|
renderUsersTable();
|
|
} catch (err) {
|
|
console.error('Failed to load users:', err);
|
|
const tbody = document.querySelector('#users-table tbody');
|
|
if (tbody) {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-center" style="padding:2rem;color:var(--red);">Failed to load users: ${err.message}</td></tr>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderUsersTable() {
|
|
const tbody = document.querySelector('#users-table tbody');
|
|
if (!tbody) return;
|
|
|
|
if (!usersData || usersData.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center" style="padding:2rem;color:var(--fg4);">No users found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = usersData.map(user => {
|
|
const roleBadge = user.role === 'admin'
|
|
? '<span class="badge badge-success">Admin</span>'
|
|
: '<span class="badge badge-info">Viewer</span>';
|
|
|
|
const statusBadge = user.must_change_password
|
|
? '<span class="badge badge-warning">Must Change Password</span>'
|
|
: '<span class="badge badge-success">Active</span>';
|
|
|
|
const created = user.created_at
|
|
? luxon.DateTime.fromISO(user.created_at).toRelative()
|
|
: 'Unknown';
|
|
|
|
return `
|
|
<tr>
|
|
<td><strong>${escapeHtml(user.username)}</strong></td>
|
|
<td>${escapeHtml(user.display_name || user.username)}</td>
|
|
<td>${roleBadge}</td>
|
|
<td>${created}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm btn-secondary" onclick="window._editUser(${user.id})" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="window._resetUserPassword(${user.id}, '${escapeHtml(user.username)}')" title="Reset Password">
|
|
<i class="fas fa-key"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="window._deleteUser(${user.id}, '${escapeHtml(user.username)}')" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
const addBtn = document.getElementById('add-user');
|
|
if (addBtn) {
|
|
addBtn.onclick = () => showCreateModal();
|
|
}
|
|
}
|
|
|
|
// ── Create User Modal ──────────────────────────────────────────
|
|
|
|
function showCreateModal() {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'modal active';
|
|
overlay.innerHTML = `
|
|
<div class="modal-content" style="max-width: 480px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Create New User</h3>
|
|
<button class="modal-close" id="user-modal-close"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="new-username">Username</label>
|
|
<input type="text" id="new-username" placeholder="e.g. jsmith" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="new-display-name">Display Name (optional)</label>
|
|
<input type="text" id="new-display-name" placeholder="e.g. John Smith">
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="new-password">Password</label>
|
|
<input type="password" id="new-password" placeholder="Minimum 4 characters" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="new-role">Role</label>
|
|
<select id="new-role">
|
|
<option value="viewer">Viewer (read-only)</option>
|
|
<option value="admin">Admin (full access)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="user-modal-cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="user-modal-save">
|
|
<i class="fas fa-user-plus"></i> Create User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
|
|
document.getElementById('user-modal-close').onclick = () => overlay.remove();
|
|
document.getElementById('user-modal-cancel').onclick = () => overlay.remove();
|
|
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
|
|
document.getElementById('user-modal-save').onclick = async () => {
|
|
const username = document.getElementById('new-username').value.trim();
|
|
const display_name = document.getElementById('new-display-name').value.trim() || null;
|
|
const password = document.getElementById('new-password').value;
|
|
const role = document.getElementById('new-role').value;
|
|
|
|
if (!username) {
|
|
window.authManager.showToast('Username is required', 'error');
|
|
return;
|
|
}
|
|
if (password.length < 4) {
|
|
window.authManager.showToast('Password must be at least 4 characters', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await window.api.post('/users', { username, password, display_name, role });
|
|
overlay.remove();
|
|
window.authManager.showToast(`User '${username}' created`, 'success');
|
|
await loadUsers();
|
|
} catch (err) {
|
|
window.authManager.showToast(err.message, 'error');
|
|
}
|
|
};
|
|
|
|
document.getElementById('new-username').focus();
|
|
}
|
|
|
|
// ── Edit User Modal ────────────────────────────────────────────
|
|
|
|
window._editUser = function (id) {
|
|
const user = usersData.find(u => u.id === id);
|
|
if (!user) return;
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'modal active';
|
|
overlay.innerHTML = `
|
|
<div class="modal-content" style="max-width: 480px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Edit User: ${escapeHtml(user.username)}</h3>
|
|
<button class="modal-close" id="edit-modal-close"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="edit-display-name">Display Name</label>
|
|
<input type="text" id="edit-display-name" value="${escapeHtml(user.display_name || '')}">
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="edit-role">Role</label>
|
|
<select id="edit-role">
|
|
<option value="viewer" ${user.role === 'viewer' ? 'selected' : ''}>Viewer (read-only)</option>
|
|
<option value="admin" ${user.role === 'admin' ? 'selected' : ''}>Admin (full access)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="edit-modal-cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="edit-modal-save">
|
|
<i class="fas fa-save"></i> Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
|
|
document.getElementById('edit-modal-close').onclick = () => overlay.remove();
|
|
document.getElementById('edit-modal-cancel').onclick = () => overlay.remove();
|
|
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
|
|
document.getElementById('edit-modal-save').onclick = async () => {
|
|
const display_name = document.getElementById('edit-display-name').value.trim() || null;
|
|
const role = document.getElementById('edit-role').value;
|
|
|
|
try {
|
|
await window.api.put(`/users/${id}`, { display_name, role });
|
|
overlay.remove();
|
|
window.authManager.showToast('User updated', 'success');
|
|
await loadUsers();
|
|
} catch (err) {
|
|
window.authManager.showToast(err.message, 'error');
|
|
}
|
|
};
|
|
};
|
|
|
|
// ── Reset Password Modal ───────────────────────────────────────
|
|
|
|
window._resetUserPassword = function (id, username) {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'modal active';
|
|
overlay.innerHTML = `
|
|
<div class="modal-content" style="max-width: 420px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Reset Password: ${escapeHtml(username)}</h3>
|
|
<button class="modal-close" id="pw-modal-close"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="reset-password">New Password</label>
|
|
<input type="password" id="reset-password" placeholder="Minimum 4 characters">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="checkbox-label" style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
|
<input type="checkbox" id="reset-must-change" checked style="width:auto;">
|
|
<span>Require password change on next login</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="pw-modal-cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="pw-modal-save">
|
|
<i class="fas fa-key"></i> Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
|
|
document.getElementById('pw-modal-close').onclick = () => overlay.remove();
|
|
document.getElementById('pw-modal-cancel').onclick = () => overlay.remove();
|
|
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
|
|
document.getElementById('pw-modal-save').onclick = async () => {
|
|
const password = document.getElementById('reset-password').value;
|
|
const must_change_password = document.getElementById('reset-must-change').checked;
|
|
|
|
if (password.length < 4) {
|
|
window.authManager.showToast('Password must be at least 4 characters', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await window.api.put(`/users/${id}`, { password, must_change_password });
|
|
overlay.remove();
|
|
window.authManager.showToast(`Password reset for '${username}'`, 'success');
|
|
await loadUsers();
|
|
} catch (err) {
|
|
window.authManager.showToast(err.message, 'error');
|
|
}
|
|
};
|
|
|
|
document.getElementById('reset-password').focus();
|
|
};
|
|
|
|
// ── Delete User ────────────────────────────────────────────────
|
|
|
|
window._deleteUser = function (id, username) {
|
|
if (!confirm(`Delete user '${username}'? This action cannot be undone.`)) return;
|
|
|
|
window.api.delete(`/users/${id}`).then(() => {
|
|
window.authManager.showToast(`User '${username}' deleted`, 'success');
|
|
loadUsers();
|
|
}).catch(err => {
|
|
window.authManager.showToast(err.message, 'error');
|
|
});
|
|
};
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
})();
|