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

@ -9,109 +9,139 @@ import {
securityAuditLogger,
} from "../../../../lib/securityAuditLogger";
/**
* Validates user authorization for audit logs access
*/
async function validateAuditLogAccess(
session: { user?: { id: string; companyId: string; role: string } } | null,
ip: string,
userAgent?: string
) {
if (!session?.user) {
await securityAuditLogger.logAuthorization(
"audit_logs_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_session",
}),
},
"Unauthorized attempt to access audit logs"
);
return { valid: false, status: 401, error: "Unauthorized" };
}
if (session.user.role !== "ADMIN") {
await securityAuditLogger.logAuthorization(
"audit_logs_insufficient_permissions",
AuditOutcome.BLOCKED,
{
userId: session.user.id,
companyId: session.user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
userRole: session.user.role,
requiredRole: "ADMIN",
}),
},
"Insufficient permissions to access audit logs"
);
return { valid: false, status: 403, error: "Insufficient permissions" };
}
return { valid: true };
}
/**
* Parses query parameters for audit log filtering
*/
function parseAuditLogFilters(url: URL) {
const page = Number.parseInt(url.searchParams.get("page") || "1");
const limit = Math.min(
Number.parseInt(url.searchParams.get("limit") || "50"),
100
);
const eventType = url.searchParams.get("eventType");
const outcome = url.searchParams.get("outcome");
const severity = url.searchParams.get("severity");
const userId = url.searchParams.get("userId");
const startDate = url.searchParams.get("startDate");
const endDate = url.searchParams.get("endDate");
return {
page,
limit,
eventType,
outcome,
severity,
userId,
startDate,
endDate,
};
}
/**
* Builds where clause for audit log filtering
*/
function buildAuditLogWhereClause(
companyId: string,
filters: ReturnType<typeof parseAuditLogFilters>
) {
const { eventType, outcome, severity, userId, startDate, endDate } = filters;
const where: {
companyId: string;
eventType?: string;
outcome?: string;
severity?: string;
userId?: string;
timestamp?: {
gte?: Date;
lte?: Date;
};
} = {
companyId, // Only show logs for user's company
};
if (eventType) where.eventType = eventType;
if (outcome) where.outcome = outcome;
if (severity) where.severity = severity;
if (userId) where.userId = userId;
if (startDate || endDate) {
where.timestamp = {};
if (startDate) where.timestamp.gte = new Date(startDate);
if (endDate) where.timestamp.lte = new Date(endDate);
}
return where;
}
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
if (!session?.user) {
await securityAuditLogger.logAuthorization(
"audit_logs_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_session",
}),
},
"Unauthorized attempt to access audit logs"
);
// Validate access authorization
const authResult = await validateAuditLogAccess(session, ip, userAgent);
if (!authResult.valid) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
// Only allow ADMIN users to view audit logs
if (session.user.role !== "ADMIN") {
await securityAuditLogger.logAuthorization(
"audit_logs_insufficient_permissions",
AuditOutcome.BLOCKED,
{
userId: session.user.id,
companyId: session.user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
userRole: session.user.role,
requiredRole: "ADMIN",
}),
},
"Insufficient permissions to access audit logs"
);
return NextResponse.json(
{ success: false, error: "Insufficient permissions" },
{ status: 403 }
{ success: false, error: authResult.error },
{ status: authResult.status }
);
}
const url = new URL(request.url);
const page = Number.parseInt(url.searchParams.get("page") || "1");
const limit = Math.min(
Number.parseInt(url.searchParams.get("limit") || "50"),
100
);
const eventType = url.searchParams.get("eventType");
const outcome = url.searchParams.get("outcome");
const severity = url.searchParams.get("severity");
const userId = url.searchParams.get("userId");
const startDate = url.searchParams.get("startDate");
const endDate = url.searchParams.get("endDate");
const filters = parseAuditLogFilters(url);
const { page, limit } = filters;
const skip = (page - 1) * limit;
// Build filter conditions
const where: {
companyId: string;
eventType?: string;
outcome?: string;
timestamp?: {
gte?: Date;
lte?: Date;
};
} = {
companyId: session.user.companyId, // Only show logs for user's company
};
if (eventType) {
where.eventType = eventType;
}
if (outcome) {
where.outcome = outcome;
}
if (severity) {
where.severity = severity;
}
if (userId) {
where.userId = userId;
}
if (startDate || endDate) {
where.timestamp = {};
if (startDate) {
where.timestamp.gte = new Date(startDate);
}
if (endDate) {
where.timestamp.lte = new Date(endDate);
}
}
const where = buildAuditLogWhereClause(session.user.companyId, filters);
// Get audit logs with pagination
const [auditLogs, totalCount] = await Promise.all([

View File

@ -7,7 +7,11 @@ import {
createAuditContext,
securityAuditLogger,
} from "@/lib/securityAuditLogger";
import { securityMonitoring, type SecurityMetrics, type AlertType } from "@/lib/securityMonitoring";
import {
type AlertType,
type SecurityMetrics,
securityMonitoring,
} from "@/lib/securityMonitoring";
const threatAnalysisSchema = z.object({
ipAddress: z.string().ip().optional(),

View File

@ -5,7 +5,6 @@
* It generates a new token and sets it as an HTTP-only cookie.
*/
import type { NextRequest } from "next/server";
import { generateCSRFTokenResponse } from "../../../middleware/csrfProtection";
/**

View File

@ -5,6 +5,69 @@ import { sessionMetrics } from "../../../../lib/metrics";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession } from "../../../../lib/types";
/**
* Converts a Prisma session to ChatSession format for metrics
*/
function convertToMockChatSession(
ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean;
forwardedHr: boolean;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
},
questions: string[]
): ChatSession {
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
}
interface SessionUser {
email: string;
name?: string;
@ -107,45 +170,8 @@ export async function GET(request: NextRequest) {
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
const chatSessions: ChatSession[] = prismaSessions.map((ps) => {
// Get questions for this session or empty array
const questions = questionsBySession[ps.id] || [];
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
return convertToMockChatSession(ps, questions);
});
// Pass company config to metrics

View File

@ -1,4 +1,4 @@
import { type NextRequest, NextResponse } from "next/server";
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";

View File

@ -2,6 +2,76 @@ import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../../../lib/prisma";
import type { ChatSession } from "../../../../../lib/types";
/**
* Maps Prisma session object to ChatSession type
*/
function mapPrismaSessionToChatSession(prismaSession: {
id: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean;
forwardedHr: boolean;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
messages: Array<{
id: string;
sessionId: string;
timestamp: Date | null;
role: string;
content: string;
order: number;
createdAt: Date;
}>;
}): ChatSession {
return {
// Spread prismaSession to include all its properties
...prismaSession,
// Override properties that need conversion or specific mapping
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
startTime: new Date(prismaSession.startTime),
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
createdAt: new Date(prismaSession.createdAt),
// Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
// Prisma.Session does not have a `userId` field.
userId: null, // Explicitly set to null or map if available from another source
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
category: prismaSession.category ?? null,
language: prismaSession.language ?? null,
country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined,
forwardedHr: prismaSession.forwardedHr ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
summary: prismaSession.summary ?? null, // New field
transcriptContent: null, // Not available in Session model
messages:
prismaSession.messages?.map((msg) => ({
id: msg.id,
sessionId: msg.sessionId,
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
role: msg.role,
content: msg.content,
order: msg.order,
createdAt: new Date(msg.createdAt),
})) ?? [], // New field - parsed messages
};
}
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
@ -30,45 +100,7 @@ export async function GET(
}
// Map Prisma session object to ChatSession type
const session: ChatSession = {
// Spread prismaSession to include all its properties
...prismaSession,
// Override properties that need conversion or specific mapping
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
startTime: new Date(prismaSession.startTime),
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
createdAt: new Date(prismaSession.createdAt),
// Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
// Or, if your business logic implies an update timestamp elsewhere, use that.
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
// Prisma.Session does not have a `userId` field.
userId: null, // Explicitly set to null or map if available from another source
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
category: prismaSession.category ?? null,
language: prismaSession.language ?? null,
country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined,
forwardedHr: prismaSession.forwardedHr ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
summary: prismaSession.summary ?? null, // New field
transcriptContent: null, // Not available in Session model
messages:
prismaSession.messages?.map((msg) => ({
id: msg.id,
sessionId: msg.sessionId,
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
role: msg.role,
content: msg.content,
order: msg.order,
createdAt: new Date(msg.createdAt),
})) ?? [], // New field - parsed messages
};
const session: ChatSession = mapPrismaSessionToChatSession(prismaSession);
return NextResponse.json({ session });
} catch (error) {

View File

@ -2,7 +2,7 @@
import { formatDistanceToNow } from "date-fns";
import { useSession } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import { useCallback, useEffect, useId, useState } from "react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
@ -108,6 +108,11 @@ const severityColors: Record<string, string> = {
export default function AuditLogsPage() {
const { data: session } = useSession();
const eventTypeId = useId();
const outcomeId = useId();
const severityId = useId();
const startDateId = useId();
const endDateId = useId();
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -194,8 +199,8 @@ export default function AuditLogsPage() {
<div className="container mx-auto py-8">
<Alert>
<AlertDescription>
You don&apos;t have permission to view audit logs. Only administrators
can access this page.
You don&apos;t have permission to view audit logs. Only
administrators can access this page.
</AlertDescription>
</Alert>
</div>
@ -219,14 +224,16 @@ export default function AuditLogsPage() {
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">Event Type</label>
<label htmlFor={eventTypeId} className="text-sm font-medium">
Event Type
</label>
<Select
value={filters.eventType}
onValueChange={(value) =>
handleFilterChange("eventType", value)
}
>
<SelectTrigger>
<SelectTrigger id={eventTypeId}>
<SelectValue placeholder="All event types" />
</SelectTrigger>
<SelectContent>
@ -241,12 +248,14 @@ export default function AuditLogsPage() {
</div>
<div>
<label className="text-sm font-medium">Outcome</label>
<label htmlFor={outcomeId} className="text-sm font-medium">
Outcome
</label>
<Select
value={filters.outcome}
onValueChange={(value) => handleFilterChange("outcome", value)}
>
<SelectTrigger>
<SelectTrigger id={outcomeId}>
<SelectValue placeholder="All outcomes" />
</SelectTrigger>
<SelectContent>
@ -261,12 +270,14 @@ export default function AuditLogsPage() {
</div>
<div>
<label className="text-sm font-medium">Severity</label>
<label htmlFor={severityId} className="text-sm font-medium">
Severity
</label>
<Select
value={filters.severity}
onValueChange={(value) => handleFilterChange("severity", value)}
>
<SelectTrigger>
<SelectTrigger id={severityId}>
<SelectValue placeholder="All severities" />
</SelectTrigger>
<SelectContent>
@ -281,8 +292,11 @@ export default function AuditLogsPage() {
</div>
<div>
<label className="text-sm font-medium">Start Date</label>
<label htmlFor={startDateId} className="text-sm font-medium">
Start Date
</label>
<Input
id={startDateId}
type="datetime-local"
value={filters.startDate}
onChange={(e) =>
@ -292,8 +306,11 @@ export default function AuditLogsPage() {
</div>
<div>
<label className="text-sm font-medium">End Date</label>
<label htmlFor={endDateId} className="text-sm font-medium">
End Date
</label>
<Input
id={endDateId}
type="datetime-local"
value={filters.endDate}
onChange={(e) => handleFilterChange("endDate", e.target.value)}
@ -442,14 +459,14 @@ export default function AuditLogsPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="font-medium">Timestamp:</label>
<span className="font-medium">Timestamp:</span>
<p className="font-mono text-sm">
{new Date(selectedLog.timestamp).toLocaleString()}
</p>
</div>
<div>
<label className="font-medium">Event Type:</label>
<span className="font-medium">Event Type:</span>
<p>
{eventTypeLabels[selectedLog.eventType] ||
selectedLog.eventType}
@ -457,26 +474,26 @@ export default function AuditLogsPage() {
</div>
<div>
<label className="font-medium">Action:</label>
<span className="font-medium">Action:</span>
<p>{selectedLog.action}</p>
</div>
<div>
<label className="font-medium">Outcome:</label>
<span className="font-medium">Outcome:</span>
<Badge className={outcomeColors[selectedLog.outcome]}>
{selectedLog.outcome}
</Badge>
</div>
<div>
<label className="font-medium">Severity:</label>
<span className="font-medium">Severity:</span>
<Badge className={severityColors[selectedLog.severity]}>
{selectedLog.severity}
</Badge>
</div>
<div>
<label className="font-medium">IP Address:</label>
<span className="font-medium">IP Address:</span>
<p className="font-mono text-sm">
{selectedLog.ipAddress || "N/A"}
</p>
@ -484,7 +501,7 @@ export default function AuditLogsPage() {
{selectedLog.user && (
<div>
<label className="font-medium">User:</label>
<span className="font-medium">User:</span>
<p>
{selectedLog.user.email} ({selectedLog.user.role})
</p>
@ -493,7 +510,7 @@ export default function AuditLogsPage() {
{selectedLog.platformUser && (
<div>
<label className="font-medium">Platform User:</label>
<span className="font-medium">Platform User:</span>
<p>
{selectedLog.platformUser.email} (
{selectedLog.platformUser.role})
@ -503,21 +520,21 @@ export default function AuditLogsPage() {
{selectedLog.country && (
<div>
<label className="font-medium">Country:</label>
<span className="font-medium">Country:</span>
<p>{selectedLog.country}</p>
</div>
)}
{selectedLog.sessionId && (
<div>
<label className="font-medium">Session ID:</label>
<span className="font-medium">Session ID:</span>
<p className="font-mono text-sm">{selectedLog.sessionId}</p>
</div>
)}
{selectedLog.requestId && (
<div>
<label className="font-medium">Request ID:</label>
<span className="font-medium">Request ID:</span>
<p className="font-mono text-sm">{selectedLog.requestId}</p>
</div>
)}
@ -525,7 +542,7 @@ export default function AuditLogsPage() {
{selectedLog.errorMessage && (
<div className="mt-4">
<label className="font-medium">Error Message:</label>
<span className="font-medium">Error Message:</span>
<p className="text-red-600 bg-red-50 p-2 rounded text-sm">
{selectedLog.errorMessage}
</p>
@ -534,14 +551,14 @@ export default function AuditLogsPage() {
{selectedLog.userAgent && (
<div className="mt-4">
<label className="font-medium">User Agent:</label>
<span className="font-medium">User Agent:</span>
<p className="text-sm break-all">{selectedLog.userAgent}</p>
</div>
)}
{selectedLog.metadata && (
<div className="mt-4">
<label className="font-medium">Metadata:</label>
<span className="font-medium">Metadata:</span>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">
{JSON.stringify(selectedLog.metadata, null, 2)}
</pre>

View File

@ -23,24 +23,24 @@ import MessageViewer from "../../../../components/MessageViewer";
import SessionDetails from "../../../../components/SessionDetails";
import type { ChatSession } from "../../../../lib/types";
export default function SessionViewPage() {
const params = useParams();
const router = useRouter(); // Initialize useRouter
const { status } = useSession(); // Get session status, removed unused sessionData
const id = params?.id as string;
/**
* Custom hook for managing session data fetching and state
*/
function useSessionData(id: string | undefined, authStatus: string) {
const [session, setSession] = useState<ChatSession | null>(null);
const [loading, setLoading] = useState(true); // This will now primarily be for data fetching
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
if (authStatus === "unauthenticated") {
router.push("/login");
return;
}
if (status === "authenticated" && id) {
if (authStatus === "authenticated" && id) {
const fetchSession = async () => {
setLoading(true); // Always set loading before fetch
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/dashboard/session/${id}`);
@ -63,222 +63,247 @@ export default function SessionViewPage() {
}
};
fetchSession();
} else if (status === "authenticated" && !id) {
} else if (authStatus === "authenticated" && !id) {
setError("Session ID is missing.");
setLoading(false);
}
}, [id, status, router]); // session removed from dependencies
}, [id, authStatus, router]);
if (status === "loading") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session...
</div>
</CardContent>
</Card>
</div>
);
}
if (status === "unauthenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Redirecting to login...
</div>
</CardContent>
</Card>
</div>
);
}
if (loading && status === "authenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session details...
</div>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
<p className="text-destructive text-lg mb-4">Error: {error}</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
if (!session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground text-lg mb-4">
Session not found.
</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
return { session, loading, error };
}
/**
* Component for rendering loading state
*/
function LoadingCard({ message }: { message: string }) {
return (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="text-center py-8 text-muted-foreground">
{message}
</div>
</CardContent>
</Card>
</div>
);
}
/**
* Component for rendering error state
*/
function ErrorCard({ error }: { error: string }) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
<p className="text-destructive text-lg mb-4">Error: {error}</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
/**
* Component for rendering session not found state
*/
function SessionNotFoundCard() {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground text-lg mb-4">
Session not found.
</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
/**
* Component for rendering session header with navigation and badges
*/
function SessionHeader({ session }: { session: ChatSession }) {
return (
<Card>
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-2">
<Link href="/dashboard/sessions">
<Button
variant="ghost"
className="gap-2 p-0 h-auto focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label="Return to sessions list"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
Back to Sessions List
</Button>
</Link>
<div className="space-y-2">
<Link href="/dashboard/sessions">
<Button
variant="ghost"
className="gap-2 p-0 h-auto focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label="Return to sessions list"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
Back to Sessions List
</Button>
</Link>
<div className="space-y-2">
<h1 className="text-3xl font-bold">Session Details</h1>
<div className="flex items-center gap-3">
<Badge variant="outline" className="font-mono text-xs">
ID
</Badge>
<code className="text-sm text-muted-foreground font-mono">
{(session.sessionId || session.id).slice(0, 8)}...
</code>
</div>
<h1 className="text-3xl font-bold">Session Details</h1>
<div className="flex items-center gap-3">
<Badge variant="outline" className="font-mono text-xs">
ID
</Badge>
<code className="text-sm text-muted-foreground font-mono">
{(session.sessionId || session.id).slice(0, 8)}...
</code>
</div>
</div>
<div className="flex flex-wrap gap-2">
{session.category && (
<Badge variant="secondary" className="gap-1">
<Activity className="h-3 w-3" />
{formatCategory(session.category)}
</Badge>
)}
{session.language && (
<Badge variant="outline" className="gap-1">
<Globe className="h-3 w-3" />
{session.language.toUpperCase()}
</Badge>
)}
{session.sentiment && (
<Badge
variant={
session.sentiment === "positive"
? "default"
: session.sentiment === "negative"
? "destructive"
: "secondary"
}
className="gap-1"
>
{session.sentiment.charAt(0).toUpperCase() +
session.sentiment.slice(1)}
</Badge>
)}
</div>
<div className="flex flex-wrap gap-2">
{session.category && (
<Badge variant="secondary" className="gap-1">
<Activity className="h-3 w-3" />
{formatCategory(session.category)}
</Badge>
)}
{session.language && (
<Badge variant="outline" className="gap-1">
<Globe className="h-3 w-3" />
{session.language.toUpperCase()}
</Badge>
)}
{session.sentiment && (
<Badge
variant={
session.sentiment === "positive"
? "default"
: session.sentiment === "negative"
? "destructive"
: "secondary"
}
className="gap-1"
>
{session.sentiment.charAt(0).toUpperCase() +
session.sentiment.slice(1)}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
);
}
/**
* Component for rendering session overview cards
*/
function SessionOverview({ session }: { session: ChatSession }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Start Time</p>
<p className="font-semibold">
{new Date(session.startTime).toLocaleString()}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Session Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Start Time</p>
<p className="font-semibold">
{new Date(session.startTime).toLocaleString()}
</p>
</div>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<MessageSquare className="h-8 w-8 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="font-semibold">{session.messages?.length || 0}</p>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<MessageSquare className="h-8 w-8 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="font-semibold">{session.messages?.length || 0}</p>
</div>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<User className="h-8 w-8 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">User ID</p>
<p className="font-semibold truncate">
{session.userId || "N/A"}
</p>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<User className="h-8 w-8 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">User ID</p>
<p className="font-semibold truncate">
{session.userId || "N/A"}
</p>
</div>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Activity className="h-8 w-8 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="font-semibold">
{session.endTime && session.startTime
? `${Math.round(
(new Date(session.endTime).getTime() -
new Date(session.startTime).getTime()) /
60000
)} min`
: "N/A"}
</p>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</div>
);
}
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Activity className="h-8 w-8 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="font-semibold">
{session.endTime && session.startTime
? `${Math.round(
(new Date(session.endTime).getTime() -
new Date(session.startTime).getTime()) /
60000
)} min`
: "N/A"}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
export default function SessionViewPage() {
const params = useParams();
const { status } = useSession();
const id = params?.id as string;
const { session, loading, error } = useSessionData(id, status);
if (status === "loading") {
return <LoadingCard message="Loading session..." />;
}
if (status === "unauthenticated") {
return <LoadingCard message="Redirecting to login..." />;
}
if (loading && status === "authenticated") {
return <LoadingCard message="Loading session details..." />;
}
if (error) {
return <ErrorCard error={error} />;
}
if (!session) {
return <SessionNotFoundCard />;
}
return (
<div className="space-y-6 max-w-6xl mx-auto">
<SessionHeader session={session} />
<SessionOverview session={session} />
{/* Session Details */}
<SessionDetails session={session} />

View File

@ -14,6 +14,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useEffect, useId, useState } from "react";
import type { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -21,8 +22,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { formatCategory } from "@/lib/format-enums";
import { trpc } from "@/lib/trpc-client";
import { sessionFilterSchema } from "@/lib/validation";
import type { z } from "zod";
import type { sessionFilterSchema } from "@/lib/validation";
import type { ChatSession } from "../../../lib/types";
interface FilterOptions {
@ -97,13 +97,13 @@ function FilterSection({
<CardHeader>
<div className="space-y-4">
<div className="relative">
<Label htmlFor="search-sessions" className="sr-only">
<Label htmlFor={searchId} className="sr-only">
Search sessions
</Label>
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="search-sessions"
id={searchId}
type="text"
placeholder="Search sessions..."
value={searchTerm}
@ -179,9 +179,9 @@ function FilterSection({
</div>
<div>
<Label htmlFor="start-date">Start Date</Label>
<Label htmlFor={startDateId}>Start Date</Label>
<Input
id="start-date"
id={startDateId}
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
@ -190,9 +190,9 @@ function FilterSection({
</div>
<div>
<Label htmlFor="end-date">End Date</Label>
<Label htmlFor={endDateId}>End Date</Label>
<Input
id="end-date"
id={endDateId}
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
@ -201,9 +201,9 @@ function FilterSection({
</div>
<div>
<Label htmlFor="sort-by">Sort By</Label>
<Label htmlFor={sortById}>Sort By</Label>
<select
id="sort-by"
id={sortById}
value={sortKey}
onChange={(e) => setSortKey(e.target.value)}
className="w-full mt-1 p-2 border border-gray-300 rounded-md"
@ -489,7 +489,9 @@ export default function SessionsPage() {
} = trpc.dashboard.getSessions.useQuery(
{
search: debouncedSearchTerm || undefined,
category: selectedCategory ? selectedCategory as z.infer<typeof sessionFilterSchema>["category"] : undefined,
category: selectedCategory
? (selectedCategory as z.infer<typeof sessionFilterSchema>["category"])
: undefined,
// language: selectedLanguage || undefined, // Not supported in schema yet
startDate: startDate || undefined,
endDate: endDate || undefined,

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) =>