@@ -0,0 +1,81 @@
|
||||
// server/api/admin/users.get.ts - 超级管理员分页查询用户列表
|
||||
//
|
||||
// 流程:鉴权 → 二次校验 superadmin → 白名单 query → 查询用户和问答数 → 返回安全字段
|
||||
// 安全边界:不返回 apiToken、密码、session、账号凭据或内部异常。
|
||||
import { getQuery, setResponseStatus } from "h3";
|
||||
|
||||
import type { Prisma } from "~~/prisma/generated/client";
|
||||
import {
|
||||
ADMIN_USER_SELECT,
|
||||
isUserRole,
|
||||
parseAdminPage,
|
||||
parseAdminPageSize,
|
||||
requireSuperAdmin,
|
||||
toAdminUser
|
||||
} from "~~/server/utils/adminUsers";
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
import type { IAdminUsersResponse } from "~~/shared/types/adminUsers";
|
||||
|
||||
const adminUsersErr = (code: number, msg: string) =>
|
||||
apiErr(code, msg) as unknown as IAdminUsersResponse;
|
||||
|
||||
export default defineEventHandler(
|
||||
async (event): Promise<IAdminUsersResponse> => {
|
||||
const forbidden = requireSuperAdmin(event);
|
||||
if (forbidden) return forbidden as unknown as IAdminUsersResponse;
|
||||
|
||||
const logger = createApiLogger(event, "admin.users.list");
|
||||
const query = getQuery(event);
|
||||
const page = parseAdminPage(query.page);
|
||||
const size = parseAdminPageSize(query.size);
|
||||
const keyword = query.keyword?.toString().trim().slice(0, 100) || "";
|
||||
const role = isUserRole(query.role) ? query.role : "";
|
||||
|
||||
const where: Prisma.UserWhereInput = {
|
||||
...(role ? { role } : {}),
|
||||
...(keyword
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: keyword } },
|
||||
{ email: { contains: keyword } }
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
try {
|
||||
const [total, rows] = await prisma.$transaction([
|
||||
prisma.user.count({ where }),
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: "desc" },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
select: ADMIN_USER_SELECT
|
||||
})
|
||||
]);
|
||||
|
||||
logger.info("finish_success", {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
count: rows.length,
|
||||
hasKeyword: Boolean(keyword),
|
||||
role: role || "all"
|
||||
});
|
||||
|
||||
return apiOk({
|
||||
users: rows.map(toAdminUser),
|
||||
page,
|
||||
size,
|
||||
total
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("query_failed", { error: toSafeLogError(error) });
|
||||
setResponseStatus(event, 500);
|
||||
return adminUsersErr(500, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user