auth-yes/ui/components/AuditLogPage.tsx

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>
);
};