Files
livedash-node/server/routers/admin.ts
Kaj Kowalski 314326400e 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
2025-07-12 00:28:12 +02:00

411 lines
9.4 KiB
TypeScript

/**
* Admin tRPC Router
*
* Handles administrative operations:
* - User management
* - Company settings
* - System administration
*/
import { router, adminProcedure } from "@/lib/trpc";
import { TRPCError } from "@trpc/server";
import { companySettingsSchema, userUpdateSchema } from "@/lib/validation";
import { z } from "zod";
import bcrypt from "bcryptjs";
export const adminRouter = router({
/**
* Get all users in the company
*/
getUsers: adminProcedure
.input(
z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
search: z.string().optional(),
})
)
.query(async ({ input, ctx }) => {
const { page, limit, search } = input;
const where = {
companyId: ctx.company!.id,
...(search && {
OR: [
{ email: { contains: search, mode: "insensitive" as const } },
// For role, search by exact enum match
...(search.toUpperCase() === "ADMIN"
? [{ role: "ADMIN" as const }]
: []),
...(search.toUpperCase() === "USER"
? [{ role: "USER" as const }]
: []),
],
}),
};
const [users, totalCount] = await Promise.all([
ctx.prisma.user.findMany({
where,
select: {
id: true,
email: true,
role: true,
createdAt: true,
name: true,
},
orderBy: { createdAt: "desc" },
skip: (page - 1) * limit,
take: limit,
}),
ctx.prisma.user.count({ where }),
]);
return {
users,
pagination: {
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit),
},
};
}),
/**
* Create a new user
*/
createUser: adminProcedure
.input(
z.object({
email: z.string().email(),
password: z.string().min(12),
role: z.enum(["ADMIN", "USER", "AUDITOR"]),
})
)
.mutation(async ({ input, ctx }) => {
const { email, password, role } = input;
// Check if user already exists
const existingUser = await ctx.prisma.user.findUnique({
where: { email },
});
if (existingUser) {
throw new TRPCError({
code: "CONFLICT",
message: "User with this email already exists",
});
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await ctx.prisma.user.create({
data: {
email,
password: hashedPassword,
role,
companyId: ctx.company!.id,
},
select: {
id: true,
email: true,
role: true,
createdAt: true,
},
});
return {
message: "User created successfully",
user,
};
}),
/**
* Update user details
*/
updateUser: adminProcedure
.input(
z.object({
userId: z.string(),
updates: userUpdateSchema,
})
)
.mutation(async ({ input, ctx }) => {
const { userId, updates } = input;
// Verify user belongs to same company
const targetUser = await ctx.prisma.user.findFirst({
where: {
id: userId,
companyId: ctx.company!.id,
},
});
if (!targetUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const updateData: {
email?: string;
name?: string;
password?: string;
isAdmin?: boolean;
} = {};
if (updates.email) {
// Check if new email is already taken
const existingUser = await ctx.prisma.user.findUnique({
where: { email: updates.email },
});
if (existingUser && existingUser.id !== userId) {
throw new TRPCError({
code: "CONFLICT",
message: "Email is already taken",
});
}
updateData.email = updates.email;
}
if (updates.password) {
updateData.password = await bcrypt.hash(updates.password, 12);
}
if (updates.role) {
updateData.role = updates.role;
}
const updatedUser = await ctx.prisma.user.update({
where: { id: userId },
data: updateData,
select: {
id: true,
email: true,
role: true,
createdAt: true,
},
});
return {
message: "User updated successfully",
user: updatedUser,
};
}),
/**
* Delete a user
*/
deleteUser: adminProcedure
.input(z.object({ userId: z.string() }))
.mutation(async ({ input, ctx }) => {
const { userId } = input;
// Verify user belongs to same company
const targetUser = await ctx.prisma.user.findFirst({
where: {
id: userId,
companyId: ctx.company!.id,
},
});
if (!targetUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
// Prevent deleting the last admin
if (targetUser.role === "ADMIN") {
const adminCount = await ctx.prisma.user.count({
where: {
companyId: ctx.company!.id,
role: "ADMIN",
},
});
if (adminCount <= 1) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Cannot delete the last admin user",
});
}
}
await ctx.prisma.user.delete({
where: { id: userId },
});
return {
message: "User deleted successfully",
};
}),
/**
* Get company settings
*/
getCompanySettings: adminProcedure.query(async ({ ctx }) => {
const company = await ctx.prisma.company.findUnique({
where: { id: ctx.company!.id },
});
if (!company) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Company not found",
});
}
return {
id: company.id,
name: company.name,
csvUrl: company.csvUrl,
csvUsername: company.csvUsername,
dashboardOpts: company.dashboardOpts,
status: company.status,
maxUsers: company.maxUsers,
createdAt: company.createdAt,
};
}),
/**
* Update company settings
*/
updateCompanySettings: adminProcedure
.input(companySettingsSchema)
.mutation(async ({ input, ctx }) => {
const updateData: {
name: string;
csvUrl: string;
csvUsername?: string | null;
csvPassword?: string | null;
maxUsers?: number;
} = {
name: input.name,
csvUrl: input.csvUrl,
};
if (input.csvUsername !== undefined) {
updateData.csvUsername = input.csvUsername;
}
if (input.csvPassword !== undefined) {
updateData.csvPassword = input.csvPassword;
}
if (input.sentimentAlert !== undefined) {
updateData.sentimentAlert = input.sentimentAlert;
}
if (input.dashboardOpts !== undefined) {
updateData.dashboardOpts = input.dashboardOpts;
}
const updatedCompany = await ctx.prisma.company.update({
where: { id: ctx.company!.id },
data: updateData,
select: {
id: true,
name: true,
csvUrl: true,
csvUsername: true,
dashboardOpts: true,
status: true,
maxUsers: true,
},
});
return {
message: "Company settings updated successfully",
company: updatedCompany,
};
}),
/**
* Get system statistics
*/
getSystemStats: adminProcedure.query(async ({ ctx }) => {
const companyId = ctx.company!.id;
const [
totalSessions,
totalMessages,
totalAIRequests,
totalCost,
userCount,
] = await Promise.all([
ctx.prisma.session.count({
where: { companyId },
}),
ctx.prisma.message.count({
where: { session: { companyId } },
}),
ctx.prisma.aIProcessingRequest.count({
where: { session: { companyId } },
}),
ctx.prisma.aIProcessingRequest.aggregate({
where: { session: { companyId } },
_sum: { totalCostEur: true },
}),
ctx.prisma.user.count({
where: { companyId },
}),
]);
return {
totalSessions,
totalMessages,
totalAIRequests,
totalCostEur: totalCost._sum.totalCostEur || 0,
userCount,
};
}),
/**
* Trigger session refresh/reprocessing
*/
refreshSessions: adminProcedure.mutation(async ({ ctx }) => {
// Mark all sessions for reprocessing by clearing AI analysis results
const updatedCount = await ctx.prisma.session.updateMany({
where: {
companyId: ctx.company!.id,
sentiment: { not: null },
},
data: {
sentiment: null,
category: null,
summary: null,
language: null,
},
});
// Clear related AI processing requests
await ctx.prisma.aIProcessingRequest.deleteMany({
where: {
session: {
companyId: ctx.company!.id,
},
},
});
// Clear session questions
await ctx.prisma.sessionQuestion.deleteMany({
where: {
session: {
companyId: ctx.company!.id,
},
},
});
return {
message: `Marked ${updatedCount.count} sessions for reprocessing`,
sessionsMarked: updatedCount.count,
};
}),
});