'use client'; import { useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/infrastructure/api/api-client'; import { queryKeys } from '@/infrastructure/api/query-keys'; import { cn } from '@/lib/utils'; // ── Types ─────────────────────────────────────────────────────────────────── interface AuditLog { id: string; timestamp: string; actionType: string; actorType: string; actorId: string; resourceType: string; resourceId: string; description: string; detail: Record; } type AuditLogsResponse = AuditLog[]; interface Filters { dateFrom: string; dateTo: string; actionType: string; actorType: string; resourceType: string; } // ── Constants ─────────────────────────────────────────────────────────────── const ACTION_TYPES = ['', 'CREATE', 'UPDATE', 'DELETE', 'LOGIN', 'EXECUTE', 'APPROVE', 'REJECT']; const ACTOR_TYPES = ['', 'user', 'system', 'agent']; const RESOURCE_TYPES = ['', 'server', 'task', 'standing_order', 'runbook', 'credential', 'user']; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; // ── Main Component ────────────────────────────────────────────────────────── export default function AuditLogsPage() { const { t } = useTranslation('audit'); const { t: tc } = useTranslation('common'); const [filters, setFilters] = useState({ dateFrom: '', dateTo: '', actionType: '', actorType: '', resourceType: '', }); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [expandedId, setExpandedId] = useState(null); // Build query params from filters const queryParams = useMemo(() => { const params: Record = { page: String(page), pageSize: String(pageSize), }; if (filters.dateFrom) params.dateFrom = filters.dateFrom; if (filters.dateTo) params.dateTo = filters.dateTo; if (filters.actionType) params.actionType = filters.actionType; if (filters.actorType) params.actorType = filters.actorType; if (filters.resourceType) params.resourceType = filters.resourceType; return params; }, [filters, page, pageSize]); const queryString = useMemo(() => { const sp = new URLSearchParams(queryParams); return sp.toString(); }, [queryParams]); const { data, isLoading, error } = useQuery({ queryKey: queryKeys.auditLogs.list(queryParams), queryFn: () => apiClient(`/api/v1/audit/logs?${queryString}`), }); const logs = data ?? []; const total = logs.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const updateFilter = useCallback((key: keyof Filters, value: string) => { setFilters((prev) => ({ ...prev, [key]: value })); setPage(1); // reset to first page on filter change }, []); const handlePageSizeChange = useCallback((newSize: number) => { setPageSize(newSize); setPage(1); }, []); // ── CSV Export ───────────────────────────────────────────────────────────── const exportCsv = useCallback(() => { if (logs.length === 0) return; const headers = [t('logs.table.timestamp'), t('logs.table.action'), t('logs.table.actorType'), t('logs.table.actorId'), t('logs.table.resourceType'), t('logs.table.resourceId'), t('logs.table.description')]; const rows = logs.map((log) => [ log.timestamp, log.actionType, log.actorType, log.actorId, log.resourceType, log.resourceId, `"${(log.description || '').replace(/"/g, '""')}"`, ]); const csvContent = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `audit-logs-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }, [logs, t]); // ── Render ───────────────────────────────────────────────────────────────── return (

{t('logs.title')}

{t('logs.subtitle')}

{/* Filters */}
updateFilter('dateFrom', e.target.value)} className="w-full px-2 py-1.5 bg-input border rounded-md text-sm" />
updateFilter('dateTo', e.target.value)} className="w-full px-2 py-1.5 bg-input border rounded-md text-sm" />
{/* Loading / Error */} {isLoading &&

{t('logs.loading')}

} {error &&

{t('logs.loadError')} {(error as Error).message}

} {/* Table */} {!isLoading && !error && ( <>
{logs.map((log) => ( setExpandedId(expandedId === log.id ? null : log.id)} /> ))} {logs.length === 0 && ( )}
{t('logs.table.timestamp')} {t('logs.table.action')} {t('logs.table.actorType')} {t('logs.table.actorId')} {t('logs.table.resourceType')} {t('logs.table.resourceId')} {t('logs.table.description')}
{t('logs.empty')}
{/* Pagination */}
{t('logs.pagination.rowsPerPage')}: {total > 0 ? `${(page - 1) * pageSize + 1}--${Math.min(page * pageSize, total)} of ${total}` : tc('noResults')}
{t('logs.pagination.pageOf', { current: page, total: totalPages })}
)}
); } // ── Row Component ─────────────────────────────────────────────────────────── const ACTION_COLORS: Record = { CREATE: 'bg-green-100 text-green-700', UPDATE: 'bg-blue-100 text-blue-700', DELETE: 'bg-red-100 text-red-700', LOGIN: 'bg-purple-100 text-purple-700', EXECUTE: 'bg-orange-100 text-orange-700', APPROVE: 'bg-emerald-100 text-emerald-700', REJECT: 'bg-rose-100 text-rose-700', }; function LogRow({ log, isExpanded, onToggle, }: { log: AuditLog; isExpanded: boolean; onToggle: () => void; }) { const { t } = useTranslation('audit'); const formattedTime = useMemo(() => { try { return new Date(log.timestamp).toLocaleString(); } catch { return log.timestamp; } }, [log.timestamp]); return ( <> {formattedTime} {log.actionType} {log.actorType} {log.actorId} {log.resourceType} {log.resourceId} {log.description} {isExpanded && (

{t('logs.fullDetail')}

              {JSON.stringify(log.detail ?? log, null, 2)}
            
)} ); }