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