Add user management

This commit is contained in:
2026-01-01 18:34:02 +01:00
parent 2fcf19c0df
commit 36fb2358dd
26 changed files with 1047 additions and 56 deletions

View File

@ -0,0 +1,39 @@
"use server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { z } from "zod/v4";
const schema = z.object({
name: z.string().min(1).max(200),
email: z.string().email().max(320),
password: z.string().min(8).max(128),
});
export async function registerFirstUser(input: z.infer<typeof schema>) {
const count = await prisma.user.count();
if (count !== 0) throw new Error("Registration is disabled.");
const { name, email, password } = schema.parse(input);
const res = await auth.api.signUpEmail({
body: { name, email, password },
});
const userId =
(res as any)?.user?.id ??
(res as any)?.data?.user?.id ??
(res as any)?.data?.id;
if (!userId) throw new Error("Signup failed: no user id returned.");
await prisma.user.update({
where: { id: userId },
data: { role: "admin" },
});
// IMPORTANT:
// Do NOT sign-in here when requireEmailVerification=true.
// User must verify first. Better Auth already sent the email (sendOnSignUp).
return { ok: true, requiresEmailVerification: true };
}

View File

@ -0,0 +1,33 @@
"use server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { z } from "zod/v4";
const schema = z.object({
name: z.string().min(1).max(200),
email: z.string().email().max(320),
password: z.string().min(8).max(128),
role: z.enum(["user", "admin"]).default("user"),
});
export async function createUser(input: z.infer<typeof schema>) {
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role;
if (!session || role !== "admin") {
throw new Error("Forbidden");
}
const data = schema.parse(input);
return auth.api.createUser({
body: {
name: data.name,
email: data.email,
password: data.password,
role: data.role,
},
headers: await headers(),
});
}

View File

@ -0,0 +1,44 @@
"use server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { headers } from "next/headers";
import { z } from "zod/v4";
export async function deleteUser(id: string) {
const userId = z.string().min(1).parse(id);
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role as string | undefined;
const currentUserId = (session as any)?.user?.id as string | undefined;
if (!session || role !== "admin") throw new Error("Forbidden");
if (!currentUserId) throw new Error("Session missing user id");
if (userId === currentUserId) {
throw new Error("You cannot delete your own account.");
}
const target = await await_attachTarget(userId);
// Prevent deleting last admin
if (target.role === "admin") {
const adminCount = await prisma.user.count({ where: { role: "admin" } });
if (adminCount <= 1) {
throw new Error("Cannot delete the last admin user.");
}
}
await prisma.user.delete({ where: { id: userId } });
return { ok: true };
}
async function await_attachTarget(userId: string) {
const target = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, role: true },
});
if (!target) throw new Error("User not found.");
return target as { id: string; role: "admin" | "user" };
}

View File

@ -0,0 +1,39 @@
"use server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { headers } from "next/headers";
export type UsersListRow = {
id: string;
name: string | null;
email: string;
role: "admin" | "user";
emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
};
export async function getUsers(): Promise<UsersListRow[]> {
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role as string | undefined;
if (!session || role !== "admin") {
throw new Error("Forbidden");
}
const rows = await prisma.user.findMany({
orderBy: { createdAt: "asc" },
select: {
id: true,
name: true,
email: true,
role: true,
emailVerified: true,
createdAt: true,
updatedAt: true,
},
});
return rows as UsersListRow[];
}

View File

@ -0,0 +1,40 @@
"use server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { z } from "zod/v4";
const schema = z.object({
email: z.string().email(),
});
export async function resendVerification(input: z.infer<typeof schema>) {
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role as string | undefined;
if (!session || role !== "admin") throw new Error("Forbidden");
const { email } = schema.parse(input);
// Uses the public auth route (same origin)
const res = await fetch("http://localhost/api/auth/send-verification-email", {
// NOTE: In production, you should use an absolute URL from env, or use authClient.
// This is kept minimal; if you want, I'll refactor to authClient to avoid hostname concerns.
method: "POST",
headers: {
"Content-Type": "application/json",
// forward cookies so Better Auth can authorize if needed
cookie: (await headers()).get("cookie") ?? "",
},
body: JSON.stringify({
email,
callbackURL: "/",
}),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
throw new Error(data?.message ?? "Failed to resend verification email.");
}
return { ok: true };
}