diff --git a/.env.example b/.env.example index 1e7be84..02f867c 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,10 @@ ENCRYPTION_KEY= # Generate with: openssl rand -hex 32 # Worker WORKER_CONCURRENCY=5 +# Audit log +AUDIT_LOG_RETENTION_DAYS=365 # Daily purge job deletes entries older than this. Set to 0 to disable. +AUDIT_LOG_EXPORT_MAX_ROWS=100000 # Cap for CSV exports; tighten the date range if exceeded. + # Webhooks (optional, for bounce/complaint tracking) # RESEND_WEBHOOK_SECRET= # From Resend dashboard diff --git a/app/(dashboard)/settings/audit-log/page.tsx b/app/(dashboard)/settings/audit-log/page.tsx index 45159e9..9448842 100644 --- a/app/(dashboard)/settings/audit-log/page.tsx +++ b/app/(dashboard)/settings/audit-log/page.tsx @@ -48,17 +48,38 @@ export default function AuditLogPage() { const [actorType, setActorType] = useState("") const [resourceType, setResourceType] = useState("") const [actionFilter, setActionFilter] = useState("") + const [fromDate, setFromDate] = useState("") + const [toDate, setToDate] = useState("") const [page, setPage] = useState(1) + function buildFilterParams(): URLSearchParams { + const params = new URLSearchParams() + if (actorType) params.set("actorType", actorType) + if (resourceType) params.set("resourceType", resourceType) + if (actionFilter) params.set("action", actionFilter) + if (fromDate) params.set("from", new Date(fromDate).toISOString()) + if (toDate) { + const to = new Date(toDate) + to.setHours(23, 59, 59, 999) + params.set("to", to.toISOString()) + } + return params + } + + function handleExport() { + const params = buildFilterParams() + params.set("format", "csv") + window.location.href = `/api/internal/audit-logs?${params.toString()}` + } + useEffect(() => { let cancelled = false async function fetchLogs() { setLoading(true) try { - const params = new URLSearchParams({ page: String(page), limit: "50" }) - if (actorType) params.set("actorType", actorType) - if (resourceType) params.set("resourceType", resourceType) - if (actionFilter) params.set("action", actionFilter) + const params = buildFilterParams() + params.set("page", String(page)) + params.set("limit", "50") const res = await fetch(`/api/internal/audit-logs?${params.toString()}`) if (!res.ok || cancelled) return const json = await res.json() @@ -72,17 +93,23 @@ export default function AuditLogPage() { } fetchLogs() return () => { cancelled = true } - }, [page, actorType, resourceType, actionFilter]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, actorType, resourceType, actionFilter, fromDate, toDate]) const totalPages = Math.max(1, Math.ceil(meta.total / meta.limit)) return (
-
-

Audit Log

-

- Append-only record of every state-changing action. -

+
+
+

Audit Log

+

+ Append-only record of every state-changing action. +

+
+
@@ -123,7 +150,27 @@ export default function AuditLogPage() { className="w-56 h-9" />
- {(actorType || resourceType || actionFilter) && ( +
+ + { setFromDate(e.target.value); setPage(1) }} + className="w-40 h-9" + /> +
+
+ + { setToDate(e.target.value); setPage(1) }} + className="w-40 h-9" + /> +
+ {(actorType || resourceType || actionFilter || fromDate || toDate) && (