mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 11:32:13 +01:00
fix: implement comprehensive UI/UX and code organization improvements
CSRF Form Enhancements: - Add optional onError callback prop for better error handling - Remove CSRF token from console logging for security - Provide user-friendly error notifications instead of silent failures Date Filter Optimization: - Refactor sessions route to avoid object mutation issues - Build date filters cleanly without relying on spreading existing objects - Prevent potential undefined startTime mutations Geographic Threat Map Optimization: - Extract country names to reusable constants in lib/constants/countries.ts - Calculate max values once to avoid repeated expensive operations - Centralize threat level color mapping to eliminate duplicated logic - Replace repeated color assignments with centralized THREAT_LEVELS configuration Accessibility Improvements: - Add keyboard support to audit log table rows (Enter/Space keys) - Include proper ARIA labels and focus management - Add tabIndex for screen reader compatibility - Enhance focus indicators with ring styling Performance & Code Organization: - Move COUNTRY_NAMES to shared constants for reusability - Optimize calculation patterns in threat mapping components - Reduce redundant logic and improve maintainability
This commit is contained in:
@ -41,19 +41,20 @@ function buildWhereClause(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Date Range Filter
|
// Date Range Filter
|
||||||
|
const dateFilters: { gte?: Date; lt?: Date } = {};
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
whereClause.startTime = {
|
dateFilters.gte = new Date(startDate);
|
||||||
...((whereClause.startTime as object) || {}),
|
|
||||||
gte: new Date(startDate),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
const inclusiveEndDate = new Date(endDate);
|
const inclusiveEndDate = new Date(endDate);
|
||||||
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
||||||
whereClause.startTime = {
|
dateFilters.lt = inclusiveEndDate;
|
||||||
...((whereClause.startTime as object) || {}),
|
}
|
||||||
lt: inclusiveEndDate,
|
|
||||||
};
|
if (Object.keys(dateFilters).length > 0) {
|
||||||
|
whereClause.startTime = dateFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
return whereClause;
|
return whereClause;
|
||||||
|
|||||||
@ -367,8 +367,16 @@ export default function AuditLogsPage() {
|
|||||||
{auditLogs.map((log) => (
|
{auditLogs.map((log) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={log.id}
|
key={log.id}
|
||||||
className="cursor-pointer hover:bg-gray-50"
|
className="cursor-pointer hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
|
||||||
onClick={() => setSelectedLog(log)}
|
onClick={() => setSelectedLog(log)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedLog(log);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`View details for ${eventTypeLabels[log.eventType] || log.eventType} event`}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">
|
||||||
{formatDistanceToNow(new Date(log.timestamp), {
|
{formatDistanceToNow(new Date(log.timestamp), {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ interface CSRFProtectedFormProps {
|
|||||||
action: string;
|
action: string;
|
||||||
method?: "POST" | "PUT" | "DELETE" | "PATCH";
|
method?: "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
onSubmit?: (formData: FormData) => Promise<void> | void;
|
onSubmit?: (formData: FormData) => Promise<void> | void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
encType?: string;
|
encType?: string;
|
||||||
}
|
}
|
||||||
@ -28,6 +29,7 @@ export function CSRFProtectedForm({
|
|||||||
action,
|
action,
|
||||||
method = "POST",
|
method = "POST",
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onError,
|
||||||
className,
|
className,
|
||||||
encType,
|
encType,
|
||||||
}: CSRFProtectedFormProps) {
|
}: CSRFProtectedFormProps) {
|
||||||
@ -59,7 +61,14 @@ export function CSRFProtectedForm({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Form submission error:", error);
|
console.error("Form submission error:", error);
|
||||||
// You might want to show an error message to the user here
|
|
||||||
|
// Notify user of the error
|
||||||
|
if (onError && error instanceof Error) {
|
||||||
|
onError(error);
|
||||||
|
} else {
|
||||||
|
// Fallback: show alert if no error handler provided
|
||||||
|
alert("An error occurred while submitting the form. Please try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,8 +99,11 @@ export function ExampleCSRFForm() {
|
|||||||
|
|
||||||
const handleCustomSubmit = async (formData: FormData) => {
|
const handleCustomSubmit = async (formData: FormData) => {
|
||||||
// Custom form submission logic
|
// Custom form submission logic
|
||||||
|
// Filter out CSRF token for security when logging
|
||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
console.log("Form data:", data);
|
// biome-ignore lint/correctness/noUnusedVariables: csrf_token is intentionally extracted and discarded for security
|
||||||
|
const { csrf_token, ...safeData } = data;
|
||||||
|
console.log("Form data (excluding CSRF token):", safeData);
|
||||||
|
|
||||||
// You can process the form data here before submission
|
// You can process the form data here before submission
|
||||||
// The CSRF token is automatically included in formData
|
// The CSRF token is automatically included in formData
|
||||||
|
|||||||
@ -8,114 +8,48 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { COUNTRY_NAMES } from "../../lib/constants/countries";
|
||||||
|
|
||||||
interface GeographicThreatMapProps {
|
interface GeographicThreatMapProps {
|
||||||
geoDistribution: Record<string, number>;
|
geoDistribution: Record<string, number>;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple country code to name mapping for common countries
|
// Threat level configuration with colors
|
||||||
const countryNames: Record<string, string> = {
|
const THREAT_LEVELS = {
|
||||||
USA: "United States",
|
high: { color: "destructive", bgColor: "bg-red-500" },
|
||||||
GBR: "United Kingdom",
|
medium: { color: "secondary", bgColor: "bg-yellow-500" },
|
||||||
DEU: "Germany",
|
low: { color: "outline", bgColor: "bg-blue-500" },
|
||||||
FRA: "France",
|
minimal: { color: "outline", bgColor: "bg-gray-400" },
|
||||||
JPN: "Japan",
|
} as const;
|
||||||
CHN: "China",
|
|
||||||
IND: "India",
|
type ThreatLevel = keyof typeof THREAT_LEVELS;
|
||||||
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({
|
export function GeographicThreatMap({
|
||||||
geoDistribution,
|
geoDistribution,
|
||||||
title = "Geographic Threat Distribution",
|
title = "Geographic Threat Distribution",
|
||||||
}: GeographicThreatMapProps) {
|
}: GeographicThreatMapProps) {
|
||||||
const sortedCountries = Object.entries(geoDistribution)
|
// Calculate values once for efficiency
|
||||||
.sort(([, a], [, b]) => b - a)
|
|
||||||
.slice(0, 12);
|
|
||||||
|
|
||||||
const totalEvents = Object.values(geoDistribution).reduce(
|
const totalEvents = Object.values(geoDistribution).reduce(
|
||||||
(sum, count) => sum + count,
|
(sum, count) => sum + count,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
const maxEventCount = Math.max(...Object.values(geoDistribution));
|
||||||
|
|
||||||
const getThreatLevel = (count: number, total: number) => {
|
const sortedCountries = Object.entries(geoDistribution)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.slice(0, 12);
|
||||||
|
|
||||||
|
const getThreatLevel = (count: number, total: number): ThreatLevel => {
|
||||||
const percentage = (count / total) * 100;
|
const percentage = (count / total) * 100;
|
||||||
if (percentage > 50) return { level: "high", color: "destructive" };
|
if (percentage > 50) return "high";
|
||||||
if (percentage > 20) return { level: "medium", color: "secondary" };
|
if (percentage > 20) return "medium";
|
||||||
if (percentage > 5) return { level: "low", color: "outline" };
|
if (percentage > 5) return "low";
|
||||||
return { level: "minimal", color: "outline" };
|
return "minimal";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCountryName = (code: string) => {
|
const getCountryName = (code: string) => {
|
||||||
return countryNames[code] || code;
|
return COUNTRY_NAMES[code] || code;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -135,7 +69,7 @@ export function GeographicThreatMap({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{sortedCountries.map(([countryCode, count]) => {
|
{sortedCountries.map(([countryCode, count]) => {
|
||||||
const threat = getThreatLevel(count, totalEvents);
|
const threatLevel = getThreatLevel(count, totalEvents);
|
||||||
const percentage = ((count / totalEvents) * 100).toFixed(1);
|
const percentage = ((count / totalEvents) * 100).toFixed(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -150,7 +84,7 @@ export function GeographicThreatMap({
|
|||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
threat.color as
|
THREAT_LEVELS[threatLevel].color as
|
||||||
| "default"
|
| "default"
|
||||||
| "secondary"
|
| "secondary"
|
||||||
| "destructive"
|
| "destructive"
|
||||||
@ -158,7 +92,7 @@ export function GeographicThreatMap({
|
|||||||
}
|
}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{threat.level}
|
{threatLevel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@ -170,17 +104,9 @@ export function GeographicThreatMap({
|
|||||||
<div className="text-2xl font-bold">{count}</div>
|
<div className="text-2xl font-bold">{count}</div>
|
||||||
<div className="w-16 bg-gray-200 rounded-full h-2">
|
<div className="w-16 bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${THREAT_LEVELS[threatLevel].bgColor}`}
|
||||||
threat.level === "high"
|
|
||||||
? "bg-red-500"
|
|
||||||
: threat.level === "medium"
|
|
||||||
? "bg-yellow-500"
|
|
||||||
: threat.level === "low"
|
|
||||||
? "bg-blue-500"
|
|
||||||
: "bg-gray-400"
|
|
||||||
}`}
|
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(100, (count / Math.max(...Object.values(geoDistribution))) * 100)}%`,
|
width: `${Math.min(100, (count / maxEventCount) * 100)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
lib/constants/countries.ts
Normal file
84
lib/constants/countries.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Country Constants
|
||||||
|
*
|
||||||
|
* Country code to name mapping for common countries
|
||||||
|
* Used throughout the application for geographic data display
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const COUNTRY_NAMES: 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",
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user