/** * Users management page. * * Multi-user admin: list, create, edit permissions, delete users. * Requires auth: rw permission. */ import { h, definePage, reactive } from '/static/hoover/index.js'; import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js'; const SUBSYSTEMS = [ { key: 'firewall', label: 'Firewall' }, { key: 'network', label: 'Network' }, { key: 'dhcp', label: 'DHCP' }, { key: 'proxy', label: 'Proxy' }, { key: 'certs', label: 'Certs' }, { key: 'wireguard', label: 'WireGuard' }, { key: 'logs', label: 'Logs' }, { key: 'status', label: 'Status' }, { key: 'auth', label: 'Auth' }, ]; function currentUser() { const u = JSON.parse(sessionStorage.getItem('vw:user') || 'null'); return u ? u.username : ''; } function hasAuthAdmin() { const perms = JSON.parse(sessionStorage.getItem('vw:permissions') || 'null'); return perms && perms.auth === 'rw'; } const state = reactive({ users: [], loading: true, refreshing: false, error: null }); async function loadUsers(abortController) { if (abortController?.signal?.aborted) return; if (state.users.length) state.refreshing = true; else state.loading = true; state.error = null; try { const [usersRes, countsRes] = await Promise.all([ apiFetch('/api/auth/users', { signal: abortController?.signal }), apiFetch('/api/auth/webauthn/credential-counts', { signal: abortController?.signal }), ]); if (abortController?.signal?.aborted) return; if (usersRes.ok) { const credCounts = countsRes.ok ? (countsRes.data || {}) : {}; state.users = (usersRes.data || []).map(u => ({ ...u, credCount: credCounts[u.username] || 0, })); } else { state.error = usersRes.error || 'Failed to load users'; } } catch (e) { if (!abortController?.signal?.aborted) { state.error = 'Failed to load users'; } } finally { if (!abortController?.signal?.aborted) { state.loading = false; state.refreshing = false; } } } function permissionLevel(perms, subsystem) { return perms[subsystem] || '—'; } function openCreateUserModal() { openModal((inner) => { const fields = [ { label: 'Username', id: 'new-username', placeholder: '3-32 chars: letters, digits, dash, underscore' }, { label: 'Password', id: 'new-password', type: 'password', placeholder: 'At least 8 characters' }, ]; // Add subsystem permission selects for (const sub of SUBSYSTEMS) { fields.push({ label: sub.label, id: 'perm-' + sub.key, tag: 'select', options: ['—', 'read', 'rw'], }); } const actions = [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() }, { label: 'Create', cls: 'btn-primary', action: 's', processing: true, handler: async () => { const username = document.getElementById('new-username').value.trim(); const password = document.getElementById('new-password').value; if (!username) { toast('Username is required', 'error'); return; } if (!password || password.length < 8) { toast('Password must be at least 8 characters', 'error'); return; } const perms = {}; for (const sub of SUBSYSTEMS) { const level = document.getElementById('perm-' + sub.key).value; if (level && level !== '—') { perms[sub.key] = level; } } const res = await apiFetch('/api/auth/users', { method: 'POST', body: { username, password, permissions: perms }, }); if (res.ok) { toast('User ' + username + ' created', 'success'); closeModal(); loadUsers(); } else { toast(res.error || 'Failed to create user', 'error'); } }, }, ]; formModal(inner, 'Create User', fields, actions); }); } function openEditPermissionsModal(user) { openModal((inner) => { const perms = user.permissions || {}; const fields = [ { label: 'Username', id: 'edit-username', value: user.username, type: 'text' }, ]; for (const sub of SUBSYSTEMS) { fields.push({ label: sub.label, id: 'edit-perm-' + sub.key, tag: 'select', options: [['', '—'], ['read', 'read'], ['rw', 'rw']], }); } const actions = [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() }, { label: 'Save', cls: 'btn-primary', action: 's', processing: true, handler: async () => { const perms = {}; for (const sub of SUBSYSTEMS) { const level = document.getElementById('edit-perm-' + sub.key).value; if (level && level !== '—') { perms[sub.key] = level; } } const res = await apiFetch('/api/auth/users/' + encodeURIComponent(user.username), { method: 'POST', body: { permissions: perms }, }); if (res.ok) { toast('Permissions updated', 'success'); closeModal(); loadUsers(); } else { toast(res.error || 'Failed to update permissions', 'error'); } }, }, ]; formModal(inner, 'Edit Permissions — ' + esc(user.username), fields, actions); // Pre-select permission values for (const sub of SUBSYSTEMS) { const el = document.getElementById('edit-perm-' + sub.key); if (el) { el.value = perms[sub.key] || ''; } } }); } function UsersPage() { if (state.loading) { return html`
Manage users and permissions
Loading...
Manage users and permissions
${esc(state.error)}