Files
wmantly dfafffe154 Add super/jump admin, per-host connection tracking; move stats to Audit
- app_super_admin (cross-app, also recognized by sso-manager-node/proxy)
  and a new app_jump_admin group are added to conf.auth: super admins are
  full admins here same as app_sso_admin; jump admins get audit page/data
  access without other admin rights (isJumpAdmin/requireJumpAdmin in
  middleware/auth.js, wired into routes/api.js's audit-data gate and the
  /audit page's client-side forceLogin -- previously the page shell
  rendered for any logged-in user, only the data was gated).
- Dashboard: moved the stat boxes and Top hosts/Top users cards to the
  Audit page (audit is now the admin-facing metrics home; dashboard stays
  focused on "hosts I can reach"). Renamed "All hosts" to "My hosts".
- Host list now shows Last connection/Last failed connection columns and
  highlights rows green (live session, from session_registry) or yellow
  (most recent attempt failed) -- backed by new per-host last-success/
  last-fail timestamps in models/metrics.js, populated by ssh_server.js
  (which now attributes grammar/TUI connect failures to the resolved host
  when one was found, not just aggregate counters) and surfaced through
  GET /api/user/hosts (routes/user.js).
2026-07-30 11:58:28 -04:00

127 lines
5.1 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%- include('top') %>
<script type="text/javascript">app.auth.forceLogin(['admin', 'app_jump_admin']);</script>
<div class="container mt-4">
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center"><div class="card-body">
<div class="display-6" id="stat-active"></div>
<div class="text-muted small text-uppercase">Active sessions</div>
</div></div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center"><div class="card-body">
<div class="display-6" id="stat-total"></div>
<div class="text-muted small text-uppercase">Total connections</div>
</div></div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center"><div class="card-body">
<div class="display-6 text-danger" id="stat-fail"></div>
<div class="text-muted small text-uppercase">Failed</div>
</div></div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm text-center"><div class="card-body">
<div class="display-6" id="stat-users"></div>
<div class="text-muted small text-uppercase">Users seen</div>
</div></div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card shadow-sm"><div class="card-header"><i class="fa-solid fa-server me-1"></i> Top hosts</div>
<table class="table table-sm mb-0"><tbody id="top-hosts"></tbody></table>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm"><div class="card-header"><i class="fa-solid fa-user me-1"></i> Top users</div>
<table class="table table-sm mb-0"><tbody id="top-users"></tbody></table>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header"><i class="fa-solid fa-clipboard-list me-1"></i> Audit log</div>
<div class="card-body pb-0">
<form class="row g-2 mb-2" onsubmit="applyFilters(); return false;">
<div class="col-auto"><input class="form-control form-control-sm" id="f-uid" placeholder="user"></div>
<div class="col-auto"><input class="form-control form-control-sm" id="f-target" placeholder="target"></div>
<div class="col-auto">
<select class="form-select form-select-sm" id="f-status">
<option value="">any result</option>
<option value="success">success</option>
<option value="fail">fail</option>
</select>
</div>
<div class="col-auto"><button class="btn btn-sm btn-primary">Filter</button></div>
</form>
</div>
<div class="table-responsive">
<table class="table table-striped table-sm mb-0">
<thead><tr><th>Time</th><th>User</th><th>Method</th><th>Mode</th><th>Target</th><th>Chan</th><th>Client</th><th>Result</th><th class="text-end">Bytes</th></tr></thead>
<tbody id="audit-body"></tbody>
</table>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<button class="btn btn-sm btn-outline-secondary" id="prev" onclick="changePage(-1)">&larr; prev</button>
<span class="text-muted small" id="page-info"></span>
<button class="btn btn-sm btn-outline-secondary" id="next" onclick="changePage(1)">next &rarr;</button>
</div>
</div>
</div>
<script type="text/javascript">
function rows(sel, list){
var $b = $(sel).empty();
if(!list || !list.length){ $b.append('<tr><td class="text-muted">No data.</td></tr>'); return; }
list.forEach(function(x){
$b.append('<tr><td>' + app.jump.esc(x.name) + '</td><td class="text-end">' + x.count + '</td></tr>');
});
}
function loadMetrics(){
app.jump.metrics(function(error, data){
if(error || !data) return;
$('#stat-active').text(data.active);
$('#stat-total').text(data.total);
$('#stat-fail').text(data.fail);
$('#stat-users').text((data.topUsers || []).length);
rows('#top-hosts', data.topHosts);
rows('#top-users', data.topUsers);
});
}
var page = 0;
function filters(){ return {page: page, uid: $('#f-uid').val(), target: $('#f-target').val(), status: $('#f-status').val()}; }
function applyFilters(){ page = 0; load(); }
function changePage(d){ page = Math.max(0, page + d); load(); }
function load(){
app.jump.audit(filters(), function(error, data){
var $b = $('#audit-body').empty();
if(error || !data || !data.results.length){ $b.append('<tr><td colspan="9" class="text-muted">No events.</td></tr>'); }
else data.results.forEach(function(e){
$b.append('<tr class="' + (e.success ? '' : 'table-danger') + '">' +
'<td>' + app.jump.fmtTime(e.ts) + '</td>' +
'<td>' + app.jump.esc(e.uid) + '</td>' +
'<td>' + app.jump.esc(e.authMethod) + '</td>' +
'<td>' + app.jump.esc(e.mode) + '</td>' +
'<td>' + app.jump.esc(e.targetSlug || e.targetAddr || '—') + '</td>' +
'<td>' + app.jump.esc(e.channel || '—') + '</td>' +
'<td>' + app.jump.esc(e.clientIp) + '</td>' +
'<td>' + app.jump.result(e) + '</td>' +
'<td class="text-end">' + ((e.bytesIn + e.bytesOut) || 0) + '</td></tr>');
});
var total = data ? data.total : 0, size = data ? data.pageSize : 50;
$('#page-info').text(total + ' events · page ' + (page + 1));
$('#prev').prop('disabled', page === 0);
$('#next').prop('disabled', (page + 1) * size >= total);
});
}
$(document).ready(function(){
loadMetrics();
load();
});
</script>
<%- include('bottom') %>