'use client'
/**
* Audit Dashboard Component
* Provides real-time monitoring and querying of audit events
*/
import React, { useState, useEffect } from 'react'
// Types
interface AuditEvent {
id: number
timestamp: string
event_type: string
user_id?: string
user_email?: string
action: string
result: 'success' | 'failure' | 'warning'
risk_level: 'low' | 'medium' | 'high' | 'critical'
database_type?: 'neo4j' | 'mysql'
operation_type?: 'CREATE' | 'MERGE' | 'SET' | 'READ'
execution_time_ms?: number
affected_nodes?: number
affected_relationships?: number
}
interface AuditFilters {
startDate?: string
endDate?: string
eventType?: string
userEmail?: string
riskLevel?: string
result?: string
databaseType?: string
search?: string
}
interface AuditResponse {
events: AuditEvent[]
pagination: {
page: number
limit: number
total: number
totalPages: number
hasNext: boolean
hasPrevious: boolean
}
}
export default function AuditDashboard() {
const [events, setEvents] = useState<AuditEvent[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [filters, setFilters] = useState<AuditFilters>({})
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
total: 0,
totalPages: 0,
hasNext: false,
hasPrevious: false
})
// Fetch audit events
const fetchAuditEvents = async (newFilters: AuditFilters = {}, page: number = 1) => {
setLoading(true)
setError(null)
try {
const params = new URLSearchParams({
page: page.toString(),
limit: pagination.limit.toString(),
...Object.fromEntries(
Object.entries({ ...filters, ...newFilters }).filter(([_, value]) => value !== '')
)
})
const response = await fetch(`/api/audit?${params}`, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || ''
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data: AuditResponse = await response.json()
setEvents(data.events)
setPagination(data.pagination)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch audit events')
console.error('Error fetching audit events:', err)
} finally {
setLoading(false)
}
}
// Export events to CSV
const exportToCSV = async () => {
try {
const params = new URLSearchParams({
format: 'csv',
...Object.fromEntries(
Object.entries(filters).filter(([_, value]) => value !== '')
)
})
const response = await fetch(`/api/audit?${params}`, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || ''
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `audit-events-${new Date().toISOString().split('T')[0]}.csv`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to export audit events')
}
}
// Format timestamp
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString()
}
// Get risk level color
const getRiskLevelColor = (level: string) => {
switch (level) {
case 'critical': return 'text-red-600 bg-red-50'
case 'high': return 'text-orange-600 bg-orange-50'
case 'medium': return 'text-yellow-600 bg-yellow-50'
case 'low': return 'text-green-600 bg-green-50'
default: return 'text-gray-600 bg-gray-50'
}
}
// Get result color
const getResultColor = (result: string) => {
switch (result) {
case 'success': return 'text-green-600 bg-green-50'
case 'failure': return 'text-red-600 bg-red-50'
case 'warning': return 'text-yellow-600 bg-yellow-50'
default: return 'text-gray-600 bg-gray-50'
}
}
// Initial load
useEffect(() => {
fetchAuditEvents()
}, [])
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Audit Dashboard</h1>
<p className="text-gray-600 mt-2">
Monitor and analyze security and database audit events
</p>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filters</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{/* Time Range */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="datetime-local"
value={filters.startDate || ''}
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="datetime-local"
value={filters.endDate || ''}
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Risk Level */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Risk Level
</label>
<select
value={filters.riskLevel || ''}
onChange={(e) => setFilters({ ...filters, riskLevel: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Risk Levels</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
{/* Result */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Result
</label>
<select
value={filters.result || ''}
onChange={(e) => setFilters({ ...filters, result: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Results</option>
<option value="success">Success</option>
<option value="failure">Failure</option>
<option value="warning">Warning</option>
</select>
</div>
{/* Event Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Event Type
</label>
<input
type="text"
placeholder="e.g., database.neo4j.%"
value={filters.eventType || ''}
onChange={(e) => setFilters({ ...filters, eventType: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Database Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Database Type
</label>
<select
value={filters.databaseType || ''}
onChange={(e) => setFilters({ ...filters, databaseType: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Databases</option>
<option value="neo4j">Neo4j</option>
<option value="mysql">MySQL</option>
</select>
</div>
{/* User Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
User Email
</label>
<input
type="email"
placeholder="user@example.com"
value={filters.userEmail || ''}
onChange={(e) => setFilters({ ...filters, userEmail: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Search */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Search
</label>
<input
type="text"
placeholder="Search actions, events..."
value={filters.search || ''}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Filter Actions */}
<div className="flex space-x-3">
<button
onClick={() => fetchAuditEvents(filters, 1)}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Loading...' : 'Apply Filters'}
</button>
<button
onClick={() => {
setFilters({})
fetchAuditEvents({}, 1)
}}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
>
Clear Filters
</button>
<button
onClick={exportToCSV}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
Export CSV
</button>
</div>
</div>
{/* Error Display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div className="text-red-800">
<strong>Error:</strong> {error}
</div>
</div>
)}
{/* Results */}
<div className="bg-white rounded-lg shadow">
{/* Results Header */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">
Audit Events ({pagination.total.toLocaleString()})
</h2>
<div className="text-sm text-gray-600">
Page {pagination.page} of {pagination.totalPages}
</div>
</div>
</div>
{/* Events Table */}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Timestamp
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Event Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Result
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Risk Level
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Database
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{events.map((event) => (
<tr key={event.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatTimestamp(event.timestamp)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-mono">
{event.event_type}
</td>
<td className="px-6 py-4 text-sm text-gray-900 max-w-xs truncate">
{event.action}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{event.user_email || event.user_id || 'System'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getResultColor(event.result)}`}>
{event.result}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getRiskLevelColor(event.risk_level)}`}>
{event.risk_level}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{event.database_type ? (
<div>
<div className="font-medium">{event.database_type}</div>
{event.operation_type && (
<div className="text-gray-500 text-xs">{event.operation_type}</div>
)}
{event.execution_time_ms && (
<div className="text-gray-500 text-xs">{event.execution_time_ms}ms</div>
)}
</div>
) : (
'-'
)}
</td>
</tr>
))}
</tbody>
</table>
{events.length === 0 && !loading && (
<div className="text-center py-12 text-gray-500">
No audit events found. Try adjusting your filters.
</div>
)}
{loading && (
<div className="text-center py-12 text-gray-500">
Loading audit events...
</div>
)}
</div>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="px-6 py-4 border-t border-gray-200">
<div className="flex justify-between items-center">
<button
onClick={() => fetchAuditEvents(filters, pagination.page - 1)}
disabled={!pagination.hasPrevious || loading}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span className="text-sm text-gray-600">
Page {pagination.page} of {pagination.totalPages}
</span>
<button
onClick={() => fetchAuditEvents(filters, pagination.page + 1)}
disabled={!pagination.hasNext || loading}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
</div>
)
}