@sapix/nestjs-better-auth-fastify
v0.3.0
Published
Better Auth integration for NestJS with Fastify adapter
Downloads
103
Maintainers
Readme
目录
✨ 特性
- 🔐 无缝集成 - 为 NestJS + Fastify 提供开箱即用的 Better Auth 支持
- 🎯 装饰器驱动 - 直观的装饰器实现认证和授权
- 📦 插件支持 - 完整支持 Better Auth 插件(Admin、Organization、API Key、Bearer 等)
- 🔄 多上下文 - 支持 HTTP、GraphQL 和 WebSocket
- 🪝 Hook 系统 - NestJS 原生的认证生命周期钩子
- 🎨 类型安全 - 完整的 TypeScript 支持,从认证配置自动推断类型
- ⚡ 高性能 - 可选依赖懒加载优化
- 🌍 国际化就绪 - 可自定义错误消息
📦 安装
# npm
npm install @sapix/nestjs-better-auth-fastify better-auth
# pnpm
pnpm add @sapix/nestjs-better-auth-fastify better-auth
# yarn
yarn add @sapix/nestjs-better-auth-fastify better-auth可选依赖
根据需要安装:
# GraphQL 支持
pnpm add @nestjs/graphql graphql
# WebSocket 支持
pnpm add @nestjs/websockets @nestjs/platform-socket.io🚀 快速开始
1. 创建 Better Auth 配置
// src/auth/auth.config.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '../db';
export const auth = betterAuth({
basePath: '/api/auth',
database: drizzleAdapter(db, { provider: 'postgresql' }),
emailAndPassword: { enabled: true },
// 按需添加更多插件
});
// 导出类型用于类型推断
export type Auth = typeof auth;2. 导入 AuthModule
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from '@sapix/nestjs-better-auth-fastify';
import { auth } from './auth/auth.config';
@Module({
imports: [
AuthModule.forRoot({ auth }),
],
})
export class AppModule {}3. 在控制器中使用装饰器
// src/user/user.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import {
AllowAnonymous,
Session,
CurrentUser,
Roles,
UserSession,
} from '@sapix/nestjs-better-auth-fastify';
@Controller('user')
export class UserController {
// 默认所有路由都需要认证
@Get('profile')
getProfile(@Session() session: UserSession) {
return session;
}
// 公开路由 - 无需认证
@Get('public')
@AllowAnonymous()
getPublicData() {
return { message: '这是公开内容' };
}
// 基于角色的访问控制
@Get('admin')
@Roles(['admin'])
getAdminData(@CurrentUser() user: UserSession['user']) {
return { message: `你好,管理员 ${user.name}` };
}
}📚 装饰器参考
访问控制装饰器
| 装饰器 | 描述 | 示例 |
| ------------------------ | ------------------------------------------ | ---------------- |
| @AllowAnonymous() | 标记为公开路由(覆盖 defaultAuthBehavior) | 公开端点 |
| @RequireAuth() | 要求认证(覆盖 defaultAuthBehavior) | 受保护端点 |
| @OptionalAuth() | 可选认证,有 session 时注入 | 混合访问端点 |
| @Roles(['admin']) | 要求特定角色 | 管理员专用路由 |
| @Permissions(['read']) | 要求特定权限 | 基于权限的访问 |
| @RequireFreshSession() | 要求最近认证的 session | 敏感操作 |
角色和权限示例
// OR 逻辑(默认):用户拥有任一角色即可
@Roles(['admin', 'moderator'])
// AND 逻辑:用户必须拥有所有角色
@Roles(['admin', 'verified'], { mode: 'all' })
// 自定义错误消息
@Roles(['admin'], { message: '需要管理员权限' })
// 基于权限(支持相同选项)
@Permissions(['user:read', 'user:write'], { mode: 'any' })
@Permissions(['read:posts', 'write:posts', 'delete:posts'], { mode: 'all' })Session 新鲜度示例
// 使用默认 freshAge(来自 auth 配置,默认 1 天)
@RequireFreshSession()
@Post('change-password')
changePassword() {}
// 自定义 freshAge(5 分钟 = 300 秒)
@RequireFreshSession({ maxAge: 300 })
@Post('enable-2fa')
enable2FA() {}
// 自定义错误消息
@RequireFreshSession({ message: '请重新登录以继续' })
@Delete('account')
deleteAccount() {}Admin 插件装饰器
需要
better-auth/plugins中的admin()插件
import { admin } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [admin()],
});| 装饰器 | 描述 |
| -------------------------- | ----------------------------------------- |
| @AdminOnly() | 要求管理员角色 |
| @BanCheck() | 实时封禁检查(Better Auth 仅在创建时检查)|
| @DisallowImpersonation() | 阻止模拟 session |
| @SecureAdminOnly() | 组合:Admin + Fresh + 禁止模拟 |
// 高安全性管理员操作
@SecureAdminOnly()
@Delete('admin/users/:id')
deleteUser() {
// 只有真正的管理员且 session 新鲜才能执行
}
// 实时封禁检查 - 适用于 session 创建后被封禁的用户
@BanCheck()
@Post('comments')
createComment() {}
// 阻止模拟 session 执行敏感操作
@DisallowImpersonation()
@Post('transfer-funds')
transferFunds() {}
// 自定义错误消息
@AdminOnly('需要管理员权限')
@Get('admin/dashboard')
getDashboard() {}替代认证方式
Bearer Token 认证
需要
better-auth/plugins中的bearer()插件
当添加 bearer() 插件后,Bearer Token 认证自动支持。无需特殊装饰器 - 默认的 session 认证会接受 Authorization 头中的 Bearer Token。
import { bearer } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [bearer()],
});客户端使用:
curl -H "Authorization: Bearer <session-token>" /api/mobile/dataAPI Key 认证
需要
better-auth/plugins中的apiKey()插件
import { apiKey } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [apiKey()],
});// 仅 API Key
@ApiKeyAuth()
@Get('api/external')
externalApi(@ApiKey() apiKey: ApiKeyValidation['key']) {
return { keyId: apiKey.id, permissions: apiKey.permissions };
}
// API Key 或 Session(灵活模式)
@ApiKeyAuth({ allowSession: true })
@Get('api/flexible')
flexibleApi() {}
// 带权限要求
@ApiKeyAuth({
permissions: {
permissions: { files: ['read', 'write'] },
message: '需要文件读写权限',
},
})
@Post('api/files')
uploadFile() {}客户端使用:
curl -H "x-api-key: <api-key>" /api/external注意:API key 必须通过专用头发送(默认:
x-api-key)。自定义头可通过 Better Auth 的apiKey插件的apiKeyHeaders选项配置。请勿使用Authorization: Bearer发送 API key - 那是为 session token 保留的。
Organization 插件装饰器
需要
better-auth/plugins中的organization()插件
import { organization } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [
organization({
roles: {
owner: { inherit: ['admin'] },
admin: { inherit: ['member'] },
member: { permissions: ['read'] },
},
}),
],
});| 装饰器 | 描述 |
| ----------------------- | ------------------------------ |
| @OrgRequired() | 要求组织上下文 |
| @OptionalOrg() | 加载组织(如可用,非必需) |
| @OrgRoles(['owner']) | 要求组织角色 |
| @OrgPermission({...}) | 要求组织权限 |
// 要求组织上下文
@OrgRequired()
@Get('org/dashboard')
getOrgDashboard(@CurrentOrg() org: Organization) {
return { name: org.name };
}
// 要求 owner 或 admin 角色
@OrgRoles(['owner', 'admin'])
@Put('org/settings')
updateOrgSettings() {}
// 多角色 AND 逻辑
@OrgRoles(['admin', 'billing'], { mode: 'all' })
@Post('org/billing')
manageBilling() {}
// 细粒度权限检查
@OrgPermission({ resource: 'member', action: 'create' })
@Post('org/members')
inviteMember() {}
// 多操作 AND 逻辑
@OrgPermission({ resource: 'member', action: ['read', 'update'], mode: 'all' })
@Put('org/members/:id')
updateMember() {}
// 自定义错误消息
@OrgPermission({
resource: 'invite',
action: 'create',
message: '你没有邀请成员的权限',
})
@Post('org/invitations')
createInvitation() {}客户端使用(必须包含组织 ID):
curl -H "x-organization-id: <org-id>" /org/dashboard参数装饰器
| 装饰器 | 描述 | 类型 |
| ------------------------ | ------------------ | ------------------------- |
| @Session() | 完整 session 对象 | UserSession |
| @SessionProperty('id') | 特定 session 属性 | string |
| @CurrentUser() | 当前用户 | UserSession['user'] |
| @UserProperty('id') | 特定用户属性 | string |
| @ApiKey() | API Key 信息 | ApiKeyValidation['key'] |
| @CurrentOrg() | 当前组织 | Organization |
| @OrgMember() | 组织成员身份 | OrganizationMember |
| @IsImpersonating() | 模拟状态 | boolean |
| @ImpersonatedBy() | 模拟者管理员 ID | string \| null |
@Get('me')
getMe(
@CurrentUser() user: UserSession['user'],
@UserProperty('email') email: string,
@UserProperty('id') userId: string,
@IsImpersonating() isImpersonating: boolean,
@ImpersonatedBy() adminId: string | null,
) {
return { user, email, userId, isImpersonating, adminId };
}
@OrgRequired()
@Get('org/context')
getOrgContext(
@CurrentOrg() org: Organization,
@OrgMember() member: OrganizationMember,
) {
return { org, member };
}自定义认证上下文装饰器
使用 createAuthParamDecorator 创建可复用的参数装饰器,减少样板代码并标准化认证上下文提取。
之前 - 重复的参数注入:
@Get(':id')
findOne(
@Session() session: UserSession,
@CurrentOrg() org: Organization | null,
@OrgMember() member: OrganizationMember | null,
@Param('id') id: string,
) {
const ctx = this.buildContext(session, org, member); // 每次手动映射
return this.resourceService.findOne(id, ctx);
}之后 - 简洁可复用:
@Get(':id')
findOne(@RequestCtx() ctx: RequestContext, @Param('id') id: string) {
return this.resourceService.findOne(id, ctx);
}基本用法
import {
createAuthParamDecorator,
AuthContext,
} from '@sapix/nestjs-better-auth-fastify';
// 定义上下文接口
interface RequestContext {
userId: string;
userEmail: string;
isAdmin: boolean;
organizationId: string | null;
}
// 创建可复用装饰器
const RequestCtx = createAuthParamDecorator<RequestContext>(
(auth: AuthContext) => ({
userId: auth.user?.id ?? 'anonymous',
userEmail: auth.user?.email ?? '',
isAdmin: (auth.user as any)?.role === 'admin',
organizationId: auth.organization?.id ?? null,
}),
);
// 在控制器中使用 - 简洁一致
@Controller('resources')
export class ResourceController {
@Get(':id')
findOne(@RequestCtx() ctx: RequestContext, @Param('id') id: string) {
return this.resourceService.findOne(id, ctx);
}
@Post()
create(@RequestCtx() ctx: RequestContext, @Body() dto: CreateDto) {
return this.resourceService.create(dto, ctx);
}
}AuthContext 属性
AuthContext 对象提供所有认证相关数据:
interface AuthContext {
session: UserSession | null;
user: UserSession['user'] | null;
organization: Organization | null;
orgMember: OrganizationMember | null;
isImpersonating: boolean;
impersonatedBy: string | null;
apiKey: ApiKeyValidation['key'] | null;
}装饰器数据可用性
重要:并非所有 AuthContext 属性默认都会填充。数据可用性取决于认证方式和使用的装饰器:
Session 认证(默认):
| AuthContext 属性 | 可用性 | 说明 |
| -------------------- | ------------------- | ---------------------------------------- |
| session | ✅ 始终可用 | 完整 session 对象 |
| user | ✅ 始终可用 | 来自 session 的用户 |
| isImpersonating | ✅ 始终可用 | 来自 session 数据 |
| impersonatedBy | ✅ 始终可用 | 模拟时的管理员 ID |
| organization | ⚠️ 需要装饰器 | 使用 @OrgRequired() 或 @OptionalOrg() |
| orgMember | ⚠️ 需要装饰器 | 使用 @OrgRequired() 或 @OptionalOrg() |
| apiKey | ❌ null | 不适用于 session 认证 |
API Key 认证(@ApiKeyAuth()):
| AuthContext 属性 | 可用性 | 说明 |
| -------------------- | ------------------- | ---------------------------------------- |
| session | ❌ null | API Key 没有 session |
| user | ✅ 始终可用 | 通过 key.userId 加载 |
| isImpersonating | ❌ false | 不适用于 API Key |
| impersonatedBy | ❌ null | 不适用于 API Key |
| organization | ❌ null | API Key 认证不加载 |
| orgMember | ❌ null | API Key 认证不加载 |
| apiKey | ✅ 始终可用 | 完整 API Key 信息 |
创建配套装饰器
当创建使用组织数据的自定义参数装饰器时,需要创建配套的方法装饰器来确保数据正确加载。命名约定 XxxCtx + XxxAccess 使配套关系一目了然。
import { applyDecorators } from '@nestjs/common';
import {
createAuthParamDecorator,
OptionalOrg,
OrgRequired,
OrgRoles,
RequireAuth,
AuthContext,
} from '@sapix/nestjs-better-auth-fastify';
// 1. 定义上下文接口
interface ResourceContext {
userId: string;
organizationId: string | null;
orgRole: string | null;
isOrgAdmin: boolean;
}
// 2. 创建参数装饰器:@ResourceCtx()
export const ResourceCtx = createAuthParamDecorator<ResourceContext>(
(auth) => ({
userId: auth.user?.id ?? '',
organizationId: auth.organization?.id ?? null,
orgRole: auth.orgMember?.role ?? null,
isOrgAdmin:
auth.orgMember?.role === 'owner' || auth.orgMember?.role === 'admin',
}),
);
// 3. 创建配套方法装饰器:@ResourceAccess()
export interface ResourceAccessOptions {
requireOrg?: boolean;
orgRoles?: string[];
}
export function ResourceAccess(options: ResourceAccessOptions = {}) {
const { requireOrg = false, orgRoles } = options;
// 指定组织角色 -> 需要组织 + 特定角色
if (orgRoles?.length) {
return applyDecorators(OrgRequired(), OrgRoles(orgRoles));
}
// 需要组织上下文
if (requireOrg) {
return OrgRequired();
}
// 默认:需要认证,如可用则加载组织
// RequireAuth() 确保即使 defaultAuthBehavior 为 'public' 也需要认证
return applyDecorators(RequireAuth(), OptionalOrg());
}用法 - 始终将 @ResourceAccess() 与 @ResourceCtx() 配套使用:
@Controller('resources')
export class ResourceController {
// 默认:需要认证,如可用则加载组织
@ResourceAccess()
@Get('my')
getMyResources(@ResourceCtx() ctx: ResourceContext) {
if (ctx.organizationId) {
return this.service.getOrgResources(ctx.organizationId);
}
return this.service.getUserResources(ctx.userId);
}
// 需要认证 + 组织上下文
@ResourceAccess({ requireOrg: true })
@Get('org')
getOrgResources(@ResourceCtx() ctx: ResourceContext) {
return this.service.getOrgResources(ctx.organizationId!);
}
// 需要认证 + 组织 + 管理员角色
@ResourceAccess({ orgRoles: ['owner', 'admin'] })
@Put('org/settings')
updateOrgSettings(@ResourceCtx() ctx: ResourceContext) {
return this.service.updateSettings(ctx.organizationId!);
}
}注意:默认的
@ResourceAccess()使用RequireAuth()确保认证,不受defaultAuthBehavior设置影响。这使装饰器行为可预测且独立于全局配置。
实际示例
多租户上下文:
interface TenantContext {
userId: string;
tenantId: string | null;
tenantRole: string;
isTenantAdmin: boolean;
}
const TenantCtx = createAuthParamDecorator<TenantContext>((auth) => ({
userId: auth.user?.id ?? 'anonymous',
tenantId: auth.organization?.id ?? null,
tenantRole: auth.orgMember?.role ?? 'none',
isTenantAdmin:
auth.orgMember?.role === 'owner' || auth.orgMember?.role === 'admin',
}));审计上下文:
interface AuditContext {
actorId: string;
actorType: 'user' | 'apiKey' | 'system';
impersonatorId: string | null;
timestamp: string;
}
const AuditCtx = createAuthParamDecorator<AuditContext>((auth) => ({
actorId: auth.apiKey?.userId ?? auth.user?.id ?? 'system',
actorType: auth.apiKey ? 'apiKey' : auth.user ? 'user' : 'system',
impersonatorId: auth.impersonatedBy,
timestamp: new Date().toISOString(),
}));服务层上下文:
interface ServiceContext {
requesterId: string;
scope: {
orgId: string | null;
permissions: string[];
};
}
const ServiceCtx = createAuthParamDecorator<ServiceContext>((auth) => {
const permissions = ['read'];
if ((auth.user as any)?.role === 'admin') {
permissions.push('write', 'delete');
}
return {
requesterId: auth.user?.id ?? 'anonymous',
scope: {
orgId: auth.organization?.id ?? null,
permissions,
},
};
});组合多个装饰器
@Get('dashboard')
getDashboard(
@RequestCtx() request: RequestContext,
@AuditCtx() audit: AuditContext,
) {
this.logger.log('Dashboard accessed', audit);
return this.dashboardService.getData(request);
}🪝 Hook 系统
Hook 系统允许你在 Better Auth 处理认证请求前后执行自定义逻辑。
创建 Hook Provider
// src/hooks/sign-up.hook.ts
import { Injectable } from '@nestjs/common';
import {
Hook,
BeforeHook,
AfterHook,
AuthHookContext,
} from '@sapix/nestjs-better-auth-fastify';
@Hook()
@Injectable()
export class SignUpHook {
constructor(
private readonly emailService: EmailService,
private readonly crmService: CrmService,
) {}
// 注册前验证
@BeforeHook('/sign-up/email')
async validateBeforeSignUp(ctx: AuthHookContext) {
const { email } = ctx.body as { email: string };
if (email.endsWith('@blocked-domain.com')) {
throw new Error('此邮箱域名不被允许');
}
}
// 注册后发送欢迎邮件
@AfterHook('/sign-up/email')
async sendWelcomeEmail(ctx: AuthHookContext) {
const user = ctx.context?.user;
if (user) {
await this.emailService.sendWelcome(user.email);
await this.crmService.createContact(user);
}
}
// 记录所有认证请求(无路径 = 匹配所有路由)
@BeforeHook()
async logAuthRequest(ctx: AuthHookContext) {
console.log('认证请求:', ctx.path);
}
}注册 Hook Provider
// src/app.module.ts
@Module({
imports: [AuthModule.forRoot({ auth })],
providers: [SignUpHook], // 注册 hook provider
})
export class AppModule {}常用 Hook 路径
| 路径 | 描述 |
| ------------------ | ------------ |
| /sign-up/email | 邮箱注册 |
| /sign-in/email | 邮箱登录 |
| /sign-out | 登出 |
| /forget-password | 忘记密码 |
| /reset-password | 重置密码 |
| /verify-email | 邮箱验证 |
🛠 AuthService API
AuthService 提供对 Better Auth 功能的程序化访问。
基本用法
import { Injectable } from '@nestjs/common';
import { AuthService, UserSession } from '@sapix/nestjs-better-auth-fastify';
import type { Auth } from './auth/auth.config';
@Injectable()
export class MyService {
constructor(private readonly authService: AuthService<Auth>) {}
async someMethod(request: FastifyRequest) {
// 从请求获取 session
const session = await this.authService.getSessionFromRequest(request);
// 验证 session(无效时抛出 UnauthorizedException)
const validSession = await this.authService.validateSession(request);
// 检查角色
if (this.authService.hasRole(session, ['admin'])) {
// 用户是管理员
}
// 检查权限
if (
this.authService.hasPermission(
session,
['user:read', 'user:write'],
'all',
)
) {
// 用户拥有所有必需权限
}
// 检查 session 新鲜度
if (!this.authService.isSessionFresh(session)) {
// 要求重新认证
}
// 直接访问 Better Auth API
const accounts = await this.authService.api.listUserAccounts({
headers: getWebHeadersFromRequest(request),
});
}
}Session 管理
// 撤销特定 session
await this.authService.revokeSession(sessionToken, request);
// 撤销所有用户 session
await this.authService.revokeAllSessions(request);
// 列出所有用户 session
const sessions = await this.authService.listUserSessions(request);Admin 功能
// 检查用户是否被封禁
if (this.authService.isUserBanned(session.user)) {
throw new ForbiddenException('用户已被封禁');
}
// 检查模拟状态
if (this.authService.isImpersonating(session)) {
const adminId = this.authService.getImpersonatedBy(session);
// 记录审计日志
}API Key 验证
const result = await this.authService.verifyApiKey(apiKey);
if (result.valid) {
console.log('Key 属于用户:', result.key?.userId);
console.log('权限:', result.key?.permissions);
}
// 带权限要求
const result = await this.authService.verifyApiKey(apiKey, {
files: ['read', 'write'],
});Organization 功能
// 获取活动组织
const org = await this.authService.getActiveOrganization(request);
// 检查组织权限
const hasPermission = await this.authService.hasOrgPermission(request, {
resource: 'member',
action: 'create',
});JWT Token(需要 JWT 插件)
const jwt = await this.authService.getJwtToken(request);
if (jwt) {
// 使用 JWT 进行服务间通信
}访问 Auth 实例
// 获取完整的 Better Auth 实例
const authInstance = this.authService.instance;
// 获取配置的 basePath
const basePath = this.authService.basePath;🎨 类型推断
该库支持从 Better Auth 配置完全推断类型。
使用 $Infer 模式
import { AuthService } from '@sapix/nestjs-better-auth-fastify';
import type { Auth } from './auth/auth.config';
@Injectable()
export class MyService {
constructor(private readonly authService: AuthService<Auth>) {}
async getUser(request: FastifyRequest) {
// Session 类型自动从你的 auth 配置推断
const session = await this.authService.getSessionFromRequest(request);
// session.user 包含你 auth 配置中的所有字段
}
}
// 直接获取类型(仅编译时)
type Session = typeof authService.$Infer.Session;
type User = typeof authService.$Infer.User;使用 InferSession 和 InferUser
import { InferSession, InferUser } from '@sapix/nestjs-better-auth-fastify';
import type { Auth } from './auth/auth.config';
type MySession = InferSession<Auth>;
type MyUser = InferUser<Auth>;自定义用户类型
interface CustomUser extends BaseUser {
role: string;
permissions: string[];
department: string;
}
@Get('profile')
getProfile(@Session() session: UserSession<CustomUser>) {
return session.user.department; // 类型安全
}⚙️ 配置选项
完整配置选项
AuthModule.forRoot({
// 必需:Better Auth 实例
// 认证路由路径从 auth.options.basePath 读取(默认 '/api/auth')
auth,
// 可选:默认认证行为
// - 'require'(默认):所有路由需要认证。使用 @AllowAnonymous() 设为公开。
// - 'optional':所有路由可选认证。有 session 时注入。
// - 'public':所有路由默认公开。使用 @RequireAuth() 要求认证。
defaultAuthBehavior: 'require',
// 可选:启用调试日志
debug: false,
// 可选:自定义中间件包装认证处理器
// 适用于 ORM 上下文(如 MikroORM RequestContext)
middleware: async (req, reply, next) => {
await next();
},
// 可选:自定义错误消息(用于国际化)
errorMessages: {
unauthorized: '请先登录',
forbidden: '权限不足',
sessionNotFresh: '请重新登录以执行此操作',
userBanned: '你的账户已被封禁',
orgRequired: '请先选择一个组织',
orgRoleRequired: '组织角色权限不足',
orgPermissionRequired: '你没有执行此操作的权限',
apiKeyRequired: '需要有效的 API Key',
apiKeyInvalidPermissions: 'API Key 权限不足',
},
// 可选:自定义组织角色权限
// 覆盖默认的角色-权限映射
orgRolePermissions: {
owner: { organization: 'all', member: 'all' },
admin: { organization: ['read', 'update'], member: ['read', 'create'] },
member: { organization: ['read'] },
},
});异步配置
// 使用 useFactory
AuthModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
auth: createAuth(config.get('AUTH_SECRET')),
}),
inject: [ConfigService],
});
// 使用 useClass
AuthModule.forRootAsync({
useClass: AuthConfigService,
});
// 使用 useExisting
AuthModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});默认认证行为
控制路由的默认行为:
'require'(默认)- 默认安全
所有路由需要认证。使用 @AllowAnonymous() 设为公开:
@Controller('api')
export class ApiController {
@Get('protected')
protectedRoute() {} // 需要认证
@AllowAnonymous()
@Get('public')
publicRoute() {} // 无需认证
}'public' - 默认开放
所有路由默认公开。使用 @RequireAuth() 要求认证:
AuthModule.forRoot({
auth,
defaultAuthBehavior: 'public',
});
@Controller('api')
export class ApiController {
@Get('public')
publicRoute() {} // 无需认证
@RequireAuth()
@Get('protected')
protectedRoute() {} // 需要认证
}'optional' - 灵活认证
所有路由同时接受已认证和匿名请求:
AuthModule.forRoot({
auth,
defaultAuthBehavior: 'optional',
});
@Controller('api')
export class ApiController {
@Get('greeting')
greet(@CurrentUser() user: User | null) {
return user ? `你好 ${user.name}` : '你好访客';
}
}🔌 多上下文支持
HTTP(默认)
开箱即用,适配 Fastify HTTP 适配器。
GraphQL
// 安装依赖
pnpm add @nestjs/graphql graphql
// 装饰器在 resolver 中同样工作
@Resolver()
export class UserResolver {
@Query(() => User)
@Roles(['admin'])
async users(@CurrentUser() user: UserSession['user']) {
return this.userService.findAll();
}
}WebSocket
// 安装依赖
pnpm add @nestjs/websockets @nestjs/platform-socket.io
// 装饰器在 gateway 中工作
@WebSocketGateway()
export class EventsGateway {
@SubscribeMessage('events')
handleEvent(@Session() session: UserSession) {
return { user: session.user };
}
}🔧 工具函数
该库导出用于 Fastify 和 Web 标准 API 的工具函数:
import {
toWebHeaders,
toWebRequest,
getHeadersFromRequest,
getWebHeadersFromRequest,
writeWebResponseToReply,
normalizeBasePath,
getRequestFromContext,
} from '@sapix/nestjs-better-auth-fastify';
// 将 Fastify headers 转换为 Web 标准 Headers
const webHeaders = toWebHeaders(request.headers);
// 从 Fastify Request 获取 Web 标准 Headers
const headers = getWebHeadersFromRequest(request);
// 从 Fastify Request 构建 Web 标准 Request
const webRequest = toWebRequest(request);
// 将 Web Response 写入 Fastify Reply
await writeWebResponseToReply(response, reply);
// 标准化 basePath(确保以 / 开头,无尾随 /)
const path = normalizeBasePath('api/auth/'); // '/api/auth'
// 从 NestJS ExecutionContext 获取 FastifyRequest(支持 HTTP、GraphQL、WebSocket)
const request = getRequestFromContext(context);📝 Request 扩展
该库为 FastifyRequest 扩展了认证相关属性:
declare module 'fastify' {
interface FastifyRequest {
session: UserSession | null;
user: UserSession['user'] | null;
apiKey?: ApiKeyValidation['key'] | null;
organization?: Organization | null;
organizationMember?: OrganizationMember | null;
isImpersonating?: boolean;
impersonatedBy?: string | null;
}
}在路由处理器中直接访问:
@Get('profile')
getProfile(@Req() request: FastifyRequest) {
return {
user: request.user,
session: request.session,
org: request.organization,
isImpersonating: request.isImpersonating,
};
}🧪 测试
单元测试
import { Test } from '@nestjs/testing';
import {
AuthModule,
AuthService,
AUTH_MODULE_OPTIONS,
} from '@sapix/nestjs-better-auth-fastify';
const module = await Test.createTestingModule({
imports: [AuthModule.forRoot({ auth })],
}).compile();
const authService = module.get(AuthService);Mock AuthService
const mockAuthService = {
getSessionFromRequest: jest.fn().mockResolvedValue(mockSession),
validateSession: jest.fn().mockResolvedValue(mockSession),
hasRole: jest.fn().mockReturnValue(true),
hasPermission: jest.fn().mockReturnValue(true),
isSessionFresh: jest.fn().mockReturnValue(true),
isUserBanned: jest.fn().mockReturnValue(false),
isImpersonating: jest.fn().mockReturnValue(false),
};
const module = await Test.createTestingModule({
providers: [MyService, { provide: AuthService, useValue: mockAuthService }],
}).compile();📋 环境要求
- Node.js >= 18.0.0
- NestJS >= 10.0.0
- Fastify >= 4.0.0
- Better Auth >= 1.0.0
🤝 贡献
欢迎贡献!请随时提交 Pull Request。
- Fork 仓库
- 创建功能分支 (
git checkout -b feature/amazing-feature) - 提交更改 (
git commit -m 'Add some amazing feature') - 推送到分支 (
git push origin feature/amazing-feature) - 打开 Pull Request
📄 许可证
MIT
