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

View File

@ -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), {

View File

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

View File

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

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