Files
livedash-node/app/dashboard/audit-logs/page.tsx
Kaj Kowalski 1eea2cc3e4 refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191
- Replace explicit 'any' types with proper TypeScript interfaces
- Fix React hooks dependencies and useCallback patterns
- Resolve unused variables and parameter assignment issues
- Improve accessibility with proper label associations
- Add comprehensive API documentation for admin and security features
- Update README.md with accurate PostgreSQL setup and current tech stack
- Create complete documentation for audit logging, CSP monitoring, and batch processing
- Fix outdated project information and missing developer workflows
2025-07-12 00:28:09 +02:00

557 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { formatDistanceToNow } from "date-fns";
import { useSession } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Input } from "../../../components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../../components/ui/table";
interface AuditLog {
id: string;
eventType: string;
action: string;
outcome: string;
severity: string;
userId?: string;
platformUserId?: string;
ipAddress?: string;
userAgent?: string;
country?: string;
metadata?: Record<string, unknown>;
errorMessage?: string;
sessionId?: string;
requestId?: string;
timestamp: string;
user?: {
id: string;
email: string;
name?: string;
role: string;
};
platformUser?: {
id: string;
email: string;
name?: string;
role: string;
};
}
interface AuditLogsResponse {
success: boolean;
data?: {
auditLogs: AuditLog[];
pagination: {
page: number;
limit: number;
totalCount: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
error?: string;
}
const eventTypeLabels: Record<string, string> = {
AUTHENTICATION: "Authentication",
AUTHORIZATION: "Authorization",
USER_MANAGEMENT: "User Management",
COMPANY_MANAGEMENT: "Company Management",
RATE_LIMITING: "Rate Limiting",
CSRF_PROTECTION: "CSRF Protection",
SECURITY_HEADERS: "Security Headers",
PASSWORD_RESET: "Password Reset",
PLATFORM_ADMIN: "Platform Admin",
DATA_PRIVACY: "Data Privacy",
SYSTEM_CONFIG: "System Config",
API_SECURITY: "API Security",
};
const outcomeColors: Record<string, string> = {
SUCCESS: "bg-green-100 text-green-800",
FAILURE: "bg-red-100 text-red-800",
BLOCKED: "bg-orange-100 text-orange-800",
RATE_LIMITED: "bg-yellow-100 text-yellow-800",
SUSPICIOUS: "bg-purple-100 text-purple-800",
};
const severityColors: Record<string, string> = {
INFO: "bg-blue-100 text-blue-800",
LOW: "bg-gray-100 text-gray-800",
MEDIUM: "bg-yellow-100 text-yellow-800",
HIGH: "bg-orange-100 text-orange-800",
CRITICAL: "bg-red-100 text-red-800",
};
export default function AuditLogsPage() {
const { data: session } = useSession();
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
totalCount: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
// Filter states
const [filters, setFilters] = useState({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const fetchAuditLogs = useCallback(async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
...filters,
});
Object.keys(filters).forEach((key) => {
if (!filters[key as keyof typeof filters]) {
params.delete(key);
}
});
const response = await fetch(
`/api/admin/audit-logs?${params.toString()}`
);
const data: AuditLogsResponse = await response.json();
if (data.success && data.data) {
setAuditLogs(data.data.auditLogs);
setPagination(data.data.pagination);
setError(null);
} else {
setError(data.error || "Failed to fetch audit logs");
}
} catch (err) {
setError("An error occurred while fetching audit logs");
console.error("Audit logs fetch error:", err);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.limit, filters]);
useEffect(() => {
if (session?.user?.role === "ADMIN") {
fetchAuditLogs();
}
}, [session, fetchAuditLogs]);
const handleFilterChange = (key: keyof typeof filters, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value }));
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
};
const clearFilters = () => {
setFilters({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
};
if (session?.user?.role !== "ADMIN") {
return (
<div className="container mx-auto py-8">
<Alert>
<AlertDescription>
You don&apos;t have permission to view audit logs. Only administrators
can access this page.
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Security Audit Logs</h1>
<Button onClick={fetchAuditLogs} disabled={loading}>
{loading ? "Loading..." : "Refresh"}
</Button>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">Event Type</label>
<Select
value={filters.eventType}
onValueChange={(value) =>
handleFilterChange("eventType", value)
}
>
<SelectTrigger>
<SelectValue placeholder="All event types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All event types</SelectItem>
{Object.entries(eventTypeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Outcome</label>
<Select
value={filters.outcome}
onValueChange={(value) => handleFilterChange("outcome", value)}
>
<SelectTrigger>
<SelectValue placeholder="All outcomes" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All outcomes</SelectItem>
<SelectItem value="SUCCESS">Success</SelectItem>
<SelectItem value="FAILURE">Failure</SelectItem>
<SelectItem value="BLOCKED">Blocked</SelectItem>
<SelectItem value="RATE_LIMITED">Rate Limited</SelectItem>
<SelectItem value="SUSPICIOUS">Suspicious</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Severity</label>
<Select
value={filters.severity}
onValueChange={(value) => handleFilterChange("severity", value)}
>
<SelectTrigger>
<SelectValue placeholder="All severities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All severities</SelectItem>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Start Date</label>
<Input
type="datetime-local"
value={filters.startDate}
onChange={(e) =>
handleFilterChange("startDate", e.target.value)
}
/>
</div>
<div>
<label className="text-sm font-medium">End Date</label>
<Input
type="datetime-local"
value={filters.endDate}
onChange={(e) => handleFilterChange("endDate", e.target.value)}
/>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={clearFilters}>
Clear Filters
</Button>
</div>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Audit Logs Table */}
<Card>
<CardHeader>
<CardTitle>Audit Logs ({pagination.totalCount} total)</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Event Type</TableHead>
<TableHead>Action</TableHead>
<TableHead>Outcome</TableHead>
<TableHead>Severity</TableHead>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Details</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditLogs.map((log) => (
<TableRow
key={log.id}
className="cursor-pointer hover:bg-gray-50"
onClick={() => setSelectedLog(log)}
>
<TableCell className="font-mono text-sm">
{formatDistanceToNow(new Date(log.timestamp), {
addSuffix: true,
})}
</TableCell>
<TableCell>
<Badge variant="outline">
{eventTypeLabels[log.eventType] || log.eventType}
</Badge>
</TableCell>
<TableCell className="max-w-48 truncate">
{log.action}
</TableCell>
<TableCell>
<Badge
className={
outcomeColors[log.outcome] ||
"bg-gray-100 text-gray-800"
}
>
{log.outcome}
</Badge>
</TableCell>
<TableCell>
<Badge
className={
severityColors[log.severity] ||
"bg-gray-100 text-gray-800"
}
>
{log.severity}
</Badge>
</TableCell>
<TableCell>
{log.user?.email || log.platformUser?.email || "System"}
</TableCell>
<TableCell className="font-mono text-sm">
{log.ipAddress || "N/A"}
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
View
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-600">
Showing {(pagination.page - 1) * pagination.limit + 1} to{" "}
{Math.min(
pagination.page * pagination.limit,
pagination.totalCount
)}{" "}
of {pagination.totalCount} results
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!pagination.hasPrev}
onClick={() =>
setPagination((prev) => ({ ...prev, page: prev.page - 1 }))
}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={!pagination.hasNext}
onClick={() =>
setPagination((prev) => ({ ...prev, page: prev.page + 1 }))
}
>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Log Detail Modal */}
{selectedLog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Audit Log Details</h2>
<Button variant="ghost" onClick={() => setSelectedLog(null)}>
×
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="font-medium">Timestamp:</label>
<p className="font-mono text-sm">
{new Date(selectedLog.timestamp).toLocaleString()}
</p>
</div>
<div>
<label className="font-medium">Event Type:</label>
<p>
{eventTypeLabels[selectedLog.eventType] ||
selectedLog.eventType}
</p>
</div>
<div>
<label className="font-medium">Action:</label>
<p>{selectedLog.action}</p>
</div>
<div>
<label className="font-medium">Outcome:</label>
<Badge className={outcomeColors[selectedLog.outcome]}>
{selectedLog.outcome}
</Badge>
</div>
<div>
<label className="font-medium">Severity:</label>
<Badge className={severityColors[selectedLog.severity]}>
{selectedLog.severity}
</Badge>
</div>
<div>
<label className="font-medium">IP Address:</label>
<p className="font-mono text-sm">
{selectedLog.ipAddress || "N/A"}
</p>
</div>
{selectedLog.user && (
<div>
<label className="font-medium">User:</label>
<p>
{selectedLog.user.email} ({selectedLog.user.role})
</p>
</div>
)}
{selectedLog.platformUser && (
<div>
<label className="font-medium">Platform User:</label>
<p>
{selectedLog.platformUser.email} (
{selectedLog.platformUser.role})
</p>
</div>
)}
{selectedLog.country && (
<div>
<label className="font-medium">Country:</label>
<p>{selectedLog.country}</p>
</div>
)}
{selectedLog.sessionId && (
<div>
<label className="font-medium">Session ID:</label>
<p className="font-mono text-sm">{selectedLog.sessionId}</p>
</div>
)}
{selectedLog.requestId && (
<div>
<label className="font-medium">Request ID:</label>
<p className="font-mono text-sm">{selectedLog.requestId}</p>
</div>
)}
</div>
{selectedLog.errorMessage && (
<div className="mt-4">
<label className="font-medium">Error Message:</label>
<p className="text-red-600 bg-red-50 p-2 rounded text-sm">
{selectedLog.errorMessage}
</p>
</div>
)}
{selectedLog.userAgent && (
<div className="mt-4">
<label className="font-medium">User Agent:</label>
<p className="text-sm break-all">{selectedLog.userAgent}</p>
</div>
)}
{selectedLog.metadata && (
<div className="mt-4">
<label className="font-medium">Metadata:</label>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">
{JSON.stringify(selectedLog.metadata, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}