settings: move Users + Audit under Settings tabs

This commit is contained in:
Ivo Oskamp 2026-05-28 16:31:35 +02:00
parent 2923f350f8
commit 3f225660c8
4 changed files with 102 additions and 33 deletions

View File

@ -35,8 +35,9 @@ window.__authReady = (async function authGate() {
function applyAuthUi(me) {
renderUserBadge(me);
if (me.role !== 'admin') {
const usersLink = document.querySelector('[data-route="users"]');
if (usersLink) usersLink.style.display = 'none';
document.querySelectorAll('[data-admin-only]').forEach(function (el) {
el.style.display = 'none';
});
}
}
@ -75,7 +76,6 @@ function renderUserBadge(me) {
'scan-mailbox': 'New Mailbox Scan',
'scan-entra': 'New Entra Group Scan',
'tenants': 'Tenants',
'users': 'Users',
'settings': 'Settings',
};
@ -1620,17 +1620,13 @@ function renderUserBadge(me) {
// Users + Audit admin view
// -------------------------------------------------------------------------
async function renderUsersView() {
const root = document.getElementById('usersViewRoot');
if (!root) return;
root.innerHTML = '<section class="users-view"><h2>Users</h2>' +
'<div class="users-toolbar"><button id="newUserBtn" class="btn btn-primary">New user</button></div>' +
'<div id="usersTable"></div>' +
'<h3 style="margin-top:24px">Audit log</h3>' +
'<div id="auditTable"></div></section>';
document.getElementById('newUserBtn').addEventListener('click', function () { openUserModal(null); });
await reloadUsersTable();
await reloadAuditTable();
let usersTabWired = false;
function wireUsersTabOnce() {
if (usersTabWired) return;
const newBtn = document.getElementById('newUserBtn');
if (!newBtn) return;
newBtn.addEventListener('click', function () { openUserModal(null); });
usersTabWired = true;
}
async function reloadUsersTable() {
@ -1795,20 +1791,23 @@ function renderUserBadge(me) {
// Hash router
// -------------------------------------------------------------------------
const SETTINGS_TABS = { 'general': 1, 'users': 1, 'audit': 1 };
function parseRoute() {
var hash = (window.location.hash || '').replace(/^#\/?/, '');
if (!hash) return 'dashboard';
if (hash.indexOf('/') !== -1) {
if (!hash) return { route: 'dashboard', sub: null };
var parts = hash.split('/');
if (parts[0] === 'scan' && parts[1]) return 'scan-' + parts[1];
return parts[0];
}
return hash;
if (parts[0] === 'scan' && parts[1]) return { route: 'scan-' + parts[1], sub: null };
if (parts[0] === 'settings') return { route: 'settings', sub: parts[1] || null };
return { route: parts[0], sub: null };
}
function applyRoute(route, moveFocus) {
function applyRoute(parsed, moveFocus) {
var route = parsed && parsed.route;
var sub = parsed && parsed.sub;
if (!ROUTE_TITLES[route]) {
route = 'dashboard';
sub = null;
}
state.currentRoute = route;
var activePage = null;
@ -1831,8 +1830,11 @@ 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); });
if (route === 'settings') {
if (!SETTINGS_TABS[sub]) sub = 'general';
var isAdmin = window.__clearviewUser && window.__clearviewUser.role === 'admin';
if ((sub === 'users' || sub === 'audit') && !isAdmin) sub = 'general';
applySettingsTab(sub);
}
// On user navigation, move focus to the new page's first heading so
// screen-reader and keyboard users land in the freshly shown content.
@ -1845,6 +1847,31 @@ function renderUserBadge(me) {
}
}
function applySettingsTab(sub) {
document.querySelectorAll('.settings-tab').forEach(function (el) {
if (el.getAttribute('data-settings-tab') === sub) {
el.classList.add('active');
el.setAttribute('aria-selected', 'true');
} else {
el.classList.remove('active');
el.setAttribute('aria-selected', 'false');
}
});
document.querySelectorAll('.settings-pane').forEach(function (el) {
if (el.getAttribute('data-settings-pane') === sub) {
el.removeAttribute('hidden');
} else {
el.setAttribute('hidden', '');
}
});
if (sub === 'users') {
wireUsersTabOnce();
reloadUsersTable().catch(function (err) { console.error('users reload failed', err); });
} else if (sub === 'audit') {
reloadAuditTable().catch(function (err) { console.error('audit reload failed', err); });
}
}
function navigateTo(route) {
var hash;
if (route === 'scan-sharepoint') hash = '#/scan/sharepoint';
@ -1854,7 +1881,7 @@ function renderUserBadge(me) {
if (window.location.hash !== hash) {
window.location.hash = hash;
} else {
applyRoute(route, true);
applyRoute({ route: route, sub: null }, true);
}
}

View File

@ -32,7 +32,6 @@
<div class="nav-section">Entra</div>
<a href="#/scan/entra" class="nav-link" data-route="scan-entra">New Entra Scan</a>
<a href="#/users" class="nav-link" data-route="users">Users</a>
<div class="nav-spacer"></div>
<a href="#/tenants" class="nav-link" data-route="tenants">Tenants</a>
<a href="#/settings" class="nav-link" data-route="settings">Settings</a>
@ -572,17 +571,30 @@
<!-- =================================================================== -->
<!-- Route: Settings (placeholder) -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="users" hidden>
<div id="usersViewRoot"></div>
</section>
<section class="route-page" data-route-page="settings" hidden>
<div class="panel">
<div class="panel-header split">
<h2>Settings</h2>
</div>
<nav class="settings-tabs" role="tablist">
<a href="#/settings/general" class="settings-tab" data-settings-tab="general" role="tab">General</a>
<a href="#/settings/users" class="settings-tab" data-settings-tab="users" data-admin-only role="tab">Users</a>
<a href="#/settings/audit" class="settings-tab" data-settings-tab="audit" data-admin-only role="tab">Audit log</a>
</nav>
<div class="settings-pane" data-settings-pane="general">
<p class="setup-hint">Runtime configuration is currently controlled via environment variables in <code>stack/.env</code>. See the <strong>TECHNICAL.md</strong> document for the full list (timeouts, retries, scan caps, onboarding).</p>
</div>
<div class="settings-pane" data-settings-pane="users" hidden>
<div class="users-toolbar"><button id="newUserBtn" class="btn btn-primary" type="button">New user</button></div>
<div id="usersTable"></div>
</div>
<div class="settings-pane" data-settings-pane="audit" hidden>
<div id="auditTable"></div>
</div>
</div>
</section>
</main>

View File

@ -824,6 +824,29 @@ html[data-auth-pending] body { visibility: hidden; }
.modal input { padding: 8px 10px; background: var(--cv-white); border: 1px solid var(--cv-border); border-radius: 6px; color: var(--cv-text-primary); font: inherit; }
.modal select { padding: 8px 10px; border: 1px solid var(--cv-border); border-radius: 6px; background: var(--cv-white); color: var(--cv-text-primary); font: inherit; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
.users-view table { width: 100%; border-collapse: collapse; }
.users-view th, .users-view td { padding: 8px 10px; border-bottom: 1px solid var(--cv-border); text-align: left; color: var(--cv-text-primary); }
.users-view table,
.settings-pane table { width: 100%; border-collapse: collapse; }
.users-view th, .users-view td,
.settings-pane th, .settings-pane td { padding: 8px 10px; border-bottom: 1px solid var(--cv-border); text-align: left; color: var(--cv-text-primary); }
.users-toolbar { margin: 12px 0; }
/* Settings sub-tabs */
.settings-tabs {
display: flex; gap: 4px; margin: 6px 0 18px;
border-bottom: 1px solid var(--cv-border);
}
.settings-tab {
padding: 8px 14px;
color: var(--cv-text-secondary);
text-decoration: none;
border-bottom: 2px solid transparent;
font-size: 14px;
font-weight: 500;
transition: color 120ms ease, border-color 120ms ease;
}
.settings-tab:hover { color: var(--cv-text-primary); }
.settings-tab.active {
color: var(--cv-accent-dark);
border-bottom-color: var(--cv-accent);
}
.settings-pane { padding-top: 4px; }

View File

@ -2,6 +2,13 @@
This file documents changes on the develop branch of this project.
## 2026-05-28 — Settings: move Users + Audit under Settings tabs
### Changed
- Removed the top-level **Users** link from the sidebar.
- `Settings` page is now tabbed: **General** (env-var pointer), **Users** (user management), **Audit log** (latest 100 events). Sub-routes `#/settings/general`, `#/settings/users`, `#/settings/audit` are direct-linkable.
- Non-admins only see the **General** tab; the Users/Audit tabs are hidden client-side and the backend still enforces `require_admin`.
## 2026-05-28 — Authentication: fix login loop on HTTP-only deployment
### Changed