UNPKG

@mguay/nestjs-better-auth

Version:
410 lines (401 loc) 13.6 kB
import { createParamDecorator, SetMetadata, Inject, Injectable, Catch, Module, Logger } from '@nestjs/common'; import { Reflector, DiscoveryModule, DiscoveryService, MetadataScanner, HttpAdapterHost, APP_FILTER } from '@nestjs/core'; import { APIError } from 'better-auth/api'; import { fromNodeHeaders, toNodeHandler } from 'better-auth/node'; import { createAuthMiddleware } from 'better-auth/plugins'; import * as express from 'express'; const BEFORE_HOOK_KEY = Symbol("BEFORE_HOOK"); const AFTER_HOOK_KEY = Symbol("AFTER_HOOK"); const HOOK_KEY = Symbol("HOOK"); const AUTH_INSTANCE_KEY = Symbol("AUTH_INSTANCE"); const AUTH_MODULE_OPTIONS_KEY = Symbol("AUTH_MODULE_OPTIONS"); const Public = () => SetMetadata("PUBLIC", true); const Optional = () => SetMetadata("OPTIONAL", true); const Session = createParamDecorator((_data, context) => { const request = context.switchToHttp().getRequest(); return request.session; }); const BeforeHook = (path) => SetMetadata(BEFORE_HOOK_KEY, path); const AfterHook = (path) => SetMetadata(AFTER_HOOK_KEY, path); const Hook = () => SetMetadata(HOOK_KEY, true); var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor; var __decorateClass$4 = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(result)) || result; return result; }; var __decorateParam$2 = (index, decorator) => (target, key) => decorator(target, key, index); let AuthService = class { constructor(auth) { this.auth = auth; } /** * Returns the API endpoints provided by the auth instance */ get api() { return this.auth.api; } /** * Returns the complete auth instance * Access this for plugin-specific functionality */ get instance() { return this.auth; } }; AuthService = __decorateClass$4([ __decorateParam$2(0, Inject(AUTH_INSTANCE_KEY)) ], AuthService); var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor; var __decorateClass$3 = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(result)) || result; return result; }; var __decorateParam$1 = (index, decorator) => (target, key) => decorator(target, key, index); let AuthGuard = class { constructor(reflector, auth) { this.reflector = reflector; this.auth = auth; } /** * Validates if the current request is authenticated * Attaches session and user information to the request object * @param context - The execution context of the current request * @returns True if the request is authorized to proceed, throws an error otherwise */ async canActivate(context) { const request = context.switchToHttp().getRequest(); const session = await this.auth.api.getSession({ headers: fromNodeHeaders(request.headers) }); request.session = session; request.user = session?.user ?? null; const isPublic = this.reflector.getAllAndOverride("PUBLIC", [ context.getHandler(), context.getClass() ]); if (isPublic) return true; const isOptional = this.reflector.getAllAndOverride("OPTIONAL", [ context.getHandler(), context.getClass() ]); if (isOptional && !session) return true; if (!session) throw new APIError(401, { code: "UNAUTHORIZED", message: "Unauthorized" }); return true; } }; AuthGuard = __decorateClass$3([ Injectable(), __decorateParam$1(0, Inject(Reflector)), __decorateParam$1(1, Inject(AUTH_INSTANCE_KEY)) ], AuthGuard); var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor; var __decorateClass$2 = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(result)) || result; return result; }; let APIErrorExceptionFilter = class { catch(exception, host) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const status = exception.statusCode; const message = exception.body?.message; response.status(status).json({ statusCode: status, message }); } }; APIErrorExceptionFilter = __decorateClass$2([ Catch(APIError) ], APIErrorExceptionFilter); var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor; var __decorateClass$1 = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(result)) || result; return result; }; let SkipBodyParsingMiddleware = class { use(req, res, next) { if (req.baseUrl.startsWith("/api/auth")) { next(); return; } express.json()(req, res, (err) => { if (err) { next(err); return; } express.urlencoded({ extended: true })(req, res, next); }); } }; SkipBodyParsingMiddleware = __decorateClass$1([ Injectable() ], SkipBodyParsingMiddleware); var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(result)) || result; return result; }; var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index); const HOOKS = [ { metadataKey: BEFORE_HOOK_KEY, hookType: "before" }, { metadataKey: AFTER_HOOK_KEY, hookType: "after" } ]; let AuthModule = class { constructor(auth, discoveryService, metadataScanner, adapter, options) { this.auth = auth; this.discoveryService = discoveryService; this.metadataScanner = metadataScanner; this.adapter = adapter; this.options = options; } logger = new Logger(AuthModule.name); onModuleInit() { if (!this.auth.options.hooks) return; const providers = this.discoveryService.getProviders().filter( ({ metatype }) => metatype && Reflect.getMetadata(HOOK_KEY, metatype) ); for (const provider of providers) { const providerPrototype = Object.getPrototypeOf(provider.instance); const methods = this.metadataScanner.getAllMethodNames(providerPrototype); for (const method of methods) { const providerMethod = providerPrototype[method]; this.setupHooks(providerMethod, provider.instance); } } } configure(consumer) { const trustedOrigins = this.auth.options.trustedOrigins; const isNotFunctionBased = trustedOrigins && Array.isArray(trustedOrigins); if (!this.options.disableTrustedOriginsCors && isNotFunctionBased) { this.adapter.httpAdapter.enableCors({ origin: trustedOrigins, methods: ["GET", "POST", "PUT", "DELETE"], credentials: true }); } else if (trustedOrigins && !this.options.disableTrustedOriginsCors && !isNotFunctionBased) throw new Error( "Function-based trustedOrigins not supported in NestJS. Use string array or disable CORS with disableTrustedOriginsCors: true." ); if (!this.options.disableBodyParser) consumer.apply(SkipBodyParsingMiddleware).forRoutes("*path"); let basePath = this.auth.options.basePath ?? "/api/auth"; if (!basePath.startsWith("/")) { basePath = `/${basePath}`; } if (basePath.endsWith("/")) { basePath = basePath.slice(0, -1); } const handler = toNodeHandler(this.auth); this.adapter.httpAdapter.getInstance().use(`${basePath}/*path`, (req, res) => { return handler(req, res); }); this.logger.log(`AuthModule initialized BetterAuth on '${basePath}/*'`); } setupHooks(providerMethod, providerClass) { if (!this.auth.options.hooks) return; for (const { metadataKey, hookType } of HOOKS) { const hookPath = Reflect.getMetadata(metadataKey, providerMethod); if (!hookPath) continue; const originalHook = this.auth.options.hooks[hookType]; this.auth.options.hooks[hookType] = createAuthMiddleware(async (ctx) => { if (originalHook) { await originalHook(ctx); } if (hookPath === ctx.path) { await providerMethod.apply(providerClass, [ctx]); } }); } } /** * Static factory method to create and configure the AuthModule. * @param auth - The Auth instance to use * @param options - Configuration options for the module */ static forRoot(auth, options = {}) { auth.options.hooks = { ...auth.options.hooks }; const providers = [ { provide: AUTH_INSTANCE_KEY, useValue: auth }, { provide: AUTH_MODULE_OPTIONS_KEY, useValue: options }, AuthService ]; if (!options.disableExceptionFilter) { providers.push({ provide: APP_FILTER, useClass: APIErrorExceptionFilter }); } return { global: true, module: AuthModule, providers, exports: [ { provide: AUTH_INSTANCE_KEY, useValue: auth }, { provide: AUTH_MODULE_OPTIONS_KEY, useValue: options }, AuthService ] }; } /** * Static factory method to create and configure the AuthModule asynchronously. * @param options - Async configuration options for the module */ static forRootAsync(options) { const asyncProviders = AuthModule.createAsyncProviders(options); return { global: true, module: AuthModule, imports: options.imports || [], providers: [...asyncProviders, AuthService], exports: [ { provide: AUTH_INSTANCE_KEY, useExisting: AUTH_INSTANCE_KEY }, { provide: AUTH_MODULE_OPTIONS_KEY, useExisting: AUTH_MODULE_OPTIONS_KEY }, AuthService ] }; } static createAsyncProviders(options) { if (options.useFactory) { return [ { provide: AUTH_INSTANCE_KEY, useFactory: async (...args) => { const result = await options.useFactory?.(...args); const auth = result.auth; auth.options.hooks = { ...auth.options.hooks }; return auth; }, inject: options.inject || [] }, { provide: AUTH_MODULE_OPTIONS_KEY, useFactory: async (...args) => { const result = await options.useFactory?.(...args); return result.options || {}; }, inject: options.inject || [] }, AuthModule.createExceptionFilterProvider() ]; } if (options.useClass) { return [ { provide: options.useClass, useClass: options.useClass }, { provide: AUTH_INSTANCE_KEY, useFactory: async (configService) => { const result = await configService.createAuthOptions(); const auth = result.auth; auth.options.hooks = { ...auth.options.hooks }; return auth; }, inject: [options.useClass] }, { provide: AUTH_MODULE_OPTIONS_KEY, useFactory: async (configService) => { const result = await configService.createAuthOptions(); return result.options || {}; }, inject: [options.useClass] }, AuthModule.createExceptionFilterProvider() ]; } if (options.useExisting) { return [ { provide: AUTH_INSTANCE_KEY, useFactory: async (configService) => { const result = await configService.createAuthOptions(); const auth = result.auth; auth.options.hooks = { ...auth.options.hooks }; return auth; }, inject: [options.useExisting] }, { provide: AUTH_MODULE_OPTIONS_KEY, useFactory: async (configService) => { const result = await configService.createAuthOptions(); return result.options || {}; }, inject: [options.useExisting] }, AuthModule.createExceptionFilterProvider() ]; } throw new Error( "Invalid async configuration. Must provide useFactory, useClass, or useExisting." ); } static createExceptionFilterProvider() { return { provide: APP_FILTER, useFactory: (options) => { return options.disableExceptionFilter ? null : new APIErrorExceptionFilter(); }, inject: [AUTH_MODULE_OPTIONS_KEY] }; } }; AuthModule = __decorateClass([ Module({ imports: [DiscoveryModule] }), __decorateParam(0, Inject(AUTH_INSTANCE_KEY)), __decorateParam(1, Inject(DiscoveryService)), __decorateParam(2, Inject(MetadataScanner)), __decorateParam(3, Inject(HttpAdapterHost)), __decorateParam(4, Inject(AUTH_MODULE_OPTIONS_KEY)) ], AuthModule); export { AFTER_HOOK_KEY, AUTH_INSTANCE_KEY, AUTH_MODULE_OPTIONS_KEY, AfterHook, AuthGuard, AuthModule, AuthService, BEFORE_HOOK_KEY, BeforeHook, HOOK_KEY, Hook, Optional, Public, Session };