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

@ -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>
);
}