@@ -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, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,87 @@
|
||||
// server/utils/adminUsers.ts - 超级管理员用户管理的校验、权限和响应映射工具
|
||||
import { type H3Event, setResponseStatus } from "h3";
|
||||
|
||||
import type { Prisma } from "~~/prisma/generated/client";
|
||||
import { apiErr } from "~~/server/utils/response";
|
||||
import { formatLocalDateTime } from "~~/server/utils/runtimeState";
|
||||
import {
|
||||
type IAdminUser,
|
||||
USER_ROLE_VALUES,
|
||||
type UserRole
|
||||
} from "~~/shared/types/adminUsers";
|
||||
|
||||
export const ADMIN_USER_PAGE_SIZE_OPTIONS = [10, 50, 100] as const;
|
||||
|
||||
export const ADMIN_USER_SELECT = {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: { select: { qaRecords: true } }
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
export type AdminUserRow = Prisma.UserGetPayload<{
|
||||
select: typeof ADMIN_USER_SELECT;
|
||||
}>;
|
||||
|
||||
export const isUserRole = (value: unknown): value is UserRole =>
|
||||
typeof value === "string" && USER_ROLE_VALUES.includes(value as UserRole);
|
||||
|
||||
export const toAdminUser = (user: AdminUserRow): IAdminUser => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: isUserRole(user.role) ? user.role : "user",
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: formatLocalDateTime(user.createdAt),
|
||||
updatedAt: formatLocalDateTime(user.updatedAt),
|
||||
qaRecordCount: user._count.qaRecords
|
||||
});
|
||||
|
||||
export const requireSuperAdmin = (event: H3Event) => {
|
||||
const role = (event.context.auth?.user as { role?: string } | undefined)
|
||||
?.role;
|
||||
|
||||
if (role !== "superadmin") {
|
||||
setResponseStatus(event, 403);
|
||||
return apiErr(403, "权限不足");
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const readTrimmedString = (value: unknown) =>
|
||||
typeof value === "string" ? value.trim() : undefined;
|
||||
|
||||
export const validateEmail = (value: string) =>
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
|
||||
|
||||
export const validatePassword = (value: string) => {
|
||||
if (value.length < 8) return "密码至少 8 位";
|
||||
if (value.length > 100) return "密码不能超过 100 位";
|
||||
if (!/[a-zA-Z]/u.test(value)) return "密码必须包含英文字母";
|
||||
if (!/[0-9]/u.test(value)) return "密码必须包含数字";
|
||||
return null;
|
||||
};
|
||||
|
||||
export const parseAdminPageSize = (value: unknown) => {
|
||||
const parsed = Number.parseInt(value?.toString() || "10", 10);
|
||||
return ADMIN_USER_PAGE_SIZE_OPTIONS.includes(
|
||||
parsed as (typeof ADMIN_USER_PAGE_SIZE_OPTIONS)[number]
|
||||
)
|
||||
? parsed
|
||||
: 10;
|
||||
};
|
||||
|
||||
export const parseAdminPage = (value: unknown) => {
|
||||
const parsed = Number.parseInt(value?.toString() || "1", 10);
|
||||
return Number.isFinite(parsed) ? Math.max(parsed, 1) : 1;
|
||||
};
|
||||
|
||||
export const getUnknownKeys = (
|
||||
body: Record<string, unknown>,
|
||||
allowedKeys: Set<string>
|
||||
) => Object.keys(body).filter((key) => !allowedKeys.has(key));
|
||||
+18
-8
@@ -3,6 +3,8 @@ import { randomBytes } from "node:crypto";
|
||||
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { admin } from "better-auth/plugins/admin";
|
||||
import { adminAc, userAc } from "better-auth/plugins/admin/access";
|
||||
import type { H3Event } from "h3";
|
||||
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
@@ -12,6 +14,7 @@ import { prisma } from "~~/server/utils/db";
|
||||
*
|
||||
* - 使用 Prisma MySQL/MariaDB 适配器
|
||||
* - 只启用邮箱/密码登录,无邮箱验证和密码重置(保持最简)
|
||||
* - 启用 Better Auth Admin 插件,但只有 superadmin 角色拥有用户管理权限
|
||||
* - 注册时通过 databaseHook 自动生成 32 位 hex apiToken
|
||||
* apiToken 供 OCS 油猴脚本跨域调用 /api/search 时在 body 中携带
|
||||
* - apiToken 不允许客户端直接设置(input: false),但随 session 返回给登录用户
|
||||
@@ -35,16 +38,23 @@ export const auth = betterAuth({
|
||||
required: false,
|
||||
// 客户端(signup/updateUser)不能设置此字段,只由 databaseHooks 写入
|
||||
input: false
|
||||
},
|
||||
role: {
|
||||
type: "string",
|
||||
required: false,
|
||||
// 角色只由服务端管理员设置,不允许客户端修改
|
||||
input: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [
|
||||
admin({
|
||||
defaultRole: "user",
|
||||
adminRoles: ["superadmin"],
|
||||
roles: {
|
||||
user: userAc,
|
||||
// admin 是业务展示角色,不授予 Better Auth Admin 管理权限
|
||||
admin: userAc,
|
||||
superadmin: adminAc
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
@@ -70,7 +80,7 @@ export const auth = betterAuth({
|
||||
* 后续 handler 再 readBody(event) 可能长时间挂起。
|
||||
* 未登录或 session 无效时返回 null
|
||||
*/
|
||||
const getHeaderSnapshot = (event: H3Event) => {
|
||||
export const getAuthHeaderSnapshot = (event: H3Event) => {
|
||||
const headers = new Headers();
|
||||
|
||||
for (const [key, value] of Object.entries(event.node.req.headers)) {
|
||||
@@ -85,4 +95,4 @@ const getHeaderSnapshot = (event: H3Event) => {
|
||||
};
|
||||
|
||||
export const getAuthSession = (event: H3Event) =>
|
||||
auth.api.getSession({ headers: getHeaderSnapshot(event) });
|
||||
auth.api.getSession({ headers: getAuthHeaderSnapshot(event) });
|
||||
|
||||
Reference in New Issue
Block a user