@mguay/nestjs-better-auth
Version:
Better Auth for NestJS
436 lines (424 loc) • 14.3 kB
JavaScript
;
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;