"use server"; import bcrypt from "bcryptjs"; import { revalidatePath } from "next/cache"; import { prisma } from "@/lib/db"; import { requireAdmin } from "@/lib/auth-helpers"; import { Role } from "@/lib/generated/prisma/enums"; import { EmailSchema, PasswordSchema } from "@/lib/validation/auth"; const ROLE_VALUES: readonly string[] = Object.values(Role); // All of these are modeled as return values rather than thrown errors -- // Next.js redacts thrown Server Action error messages in production, and // these are all expected, user-facing validation outcomes (bad input, // "last admin" guard, etc.), not exceptional failures. export async function updateUserRole( userId: string, role: string ): Promise<{ error?: string }> { const admin = await requireAdmin(); if (!ROLE_VALUES.includes(role)) return { error: "Invalid role." }; // The acting admin is always at least one admin; the only way this // action could zero out admins is by demoting *themselves* while // they're the last one. if (userId === admin.id && role !== Role.ADMIN) { const adminCount = await prisma.user.count({ where: { role: Role.ADMIN } }); if (adminCount <= 1) { return { error: "You can't remove the last administrator." }; } } const target = await prisma.user.findUnique({ where: { id: userId }, select: { id: true } }); if (!target) return { error: "User not found." }; await prisma.user.update({ where: { id: userId }, data: { role: role as Role } }); revalidatePath("/admin"); return {}; } export async function updateUserEmail( userId: string, email: string ): Promise<{ error?: string }> { await requireAdmin(); const parsed = EmailSchema.safeParse(email); if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Invalid email." }; const existing = await prisma.user.findUnique({ where: { email: parsed.data } }); if (existing && existing.id !== userId) { return { error: "Another account already uses that email." }; } await prisma.user.update({ where: { id: userId }, data: { email: parsed.data } }); revalidatePath("/admin"); return {}; } export async function updateUserPassword( userId: string, password: string ): Promise<{ error?: string }> { await requireAdmin(); const parsed = PasswordSchema.safeParse(password); if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? "Invalid password." }; const passwordHash = await bcrypt.hash(parsed.data, 12); await prisma.user.update({ where: { id: userId }, data: { passwordHash } }); return {}; } /** * Deletes a user's account entirely, cascading through their categories, * groups, and to-dos (`onDelete: Cascade` all the way down the schema). */ export async function deleteUser(userId: string): Promise<{ error?: string }> { const admin = await requireAdmin(); if (userId === admin.id) { return { error: "You can't delete your own account." }; } const target = await prisma.user.findUnique({ where: { id: userId }, select: { id: true } }); if (!target) return { error: "User not found." }; await prisma.user.delete({ where: { id: userId } }); revalidatePath("/admin"); return {}; }