111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
"use server";
|
|
|
|
import bcrypt from "bcryptjs";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { requireAdmin } from "@/lib/auth-helpers";
|
|
import { Role, SignupMode } from "@/lib/generated/prisma/enums";
|
|
import { EmailSchema, PasswordSchema } from "@/lib/validation/auth";
|
|
import { setSignupMode } from "@/lib/settings";
|
|
|
|
const ROLE_VALUES: readonly string[] = Object.values(Role);
|
|
// CONFIRMED is a listed option (greyed out in the UI) but email
|
|
// confirmation isn't built yet -- reject it here too, not just in the UI,
|
|
// so it can't be turned on until that's actually implemented.
|
|
const SETTABLE_SIGNUP_MODES: readonly string[] = [
|
|
SignupMode.OPEN,
|
|
SignupMode.APPROVED,
|
|
SignupMode.CLOSED,
|
|
];
|
|
|
|
// 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 {};
|
|
}
|
|
|
|
export async function updateSignupMode(mode: string): Promise<{ error?: string }> {
|
|
await requireAdmin();
|
|
if (!SETTABLE_SIGNUP_MODES.includes(mode)) {
|
|
return { error: "That sign-up option isn't available yet." };
|
|
}
|
|
|
|
await setSignupMode(mode as SignupMode);
|
|
revalidatePath("/admin");
|
|
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 {};
|
|
}
|