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:
2025-07-11 21:50:53 +02:00
committed by Kaj Kowalski
parent 3e9e75e854
commit 1eea2cc3e4
121 changed files with 28687 additions and 4895 deletions

View File

@ -5,7 +5,7 @@ import { useEffect, useId, useState } from "react";
interface DateRangePickerProps {
minDate: string;
maxDate: string;
onDateRangeChange: (startDate: string, endDate: string) => void;
onDateRangeChange: (_startDate: string, _endDate: string) => void;
initialStartDate?: string;
initialEndDate?: string;
}
@ -25,7 +25,11 @@ export default function DateRangePicker({
useEffect(() => {
// Only notify parent component when dates change, not when the callback changes
onDateRangeChange(startDate, endDate);
}, [startDate, endDate]);
}, [
startDate,
endDate, // Only notify parent component when dates change, not when the callback changes
onDateRangeChange,
]);
const handleStartDateChange = (newStartDate: string) => {
// Ensure start date is not before min date

View File

@ -122,11 +122,11 @@ export default function GeographicMap({
/**
* Process a single country entry into CountryData
*/
function processCountryEntry(
const processCountryEntry = useCallback((
code: string,
count: number,
countryCoordinates: Record<string, [number, number]>
): CountryData | null {
): CountryData | null => {
const coordinates = getCountryCoordinates(code, countryCoordinates);
if (coordinates) {
@ -134,7 +134,7 @@ export default function GeographicMap({
}
return null; // Skip if no coordinates found
}
}, []);
/**
* Process all countries data into CountryData array
@ -156,7 +156,7 @@ export default function GeographicMap({
return data;
},
[]
[processCountryEntry]
);
// Process country data when client is ready and dependencies change

View File

@ -99,6 +99,24 @@ const SessionsIcon = () => (
</svg>
);
const AuditLogIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Audit Logs</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
);
const LogoutIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
@ -352,6 +370,14 @@ export default function Sidebar({
isActive={pathname === "/dashboard/users"}
onNavigate={onNavigate}
/>
<NavItem
href="/dashboard/audit-logs"
label="Audit Logs"
icon={<AuditLogIcon />}
isExpanded={isExpanded}
isActive={pathname === "/dashboard/audit-logs"}
onNavigate={onNavigate}
/>
</nav>
<div className="p-4 border-t mt-auto space-y-2">
{/* Theme Toggle */}

View File

@ -0,0 +1,499 @@
"use client";
import {
Activity,
AlertCircle,
AlertTriangle,
CheckCircle,
Clock,
Download,
RefreshCw,
TrendingUp,
XCircle,
Zap,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
interface BatchMetrics {
operationStartTime: number;
requestCount: number;
successCount: number;
failureCount: number;
retryCount: number;
totalCost: number;
averageLatency: number;
circuitBreakerTrips: number;
performanceStats: {
p50: number;
p95: number;
p99: number;
};
}
interface CircuitBreakerStatus {
isOpen: boolean;
failures: number;
lastFailureTime: number;
}
interface SchedulerStatus {
isRunning: boolean;
createBatchesRunning: boolean;
checkStatusRunning: boolean;
processResultsRunning: boolean;
retryFailedRunning: boolean;
isPaused: boolean;
consecutiveErrors: number;
lastErrorTime: Date | null;
circuitBreakers: Record<string, CircuitBreakerStatus>;
config: any;
}
interface MonitoringData {
timestamp: string;
metrics: Record<string, BatchMetrics> | BatchMetrics;
schedulerStatus: SchedulerStatus;
circuitBreakerStatus: Record<string, CircuitBreakerStatus>;
systemHealth: {
schedulerRunning: boolean;
circuitBreakersOpen: boolean;
pausedDueToErrors: boolean;
consecutiveErrors: number;
};
}
export default function BatchMonitoringDashboard() {
const [monitoringData, setMonitoringData] = useState<MonitoringData | null>(
null
);
const [isLoading, setIsLoading] = useState(true);
const [selectedCompany, setSelectedCompany] = useState<string>("all");
const [autoRefresh, setAutoRefresh] = useState(true);
const { toast } = useToast();
const fetchMonitoringData = useCallback(async () => {
try {
const params = new URLSearchParams();
if (selectedCompany !== "all") {
params.set("companyId", selectedCompany);
}
const response = await fetch(`/api/admin/batch-monitoring?${params}`);
if (response.ok) {
const data = await response.json();
setMonitoringData(data);
} else {
throw new Error("Failed to fetch monitoring data");
}
} catch (error) {
console.error("Failed to fetch batch monitoring data:", error);
toast({
title: "Error",
description: "Failed to load batch monitoring data",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
}, [selectedCompany, toast]);
useEffect(() => {
fetchMonitoringData();
}, [fetchMonitoringData]);
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(fetchMonitoringData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, [autoRefresh, fetchMonitoringData]);
const exportLogs = async (format: "json" | "csv") => {
try {
const response = await fetch("/api/admin/batch-monitoring/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
startDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Last 24 hours
endDate: new Date().toISOString(),
format,
}),
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `batch-logs-${Date.now()}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast({
title: "Success",
description: `Batch logs exported as ${format.toUpperCase()}`,
});
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to export logs",
variant: "destructive",
});
}
};
const getHealthStatus = () => {
if (!monitoringData) return { status: "unknown", color: "gray" };
const { systemHealth } = monitoringData;
if (!systemHealth.schedulerRunning) {
return {
status: "critical",
color: "red",
message: "Scheduler not running",
};
}
if (systemHealth.pausedDueToErrors) {
return {
status: "warning",
color: "yellow",
message: "Paused due to errors",
};
}
if (systemHealth.circuitBreakersOpen) {
return {
status: "warning",
color: "yellow",
message: "Circuit breakers open",
};
}
if (systemHealth.consecutiveErrors > 0) {
return {
status: "warning",
color: "yellow",
message: `${systemHealth.consecutiveErrors} consecutive errors`,
};
}
return {
status: "healthy",
color: "green",
message: "All systems operational",
};
};
const renderMetricsCards = () => {
if (!monitoringData) return null;
const metrics = Array.isArray(monitoringData.metrics)
? monitoringData.metrics[0]
: typeof monitoringData.metrics === "object" &&
"operationStartTime" in monitoringData.metrics
? monitoringData.metrics
: Object.values(monitoringData.metrics)[0];
if (!metrics) return null;
const successRate =
metrics.requestCount > 0
? ((metrics.successCount / metrics.requestCount) * 100).toFixed(1)
: "0";
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Requests
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics.requestCount}</div>
<p className="text-xs text-muted-foreground">
{metrics.successCount} successful, {metrics.failureCount} failed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{successRate}%</div>
<p className="text-xs text-muted-foreground">
{metrics.retryCount} retries performed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Average Latency
</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics.averageLatency.toFixed(0)}ms
</div>
<p className="text-xs text-muted-foreground">
P95: {metrics.performanceStats.p95}ms
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Cost</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics.totalCost.toFixed(4)}
</div>
<p className="text-xs text-muted-foreground">
Circuit breaker trips: {metrics.circuitBreakerTrips}
</p>
</CardContent>
</Card>
</div>
);
};
const renderSystemStatus = () => {
if (!monitoringData) return null;
const health = getHealthStatus();
const { schedulerStatus, circuitBreakerStatus } = monitoringData;
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
System Health
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 mb-4">
{health.status === "healthy" && (
<CheckCircle className="h-5 w-5 text-green-500" />
)}
{health.status === "warning" && (
<AlertTriangle className="h-5 w-5 text-yellow-500" />
)}
{health.status === "critical" && (
<XCircle className="h-5 w-5 text-red-500" />
)}
{health.status === "unknown" && (
<AlertCircle className="h-5 w-5 text-gray-500" />
)}
<Badge
variant={
health.status === "healthy" ? "default" : "destructive"
}
>
{health.message}
</Badge>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Scheduler Running:</span>
<Badge
variant={
schedulerStatus.isRunning ? "default" : "destructive"
}
>
{schedulerStatus.isRunning ? "Yes" : "No"}
</Badge>
</div>
<div className="flex justify-between">
<span>Paused:</span>
<Badge
variant={schedulerStatus.isPaused ? "destructive" : "default"}
>
{schedulerStatus.isPaused ? "Yes" : "No"}
</Badge>
</div>
<div className="flex justify-between">
<span>Consecutive Errors:</span>
<span>{schedulerStatus.consecutiveErrors}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5" />
Circuit Breakers
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Object.entries(circuitBreakerStatus).map(([name, status]) => (
<div key={name} className="flex justify-between items-center">
<span className="text-sm capitalize">
{name.replace(/([A-Z])/g, " $1").trim()}
</span>
<Badge variant={status.isOpen ? "destructive" : "default"}>
{status.isOpen ? "Open" : "Closed"}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4" />
<p>Loading batch monitoring data...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold">Batch Processing Monitor</h2>
<p className="text-sm text-muted-foreground">
Real-time monitoring of OpenAI Batch API operations
</p>
</div>
<div className="flex gap-2">
<Select value={selectedCompany} onValueChange={setSelectedCompany}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select company" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Companies</SelectItem>
{/* Add company options here */}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${autoRefresh ? "animate-spin" : ""}`}
/>
{autoRefresh ? "Auto" : "Manual"}
</Button>
<Button variant="outline" size="sm" onClick={fetchMonitoringData}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
{renderSystemStatus()}
{renderMetricsCards()}
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="export">Export</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Batch Processing Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground mb-4">
Last updated:{" "}
{monitoringData?.timestamp
? new Date(monitoringData.timestamp).toLocaleString()
: "Never"}
</div>
{monitoringData && (
<pre className="bg-muted p-4 rounded text-xs overflow-auto">
{JSON.stringify(monitoringData, null, 2)}
</pre>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="logs" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Recent Batch Processing Logs</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Real-time batch processing logs will be displayed here. For
detailed log analysis, use the export feature.
</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="export" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Export Batch Processing Data</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Export batch processing logs and metrics for detailed analysis.
</p>
<div className="flex gap-2">
<Button onClick={() => exportLogs("json")}>
<Download className="h-4 w-4 mr-2" />
Export JSON
</Button>
<Button variant="outline" onClick={() => exportLogs("csv")}>
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@ -7,14 +7,14 @@
"use client";
import React, { FormEvent, ReactNode } from "react";
import type { FormEvent, ReactNode } from "react";
import { useCSRFForm } from "../../lib/hooks/useCSRF";
interface CSRFProtectedFormProps {
children: ReactNode;
action: string;
method?: "POST" | "PUT" | "DELETE" | "PATCH";
onSubmit?: (formData: FormData) => Promise<void> | void;
onSubmit?: (_formData: FormData) => Promise<void> | void;
className?: string;
encType?: string;
}
@ -71,13 +71,7 @@ export function CSRFProtectedForm({
encType={encType}
>
{/* Hidden CSRF token field for non-JS fallback */}
{token && (
<input
type="hidden"
name="csrf_token"
value={token}
/>
)}
{token && <input type="hidden" name="csrf_token" value={token} />}
{children}
</form>
@ -99,7 +93,9 @@ export function ExampleCSRFForm() {
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">CSRF Protected Form Example</h2>
<h2 className="text-xl font-semibold mb-4">
CSRF Protected Form Example
</h2>
<CSRFProtectedForm
action="/api/example-endpoint"
@ -107,7 +103,10 @@ export function ExampleCSRFForm() {
className="space-y-4"
>
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700"
>
Name
</label>
<input
@ -120,7 +119,10 @@ export function ExampleCSRFForm() {
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
@ -133,7 +135,10 @@ export function ExampleCSRFForm() {
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700"
>
Message
</label>
<textarea
@ -153,4 +158,4 @@ export function ExampleCSRFForm() {
</CSRFProtectedForm>
</div>
);
}
}

View File

@ -21,7 +21,7 @@ import {
import { Button } from "@/components/ui/button";
type Api = {
fire: (options?: ConfettiOptions) => void;
fire: (_options?: ConfettiOptions) => void;
};
type Props = React.ComponentPropsWithRef<"canvas"> & {

View File

@ -7,7 +7,8 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from "react";
import type React from "react";
import { createContext, useContext, useEffect, useState, useCallback } from "react";
import { CSRFClient } from "../../lib/csrf";
interface CSRFContextType {
@ -15,9 +16,11 @@ interface CSRFContextType {
loading: boolean;
error: string | null;
refreshToken: () => Promise<void>;
addTokenToFetch: (options: RequestInit) => RequestInit;
addTokenToFormData: (formData: FormData) => FormData;
addTokenToObject: <T extends Record<string, unknown>>(obj: T) => T & { csrfToken: string };
addTokenToFetch: (_options: RequestInit) => RequestInit;
addTokenToFormData: (_formData: FormData) => FormData;
addTokenToObject: <T extends Record<string, unknown>>(
_obj: T
) => T & { csrfToken: string };
}
const CSRFContext = createContext<CSRFContextType | undefined>(undefined);
@ -37,7 +40,7 @@ export function CSRFProvider({ children }: CSRFProviderProps) {
/**
* Fetch CSRF token from server
*/
const fetchToken = async () => {
const fetchToken = useCallback(async () => {
try {
setLoading(true);
setError(null);
@ -68,13 +71,14 @@ export function CSRFProvider({ children }: CSRFProviderProps) {
throw new Error("Invalid response from CSRF endpoint");
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to fetch CSRF token";
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch CSRF token";
setError(errorMessage);
console.error("CSRF token fetch error:", errorMessage);
} finally {
setLoading(false);
}
};
}, []);
/**
* Refresh token manually
@ -88,7 +92,7 @@ export function CSRFProvider({ children }: CSRFProviderProps) {
*/
useEffect(() => {
fetchToken();
}, []);
}, [fetchToken]);
/**
* Monitor token changes in cookies
@ -118,9 +122,7 @@ export function CSRFProvider({ children }: CSRFProviderProps) {
};
return (
<CSRFContext.Provider value={contextValue}>
{children}
</CSRFContext.Provider>
<CSRFContext.Provider value={contextValue}>{children}</CSRFContext.Provider>
);
}
@ -150,4 +152,4 @@ export function withCSRF<P extends object>(Component: React.ComponentType<P>) {
WrappedComponent.displayName = `withCSRF(${Component.displayName || Component.name})`;
return WrappedComponent;
}
}

View File

@ -0,0 +1,200 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
interface GeographicThreatMapProps {
geoDistribution: Record<string, number>;
title?: string;
}
// Simple country code to name mapping for common countries
const countryNames: Record<string, string> = {
USA: "United States",
GBR: "United Kingdom",
DEU: "Germany",
FRA: "France",
JPN: "Japan",
CHN: "China",
IND: "India",
BRA: "Brazil",
CAN: "Canada",
AUS: "Australia",
RUS: "Russia",
ESP: "Spain",
ITA: "Italy",
NLD: "Netherlands",
KOR: "South Korea",
MEX: "Mexico",
CHE: "Switzerland",
SWE: "Sweden",
NOR: "Norway",
DNK: "Denmark",
FIN: "Finland",
POL: "Poland",
BEL: "Belgium",
AUT: "Austria",
NZL: "New Zealand",
SGP: "Singapore",
THA: "Thailand",
IDN: "Indonesia",
MYS: "Malaysia",
PHL: "Philippines",
VNM: "Vietnam",
ARE: "UAE",
SAU: "Saudi Arabia",
ISR: "Israel",
ZAF: "South Africa",
EGY: "Egypt",
TUR: "Turkey",
GRC: "Greece",
PRT: "Portugal",
CZE: "Czech Republic",
HUN: "Hungary",
ROU: "Romania",
BGR: "Bulgaria",
HRV: "Croatia",
SVN: "Slovenia",
SVK: "Slovakia",
EST: "Estonia",
LVA: "Latvia",
LTU: "Lithuania",
LUX: "Luxembourg",
MLT: "Malta",
CYP: "Cyprus",
ISL: "Iceland",
IRL: "Ireland",
ARG: "Argentina",
CHL: "Chile",
COL: "Colombia",
PER: "Peru",
URY: "Uruguay",
ECU: "Ecuador",
BOL: "Bolivia",
PRY: "Paraguay",
VEN: "Venezuela",
UKR: "Ukraine",
BLR: "Belarus",
MDA: "Moldova",
GEO: "Georgia",
ARM: "Armenia",
AZE: "Azerbaijan",
KAZ: "Kazakhstan",
UZB: "Uzbekistan",
KGZ: "Kyrgyzstan",
TJK: "Tajikistan",
TKM: "Turkmenistan",
MNG: "Mongolia",
};
export function GeographicThreatMap({
geoDistribution,
title = "Geographic Threat Distribution",
}: GeographicThreatMapProps) {
const sortedCountries = Object.entries(geoDistribution)
.sort(([, a], [, b]) => b - a)
.slice(0, 12);
const totalEvents = Object.values(geoDistribution).reduce(
(sum, count) => sum + count,
0
);
const getThreatLevel = (count: number, total: number) => {
const percentage = (count / total) * 100;
if (percentage > 50) return { level: "high", color: "destructive" };
if (percentage > 20) return { level: "medium", color: "secondary" };
if (percentage > 5) return { level: "low", color: "outline" };
return { level: "minimal", color: "outline" };
};
const getCountryName = (code: string) => {
return countryNames[code] || code;
};
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>
Security events by country ({totalEvents} total events)
</CardDescription>
</CardHeader>
<CardContent>
{sortedCountries.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<p>No geographic data available</p>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sortedCountries.map(([countryCode, count]) => {
const threat = getThreatLevel(count, totalEvents);
const percentage = ((count / totalEvents) * 100).toFixed(1);
return (
<div
key={countryCode}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="font-medium">
{getCountryName(countryCode)}
</span>
<Badge
variant={threat.color as "default" | "secondary" | "destructive" | "outline"}
className="text-xs"
>
{threat.level}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{count} events ({percentage}%)
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold">{count}</div>
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
threat.level === "high"
? "bg-red-500"
: threat.level === "medium"
? "bg-yellow-500"
: threat.level === "low"
? "bg-blue-500"
: "bg-gray-400"
}`}
style={{
width: `${Math.min(100, (count / Math.max(...Object.values(geoDistribution))) * 100)}%`,
}}
/>
</div>
</div>
</div>
);
})}
</div>
{Object.keys(geoDistribution).length > 12 && (
<div className="text-center pt-4 border-t">
<p className="text-sm text-muted-foreground">
And {Object.keys(geoDistribution).length - 12} more
countries...
</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,274 @@
"use client";
import { AlertTriangle, CheckCircle, Eye, EyeOff } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface SecurityAlert {
id: string;
timestamp: string;
severity: string;
type: string;
title: string;
description: string;
eventType: string;
context: Record<string, unknown>;
metadata: Record<string, unknown>;
acknowledged: boolean;
}
interface SecurityAlertsTableProps {
alerts: SecurityAlert[];
onAcknowledge: (_alertId: string) => void;
}
export function SecurityAlertsTable({
alerts,
onAcknowledge,
}: SecurityAlertsTableProps) {
const [showAcknowledged, setShowAcknowledged] = useState(false);
const [selectedAlert, setSelectedAlert] = useState<SecurityAlert | null>(
null
);
const getSeverityColor = (severity: string) => {
switch (severity?.toLowerCase()) {
case "critical":
return "destructive";
case "high":
return "destructive";
case "medium":
return "secondary";
case "low":
return "outline";
default:
return "outline";
}
};
const filteredAlerts = alerts.filter(
(alert) => showAcknowledged || !alert.acknowledged
);
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString();
};
const formatAlertType = (type: string) => {
return type
.replace(/_/g, " ")
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase());
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="text-lg font-semibold">Security Alerts</h3>
<p className="text-sm text-muted-foreground">
{filteredAlerts.length} alerts{" "}
{showAcknowledged ? "total" : "active"}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowAcknowledged(!showAcknowledged)}
>
{showAcknowledged ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
{showAcknowledged ? "Hide Acknowledged" : "Show All"}
</Button>
</div>
{filteredAlerts.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-8">
<CheckCircle className="h-12 w-12 text-green-500 mb-4" />
<h3 className="text-lg font-semibold mb-2">No Active Alerts</h3>
<p className="text-muted-foreground text-center">
All security alerts have been addressed. System is operating
normally.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Severity</TableHead>
<TableHead>Type</TableHead>
<TableHead>Description</TableHead>
<TableHead>Timestamp</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAlerts.map((alert) => (
<TableRow
key={alert.id}
className={alert.acknowledged ? "opacity-60" : ""}
>
<TableCell>
<Badge variant={getSeverityColor(alert.severity)}>
{alert.severity}
</Badge>
</TableCell>
<TableCell>
<div className="space-y-1">
<span className="font-medium">
{formatAlertType(alert.type)}
</span>
<p className="text-xs text-muted-foreground">
{alert.eventType}
</p>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<span className="font-medium">{alert.title}</span>
<p className="text-sm text-muted-foreground line-clamp-2">
{alert.description}
</p>
</div>
</TableCell>
<TableCell>
<span className="text-sm">
{formatTimestamp(alert.timestamp)}
</span>
</TableCell>
<TableCell>
{alert.acknowledged ? (
<Badge variant="outline">
<CheckCircle className="h-3 w-3 mr-1" />
Acknowledged
</Badge>
) : (
<Badge variant="secondary">
<AlertTriangle className="h-3 w-3 mr-1" />
Active
</Badge>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setSelectedAlert(alert)}
>
<Eye className="h-3 w-3" />
</Button>
{!alert.acknowledged && (
<Button
size="sm"
onClick={() => onAcknowledge(alert.id)}
>
Acknowledge
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Alert Details Modal */}
{selectedAlert && (
<Card className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<Card className="max-w-2xl w-full max-h-[80vh] overflow-auto">
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-2">
<CardTitle>{selectedAlert.title}</CardTitle>
<div className="flex items-center gap-2">
<Badge variant={getSeverityColor(selectedAlert.severity)}>
{selectedAlert.severity}
</Badge>
<Badge variant="outline">
{formatAlertType(selectedAlert.type)}
</Badge>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedAlert(null)}
>
Close
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">Description</h4>
<p className="text-sm text-muted-foreground">
{selectedAlert.description}
</p>
</div>
<div>
<h4 className="font-medium mb-2">Context</h4>
<div className="bg-muted p-3 rounded-md">
<pre className="text-xs overflow-auto">
{JSON.stringify(selectedAlert.context, null, 2)}
</pre>
</div>
</div>
{selectedAlert.metadata &&
Object.keys(selectedAlert.metadata).length > 0 && (
<div>
<h4 className="font-medium mb-2">Metadata</h4>
<div className="bg-muted p-3 rounded-md">
<pre className="text-xs overflow-auto">
{JSON.stringify(selectedAlert.metadata, null, 2)}
</pre>
</div>
</div>
)}
<div className="flex items-center justify-between pt-4 border-t">
<span className="text-sm text-muted-foreground">
{formatTimestamp(selectedAlert.timestamp)}
</span>
{!selectedAlert.acknowledged && (
<Button
onClick={() => {
onAcknowledge(selectedAlert.id);
setSelectedAlert(null);
}}
>
Acknowledge Alert
</Button>
)}
</div>
</CardContent>
</Card>
</Card>
)}
</div>
);
}

View File

@ -0,0 +1,489 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface SecurityConfig {
thresholds: {
failedLoginsPerMinute: number;
failedLoginsPerHour: number;
rateLimitViolationsPerMinute: number;
cspViolationsPerMinute: number;
adminActionsPerHour: number;
massDataAccessThreshold: number;
suspiciousIPThreshold: number;
};
alerting: {
enabled: boolean;
channels: string[];
suppressDuplicateMinutes: number;
escalationTimeoutMinutes: number;
};
retention: {
alertRetentionDays: number;
metricsRetentionDays: number;
};
}
interface SecurityConfigModalProps {
onClose: () => void;
onSave: () => void;
}
export function SecurityConfigModal({
onClose,
onSave,
}: SecurityConfigModalProps) {
const [config, setConfig] = useState<SecurityConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const loadConfig = useCallback(async () => {
try {
const response = await fetch("/api/admin/security-monitoring");
if (!response.ok) throw new Error("Failed to load config");
const data = await response.json();
setConfig(data.config);
} catch (error) {
console.error("Error loading config:", error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadConfig();
}, [loadConfig]);
const saveConfig = async () => {
if (!config) return;
setSaving(true);
try {
const response = await fetch("/api/admin/security-monitoring", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
});
if (!response.ok) throw new Error("Failed to save config");
onSave();
} catch (error) {
console.error("Error saving config:", error);
} finally {
setSaving(false);
}
};
const updateThreshold = (
key: keyof SecurityConfig["thresholds"],
value: number
) => {
if (!config) return;
setConfig({
...config,
thresholds: {
...config.thresholds,
[key]: value,
},
});
};
const updateAlerting = (
key: keyof SecurityConfig["alerting"],
value: unknown
) => {
if (!config) return;
setConfig({
...config,
alerting: {
...config.alerting,
[key]: value,
},
});
};
const updateRetention = (
key: keyof SecurityConfig["retention"],
value: number
) => {
if (!config) return;
setConfig({
...config,
retention: {
...config.retention,
[key]: value,
},
});
};
const toggleAlertChannel = (channel: string) => {
if (!config) return;
const channels = config.alerting.channels.includes(channel)
? config.alerting.channels.filter((c) => c !== channel)
: [...config.alerting.channels, channel];
updateAlerting("channels", channels);
};
if (loading) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
</div>
</DialogContent>
</Dialog>
);
}
if (!config) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Error</DialogTitle>
<DialogDescription>
Failed to load security configuration
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={onClose}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Security Monitoring Configuration</DialogTitle>
<DialogDescription>
Configure security monitoring thresholds, alerting, and data
retention
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="thresholds" className="space-y-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="thresholds">Thresholds</TabsTrigger>
<TabsTrigger value="alerting">Alerting</TabsTrigger>
<TabsTrigger value="retention">Data Retention</TabsTrigger>
</TabsList>
<TabsContent value="thresholds" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Detection Thresholds</CardTitle>
<CardDescription>
Configure when security alerts should be triggered
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="failedLoginsPerMinute">
Failed Logins per Minute
</Label>
<Input
id="failedLoginsPerMinute"
type="number"
min="1"
max="100"
value={config.thresholds.failedLoginsPerMinute}
onChange={(e) =>
updateThreshold(
"failedLoginsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="failedLoginsPerHour">
Failed Logins per Hour
</Label>
<Input
id="failedLoginsPerHour"
type="number"
min="1"
max="1000"
value={config.thresholds.failedLoginsPerHour}
onChange={(e) =>
updateThreshold(
"failedLoginsPerHour",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="rateLimitViolationsPerMinute">
Rate Limit Violations per Minute
</Label>
<Input
id="rateLimitViolationsPerMinute"
type="number"
min="1"
max="100"
value={config.thresholds.rateLimitViolationsPerMinute}
onChange={(e) =>
updateThreshold(
"rateLimitViolationsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cspViolationsPerMinute">
CSP Violations per Minute
</Label>
<Input
id="cspViolationsPerMinute"
type="number"
min="1"
max="100"
value={config.thresholds.cspViolationsPerMinute}
onChange={(e) =>
updateThreshold(
"cspViolationsPerMinute",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminActionsPerHour">
Admin Actions per Hour
</Label>
<Input
id="adminActionsPerHour"
type="number"
min="1"
max="100"
value={config.thresholds.adminActionsPerHour}
onChange={(e) =>
updateThreshold(
"adminActionsPerHour",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="suspiciousIPThreshold">
Suspicious IP Threshold
</Label>
<Input
id="suspiciousIPThreshold"
type="number"
min="1"
max="100"
value={config.thresholds.suspiciousIPThreshold}
onChange={(e) =>
updateThreshold(
"suspiciousIPThreshold",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="alerting" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Alert Configuration</CardTitle>
<CardDescription>
Configure how and when alerts are sent
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Switch
id="alerting-enabled"
checked={config.alerting.enabled}
onCheckedChange={(checked) =>
updateAlerting("enabled", checked)
}
/>
<Label htmlFor="alerting-enabled">
Enable Security Alerting
</Label>
</div>
<div className="space-y-2">
<Label>Alert Channels</Label>
<div className="flex flex-wrap gap-2">
{["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"].map(
(channel) => (
<Badge
key={channel}
variant={
config.alerting.channels.includes(channel)
? "default"
: "outline"
}
className="cursor-pointer"
onClick={() => toggleAlertChannel(channel)}
>
{channel}
</Badge>
)
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="suppressDuplicateMinutes">
Suppress Duplicates (minutes)
</Label>
<Input
id="suppressDuplicateMinutes"
type="number"
min="1"
max="1440"
value={config.alerting.suppressDuplicateMinutes}
onChange={(e) =>
updateAlerting(
"suppressDuplicateMinutes",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="escalationTimeoutMinutes">
Escalation Timeout (minutes)
</Label>
<Input
id="escalationTimeoutMinutes"
type="number"
min="5"
max="1440"
value={config.alerting.escalationTimeoutMinutes}
onChange={(e) =>
updateAlerting(
"escalationTimeoutMinutes",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="retention" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Data Retention</CardTitle>
<CardDescription>
Configure how long security data is stored
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="alertRetentionDays">
Alert Retention (days)
</Label>
<Input
id="alertRetentionDays"
type="number"
min="1"
max="3650"
value={config.retention.alertRetentionDays}
onChange={(e) =>
updateRetention(
"alertRetentionDays",
Number.parseInt(e.target.value)
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="metricsRetentionDays">
Metrics Retention (days)
</Label>
<Input
id="metricsRetentionDays"
type="number"
min="1"
max="3650"
value={config.retention.metricsRetentionDays}
onChange={(e) =>
updateRetention(
"metricsRetentionDays",
Number.parseInt(e.target.value)
)
}
/>
</div>
</div>
<div className="text-sm text-muted-foreground">
<p>
Alert data includes security alerts and acknowledgments
</p>
<p> Metrics data includes aggregated security statistics</p>
<p>
Audit logs are retained separately according to audit
policy
</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={saveConfig} disabled={saving}>
{saving ? "Saving..." : "Save Configuration"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import {
Bar,
BarChart,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
interface SecurityMetricsChartProps {
data: Array<{ hour: number; count: number }>;
type?: "line" | "bar";
title?: string;
}
export function SecurityMetricsChart({
data,
type = "line",
title,
}: SecurityMetricsChartProps) {
const chartData = data.map((item) => ({
hour: `${item.hour}:00`,
count: item.count,
}));
const ChartComponent = type === "line" ? LineChart : BarChart;
const DataComponent =
type === "line" ? (
<Line
type="monotone"
dataKey="count"
stroke="#8884d8"
strokeWidth={2}
dot={{ fill: "#8884d8", strokeWidth: 2 }}
/>
) : (
<Bar dataKey="count" fill="#8884d8" />
);
return (
<div className="space-y-2">
{title && <h3 className="text-lg font-semibold">{title}</h3>}
<ResponsiveContainer width="100%" height={300}>
<ChartComponent data={chartData}>
<XAxis
dataKey="hour"
tick={{ fontSize: 12 }}
tickLine={{ stroke: "#e5e7eb" }}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={{ stroke: "#e5e7eb" }}
axisLine={{ stroke: "#e5e7eb" }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#f9fafb",
border: "1px solid #e5e7eb",
borderRadius: "6px",
}}
/>
{DataComponent}
</ChartComponent>
</ResponsiveContainer>
</div>
);
}

View File

@ -0,0 +1,84 @@
"use client";
import { AlertCircle, AlertTriangle, Shield, Zap } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface ThreatLevelIndicatorProps {
level: "LOW" | "MODERATE" | "HIGH" | "CRITICAL";
score?: number;
size?: "sm" | "md" | "lg";
}
export function ThreatLevelIndicator({
level,
score,
size = "md",
}: ThreatLevelIndicatorProps) {
const getConfig = (threatLevel: string) => {
switch (threatLevel) {
case "CRITICAL":
return {
color: "destructive",
bgColor: "bg-red-500",
icon: Zap,
text: "Critical Threat",
description: "Immediate action required",
};
case "HIGH":
return {
color: "destructive",
bgColor: "bg-orange-500",
icon: AlertCircle,
text: "High Threat",
description: "Urgent attention needed",
};
case "MODERATE":
return {
color: "secondary",
bgColor: "bg-yellow-500",
icon: AlertTriangle,
text: "Moderate Threat",
description: "Monitor closely",
};
default:
return {
color: "outline",
bgColor: "bg-green-500",
icon: Shield,
text: "Low Threat",
description: "System is secure",
};
}
};
const config = getConfig(level);
const Icon = config.icon;
const sizeClasses = {
sm: { icon: "h-4 w-4", text: "text-sm", badge: "text-xs" },
md: { icon: "h-5 w-5", text: "text-base", badge: "text-sm" },
lg: { icon: "h-6 w-6", text: "text-lg", badge: "text-base" },
};
const classes = sizeClasses[size];
return (
<div className="flex items-center gap-2">
<div className={`p-2 rounded-full ${config.bgColor}`}>
<Icon className={`${classes.icon} text-white`} />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant={config.color as "default" | "secondary" | "destructive" | "outline"} className={classes.badge}>
{config.text}
</Badge>
{score !== undefined && (
<span className={`font-medium ${classes.text}`}>{score}/100</span>
)}
</div>
<p className="text-xs text-muted-foreground">{config.description}</p>
</div>
</div>
);
}