mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 13:12:10 +01:00
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
This commit is contained in:
556
app/dashboard/audit-logs/page.tsx
Normal file
556
app/dashboard/audit-logs/page.tsx
Normal file
@ -0,0 +1,556 @@
|
||||
"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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user