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 = '| Username | Role | Active | Created | |
';
+ 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 = '' +
+ '
' + (isNew ? 'New user' : 'Edit ' + usernameSafe) + '
' +
+ '
' +
+ (isNew ? '
' : '') +
+ '
' +
+ (isNew ? '' : '
') +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
';
+ 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 = '' +
+ '
Reset password for ' + escHtml(u.username) + '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
';
+ 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 = '| When | Event | User | IP | Detail |
';
+ 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 @@
+
+