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:
2025-07-13 16:32:57 +02:00
parent efe0a3f79c
commit 33981b87dd
5 changed files with 143 additions and 112 deletions

View File

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

View File

@ -367,8 +367,16 @@ export default function AuditLogsPage() {
{auditLogs.map((log) => (
<TableRow
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)}
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">
{formatDistanceToNow(new Date(log.timestamp), {

View File

@ -16,6 +16,7 @@ interface CSRFProtectedFormProps {
action: string;
method?: "POST" | "PUT" | "DELETE" | "PATCH";
onSubmit?: (formData: FormData) => Promise<void> | 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

View File

@ -8,114 +8,48 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { COUNTRY_NAMES } from "../../lib/constants/countries";
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",
};
// 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({
<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 threatLevel = getThreatLevel(count, totalEvents);
const percentage = ((count / totalEvents) * 100).toFixed(1);
return (
@ -150,7 +84,7 @@ export function GeographicThreatMap({
</span>
<Badge
variant={
threat.color as
THREAT_LEVELS[threatLevel].color as
| "default"
| "secondary"
| "destructive"
@ -158,7 +92,7 @@ export function GeographicThreatMap({
}
className="text-xs"
>
{threat.level}
{threatLevel}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
@ -170,17 +104,9 @@ export function GeographicThreatMap({
<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"
}`}
className={`h-2 rounded-full ${THREAT_LEVELS[threatLevel].bgColor}`}
style={{
width: `${Math.min(100, (count / Math.max(...Object.values(geoDistribution))) * 100)}%`,
width: `${Math.min(100, (count / maxEventCount) * 100)}%`,
}}
/>
</div>

View 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",
};