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>
146 lines
5 KiB
TypeScript
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 };
|
|
}
|