pelagia-portal/App/app/(portal)/admin/users/actions.ts
Hardik a16f418e71 fix(admin): guard user deletion against all FK constraints
The delete action was only checking for submitted POs, leaving POAction
(actorId) and ItemConsumption (recordedById) to throw FK constraint
errors at the DB level. Now returns a clear error for each case and
also cleans up SuperUserRequest rows (requester + resolver) inside the
transaction before deleting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 23:20:18 +05:30

146 lines
5 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { revalidatePath } from "next/cache";
import type { Role } from "@prisma/client";
import { ROLE_PREFIX, nextId } from "@/lib/id-generators";
type ActionResult = { ok: true } | { error: string };
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
role: z.enum(["TECHNICAL", "MANNING", "ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"]),
password: z.string().min(8, "Password must be at least 8 characters").optional(),
});
export async function createUser(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
const parsed = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
password: formData.get("password") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
const exists = await db.user.findFirst({
where: { email: data.email },
});
if (exists) return { error: "A user with that email already exists" };
// Auto-generate employeeId based on role prefix
const prefix = ROLE_PREFIX[data.role];
const existingIds = await db.user.findMany({
where: { role: data.role as Role },
select: { employeeId: true },
});
const employeeId = nextId(prefix, existingIds.map((u) => u.employeeId));
const passwordHash = data.password ? await bcrypt.hash(data.password, 12) : null;
await db.user.create({
data: {
employeeId,
name: data.name,
email: data.email,
role: data.role as Role,
passwordHash,
},
});
revalidatePath("/admin/users");
return { ok: true };
}
export async function updateUser(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
const id = formData.get("id") as string;
if (!id) return { error: "User ID is required" };
const parsed = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
password: formData.get("password") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
const conflict = await db.user.findFirst({
where: {
AND: [
{ id: { not: id } },
{ email: data.email },
],
},
});
if (conflict) return { error: "Another user already has that email" };
const updateData: Parameters<typeof db.user.update>[0]["data"] = {
name: data.name,
email: data.email,
role: data.role as Role,
};
if (data.password) {
updateData.passwordHash = await bcrypt.hash(data.password, 12);
}
await db.user.update({ where: { id }, data: updateData });
revalidatePath("/admin/users");
return { ok: true };
}
export async function deleteUser(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) return { error: "Unauthorized" };
if (id === session.user.id) return { error: "Cannot delete your own account." };
const hasPos = await db.purchaseOrder.findFirst({ where: { submitterId: id } });
if (hasPos) return { error: "Cannot delete: user has submitted purchase orders. Deactivate them instead." };
const hasActions = await db.pOAction.findFirst({ where: { actorId: id } });
if (hasActions) return { error: "Cannot delete: user has purchase order activity on record. Deactivate them instead." };
const hasConsumption = await db.itemConsumption.findFirst({ where: { recordedById: id } });
if (hasConsumption) return { error: "Cannot delete: user has inventory consumption records. Deactivate them instead." };
await db.$transaction(async (tx) => {
await tx.notification.deleteMany({ where: { userId: id } });
await tx.superUserRequest.deleteMany({
where: { OR: [{ userId: id }, { resolvedById: id }] },
});
await tx.user.delete({ where: { id } });
});
revalidatePath("/admin/users");
return { ok: true };
}
export async function toggleUserActive(userId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
if (userId === session.user.id) return { error: "You cannot deactivate your own account" };
const user = await db.user.findUnique({ where: { id: userId }, select: { isActive: true } });
if (!user) return { error: "User not found" };
await db.user.update({ where: { id: userId }, data: { isActive: !user.isActive } });
revalidatePath("/admin/users");
return { ok: true };
}