mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: guard user deletion with business data checks
This commit is contained in:
@@ -21,6 +21,23 @@ const updateUserSchema = z.object({
|
|||||||
role: z.enum(["ADMIN", "USER"]),
|
role: z.enum(["ADMIN", "USER"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userDeleteBlockerLabels: Record<string, string> = {
|
||||||
|
subscriptions: "订阅",
|
||||||
|
orders: "订单",
|
||||||
|
nodeClients: "节点客户端",
|
||||||
|
streamingSlots: "流媒体分配",
|
||||||
|
supportTickets: "工单",
|
||||||
|
inviteRewardLedgers: "邀请返利记录",
|
||||||
|
inviteeRewardLedgers: "被邀请返利记录",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDeleteBlockers(counts: Record<string, number>) {
|
||||||
|
return Object.entries(counts)
|
||||||
|
.filter(([, count]) => count > 0)
|
||||||
|
.map(([key, count]) => `${userDeleteBlockerLabels[key] ?? key} ${count} 条`)
|
||||||
|
.join("、");
|
||||||
|
}
|
||||||
|
|
||||||
export async function createUser(formData: FormData) {
|
export async function createUser(formData: FormData) {
|
||||||
const session = await requireAdmin();
|
const session = await requireAdmin();
|
||||||
const data = createUserSchema.parse(Object.fromEntries(formData));
|
const data = createUserSchema.parse(Object.fromEntries(formData));
|
||||||
@@ -93,7 +110,55 @@ export async function updateUserStatus(id: string, status: "ACTIVE" | "DISABLED"
|
|||||||
|
|
||||||
export async function deleteUser(id: string) {
|
export async function deleteUser(id: string) {
|
||||||
const session = await requireAdmin();
|
const session = await requireAdmin();
|
||||||
const user = await prisma.user.delete({ where: { id } });
|
if (id === session.user.id) {
|
||||||
|
throw new Error("不能删除当前登录的管理员账号");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
role: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
subscriptions: true,
|
||||||
|
orders: true,
|
||||||
|
nodeClients: true,
|
||||||
|
streamingSlots: true,
|
||||||
|
supportTickets: true,
|
||||||
|
inviteRewardLedgers: true,
|
||||||
|
inviteeRewardLedgers: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("用户不存在,可能已经被删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role === "ADMIN") {
|
||||||
|
const adminCount = await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
role: "ADMIN",
|
||||||
|
status: "ACTIVE",
|
||||||
|
id: { not: user.id },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (adminCount === 0) {
|
||||||
|
throw new Error("不能删除最后一个可用管理员账号");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockers = formatDeleteBlockers(user._count);
|
||||||
|
if (blockers) {
|
||||||
|
throw new Error(
|
||||||
|
`无法直接删除该用户:存在 ${blockers}。为避免订单、订阅和客服记录丢失,请改用“禁用”或“封禁”;如确需彻底清理,请先人工处理关联数据。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.delete({ where: { id: user.id } });
|
||||||
await recordAuditLog({
|
await recordAuditLog({
|
||||||
actor: actorFromSession(session),
|
actor: actorFromSession(session),
|
||||||
action: "user.delete",
|
action: "user.delete",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function UserActions({ user }: { user: User }) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
title="删除这个用户?"
|
title="删除这个用户?"
|
||||||
description="用户账号及相关数据会被清理。请确认已完成必要备份。"
|
description="仅无业务数据的空账号会被物理删除;有订单、订阅或工单的用户请改用禁用/封禁。"
|
||||||
confirmLabel="删除用户"
|
confirmLabel="删除用户"
|
||||||
successMessage="用户已删除"
|
successMessage="用户已删除"
|
||||||
errorMessage="删除失败"
|
errorMessage="删除失败"
|
||||||
|
|||||||
Reference in New Issue
Block a user