From c93a611898a8223c51272fd103cc37301fd4f61b Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 28 May 2026 16:12:11 +0200 Subject: [PATCH] auth: add Users + Audit admin view in SPA --- containers/clearview/site/app.js | 179 +++++++++++++++++++++++++++ containers/clearview/site/index.html | 4 + containers/clearview/site/styles.css | 9 ++ 3 files changed, 192 insertions(+) diff --git a/containers/clearview/site/app.js b/containers/clearview/site/app.js index f145edc..8c68b08 100644 --- a/containers/clearview/site/app.js +++ b/containers/clearview/site/app.js @@ -59,6 +59,7 @@ function renderUserBadge(me) { 'scan-mailbox': 'New Mailbox Scan', 'scan-entra': 'New Entra Group Scan', 'tenants': 'Tenants', + 'users': 'Users', 'settings': 'Settings', }; @@ -1599,6 +1600,181 @@ function renderUserBadge(me) { if (els.csvEntraForm) els.csvEntraForm.addEventListener('submit', createCsvEntraJob); if (els.allEntraForm) els.allEntraForm.addEventListener('submit', createAllEntraJob); + // ------------------------------------------------------------------------- + // Users + Audit admin view + // ------------------------------------------------------------------------- + + async function renderUsersView() { + const root = document.getElementById('usersViewRoot'); + if (!root) return; + root.innerHTML = '

Users

' + + '
' + + '
' + + '

Audit log

' + + '
'; + document.getElementById('newUserBtn').addEventListener('click', function () { openUserModal(null); }); + await reloadUsersTable(); + await reloadAuditTable(); + } + + async function reloadUsersTable() { + const target = document.getElementById('usersTable'); + if (!target) return; + let rows; + try { + rows = await requestJson('/api/users'); + } catch (e) { + target.innerHTML = '

' + escHtml((e && e.message) || 'Load failed') + '

'; + return; + } + target.innerHTML = ''; + const tbl = document.createElement('table'); + tbl.innerHTML = 'UsernameRoleActiveCreated'; + const tb = document.createElement('tbody'); + rows.forEach(function (u) { + const tr = document.createElement('tr'); + tr.innerHTML = + '' + escHtml(u.username) + '' + + '' + escHtml(u.role) + '' + + '' + (u.is_active ? 'yes' : 'no') + '' + + '' + escHtml(new Date(u.created_at).toLocaleString()) + ''; + const actions = document.createElement('td'); + const edit = Object.assign(document.createElement('button'), { textContent: 'Edit', className: 'btn btn-outline btn-small' }); + edit.addEventListener('click', function () { openUserModal(u); }); + const reset = Object.assign(document.createElement('button'), { textContent: 'Reset pw', className: 'btn btn-outline btn-small' }); + reset.addEventListener('click', function () { openResetModal(u); }); + const del = Object.assign(document.createElement('button'), { textContent: 'Delete', className: 'btn btn-outline btn-small' }); + del.addEventListener('click', async function () { + if (!confirm('Delete ' + u.username + '?')) return; + const r = await fetch('/api/users/' + encodeURIComponent(u.id), { method: 'DELETE', credentials: 'same-origin' }); + if (!r.ok) { alert('Delete failed'); return; } + await reloadUsersTable(); + await reloadAuditTable(); + }); + actions.append(edit, ' ', reset, ' ', del); + tr.append(actions); + tb.append(tr); + }); + tbl.append(tb); + target.append(tbl); + } + + function openUserModal(existing) { + const isNew = !existing; + const usernameSafe = existing ? escHtml(existing.username) : ''; + const html = ''; + const wrap = document.createElement('div'); + wrap.innerHTML = html; + document.body.append(wrap); + const close = function () { wrap.remove(); }; + document.getElementById('muCancel').addEventListener('click', close); + document.getElementById('muSave').addEventListener('click', async function () { + const err = document.getElementById('muError'); + err.hidden = true; + try { + if (isNew) { + await requestJson('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: document.getElementById('muUsername').value, + password: document.getElementById('muPassword').value, + role: document.getElementById('muRole').value, + }), + }); + } else { + await requestJson('/api/users/' + encodeURIComponent(existing.id), { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + role: document.getElementById('muRole').value, + is_active: document.getElementById('muActive').checked, + }), + }); + } + close(); + await reloadUsersTable(); + await reloadAuditTable(); + } catch (e) { + err.textContent = (e && e.message) || 'Save failed'; + err.hidden = false; + } + }); + } + + function openResetModal(u) { + const wrap = document.createElement('div'); + wrap.innerHTML = ''; + document.body.append(wrap); + const close = function () { wrap.remove(); }; + document.getElementById('rpCancel').addEventListener('click', close); + document.getElementById('rpSave').addEventListener('click', async function () { + const err = document.getElementById('rpErr'); + err.hidden = true; + try { + await requestJson('/api/users/' + encodeURIComponent(u.id) + '/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: document.getElementById('rpPw').value }), + }); + close(); + await reloadAuditTable(); + } catch (e) { + err.textContent = (e && e.message) || 'Reset failed'; + err.hidden = false; + } + }); + } + + async function reloadAuditTable() { + const target = document.getElementById('auditTable'); + if (!target) return; + let rows; + try { + rows = await requestJson('/api/audit?limit=100'); + } catch (e) { + target.innerHTML = '

' + escHtml((e && e.message) || 'Load failed') + '

'; + return; + } + target.innerHTML = ''; + const tbl = document.createElement('table'); + tbl.innerHTML = 'WhenEventUserIPDetail'; + const tb = document.createElement('tbody'); + rows.forEach(function (r) { + const tr = document.createElement('tr'); + const detailText = r.detail ? JSON.stringify(r.detail) : ''; + tr.innerHTML = + '' + escHtml(new Date(r.ts).toLocaleString()) + '' + + '' + escHtml(r.event) + '' + + '' + escHtml(r.user_id == null ? '' : String(r.user_id)) + '' + + '' + escHtml(r.ip == null ? '' : String(r.ip)) + '' + + '' + escHtml(detailText) + ''; + tb.append(tr); + }); + tbl.append(tb); + target.append(tbl); + } + // ------------------------------------------------------------------------- // Hash router // ------------------------------------------------------------------------- @@ -1639,6 +1815,9 @@ function renderUserBadge(me) { els.contentTitle.textContent = ROUTE_TITLES[route]; } document.title = 'Clearview | ' + ROUTE_TITLES[route]; + if (route === 'users') { + renderUsersView().catch(function (err) { console.error('Users view failed', err); }); + } // On user navigation, move focus to the new page's first heading so // screen-reader and keyboard users land in the freshly shown content. if (moveFocus && activePage) { diff --git a/containers/clearview/site/index.html b/containers/clearview/site/index.html index 257229e..7caf5c6 100644 --- a/containers/clearview/site/index.html +++ b/containers/clearview/site/index.html @@ -572,6 +572,10 @@ + +