refactor: achieve 100% biome compliance with comprehensive code quality improvements

- Fix all cognitive complexity violations (63→0 errors)
- Replace 'any' types with proper TypeScript interfaces and generics
- Extract helper functions and custom hooks to reduce complexity
- Fix React hook dependency arrays and useCallback patterns
- Remove unused imports, variables, and functions
- Implement proper formatting across all files
- Add type safety with interfaces like AIProcessingRequestWithSession
- Fix circuit breaker implementation with proper reset() method
- Resolve all accessibility and form labeling issues
- Clean up mysterious './0' file containing biome output

Total: 63 errors → 0 errors, 42 warnings → 0 warnings
This commit is contained in:
2025-07-11 23:49:45 +02:00
committed by Kaj Kowalski
parent 1eea2cc3e4
commit 314326400e
42 changed files with 3171 additions and 2781 deletions

View File

@ -39,6 +39,43 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
type ToastFunction = (props: {
title: string;
description: string;
variant?: "default" | "destructive";
}) => void;
interface CompanyManagementState {
company: Company | null;
setCompany: (company: Company | null) => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
isSaving: boolean;
setIsSaving: (saving: boolean) => void;
editData: Partial<Company>;
setEditData: (
data: Partial<Company> | ((prev: Partial<Company>) => Partial<Company>)
) => void;
originalData: Partial<Company>;
setOriginalData: (data: Partial<Company>) => void;
showInviteUser: boolean;
setShowInviteUser: (show: boolean) => void;
inviteData: { name: string; email: string; role: string };
setInviteData: (
data:
| { name: string; email: string; role: string }
| ((prev: { name: string; email: string; role: string }) => {
name: string;
email: string;
role: string;
})
) => void;
showUnsavedChangesDialog: boolean;
setShowUnsavedChangesDialog: (show: boolean) => void;
pendingNavigation: string | null;
setPendingNavigation: (navigation: string | null) => void;
}
interface User {
id: string;
name: string;
@ -64,51 +101,10 @@ interface Company {
};
}
export default function CompanyManagement() {
const { data: session, status } = useSession();
const router = useRouter();
const params = useParams();
const { toast } = useToast();
const companyNameFieldId = useId();
const companyEmailFieldId = useId();
const maxUsersFieldId = useId();
const inviteNameFieldId = useId();
const inviteEmailFieldId = useId();
const fetchCompany = useCallback(async () => {
try {
const response = await fetch(`/api/platform/companies/${params.id}`);
if (response.ok) {
const data = await response.json();
setCompany(data);
const companyData = {
name: data.name,
email: data.email,
status: data.status,
maxUsers: data.maxUsers,
};
setEditData(companyData);
setOriginalData(companyData);
} else {
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
}
} catch (error) {
console.error("Failed to fetch company:", error);
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
}, [params.id, toast]);
/**
* Custom hook for company management state
*/
function useCompanyManagementState() {
const [company, setCompany] = useState<Company | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@ -126,9 +122,55 @@ export default function CompanyManagement() {
null
);
// Function to check if data has been modified
return {
company,
setCompany,
isLoading,
setIsLoading,
isSaving,
setIsSaving,
editData,
setEditData,
originalData,
setOriginalData,
showInviteUser,
setShowInviteUser,
inviteData,
setInviteData,
showUnsavedChangesDialog,
setShowUnsavedChangesDialog,
pendingNavigation,
setPendingNavigation,
};
}
/**
* Custom hook for form IDs
*/
function useCompanyFormIds() {
const companyNameFieldId = useId();
const companyEmailFieldId = useId();
const maxUsersFieldId = useId();
const inviteNameFieldId = useId();
const inviteEmailFieldId = useId();
return {
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
inviteNameFieldId,
inviteEmailFieldId,
};
}
/**
* Custom hook for data validation and comparison
*/
function useDataComparison(
editData: Partial<Company>,
originalData: Partial<Company>
) {
const hasUnsavedChanges = useCallback(() => {
// Normalize data for comparison (handle null/undefined/empty string equivalence)
const normalizeValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === "") {
return "";
@ -156,24 +198,276 @@ export default function CompanyManagement() {
);
}, [editData, originalData]);
// Handle navigation protection - must be at top level
return { hasUnsavedChanges };
}
/**
* Custom hook for company data fetching
*/
function useCompanyData(
params: { id: string | string[] },
toast: ToastFunction,
state: CompanyManagementState
) {
const fetchCompany = useCallback(async () => {
try {
const response = await fetch(`/api/platform/companies/${params.id}`);
if (response.ok) {
const data = await response.json();
state.setCompany(data);
const companyData = {
name: data.name,
email: data.email,
status: data.status,
maxUsers: data.maxUsers,
};
state.setEditData(companyData);
state.setOriginalData(companyData);
} else {
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
}
} catch (error) {
console.error("Failed to fetch company:", error);
toast({
title: "Error",
description: "Failed to load company data",
variant: "destructive",
});
} finally {
state.setIsLoading(false);
}
}, [params.id, toast, state]);
return { fetchCompany };
}
/**
* Custom hook for navigation handling
*/
function useNavigationControl(
router: { push: (url: string) => void },
params: { id: string | string[] },
hasUnsavedChanges: () => boolean,
state: CompanyManagementState
) {
const handleNavigation = useCallback(
(url: string) => {
// Allow navigation within the same company (different tabs, etc.)
if (url.includes(`/platform/companies/${params.id}`)) {
router.push(url);
return;
}
// If there are unsaved changes, show confirmation dialog
if (hasUnsavedChanges()) {
setPendingNavigation(url);
setShowUnsavedChangesDialog(true);
state.setPendingNavigation(url);
state.setShowUnsavedChangesDialog(true);
} else {
router.push(url);
}
},
[router, params.id, hasUnsavedChanges]
[router, params.id, hasUnsavedChanges, state]
);
return { handleNavigation };
}
/**
* Helper function to render company information card
*/
function renderCompanyInfoCard(
state: CompanyManagementState,
canEdit: boolean,
companyNameFieldId: string,
companyEmailFieldId: string,
maxUsersFieldId: string,
hasUnsavedChanges: () => boolean,
handleSave: () => Promise<void>
) {
return (
<Card>
<CardHeader>
<CardTitle>Company Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor={companyNameFieldId}>Company Name</Label>
<Input
id={companyNameFieldId}
value={state.editData.name || ""}
onChange={(e) =>
state.setEditData((prev) => ({
...prev,
name: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={companyEmailFieldId}>Contact Email</Label>
<Input
id={companyEmailFieldId}
type="email"
value={state.editData.email || ""}
onChange={(e) =>
state.setEditData((prev) => ({
...prev,
email: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={maxUsersFieldId}>Max Users</Label>
<Input
id={maxUsersFieldId}
type="number"
value={state.editData.maxUsers || 0}
onChange={(e) =>
state.setEditData((prev) => ({
...prev,
maxUsers: Number.parseInt(e.target.value),
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor="status">Status</Label>
<Select
value={state.editData.status}
onValueChange={(value) =>
state.setEditData((prev) => ({
...prev,
status: value,
}))
}
disabled={!canEdit}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACTIVE">Active</SelectItem>
<SelectItem value="TRIAL">Trial</SelectItem>
<SelectItem value="SUSPENDED">Suspended</SelectItem>
<SelectItem value="ARCHIVED">Archived</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{canEdit && hasUnsavedChanges() && (
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => {
state.setEditData(state.originalData);
}}
>
Cancel Changes
</Button>
<Button onClick={handleSave} disabled={state.isSaving}>
<Save className="w-4 h-4 mr-2" />
{state.isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
)}
</CardContent>
</Card>
);
}
/**
* Helper function to render users tab content
*/
function renderUsersTab(state: CompanyManagementState, canEdit: boolean) {
return (
<TabsContent value="users" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Users className="w-5 h-5" />
Users ({state.company?.users.length || 0})
</span>
{canEdit && (
<Button size="sm" onClick={() => state.setShowInviteUser(true)}>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{state.company?.users.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600 dark:text-blue-300">
{user.name?.charAt(0) ||
user.email.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="font-medium">{user.name || "No name"}</div>
<div className="text-sm text-muted-foreground">
{user.email}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline">{user.role}</Badge>
<div className="text-sm text-muted-foreground">
Joined {new Date(user.createdAt).toLocaleDateString()}
</div>
</div>
</div>
))}
{(state.company?.users.length || 0) === 0 && (
<div className="text-center py-8 text-muted-foreground">
No users found. Invite the first user to get started.
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
);
}
export default function CompanyManagement() {
const { data: session, status } = useSession();
const router = useRouter();
const params = useParams();
const { toast } = useToast();
const state = useCompanyManagementState();
const {
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
inviteNameFieldId,
inviteEmailFieldId,
} = useCompanyFormIds();
const { hasUnsavedChanges } = useDataComparison(
state.editData,
state.originalData
);
const { fetchCompany } = useCompanyData(params, toast, state);
const { handleNavigation } = useNavigationControl(
router,
params,
hasUnsavedChanges,
state
);
useEffect(() => {
@ -188,24 +482,24 @@ export default function CompanyManagement() {
}, [session, status, router, fetchCompany]);
const handleSave = async () => {
setIsSaving(true);
state.setIsSaving(true);
try {
const response = await fetch(`/api/platform/companies/${params.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(editData),
body: JSON.stringify(state.editData),
});
if (response.ok) {
const updatedCompany = await response.json();
setCompany(updatedCompany);
state.setCompany(updatedCompany);
const companyData = {
name: updatedCompany.name,
email: updatedCompany.email,
status: updatedCompany.status,
maxUsers: updatedCompany.maxUsers,
};
setOriginalData(companyData);
state.setOriginalData(companyData);
toast({
title: "Success",
description: "Company updated successfully",
@ -220,7 +514,7 @@ export default function CompanyManagement() {
variant: "destructive",
});
} finally {
setIsSaving(false);
state.setIsSaving(false);
}
};
@ -235,8 +529,10 @@ export default function CompanyManagement() {
});
if (response.ok) {
setCompany((prev) => (prev ? { ...prev, status: newStatus } : null));
setEditData((prev) => ({ ...prev, status: newStatus }));
state.setCompany((prev) =>
prev ? { ...prev, status: newStatus } : null
);
state.setEditData((prev) => ({ ...prev, status: newStatus }));
toast({
title: "Success",
description: `Company ${statusAction}d successfully`,
@ -254,16 +550,47 @@ export default function CompanyManagement() {
};
const confirmNavigation = () => {
if (pendingNavigation) {
router.push(pendingNavigation);
setPendingNavigation(null);
if (state.pendingNavigation) {
router.push(state.pendingNavigation);
state.setPendingNavigation(null);
}
setShowUnsavedChangesDialog(false);
state.setShowUnsavedChangesDialog(false);
};
const cancelNavigation = () => {
setPendingNavigation(null);
setShowUnsavedChangesDialog(false);
state.setPendingNavigation(null);
state.setShowUnsavedChangesDialog(false);
};
const handleInviteUser = async () => {
try {
const response = await fetch(
`/api/platform/companies/${params.id}/users`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state.inviteData),
}
);
if (response.ok) {
state.setShowInviteUser(false);
state.setInviteData({ name: "", email: "", role: "USER" });
fetchCompany();
toast({
title: "Success",
description: "User invited successfully",
});
} else {
throw new Error("Failed to invite user");
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to invite user",
variant: "destructive",
});
}
};
// Protect against browser back/forward and other navigation
@ -281,7 +608,6 @@ export default function CompanyManagement() {
"You have unsaved changes. Are you sure you want to leave this page?"
);
if (!confirmLeave) {
// Push the current state back to prevent navigation
window.history.pushState(null, "", window.location.href);
e.preventDefault();
}
@ -297,37 +623,6 @@ export default function CompanyManagement() {
};
}, [hasUnsavedChanges]);
const handleInviteUser = async () => {
try {
const response = await fetch(
`/api/platform/companies/${params.id}/users`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(inviteData),
}
);
if (response.ok) {
setShowInviteUser(false);
setInviteData({ name: "", email: "", role: "USER" });
fetchCompany(); // Refresh company data
toast({
title: "Success",
description: "User invited successfully",
});
} else {
throw new Error("Failed to invite user");
}
} catch (_error) {
toast({
title: "Error",
description: "Failed to invite user",
variant: "destructive",
});
}
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "ACTIVE":
@ -343,7 +638,7 @@ export default function CompanyManagement() {
}
};
if (status === "loading" || isLoading) {
if (status === "loading" || state.isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">Loading company details...</div>
@ -351,7 +646,7 @@ export default function CompanyManagement() {
);
}
if (!session?.user?.isPlatformUser || !company) {
if (!session?.user?.isPlatformUser || !state.company) {
return null;
}
@ -374,10 +669,10 @@ export default function CompanyManagement() {
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{company.name}
{state.company.name}
</h1>
<Badge variant={getStatusBadgeVariant(company.status)}>
{company.status}
<Badge variant={getStatusBadgeVariant(state.company.status)}>
{state.company.status}
</Badge>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
@ -390,7 +685,7 @@ export default function CompanyManagement() {
<Button
variant="outline"
size="sm"
onClick={() => setShowInviteUser(true)}
onClick={() => state.setShowInviteUser(true)}
>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
@ -422,10 +717,10 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{company.users.length}
{state.company.users.length}
</div>
<p className="text-xs text-muted-foreground">
of {company.maxUsers} maximum
of {state.company.maxUsers} maximum
</p>
</CardContent>
</Card>
@ -439,7 +734,7 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{company._count.sessions}
{state.company._count.sessions}
</div>
</CardContent>
</Card>
@ -453,7 +748,7 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{company._count.imports}
{state.company._count.imports}
</div>
</CardContent>
</Card>
@ -465,160 +760,25 @@ export default function CompanyManagement() {
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{new Date(company.createdAt).toLocaleDateString()}
{new Date(state.company.createdAt).toLocaleDateString()}
</div>
</CardContent>
</Card>
</div>
{/* Company Info */}
<Card>
<CardHeader>
<CardTitle>Company Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor={companyNameFieldId}>Company Name</Label>
<Input
id={companyNameFieldId}
value={editData.name || ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
name: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={companyEmailFieldId}>Contact Email</Label>
<Input
id={companyEmailFieldId}
type="email"
value={editData.email || ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
email: e.target.value,
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor={maxUsersFieldId}>Max Users</Label>
<Input
id={maxUsersFieldId}
type="number"
value={editData.maxUsers || 0}
onChange={(e) =>
setEditData((prev) => ({
...prev,
maxUsers: Number.parseInt(e.target.value),
}))
}
disabled={!canEdit}
/>
</div>
<div>
<Label htmlFor="status">Status</Label>
<Select
value={editData.status}
onValueChange={(value) =>
setEditData((prev) => ({ ...prev, status: value }))
}
disabled={!canEdit}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACTIVE">Active</SelectItem>
<SelectItem value="TRIAL">Trial</SelectItem>
<SelectItem value="SUSPENDED">Suspended</SelectItem>
<SelectItem value="ARCHIVED">Archived</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{canEdit && hasUnsavedChanges() && (
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => {
setEditData(originalData);
}}
>
Cancel Changes
</Button>
<Button onClick={handleSave} disabled={isSaving}>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
)}
</CardContent>
</Card>
{renderCompanyInfoCard(
state,
canEdit,
companyNameFieldId,
companyEmailFieldId,
maxUsersFieldId,
hasUnsavedChanges,
handleSave
)}
</TabsContent>
<TabsContent value="users" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Users className="w-5 h-5" />
Users ({company.users.length})
</span>
{canEdit && (
<Button size="sm" onClick={() => setShowInviteUser(true)}>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{company.users.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600 dark:text-blue-300">
{user.name?.charAt(0) ||
user.email.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="font-medium">
{user.name || "No name"}
</div>
<div className="text-sm text-muted-foreground">
{user.email}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline">{user.role}</Badge>
<div className="text-sm text-muted-foreground">
Joined {new Date(user.createdAt).toLocaleDateString()}
</div>
</div>
</div>
))}
{company.users.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No users found. Invite the first user to get started.
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
{renderUsersTab(state, canEdit)}
<TabsContent value="settings" className="space-y-6">
<Card>
@ -641,9 +801,9 @@ export default function CompanyManagement() {
<AlertDialogTrigger asChild>
<Button
variant="destructive"
disabled={company.status === "SUSPENDED"}
disabled={state.company.status === "SUSPENDED"}
>
{company.status === "SUSPENDED"
{state.company.status === "SUSPENDED"
? "Already Suspended"
: "Suspend"}
</Button>
@ -668,7 +828,7 @@ export default function CompanyManagement() {
</AlertDialog>
</div>
{company.status === "SUSPENDED" && (
{state.company.status === "SUSPENDED" && (
<div className="flex items-center justify-between p-4 border border-green-200 dark:border-green-800 rounded-lg">
<div>
<h3 className="font-medium">Reactivate Company</h3>
@ -706,7 +866,7 @@ export default function CompanyManagement() {
</div>
{/* Invite User Dialog */}
{showInviteUser && (
{state.showInviteUser && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-md mx-4">
<CardHeader>
@ -717,9 +877,12 @@ export default function CompanyManagement() {
<Label htmlFor={inviteNameFieldId}>Name</Label>
<Input
id={inviteNameFieldId}
value={inviteData.name}
value={state.inviteData.name}
onChange={(e) =>
setInviteData((prev) => ({ ...prev, name: e.target.value }))
state.setInviteData((prev) => ({
...prev,
name: e.target.value,
}))
}
placeholder="User's full name"
/>
@ -729,9 +892,9 @@ export default function CompanyManagement() {
<Input
id={inviteEmailFieldId}
type="email"
value={inviteData.email}
value={state.inviteData.email}
onChange={(e) =>
setInviteData((prev) => ({
state.setInviteData((prev) => ({
...prev,
email: e.target.value,
}))
@ -742,9 +905,9 @@ export default function CompanyManagement() {
<div>
<Label htmlFor="inviteRole">Role</Label>
<Select
value={inviteData.role}
value={state.inviteData.role}
onValueChange={(value) =>
setInviteData((prev) => ({ ...prev, role: value }))
state.setInviteData((prev) => ({ ...prev, role: value }))
}
>
<SelectTrigger>
@ -759,7 +922,7 @@ export default function CompanyManagement() {
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={() => setShowInviteUser(false)}
onClick={() => state.setShowInviteUser(false)}
className="flex-1"
>
Cancel
@ -767,7 +930,7 @@ export default function CompanyManagement() {
<Button
onClick={handleInviteUser}
className="flex-1"
disabled={!inviteData.email || !inviteData.name}
disabled={!state.inviteData.email || !state.inviteData.name}
>
<Mail className="w-4 h-4 mr-2" />
Send Invite
@ -780,8 +943,8 @@ export default function CompanyManagement() {
{/* Unsaved Changes Dialog */}
<AlertDialog
open={showUnsavedChangesDialog}
onOpenChange={setShowUnsavedChangesDialog}
open={state.showUnsavedChangesDialog}
onOpenChange={state.setShowUnsavedChangesDialog}
>
<AlertDialogContent>
<AlertDialogHeader>

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ import {
Settings,
Shield,
} from "lucide-react";
import { useEffect, useState, useCallback } from "react";
import { useCallback, useEffect, useState } from "react";
import { SecurityConfigModal } from "@/components/security/SecurityConfigModal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -51,7 +51,10 @@ interface SecurityAlert {
acknowledged: boolean;
}
export default function SecurityMonitoringPage() {
/**
* Custom hook for security monitoring state
*/
function useSecurityMonitoringState() {
const [metrics, setMetrics] = useState<SecurityMetrics | null>(null);
const [alerts, setAlerts] = useState<SecurityAlert[]>([]);
const [loading, setLoading] = useState(true);
@ -59,14 +62,29 @@ export default function SecurityMonitoringPage() {
const [showConfig, setShowConfig] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
useEffect(() => {
loadSecurityData();
return {
metrics,
setMetrics,
alerts,
setAlerts,
loading,
setLoading,
selectedTimeRange,
setSelectedTimeRange,
showConfig,
setShowConfig,
autoRefresh,
setAutoRefresh,
};
}
if (autoRefresh) {
const interval = setInterval(loadSecurityData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}
}, [autoRefresh, loadSecurityData]);
/**
* Custom hook for security data fetching
*/
function useSecurityData(selectedTimeRange: string, autoRefresh: boolean) {
const [metrics, setMetrics] = useState<SecurityMetrics | null>(null);
const [alerts, setAlerts] = useState<SecurityAlert[]>([]);
const [loading, setLoading] = useState(true);
const loadSecurityData = useCallback(async () => {
try {
@ -89,6 +107,228 @@ export default function SecurityMonitoringPage() {
}
}, [selectedTimeRange]);
useEffect(() => {
loadSecurityData();
if (autoRefresh) {
const interval = setInterval(loadSecurityData, 30000);
return () => clearInterval(interval);
}
}, [autoRefresh, loadSecurityData]);
return { metrics, alerts, loading, loadSecurityData, setAlerts };
}
/**
* Helper function to get date range for filtering
*/
function getStartDateForRange(range: string): string {
const now = new Date();
switch (range) {
case "1h":
return new Date(now.getTime() - 60 * 60 * 1000).toISOString();
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
case "7d":
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
case "30d":
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
default:
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
}
}
/**
* Helper function to get threat level color
*/
function getThreatLevelColor(level: string) {
switch (level?.toLowerCase()) {
case "critical":
return "bg-red-500";
case "high":
return "bg-orange-500";
case "moderate":
return "bg-yellow-500";
case "low":
return "bg-green-500";
default:
return "bg-gray-500";
}
}
/**
* Helper function to get severity color
*/
function 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";
}
}
/**
* Helper function to render dashboard header
*/
function renderDashboardHeader(
autoRefresh: boolean,
setAutoRefresh: (refresh: boolean) => void,
setShowConfig: (show: boolean) => void,
exportData: (format: "json" | "csv", type: "alerts" | "metrics") => void
) {
return (
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Security Monitoring
</h1>
<p className="text-muted-foreground">
Real-time security monitoring and threat detection
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? (
<Bell className="h-4 w-4" />
) : (
<BellOff className="h-4 w-4" />
)}
Auto Refresh
</Button>
<Button variant="outline" size="sm" onClick={() => setShowConfig(true)}>
<Settings className="h-4 w-4" />
Configure
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportData("json", "alerts")}
>
<Download className="h-4 w-4" />
Export
</Button>
</div>
</div>
);
}
/**
* Helper function to render time range selector
*/
function renderTimeRangeSelector(
selectedTimeRange: string,
setSelectedTimeRange: (range: string) => void
) {
return (
<div className="flex gap-2">
{["1h", "24h", "7d", "30d"].map((range) => (
<Button
key={range}
variant={selectedTimeRange === range ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTimeRange(range)}
>
{range}
</Button>
))}
</div>
);
}
/**
* Helper function to render security overview cards
*/
function renderSecurityOverview(metrics: SecurityMetrics | null) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Security Score</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.securityScore || 0}/100
</div>
<div
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getThreatLevelColor(metrics?.threatLevel || "")}`}
>
{metrics?.threatLevel || "Unknown"} Threat Level
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.activeAlerts || 0}</div>
<p className="text-xs text-muted-foreground">
{metrics?.resolvedAlerts || 0} resolved
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Security Events</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.totalEvents || 0}</div>
<p className="text-xs text-muted-foreground">
{metrics?.criticalEvents || 0} critical
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Top Threat</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{metrics?.topThreats?.[0]?.type?.replace(/_/g, " ") || "None"}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.topThreats?.[0]?.count || 0} instances
</p>
</CardContent>
</Card>
</div>
);
}
export default function SecurityMonitoringPage() {
const {
selectedTimeRange,
setSelectedTimeRange,
showConfig,
setShowConfig,
autoRefresh,
setAutoRefresh,
} = useSecurityMonitoringState();
const { metrics, alerts, loading, setAlerts, loadSecurityData } =
useSecurityData(selectedTimeRange, autoRefresh);
const acknowledgeAlert = async (alertId: string) => {
try {
const response = await fetch("/api/admin/security-monitoring/alerts", {
@ -135,52 +375,6 @@ export default function SecurityMonitoringPage() {
}
};
const getStartDateForRange = (range: string): string => {
const now = new Date();
switch (range) {
case "1h":
return new Date(now.getTime() - 60 * 60 * 1000).toISOString();
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
case "7d":
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
case "30d":
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
default:
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
}
};
const getThreatLevelColor = (level: string) => {
switch (level?.toLowerCase()) {
case "critical":
return "bg-red-500";
case "high":
return "bg-orange-500";
case "moderate":
return "bg-yellow-500";
case "low":
return "bg-green-500";
default:
return "bg-gray-500";
}
};
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";
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
@ -191,132 +385,14 @@ export default function SecurityMonitoringPage() {
return (
<div className="container mx-auto px-4 py-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Security Monitoring
</h1>
<p className="text-muted-foreground">
Real-time security monitoring and threat detection
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? (
<Bell className="h-4 w-4" />
) : (
<BellOff className="h-4 w-4" />
)}
Auto Refresh
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowConfig(true)}
>
<Settings className="h-4 w-4" />
Configure
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportData("json", "alerts")}
>
<Download className="h-4 w-4" />
Export
</Button>
</div>
</div>
{/* Time Range Selector */}
<div className="flex gap-2">
{["1h", "24h", "7d", "30d"].map((range) => (
<Button
key={range}
variant={selectedTimeRange === range ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTimeRange(range)}
>
{range}
</Button>
))}
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Security Score
</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.securityScore || 0}/100
</div>
<div
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getThreatLevelColor(metrics?.threatLevel || "")}`}
>
{metrics?.threatLevel || "Unknown"} Threat Level
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.activeAlerts || 0}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.resolvedAlerts || 0} resolved
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Security Events
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.totalEvents || 0}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.criticalEvents || 0} critical
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Top Threat</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{metrics?.topThreats?.[0]?.type?.replace(/_/g, " ") || "None"}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.topThreats?.[0]?.count || 0} instances
</p>
</CardContent>
</Card>
</div>
{renderDashboardHeader(
autoRefresh,
setAutoRefresh,
setShowConfig,
exportData
)}
{renderTimeRangeSelector(selectedTimeRange, setSelectedTimeRange)}
{renderSecurityOverview(metrics)}
<Tabs defaultValue="alerts" className="space-y-4">
<TabsList>

View File

@ -2,7 +2,7 @@
import { ArrowLeft, Key, Shield, User } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useId, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
@ -62,6 +62,13 @@ export default function PlatformSettings() {
const router = useRouter();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
// Generate unique IDs for form elements
const nameId = useId();
const emailId = useId();
const currentPasswordId = useId();
const newPasswordId = useId();
const confirmPasswordId = useId();
const [profileData, setProfileData] = useState({
name: "",
email: "",
@ -223,9 +230,9 @@ export default function PlatformSettings() {
<CardContent>
<form onSubmit={handleProfileUpdate} className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Label htmlFor={nameId}>Name</Label>
<Input
id="name"
id={nameId}
value={profileData.name}
onChange={(e) =>
setProfileData({ ...profileData, name: e.target.value })
@ -234,9 +241,9 @@ export default function PlatformSettings() {
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Label htmlFor={emailId}>Email</Label>
<Input
id="email"
id={emailId}
type="email"
value={profileData.email}
disabled
@ -273,9 +280,9 @@ export default function PlatformSettings() {
<CardContent>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<Label htmlFor="current-password">Current Password</Label>
<Label htmlFor={currentPasswordId}>Current Password</Label>
<Input
id="current-password"
id={currentPasswordId}
type="password"
value={passwordData.currentPassword}
onChange={(e) =>
@ -288,9 +295,9 @@ export default function PlatformSettings() {
/>
</div>
<div>
<Label htmlFor="new-password">New Password</Label>
<Label htmlFor={newPasswordId}>New Password</Label>
<Input
id="new-password"
id={newPasswordId}
type="password"
value={passwordData.newPassword}
onChange={(e) =>
@ -306,11 +313,11 @@ export default function PlatformSettings() {
</p>
</div>
<div>
<Label htmlFor="confirm-password">
<Label htmlFor={confirmPasswordId}>
Confirm New Password
</Label>
<Input
id="confirm-password"
id={confirmPasswordId}
type="password"
value={passwordData.confirmPassword}
onChange={(e) =>