127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
// server/api/admin/users.post.ts - 超级管理员创建用户
|
|
//
|
|
// 流程:鉴权 → 二次校验 superadmin → JSON 对象和字段白名单校验 → Better Auth Admin createUser
|
|
// 安全边界:不使用公开注册接口,不返回密码、apiToken 或 Better Auth 原始错误。
|
|
import { 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 {
|
|
IAdminUserCreateRequest,
|
|
IAdminUserResponse
|
|
} from "~~/shared/types/adminUsers";
|
|
|
|
const ALLOWED_KEYS = new Set(["name", "email", "password", "role"]);
|
|
|
|
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.create");
|
|
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: Partial<IAdminUserCreateRequest> = {
|
|
name: readTrimmedString(body.name),
|
|
email: readTrimmedString(body.email)?.toLowerCase(),
|
|
password: typeof body.password === "string" ? body.password : undefined,
|
|
role: isUserRole(body.role) ? body.role : undefined
|
|
};
|
|
|
|
if (!req.name) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "昵称不能为空");
|
|
}
|
|
if (req.name.length > 20) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "昵称不能超过 20 个字符");
|
|
}
|
|
if (!req.email || !validateEmail(req.email)) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "请输入有效的邮箱地址");
|
|
}
|
|
if (!req.password) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "密码不能为空");
|
|
}
|
|
const passwordError = validatePassword(req.password);
|
|
if (passwordError) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, passwordError);
|
|
}
|
|
if (!req.role) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "用户角色无效");
|
|
}
|
|
|
|
try {
|
|
const existing = await prisma.user.findUnique({
|
|
where: { email: req.email },
|
|
select: { id: true }
|
|
});
|
|
if (existing) {
|
|
setResponseStatus(event, 400);
|
|
return adminUserErr(400, "邮箱已被使用");
|
|
}
|
|
|
|
await auth.api.createUser({
|
|
body: {
|
|
name: req.name,
|
|
email: req.email,
|
|
password: req.password,
|
|
role: req.role
|
|
},
|
|
headers: getAuthHeaderSnapshot(event)
|
|
});
|
|
|
|
const created = await prisma.user.findUnique({
|
|
where: { email: req.email },
|
|
select: ADMIN_USER_SELECT
|
|
});
|
|
if (!created) {
|
|
setResponseStatus(event, 500);
|
|
return adminUserErr(500, "服务器内部错误");
|
|
}
|
|
|
|
logger.info("finish_success", {
|
|
targetUserId: created.id,
|
|
role: created.role
|
|
});
|
|
|
|
return apiOk(toAdminUser(created));
|
|
} catch (error) {
|
|
logger.error("create_failed", { error: toSafeLogError(error) });
|
|
setResponseStatus(event, 500);
|
|
return adminUserErr(500, "服务器内部错误");
|
|
}
|
|
}
|
|
);
|