168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
import { AdminLayout } from "./AdminLayout.tsx";
|
|
|
|
export const AuditLogPage = ({
|
|
logs,
|
|
}: {
|
|
logs: any[];
|
|
}) => {
|
|
return (
|
|
<AdminLayout title="Audit Logs" currentPath="/admin/audit-logs">
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
|
|
Immutable Audit Ledger
|
|
</h1>
|
|
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
|
|
Cryptographically chained Merkle audit trail for authentication,
|
|
authorization, and administrative events.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Desktop Table (≥ 768px) */}
|
|
<div class="card desktop-only" style="display: none;">
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Timestamp</th>
|
|
<th>Event Action</th>
|
|
<th>User / Subject</th>
|
|
<th>Resource</th>
|
|
<th>IP Address</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map((log) => {
|
|
const isFail = log.action.includes("fail") ||
|
|
log.action.includes("denied");
|
|
const isSuccess = log.action.includes("success") ||
|
|
log.action.includes("create") ||
|
|
log.action.includes("activate");
|
|
const badgeClass = isFail
|
|
? "badge-danger"
|
|
: isSuccess
|
|
? "badge-success"
|
|
: "badge-info";
|
|
|
|
return (
|
|
<tr key={log.id}>
|
|
<td style="font-size: 0.8rem; color: var(--text-muted); white-space: nowrap;">
|
|
{new Date(log.created_at).toLocaleString()}
|
|
</td>
|
|
<td>
|
|
<span class={`badge ${badgeClass}`}>
|
|
{log.action}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<strong style="color: var(--text-primary);">
|
|
{log.user || "System"}
|
|
</strong>
|
|
</td>
|
|
<td style="color: var(--text-secondary);">
|
|
{log.resource || "-"}
|
|
</td>
|
|
<td style="font-family: monospace; font-size: 0.85rem;">
|
|
{log.ip_address || "-"}
|
|
</td>
|
|
<td>
|
|
<pre style="margin: 0; font-family: monospace; font-size: 0.75rem; background: var(--surface-muted); padding: 0.35rem 0.6rem; border-radius: var(--radius-sm); max-width: 300px; overflow-x: auto;">
|
|
{log.details ? JSON.stringify(log.details) : "{}"}
|
|
</pre>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{logs.length === 0 && (
|
|
<tr>
|
|
<td
|
|
colSpan={6}
|
|
style="text-align: center; color: var(--text-muted); padding: 2rem;"
|
|
>
|
|
No audit records recorded yet.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Feed (< 768px) */}
|
|
<div
|
|
class="mobile-only"
|
|
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
|
>
|
|
{logs.map((log) => {
|
|
const isFail = log.action.includes("fail") ||
|
|
log.action.includes("denied");
|
|
const isSuccess = log.action.includes("success") ||
|
|
log.action.includes("create") || log.action.includes("activate");
|
|
const badgeClass = isFail
|
|
? "badge-danger"
|
|
: isSuccess
|
|
? "badge-success"
|
|
: "badge-info";
|
|
|
|
return (
|
|
<div
|
|
class="card"
|
|
key={log.id}
|
|
style="margin-bottom: 0; padding: 1rem;"
|
|
>
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
|
<span class={`badge ${badgeClass}`}>
|
|
{log.action}
|
|
</span>
|
|
<span style="font-size: 0.75rem; color: var(--text-muted);">
|
|
{new Date(log.created_at).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
})}
|
|
</span>
|
|
</div>
|
|
|
|
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
|
|
<div>
|
|
<strong>User:</strong> {log.user || "System"}
|
|
</div>
|
|
<div>
|
|
<strong>IP:</strong>{" "}
|
|
<span style="font-family: monospace;">
|
|
{log.ip_address || "Internal"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{log.details && (
|
|
<details style="font-size: 0.8rem; color: var(--text-muted);">
|
|
<summary style="cursor: pointer; font-weight: 600;">
|
|
View Event Payload
|
|
</summary>
|
|
<pre style="margin-top: 0.5rem; background: var(--surface-muted); padding: 0.5rem; border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow-x: auto;">
|
|
{JSON.stringify(log.details, null, 2)}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<style>
|
|
{`
|
|
@media (min-width: 768px) {
|
|
.desktop-only { display: block !important; }
|
|
.mobile-only { display: none !important; }
|
|
}
|
|
@media (max-width: 767px) {
|
|
.desktop-only { display: none !important; }
|
|
.mobile-only { display: flex !important; }
|
|
}
|
|
`}
|
|
</style>
|
|
</AdminLayout>
|
|
);
|
|
};
|