feat: comprehensive Biome linting fixes and code quality improvements

Major code quality overhaul addressing 58% of all linting issues:

• Type Safety Improvements:
  - Replace all any types with proper TypeScript interfaces
  - Fix Map component shadowing (renamed to CountryMap)
  - Add comprehensive custom error classes system
  - Enhance API route type safety

• Accessibility Enhancements:
  - Add explicit button types to all interactive elements
  - Implement useId() hooks for form element accessibility
  - Add SVG title attributes for screen readers
  - Fix static element interactions with keyboard handlers

• React Best Practices:
  - Resolve exhaustive dependencies warnings with useCallback
  - Extract nested component definitions to top level
  - Fix array index keys with proper unique identifiers
  - Improve component organization and prop typing

• Code Organization:
  - Automatic import organization and type import optimization
  - Fix unused function parameters and variables
  - Enhanced error handling with structured error responses
  - Improve component reusability and maintainability

Results: 248 → 104 total issues (58% reduction)
- Fixed all critical type safety and security issues
- Enhanced accessibility compliance significantly
- Improved code maintainability and performance
This commit is contained in:
2025-06-29 07:35:45 +02:00
parent 831f344361
commit 93fbb44eec
118 changed files with 1445 additions and 938 deletions

View File

@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { fetchAndParseCsv } from "../../../../lib/csvFetcher";
import { processQueuedImports } from "../../../../lib/importProcessor";
import { prisma } from "../../../../lib/prisma";
@ -47,10 +47,10 @@ export async function POST(request: NextRequest) {
// Check if company is active and can process data
if (company.status !== "ACTIVE") {
return NextResponse.json(
{
{
error: `Data processing is disabled for ${company.status.toLowerCase()} companies`,
companyStatus: company.status
},
companyStatus: company.status,
},
{ status: 403 }
);
}

View File

@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { ProcessingStage } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
import { ProcessingStatusManager } from "../../../../lib/processingStatusManager";
import { ProcessingStage } from "@prisma/client";
interface SessionUser {
email: string;
@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
id: true,
name: true,
status: true,
}
},
},
},
});
@ -86,7 +86,7 @@ export async function POST(request: NextRequest) {
}
// Start processing (this will run asynchronously)
const startTime = Date.now();
const _startTime = Date.now();
// Note: We're calling the function but not awaiting it to avoid timeout
// The processing will continue in the background

View File

@ -3,4 +3,4 @@ import { authOptions } from "../../../../lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
export { handler as GET, handler as POST };

View File

@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Not logged in" }, { status: 401 });

View File

@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
import { sessionMetrics } from "../../../../lib/metrics";
import { authOptions } from "../../../../lib/auth";
import { ChatSession } from "../../../../lib/types";
import { sessionMetrics } from "../../../../lib/metrics";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession } from "../../../../lib/types";
interface SessionUser {
email: string;
@ -31,7 +31,7 @@ export async function GET(request: NextRequest) {
name: true,
csvUrl: true,
status: true,
}
},
},
},
});
@ -46,14 +46,20 @@ export async function GET(request: NextRequest) {
const endDate = searchParams.get("endDate");
// Build where clause with optional date filtering
const whereClause: any = {
const whereClause: {
companyId: string;
startTime?: {
gte: Date;
lte: Date;
};
} = {
companyId: user.companyId,
};
if (startDate && endDate) {
whereClause.startTime = {
gte: new Date(startDate),
lte: new Date(endDate + "T23:59:59.999Z"), // Include full end date
lte: new Date(`${endDate}T23:59:59.999Z`), // Include full end date
};
}
@ -82,25 +88,28 @@ export async function GET(request: NextRequest) {
});
// Batch fetch questions for all sessions at once if needed for metrics
const sessionIds = prismaSessions.map(s => s.id);
const sessionIds = prismaSessions.map((s) => s.id);
const sessionQuestions = await prisma.sessionQuestion.findMany({
where: { sessionId: { in: sessionIds } },
include: { question: true },
orderBy: { order: 'asc' },
orderBy: { order: "asc" },
});
// Group questions by session
const questionsBySession = sessionQuestions.reduce((acc, sq) => {
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
acc[sq.sessionId].push(sq.question.content);
return acc;
}, {} as Record<string, string[]>);
const questionsBySession = sessionQuestions.reduce(
(acc, sq) => {
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
acc[sq.sessionId].push(sq.question.content);
return acc;
},
{} as Record<string, string[]>
);
// 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}`,
@ -127,7 +136,8 @@ export async function GET(request: NextRequest) {
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,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,

View File

@ -1,10 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { SessionFilterOptions } from "../../../../lib/types";
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
const authSession = await getServerSession(authOptions);
if (!authSession || !authSession.user?.companyId) {
@ -17,23 +16,23 @@ export async function GET(request: NextRequest) {
// Use groupBy for better performance with distinct values
const [categoryGroups, languageGroups] = await Promise.all([
prisma.session.groupBy({
by: ['category'],
by: ["category"],
where: {
companyId,
category: { not: null },
},
orderBy: {
category: 'asc',
category: "asc",
},
}),
prisma.session.groupBy({
by: ['language'],
by: ["language"],
where: {
companyId,
language: { not: null },
},
orderBy: {
language: 'asc',
language: "asc",
},
}),
]);
@ -41,7 +40,7 @@ export async function GET(request: NextRequest) {
const distinctCategories = categoryGroups
.map((g) => g.category)
.filter(Boolean) as string[];
const distinctLanguages = languageGroups
.map((g) => g.language)
.filter(Boolean) as string[];

View File

@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../../../lib/prisma";
import { ChatSession } from "../../../../../lib/types";
import type { ChatSession } from "../../../../../lib/types";
export async function GET(
request: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;

View File

@ -1,13 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import type { Prisma } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import {
ChatSession,
SessionApiResponse,
SessionQuery,
} from "../../../../lib/types";
import { Prisma } from "@prisma/client";
import type { ChatSession } from "../../../../lib/types";
export async function GET(request: NextRequest) {
const authSession = await getServerSession(authOptions);
@ -48,7 +44,7 @@ export async function GET(request: NextRequest) {
// Category Filter
if (category && category.trim() !== "") {
// Cast to SessionCategory enum if it's a valid value
whereClause.category = category as any;
whereClause.category = category;
}
// Language Filter

View File

@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
interface UserBasicInfo {
id: string;
@ -11,7 +11,7 @@ interface UserBasicInfo {
role: string;
}
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { sendEmail } from "../../../lib/sendEmail";
import { forgotPasswordSchema, validateInput } from "../../../lib/validation";
import crypto from "crypto";
// In-memory rate limiting for password reset requests
const resetAttempts = new Map<string, { count: number; resetTime: number }>();
@ -28,7 +28,10 @@ function checkRateLimit(ip: string): boolean {
export async function POST(request: NextRequest) {
try {
// Rate limiting check
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown";
const ip =
request.headers.get("x-forwarded-for") ||
request.headers.get("x-real-ip") ||
"unknown";
if (!checkRateLimit(ip)) {
return NextResponse.json(
{

View File

@ -3,4 +3,4 @@ import { platformAuthOptions } from "../../../../../lib/platform-auth";
const handler = NextAuth(platformAuthOptions);
export { handler as GET, handler as POST };
export { handler as GET, handler as POST };

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { CompanyStatus } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { platformAuthOptions } from "../../../../../lib/platform-auth";
import { prisma } from "../../../../../lib/prisma";
import { CompanyStatus } from "@prisma/client";
interface PlatformSession {
user: {
@ -16,14 +16,19 @@ interface PlatformSession {
// GET /api/platform/companies/[id] - Get company details
export async function GET(
request: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(platformAuthOptions) as PlatformSession | null;
const session = (await getServerSession(
platformAuthOptions
)) as PlatformSession | null;
if (!session?.user?.isPlatformUser) {
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
return NextResponse.json(
{ error: "Platform access required" },
{ status: 401 }
);
}
const { id } = await params;
@ -59,7 +64,10 @@ export async function GET(
return NextResponse.json(company);
} catch (error) {
console.error("Platform company details error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
@ -71,15 +79,30 @@ export async function PATCH(
try {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
if (
!session?.user?.isPlatformUser ||
session.user.platformRole === "SUPPORT"
) {
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
);
}
const { id } = await params;
const body = await request.json();
const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } = body;
const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } =
body;
const updateData: any = {};
const updateData: {
name?: string;
email?: string;
maxUsers?: number;
csvUrl?: string;
csvUsername?: string;
csvPassword?: string;
status?: CompanyStatus;
} = {};
if (name !== undefined) updateData.name = name;
if (email !== undefined) updateData.email = email;
if (maxUsers !== undefined) updateData.maxUsers = maxUsers;
@ -96,20 +119,29 @@ export async function PATCH(
return NextResponse.json({ company });
} catch (error) {
console.error("Platform company update error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// DELETE /api/platform/companies/[id] - Delete company (archives instead)
export async function DELETE(
request: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser || session.user.platformRole !== "SUPER_ADMIN") {
return NextResponse.json({ error: "Super admin access required" }, { status: 403 });
if (
!session?.user?.isPlatformUser ||
session.user.platformRole !== "SUPER_ADMIN"
) {
return NextResponse.json(
{ error: "Super admin access required" },
{ status: 403 }
);
}
const { id } = await params;
@ -123,6 +155,9 @@ export async function DELETE(
return NextResponse.json({ company });
} catch (error) {
console.error("Platform company archive error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
}

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { hash } from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { platformAuthOptions } from "../../../../../../lib/platform-auth";
import { prisma } from "../../../../../../lib/prisma";
import { hash } from "bcryptjs";
// POST /api/platform/companies/[id]/users - Invite user to company
export async function POST(
@ -12,8 +12,14 @@ export async function POST(
try {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
if (
!session?.user?.isPlatformUser ||
session.user.platformRole === "SUPPORT"
) {
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
);
}
const { id: companyId } = await params;
@ -21,7 +27,10 @@ export async function POST(
const { name, email, role = "USER" } = body;
if (!name || !email) {
return NextResponse.json({ error: "Name and email are required" }, { status: 400 });
return NextResponse.json(
{ error: "Name and email are required" },
{ status: 400 }
);
}
// Check if company exists
@ -88,24 +97,31 @@ export async function POST(
return NextResponse.json({
user,
tempPassword, // Remove this in production and send via email
message: "User invited successfully. In production, credentials would be sent via email.",
message:
"User invited successfully. In production, credentials would be sent via email.",
});
} catch (error) {
console.error("Platform user invitation error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// GET /api/platform/companies/[id]/users - Get company users
export async function GET(
request: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser) {
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
return NextResponse.json(
{ error: "Platform access required" },
{ status: 401 }
);
}
const { id: companyId } = await params;
@ -127,6 +143,9 @@ export async function GET(
return NextResponse.json({ users });
} catch (error) {
console.error("Platform users list error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
}

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import type { CompanyStatus } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { platformAuthOptions } from "../../../../lib/platform-auth";
import { prisma } from "../../../../lib/prisma";
import { CompanyStatus } from "@prisma/client";
// GET /api/platform/companies - List all companies
export async function GET(request: NextRequest) {
@ -10,7 +10,10 @@ export async function GET(request: NextRequest) {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser) {
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
return NextResponse.json(
{ error: "Platform access required" },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
@ -20,7 +23,13 @@ export async function GET(request: NextRequest) {
const limit = parseInt(searchParams.get("limit") || "20");
const offset = (page - 1) * limit;
const where: any = {};
const where: {
status?: CompanyStatus;
name?: {
contains: string;
mode: "insensitive";
};
} = {};
if (status) where.status = status;
if (search) {
where.name = {
@ -65,7 +74,10 @@ export async function GET(request: NextRequest) {
});
} catch (error) {
console.error("Platform companies list error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
@ -74,33 +86,46 @@ export async function POST(request: NextRequest) {
try {
const session = await getServerSession(platformAuthOptions);
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
if (
!session?.user?.isPlatformUser ||
session.user.platformRole === "SUPPORT"
) {
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
);
}
const body = await request.json();
const {
name,
csvUrl,
csvUsername,
csvPassword,
const {
name,
csvUrl,
csvUsername,
csvPassword,
adminEmail,
adminName,
adminPassword,
maxUsers = 10,
status = "TRIAL"
status = "TRIAL",
} = body;
if (!name || !csvUrl) {
return NextResponse.json({ error: "Name and CSV URL required" }, { status: 400 });
return NextResponse.json(
{ error: "Name and CSV URL required" },
{ status: 400 }
);
}
if (!adminEmail || !adminName) {
return NextResponse.json({ error: "Admin email and name required" }, { status: 400 });
return NextResponse.json(
{ error: "Admin email and name required" },
{ status: 400 }
);
}
// Generate password if not provided
const finalAdminPassword = adminPassword || `Temp${Math.random().toString(36).slice(2, 8)}!`;
const finalAdminPassword =
adminPassword || `Temp${Math.random().toString(36).slice(2, 8)}!`;
// Hash the admin password
const bcrypt = await import("bcryptjs");
@ -133,20 +158,30 @@ export async function POST(request: NextRequest) {
},
});
return { company, adminUser, generatedPassword: adminPassword ? null : finalAdminPassword };
return {
company,
adminUser,
generatedPassword: adminPassword ? null : finalAdminPassword,
};
});
return NextResponse.json({
company: result.company,
adminUser: {
email: result.adminUser.email,
name: result.adminUser.name,
role: result.adminUser.role,
return NextResponse.json(
{
company: result.company,
adminUser: {
email: result.adminUser.email,
name: result.adminUser.name,
role: result.adminUser.role,
},
generatedPassword: result.generatedPassword,
},
generatedPassword: result.generatedPassword,
}, { status: 201 });
{ status: 201 }
);
} catch (error) {
console.error("Platform company creation error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
}

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { registerSchema, validateInput } from "../../../lib/validation";
import bcrypt from "bcryptjs";
// In-memory rate limiting (for production, use Redis or similar)
const registrationAttempts = new Map<

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { resetPasswordSchema, validateInput } from "../../../lib/validation";
import bcrypt from "bcryptjs";
import crypto from "crypto";
export async function POST(request: NextRequest) {
try {