'use client' import { useState, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Download, Filter, ChevronDown, ChevronUp, Clock, User, Activity, Database, Globe, ChevronLeft, ChevronRight, RefreshCw, RotateCcw, AlertTriangle, Layers, ArrowLeftRight, } from 'lucide-react' import { Switch } from '@/components/ui/switch' import { formatDate } from '@/lib/utils' import { cn } from '@/lib/utils' // Action type options const ACTION_TYPES = [ 'CREATE', 'UPDATE', 'DELETE', 'IMPORT', 'EXPORT', 'LOGIN', 'LOGIN_SUCCESS', 'LOGIN_FAILED', 'INVITATION_ACCEPTED', 'SUBMIT_EVALUATION', 'EVALUATION_SUBMITTED', 'UPDATE_STATUS', 'ROUND_ACTIVATED', 'ROUND_CLOSED', 'ROUND_ARCHIVED', 'UPLOAD_FILE', 'DELETE_FILE', 'FILE_DOWNLOADED', 'BULK_CREATE', 'BULK_UPDATE_STATUS', 'UPDATE_EVALUATION_FORM', 'ROLE_CHANGED', 'PASSWORD_SET', 'PASSWORD_CHANGED', ] // Entity type options const ENTITY_TYPES = [ 'User', 'Program', 'Round', 'Project', 'Assignment', 'Evaluation', 'EvaluationForm', 'ProjectFile', 'GracePeriod', ] // Color map for action types const actionColors: Record = { CREATE: 'default', UPDATE: 'secondary', DELETE: 'destructive', IMPORT: 'default', EXPORT: 'outline', LOGIN: 'outline', LOGIN_SUCCESS: 'outline', LOGIN_FAILED: 'destructive', INVITATION_ACCEPTED: 'default', SUBMIT_EVALUATION: 'default', EVALUATION_SUBMITTED: 'default', ROUND_ACTIVATED: 'default', ROUND_CLOSED: 'secondary', ROUND_ARCHIVED: 'secondary', FILE_DOWNLOADED: 'outline', ROLE_CHANGED: 'secondary', PASSWORD_SET: 'outline', PASSWORD_CHANGED: 'outline', } export default function AuditLogPage() { // Filter state const [filters, setFilters] = useState({ userId: '', action: '', entityType: '', startDate: '', endDate: '', }) const [page, setPage] = useState(1) const [expandedRows, setExpandedRows] = useState>(new Set()) const [showFilters, setShowFilters] = useState(true) const [groupBySession, setGroupBySession] = useState(false) // Build query input const queryInput = useMemo( () => ({ userId: filters.userId || undefined, action: filters.action || undefined, entityType: filters.entityType || undefined, startDate: filters.startDate ? new Date(filters.startDate) : undefined, endDate: filters.endDate ? new Date(filters.endDate + 'T23:59:59') : undefined, page, perPage: 50, }), [filters, page] ) // Fetch audit logs const { data, isLoading, refetch } = trpc.audit.list.useQuery(queryInput) // Fetch users for filter dropdown const { data: usersData } = trpc.user.list.useQuery({ page: 1, perPage: 100, }) // Fetch anomalies const { data: anomalyData } = trpc.audit.getAnomalies.useQuery({}, { retry: false, }) // Export mutation const exportLogs = trpc.export.auditLogs.useQuery( { userId: filters.userId || undefined, action: filters.action || undefined, entityType: filters.entityType || undefined, startDate: filters.startDate ? new Date(filters.startDate) : undefined, endDate: filters.endDate ? new Date(filters.endDate + 'T23:59:59') : undefined, }, { enabled: false } ) // Handle export const handleExport = async () => { const result = await exportLogs.refetch() if (result.data) { const { data: rows, columns } = result.data // Build CSV const csvContent = [ columns.join(','), ...rows.map((row) => columns .map((col) => { const value = row[col as keyof typeof row] // Escape quotes and wrap in quotes if contains comma if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) { return `"${value.replace(/"/g, '""')}"` } return value ?? '' }) .join(',') ), ].join('\n') // Download 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().split('T')[0]}.csv` link.click() URL.revokeObjectURL(url) } } // Reset filters const resetFilters = () => { setFilters({ userId: '', action: '', entityType: '', startDate: '', endDate: '', }) setPage(1) } // Toggle row expansion const toggleRow = (id: string) => { const newExpanded = new Set(expandedRows) if (newExpanded.has(id)) { newExpanded.delete(id) } else { newExpanded.add(id) } setExpandedRows(newExpanded) } const hasFilters = Object.values(filters).some((v) => v !== '') return (
{/* Header */}

Audit Logs

View system activity and user actions

{/* Filters */}
Filters {hasFilters && ( Active )}
{showFilters ? ( ) : ( )}
{/* User Filter */}
{/* Action Filter */}
{/* Entity Type Filter */}
{/* Start Date */}
setFilters({ ...filters, startDate: e.target.value }) } />
{/* End Date */}
setFilters({ ...filters, endDate: e.target.value }) } />
{hasFilters && (
)}
{/* Anomaly Alerts */} {anomalyData && anomalyData.anomalies.length > 0 && ( Anomaly Alerts ({anomalyData.anomalies.length})
{anomalyData.anomalies.slice(0, 5).map((anomaly, i) => (

{anomaly.isRapid ? 'Rapid Activity' : 'Bulk Operations'}

{String(anomaly.actionCount)} actions in {String(anomaly.timeWindowMinutes)} min ({anomaly.actionsPerMinute.toFixed(1)}/min)

{anomaly.userId && (

User: {String(anomaly.user?.name || anomaly.userId)}

)}
{String(anomaly.actionCount)} actions
))}
)} {/* Session Grouping Toggle */}
{/* Results */} {isLoading ? ( ) : data && data.logs.length > 0 ? ( <> {/* Desktop Table View */} Timestamp User Action Entity IP Address {data.logs.map((log) => { const isExpanded = expandedRows.has(log.id) return ( <> toggleRow(log.id)} > {formatDate(log.timestamp)}

{log.user?.name || 'System'}

{log.user?.email}

{log.action.replace(/_/g, ' ')}

{log.entityType}

{log.entityId && (

{log.entityId.slice(0, 8)}...

)}
{log.ipAddress || '-'} {isExpanded ? ( ) : ( )}
{isExpanded && (

Entity ID

{log.entityId || 'N/A'}

User Agent

{log.userAgent || 'N/A'}

{log.detailsJson && (

Details

                                    {JSON.stringify(log.detailsJson, null, 2)}
                                  
)} {!!(log as Record).previousDataJson && (

Changes (Before / After)

).previousDataJson} after={log.detailsJson} />
)} {groupBySession && !!(log as Record).sessionId && (

Session ID

{String((log as Record).sessionId)}

)}
)} ) })}
{/* Mobile Card View */}
{data.logs.map((log) => { const isExpanded = expandedRows.has(log.id) return ( toggleRow(log.id)} >
{log.action.replace(/_/g, ' ')} {log.entityType}
{isExpanded ? ( ) : ( )}
{formatDate(log.timestamp)}
{log.user?.name || 'System'}
{isExpanded && (

Entity ID

{log.entityId || 'N/A'}

IP Address

{log.ipAddress || 'N/A'}

{log.detailsJson && (

Details

                              {JSON.stringify(log.detailsJson, null, 2)}
                            
)}
)}
) })}
{/* Pagination */}

Showing {(page - 1) * 50 + 1} to{' '} {Math.min(page * 50, data.total)} of {data.total} results

Page {page} of {data.totalPages}
) : (

No audit logs found

{hasFilters ? 'Try adjusting your filters' : 'Activity will appear here as users interact with the system'}

)}
) } function DiffViewer({ before, after }: { before: unknown; after: unknown }) { const beforeObj = typeof before === 'object' && before !== null ? before as Record : {} const afterObj = typeof after === 'object' && after !== null ? after as Record : {} const allKeys = Array.from(new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)])) const changedKeys = allKeys.filter( (key) => JSON.stringify(beforeObj[key]) !== JSON.stringify(afterObj[key]) ) if (changedKeys.length === 0) { return (

No differences detected

) } return (
Field Before After
{changedKeys.map((key) => (
{key} {beforeObj[key] !== undefined ? JSON.stringify(beforeObj[key]) : '--'} {afterObj[key] !== undefined ? JSON.stringify(afterObj[key]) : '--'}
))}
) } function AuditLogSkeleton() { return (
{[...Array(10)].map((_, i) => (
))}
) }