"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { Role } from "@prisma/client"; import bcrypt from "bcryptjs"; const MANAGED_BY_ADMIN_ONLY: Role[] = [Role.ADMIN]; function requireAdmin(role: string) { if (role !== "ADMIN") throw new Error("Admin access required"); } // ── Create User ────────────────────────────────────────────────────────────── const createUserSchema = z.object({ name: z.string().max(100).optional(), email: z.string().email("Invalid email"), password: z.string().min(8, "Password must be at least 8 characters"), role: z.nativeEnum(Role), }); export async function createUser(data: z.infer) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); requireAdmin(session.user.role as string); const parsed = createUserSchema.parse(data); const existing = await db.user.findUnique({ where: { email: parsed.email } }); if (existing) throw new Error("A user with that email already exists"); const passwordHash = await bcrypt.hash(parsed.password, 12); const user = await db.user.create({ data: { name: parsed.name || null, email: parsed.email, passwordHash, role: parsed.role, isActive: true, mustChangePassword: true, }, }); revalidatePath("/users"); return { success: true, userId: user.id }; } // ── Update User ────────────────────────────────────────────────────────────── const updateUserSchema = z.object({ userId: z.string().cuid(), name: z.string().max(100).optional(), role: z.nativeEnum(Role), isActive: z.boolean(), newPassword: z.string().min(8, "Password must be at least 8 characters").optional().or(z.literal("")), }); export async function updateUser(data: z.infer) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); requireAdmin(session.user.role as string); const parsed = updateUserSchema.parse(data); // Prevent demoting or deactivating yourself if (parsed.userId === session.user.id) { if (!parsed.isActive) throw new Error("You cannot deactivate your own account"); if (parsed.role !== "ADMIN") throw new Error("You cannot change your own role"); } const updateData: Record = { name: parsed.name || null, role: parsed.role, isActive: parsed.isActive, }; if (parsed.newPassword) { updateData.passwordHash = await bcrypt.hash(parsed.newPassword, 12); updateData.mustChangePassword = true; } await db.user.update({ where: { id: parsed.userId }, data: updateData }); revalidatePath("/users"); return { success: true }; } // ── Change Own Password ─────────────────────────────────────────────────────── const changeOwnPasswordSchema = z.object({ currentPassword: z.string().min(1, "Current password is required"), newPassword: z.string().min(8, "New password must be at least 8 characters"), }); export async function changeOwnPassword(data: z.infer) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); const parsed = changeOwnPasswordSchema.parse(data); const user = await db.user.findUnique({ where: { id: session.user.id }, select: { passwordHash: true }, }); if (!user?.passwordHash) throw new Error("No password set on this account"); const isValid = await bcrypt.compare(parsed.currentPassword, user.passwordHash); if (!isValid) throw new Error("Current password is incorrect"); if (parsed.currentPassword === parsed.newPassword) { throw new Error("New password must be different from the current password"); } const passwordHash = await bcrypt.hash(parsed.newPassword, 12); await db.user.update({ where: { id: session.user.id }, data: { passwordHash, mustChangePassword: false }, }); revalidatePath("/settings"); return { success: true }; } // ── Delete User ─────────────────────────────────────────────────────────────── export async function deleteUser(userId: string) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); requireAdmin(session.user.role as string); if (userId === session.user.id) throw new Error("You cannot delete your own account"); await db.user.delete({ where: { id: userId } }); revalidatePath("/users"); return { success: true }; }