feat: 新增用户管理
ocs-nuxt / deploy (push) Successful in 1m46s

This commit is contained in:
2026-05-24 23:26:43 +08:00
parent 3c467df01d
commit 90412a3688
22 changed files with 1620 additions and 114 deletions
+68
View File
@@ -0,0 +1,68 @@
// server/api/admin/users/[id].delete.ts - 超级管理员硬删除用户
//
// 流程:鉴权 → 二次校验 superadmin → 防止删除自己/最后一个 superadmin → Better Auth Admin removeUser
// 安全边界:删除依赖 Prisma cascade 清理关联记录,不返回被删用户敏感信息。
import { getRouterParam, setResponseStatus } from "h3";
import { requireSuperAdmin } from "~~/server/utils/adminUsers";
import { auth, getAuthHeaderSnapshot } from "~~/server/utils/auth";
import { prisma } from "~~/server/utils/db";
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
import { apiErr, apiOk } from "~~/server/utils/response";
import type { IAdminUserMutationResponse } from "~~/shared/types/adminUsers";
const adminUserMutationErr = (code: number, msg: string) =>
apiErr(code, msg) as unknown as IAdminUserMutationResponse;
export default defineEventHandler(
async (event): Promise<IAdminUserMutationResponse> => {
const forbidden = requireSuperAdmin(event);
if (forbidden) return forbidden as unknown as IAdminUserMutationResponse;
const logger = createApiLogger(event, "admin.users.delete");
const userId = getRouterParam(event, "id");
if (!userId) {
setResponseStatus(event, 400);
return adminUserMutationErr(400, "用户不存在");
}
const currentUserId = event.context.auth!.user.id;
if (userId === currentUserId) {
setResponseStatus(event, 400);
return adminUserMutationErr(400, "不能删除当前登录用户");
}
try {
const target = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, role: true }
});
if (!target) {
setResponseStatus(event, 404);
return adminUserMutationErr(404, "用户不存在");
}
if (target.role === "superadmin") {
const superAdminCount = await prisma.user.count({
where: { role: "superadmin" }
});
if (superAdminCount <= 1) {
setResponseStatus(event, 400);
return adminUserMutationErr(400, "至少保留一个超级管理员");
}
}
await auth.api.removeUser({
body: { userId },
headers: getAuthHeaderSnapshot(event)
});
logger.info("finish_success", { targetUserId: userId });
return apiOk(null);
} catch (error) {
logger.error("delete_failed", { error: toSafeLogError(error) });
setResponseStatus(event, 500);
return adminUserMutationErr(500, "服务器内部错误");
}
}
);
+176
View File
@@ -0,0 +1,176 @@
// server/api/admin/users/[id].put.ts - 超级管理员更新用户资料和密码
//
// 流程:鉴权 → 二次校验 superadmin → 白名单校验 → 防止最后一个 superadmin 被降级 → Better Auth Admin 更新
// 安全边界:密码只写入 Better Auth 账号,不返回明文或内部异常。
import { getRouterParam, readBody, setResponseStatus } from "h3";
import {
ADMIN_USER_SELECT,
getUnknownKeys,
isUserRole,
readTrimmedString,
requireSuperAdmin,
toAdminUser,
validateEmail,
validatePassword
} from "~~/server/utils/adminUsers";
import { auth, getAuthHeaderSnapshot } from "~~/server/utils/auth";
import { prisma } from "~~/server/utils/db";
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
import { apiErr, apiOk } from "~~/server/utils/response";
import type {
IAdminUserResponse,
IAdminUserUpdateRequest
} from "~~/shared/types/adminUsers";
const ALLOWED_KEYS = new Set(["name", "email", "role", "password"]);
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const adminUserErr = (code: number, msg: string) =>
apiErr(code, msg) as unknown as IAdminUserResponse;
export default defineEventHandler(
async (event): Promise<IAdminUserResponse> => {
const forbidden = requireSuperAdmin(event);
if (forbidden) return forbidden as unknown as IAdminUserResponse;
const logger = createApiLogger(event, "admin.users.update");
const userId = getRouterParam(event, "id");
if (!userId) {
setResponseStatus(event, 400);
return adminUserErr(400, "用户不存在");
}
const body = await readBody<unknown>(event).catch(() => null);
if (!isRecord(body)) {
setResponseStatus(event, 400);
return adminUserErr(400, "请求体必须是 JSON 对象");
}
const unknownKeys = getUnknownKeys(body, ALLOWED_KEYS);
if (unknownKeys.length > 0) {
setResponseStatus(event, 400);
return adminUserErr(400, "请求体包含不支持的字段");
}
const req: IAdminUserUpdateRequest = {};
if (body.name !== undefined) req.name = readTrimmedString(body.name);
if (body.email !== undefined) {
req.email = readTrimmedString(body.email)?.toLowerCase();
}
if (body.role !== undefined && isUserRole(body.role)) {
req.role = body.role;
} else if (body.role !== undefined) {
setResponseStatus(event, 400);
return adminUserErr(400, "用户角色无效");
}
if (typeof body.password === "string") {
req.password = body.password;
}
if (req.name !== undefined && req.name.length === 0) {
setResponseStatus(event, 400);
return adminUserErr(400, "昵称不能为空");
}
if (req.name && req.name.length > 20) {
setResponseStatus(event, 400);
return adminUserErr(400, "昵称不能超过 20 个字符");
}
if (req.email !== undefined && !validateEmail(req.email)) {
setResponseStatus(event, 400);
return adminUserErr(400, "请输入有效的邮箱地址");
}
const shouldUpdatePassword = Boolean(req.password);
if (shouldUpdatePassword) {
const passwordError = validatePassword(req.password!);
if (passwordError) {
setResponseStatus(event, 400);
return adminUserErr(400, passwordError);
}
}
try {
const target = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true, role: true }
});
if (!target) {
setResponseStatus(event, 404);
return adminUserErr(404, "用户不存在");
}
const currentUserId = event.context.auth!.user.id;
if (userId === currentUserId && req.role && req.role !== "superadmin") {
setResponseStatus(event, 400);
return adminUserErr(400, "不能移除自己的超级管理员角色");
}
if (
target.role === "superadmin" &&
req.role &&
req.role !== "superadmin"
) {
const superAdminCount = await prisma.user.count({
where: { role: "superadmin" }
});
if (superAdminCount <= 1) {
setResponseStatus(event, 400);
return adminUserErr(400, "至少保留一个超级管理员");
}
}
if (req.email && req.email !== target.email) {
const existing = await prisma.user.findUnique({
where: { email: req.email },
select: { id: true }
});
if (existing && existing.id !== userId) {
setResponseStatus(event, 400);
return adminUserErr(400, "邮箱已被使用");
}
}
const data: Record<string, string> = {};
if (req.name !== undefined) data.name = req.name;
if (req.email !== undefined) data.email = req.email;
if (req.role !== undefined) data.role = req.role;
if (Object.keys(data).length > 0) {
await auth.api.adminUpdateUser({
body: { userId, data },
headers: getAuthHeaderSnapshot(event)
});
}
if (shouldUpdatePassword) {
await auth.api.setUserPassword({
body: { userId, newPassword: req.password! },
headers: getAuthHeaderSnapshot(event)
});
}
const updated = await prisma.user.findUnique({
where: { id: userId },
select: ADMIN_USER_SELECT
});
if (!updated) {
setResponseStatus(event, 404);
return adminUserErr(404, "用户不存在");
}
logger.info("finish_success", {
targetUserId: userId,
changedFields: Object.keys(data),
passwordChanged: shouldUpdatePassword
});
return apiOk(toAdminUser(updated));
} catch (error) {
logger.error("update_failed", { error: toSafeLogError(error) });
setResponseStatus(event, 500);
return adminUserErr(500, "服务器内部错误");
}
}
);