From 33981b87dd20244a123aef4bc16885b1adcef544 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sun, 13 Jul 2025 16:32:57 +0200 Subject: [PATCH] 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 --- app/api/dashboard/sessions/route.ts | 17 +-- app/dashboard/audit-logs/page.tsx | 10 +- components/forms/CSRFProtectedForm.tsx | 16 ++- components/security/GeographicThreatMap.tsx | 128 +++++--------------- lib/constants/countries.ts | 84 +++++++++++++ 5 files changed, 143 insertions(+), 112 deletions(-) create mode 100644 lib/constants/countries.ts diff --git a/app/api/dashboard/sessions/route.ts b/app/api/dashboard/sessions/route.ts index 096eda6..8b90721 100644 --- a/app/api/dashboard/sessions/route.ts +++ b/app/api/dashboard/sessions/route.ts @@ -41,19 +41,20 @@ function buildWhereClause( } // Date Range Filter + const dateFilters: { gte?: Date; lt?: Date } = {}; + if (startDate) { - whereClause.startTime = { - ...((whereClause.startTime as object) || {}), - gte: new Date(startDate), - }; + dateFilters.gte = new Date(startDate); } + if (endDate) { const inclusiveEndDate = new Date(endDate); inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1); - whereClause.startTime = { - ...((whereClause.startTime as object) || {}), - lt: inclusiveEndDate, - }; + dateFilters.lt = inclusiveEndDate; + } + + if (Object.keys(dateFilters).length > 0) { + whereClause.startTime = dateFilters; } return whereClause; diff --git a/app/dashboard/audit-logs/page.tsx b/app/dashboard/audit-logs/page.tsx index b35b020..79ab2d4 100644 --- a/app/dashboard/audit-logs/page.tsx +++ b/app/dashboard/audit-logs/page.tsx @@ -367,8 +367,16 @@ export default function AuditLogsPage() { {auditLogs.map((log) => ( 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`} > {formatDistanceToNow(new Date(log.timestamp), { diff --git a/components/forms/CSRFProtectedForm.tsx b/components/forms/CSRFProtectedForm.tsx index 13e62cd..5d520a6 100644 --- a/components/forms/CSRFProtectedForm.tsx +++ b/components/forms/CSRFProtectedForm.tsx @@ -16,6 +16,7 @@ interface CSRFProtectedFormProps { action: string; method?: "POST" | "PUT" | "DELETE" | "PATCH"; onSubmit?: (formData: FormData) => Promise | void; + onError?: (error: Error) => void; className?: string; encType?: string; } @@ -28,6 +29,7 @@ export function CSRFProtectedForm({ action, method = "POST", onSubmit, + onError, className, encType, }: CSRFProtectedFormProps) { @@ -59,7 +61,14 @@ export function CSRFProtectedForm({ } } catch (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) => { // Custom form submission logic + // Filter out CSRF token for security when logging 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 // The CSRF token is automatically included in formData diff --git a/components/security/GeographicThreatMap.tsx b/components/security/GeographicThreatMap.tsx index c8f6f9c..ec6fc22 100644 --- a/components/security/GeographicThreatMap.tsx +++ b/components/security/GeographicThreatMap.tsx @@ -8,114 +8,48 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { COUNTRY_NAMES } from "../../lib/constants/countries"; interface GeographicThreatMapProps { geoDistribution: Record; title?: string; } -// Simple country code to name mapping for common countries -const countryNames: Record = { - 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", -}; +// Threat level configuration with colors +const THREAT_LEVELS = { + high: { color: "destructive", bgColor: "bg-red-500" }, + medium: { color: "secondary", bgColor: "bg-yellow-500" }, + low: { color: "outline", bgColor: "bg-blue-500" }, + minimal: { color: "outline", bgColor: "bg-gray-400" }, +} as const; + +type ThreatLevel = keyof typeof THREAT_LEVELS; export function GeographicThreatMap({ geoDistribution, title = "Geographic Threat Distribution", }: GeographicThreatMapProps) { - const sortedCountries = Object.entries(geoDistribution) - .sort(([, a], [, b]) => b - a) - .slice(0, 12); - + // Calculate values once for efficiency const totalEvents = Object.values(geoDistribution).reduce( (sum, count) => sum + count, 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; - 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" }; + if (percentage > 50) return "high"; + if (percentage > 20) return "medium"; + if (percentage > 5) return "low"; + return "minimal"; }; const getCountryName = (code: string) => { - return countryNames[code] || code; + return COUNTRY_NAMES[code] || code; }; return ( @@ -135,7 +69,7 @@ export function GeographicThreatMap({
{sortedCountries.map(([countryCode, count]) => { - const threat = getThreatLevel(count, totalEvents); + const threatLevel = getThreatLevel(count, totalEvents); const percentage = ((count / totalEvents) * 100).toFixed(1); return ( @@ -150,7 +84,7 @@ export function GeographicThreatMap({ - {threat.level} + {threatLevel}

@@ -170,17 +104,9 @@ export function GeographicThreatMap({

{count}
diff --git a/lib/constants/countries.ts b/lib/constants/countries.ts new file mode 100644 index 0000000..4ff0aed --- /dev/null +++ b/lib/constants/countries.ts @@ -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 = { + 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", +};