@speles7172/audit-console
v0.4.1
Published
React audit trail UI — a per-entity timeline and a filterable cross-entity log, over a transport you supply.
Readme
@speles7172/audit-console
The audit trail @speles7172/audit-client writes, on screen.
Two components, because an audit trail is read in two very different situations:
<AuditTrail>— one record's history, as a timeline, on that record's own page. Collapsed by default and loaded on first open.<AuditConsole>— the cross-entity log: filters, a table, pagination, and one record at a time in detail.
npm install @speles7172/audit-consoleimport { AuditTrail, AuditConsole } from '@speles7172/audit-console';
import '@speles7172/audit-console/styles.css'; // optionalThe transport
The console queries nothing itself. A browser has no database credentials, and
any it were given would be readable by everyone the page is served to — so your
application exposes an endpoint backed by audit-client, and supplies this:
const transport = {
list: (filter) => api.post('/admin/audit', filter),
listForEntity: (entityType, entityId, filter) =>
api.post(`/audit/${entityType}/${entityId}`, filter),
listFacets: () => api.get('/admin/audit/facets'),
};That endpoint is the security seam. The console sends a filter object, never SQL, so a filter can never become a query of the caller's choosing — but who may read which trail is decided there and nowhere else.
listForEntity is worth implementing separately, and worth pointing at a
different endpoint from list: a record's own history is normally visible to
whoever can read that record, while the cross-entity log is administrator-only.
Backing both with one route means either the trail is too restricted to show on
a detail page, or the log is too open. Omit it and the trail falls back to a
narrowed list.
listFacets is optional; without it the entity-type, action and user pickers
are simply not rendered.
On an entity's page
<AuditTrail transport={transport} entityType="invoice" entityId={invoice?.id} />Pass undefined for entityId while the record is still loading and the trail
waits rather than asking for nothing. Each entry shows the action, who did it,
how long ago, the field-by-field diff, and the device and location it came
from; anything with more behind it opens a detail panel.
As a log
<AuditConsole transport={transport} />Filters: a date range (presets or custom), entity type, action, user, an exact IP address, and a search over the entity name and the message. Every value in the table is clickable to narrow by it, addresses included. The detail panel shows the full diff, the redacted request payload and the device rows.
The address has its own filter rather than going through the search box,
because search is a substring match over the entity name and the message —
an IP put through it matches nothing.
This shows everything anyone has done. Put it behind whatever your application means by "administrator": the component cannot, and the endpoint behind the transport is the only place that check belongs.
Styling
Both components render plain semantic HTML with auditc- class names and no
styling dependency. styles.css is optional and entirely driven by custom
properties — override them on .auditc to re-theme without touching a rule:
.auditc { --auditc-accent: #7c3aed; --auditc-radius: 4px; }theme="dark" picks the dark palette; theme="auto" follows
prefers-color-scheme, which is right only when the host page does too. The
default is light, because this is embedded in someone else's page and the host
decides what that page looks like.
Without the markup
An application with its own design system takes the state machines and none of the HTML:
const { records, status, reload } = useAuditTrail({ transport, entityType, entityId, enabled: true });
const state = useAuditConsole({ transport });AuditChanges, AuditDeviceInfo and AuditDetail are exported individually
too, along with formatTimestamp, formatRelative, actorLabel, summarize
and the filter helpers.
Everything this package imports at runtime comes from
@speles7172/audit-client/core, which is dependency-free — boundary.test.ts
fails the build if that stops being true.
MIT.
