@@ -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