mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 15:52:10 +01:00
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:
@ -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([
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user