// server/utils/auth.ts - Better Auth 服务端实例,邮箱密码登录 + 用户 API token import { randomBytes } from "node:crypto"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import type { H3Event } from "h3"; import { prisma } from "~~/server/utils/db"; /** * Better Auth 全局实例 * * - 使用 Prisma MySQL/MariaDB 适配器 * - 只启用邮箱/密码登录,无邮箱验证和密码重置(保持最简) * - 注册时通过 databaseHook 自动生成 32 位 hex apiToken * apiToken 供 OCS 油猴脚本跨域调用 /api/search 时在 body 中携带 * - apiToken 不允许客户端直接设置(input: false),但随 session 返回给登录用户 */ export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "mysql" }), session: { expiresIn: 60 * 60 * 24 * 14, // 14 天过期 updateAge: 60 * 60 * 24 // 每天自动续期 }, emailAndPassword: { enabled: true }, user: { additionalFields: { apiToken: { type: "string", required: false, // 客户端(signup/updateUser)不能设置此字段,只由 databaseHooks 写入 input: false }, role: { type: "string", required: false, // 角色只由服务端管理员设置,不允许客户端修改 input: false } } }, databaseHooks: { user: { create: { // 注册时自动生成 apiToken,格式与旧 ACCESS_TOKEN 字段一致(32 位 hex) before: async (user) => { return { data: { ...user, apiToken: randomBytes(16).toString("hex") } }; } } } } }); /** * 从 H3 事件中读取 Better Auth session * * 只复制 headers,不调用 toWebRequest(event)。 * 对 POST/PUT 请求,toWebRequest 会包装 body stream;全局鉴权中调用后, * 后续 handler 再 readBody(event) 可能长时间挂起。 * 未登录或 session 无效时返回 null */ const getHeaderSnapshot = (event: H3Event) => { const headers = new Headers(); for (const [key, value] of Object.entries(event.node.req.headers)) { if (Array.isArray(value)) { for (const item of value) headers.append(key, item); } else if (value !== undefined) { headers.set(key, value); } } return headers; }; export const getAuthSession = (event: H3Event) => auth.api.getSession({ headers: getHeaderSnapshot(event) });