v_2.3
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Logs · Infosec</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="/js/app.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const shell = await buildShell('logs'); if (!shell) return;
|
||||
const { content } = shell;
|
||||
let meta = { actions: [], pages: [] };
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="page-head">
|
||||
<h1>Logs</h1>
|
||||
<p>Every action on the View Data and Data Management pages is recorded here.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h2>Audit trail</h2>
|
||||
<div class="toolbar" style="margin:0">
|
||||
<button class="btn" id="exportBtn">${ICON.download} Export CSV</button>
|
||||
<button class="btn btn-primary" id="emailBtn">${ICON.mail} Email logs</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="toolbar">
|
||||
<input type="search" id="q" placeholder="Search actor or detail…" style="max-width:240px">
|
||||
<select id="action" style="max-width:200px"><option value="">All actions</option></select>
|
||||
<select id="page" style="max-width:200px"><option value="">All pages</option></select>
|
||||
<button class="btn" id="applyBtn">Filter</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="hint" id="count"></span>
|
||||
</div>
|
||||
<div id="table"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function params() {
|
||||
const p = new URLSearchParams();
|
||||
if ($('q').value.trim()) p.set('q', $('q').value.trim());
|
||||
if ($('action').value) p.set('action', $('action').value);
|
||||
if ($('page').value) p.set('page', $('page').value);
|
||||
return p;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const data = await api('/api/logs?' + params().toString());
|
||||
meta = data;
|
||||
// populate selects once
|
||||
if ($('action').options.length <= 1) {
|
||||
$('action').innerHTML = '<option value="">All actions</option>' +
|
||||
data.actions.map((a) => `<option value="${esc(a)}">${esc(a)}</option>`).join('');
|
||||
$('page').innerHTML = '<option value="">All pages</option>' +
|
||||
data.pages.map((p) => `<option value="${esc(p)}">${esc(p)}</option>`).join('');
|
||||
}
|
||||
$('count').textContent = `${data.logs.length} of ${data.total} entries`;
|
||||
$('table').innerHTML = data.logs.length ? `
|
||||
<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Page</th><th>Detail</th><th>IP</th></tr></thead>
|
||||
<tbody>${data.logs.map((l) => `<tr>
|
||||
<td class="mono">${esc(fmtDateTime(l.ts))}</td>
|
||||
<td>${esc(l.actor)}</td>
|
||||
<td><span class="badge badge-admin">${esc(l.action)}</span></td>
|
||||
<td>${esc(l.page || '—')}</td>
|
||||
<td>${esc(l.detail || '')}</td>
|
||||
<td class="mono">${esc(l.ip || '')}</td>
|
||||
</tr>`).join('')}</tbody></table>` :
|
||||
`<div class="empty">${ICON.logs}<div>No log entries match your filter.</div></div>`;
|
||||
}
|
||||
|
||||
$('applyBtn').onclick = load;
|
||||
$('q').addEventListener('keydown', (e) => { if (e.key === 'Enter') load(); });
|
||||
$('action').onchange = load;
|
||||
$('page').onchange = load;
|
||||
|
||||
$('exportBtn').onclick = () => { location.href = '/api/logs/export?' + params().toString(); };
|
||||
|
||||
$('emailBtn').onclick = () => {
|
||||
modal('Email audit logs', `
|
||||
<p>Send the currently filtered logs as a CSV attachment.</p>
|
||||
<div class="field">
|
||||
<label>Recipient (leave blank to send to all admins)</label>
|
||||
<input type="email" id="to" placeholder="security@yourdomain.com">
|
||||
</div>`, {
|
||||
buttons: [{ label: 'Cancel' }, { label: 'Send', className: 'btn-primary', onClick: async (back) => {
|
||||
const body = Object.fromEntries(params());
|
||||
const to = back.querySelector('#to').value.trim();
|
||||
if (to) body.to = to;
|
||||
try {
|
||||
const r = await api('/api/logs/email', { method: 'POST', body });
|
||||
back.remove();
|
||||
toast(r.delivered ? `Sent ${r.count} entries` : `Queued ${r.count} entries (SMTP not configured)`, 'ok');
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
return false;
|
||||
} }],
|
||||
});
|
||||
};
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user