UNPKG

@mguay/nestjs-better-auth

Version:
436 lines (424 loc) 14.3 kB
'use strict'; const common = require('@nestjs/common'); const core = require('@nestjs/core'); const api = require('better-auth/api'); const node = require('better-auth/node'); const plugins = require('better-auth/plugins'); const express = require('express'); function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const express__namespace = /*#__PURE__*/_interopNamespaceCompat(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 = () => common.SetMetadata("PUBLIC", true); const Optional = () => common.SetMetadata("OPTIONAL", true); const Session = common.createParamDecorator((_data, context) => { const request = context.switchToHttp().getRequest(); return request.session; }); const BeforeHook = (path) => common.SetMetadata(BEFORE_HOOK_KEY, path); const AfterHook = (path) => common.SetMetadata(AFTER_HOOK_KEY, path); const Hook = () => common.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); exports.AuthService = class AuthService { 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; } }; exports.AuthService = __decorateClass$4([ __decorateParam$2(0, common.Inject(AUTH_INSTANCE_KEY)) ], exports.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); exports.AuthGuard = class AuthGuard { 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: node.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 api.APIError(401, { code: "UNAUTHORIZED", message: "Unauthorized" }); return true; } }; exports.AuthGuard = __decorateClass$3([ common.Injectable(), __decorateParam$1(0, common.Inject(core.Reflector)), __decorateParam$1(1, common.Inject(AUTH_INSTANCE_KEY)) ], exports.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([ common.Catch(api.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__namespace.json()(req, res, (err) => { if (err) { next(err); return; } express__namespace.urlencoded({ extended: true })(req, res, next); }); } }; SkipBodyParsingMiddleware = __decorateClass$1([ common.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" } ]; exports.AuthModule = class AuthModule { constructor(auth, discoveryService, metadataScanner, adapter, options) { this.auth = auth; this.discoveryService = discoveryService; this.metadataScanner = metadataScanner; this.adapter = adapter; this.options = options; } logger = new common.Logger(exports.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 = node.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] = plugins.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 }, exports.AuthService ]; if (!options.disableExceptionFilter) { providers.push({ provide: core.APP_FILTER, useClass: APIErrorExceptionFilter }); } return { global: true, module: exports.AuthModule, providers, exports: [ { provide: AUTH_INSTANCE_KEY, useValue: auth }, { provide: AUTH_MODULE_OPTIONS_KEY, useValue: options }, exports.AuthService ] }; } /** * Static factory method to create and configure the AuthModule asynchronously. * @param options - Async configuration options for the module */ static forRootAsync(options) { const asyncProviders = exports.AuthModule.createAsyncProviders(options); return { global: true, module: exports.AuthModule, imports: options.imports || [], providers: [...asyncProviders, exports.AuthService], exports: [ { provide: AUTH_INSTANCE_KEY, useExisting: AUTH_INSTANCE_KEY }, { provide: AUTH_MODULE_OPTIONS_KEY, useExisting: AUTH_MODULE_OPTIONS_KEY }, exports.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 || [] }, exports.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] }, exports.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] }, exports.AuthModule.createExceptionFilterProvider() ]; } throw new Error( "Invalid async configuration. Must provide useFactory, useClass, or useExisting." ); } static createExceptionFilterProvider() { return { provide: core.APP_FILTER, useFactory: (options) => { return options.disableExceptionFilter ? null : new APIErrorExceptionFilter(); }, inject: [AUTH_MODULE_OPTIONS_KEY] }; } }; exports.AuthModule = __decorateClass([ common.Module({ imports: [core.DiscoveryModule] }), __decorateParam(0, common.Inject(AUTH_INSTANCE_KEY)), __decorateParam(1, common.Inject(core.DiscoveryService)), __decorateParam(2, common.Inject(core.MetadataScanner)), __decorateParam(3, common.Inject(core.HttpAdapterHost)), __decorateParam(4, common.Inject(AUTH_MODULE_OPTIONS_KEY)) ], exports.AuthModule); exports.AFTER_HOOK_KEY = AFTER_HOOK_KEY; exports.AUTH_INSTANCE_KEY = AUTH_INSTANCE_KEY; exports.AUTH_MODULE_OPTIONS_KEY = AUTH_MODULE_OPTIONS_KEY; exports.AfterHook = AfterHook; exports.BEFORE_HOOK_KEY = BEFORE_HOOK_KEY; exports.BeforeHook = BeforeHook; exports.HOOK_KEY = HOOK_KEY; exports.Hook = Hook; exports.Optional = Optional; exports.Public = Public; exports.Session = Session;