diff --git a/documentation/docs/graphql/authorization.mdx b/documentation/docs/graphql/authorization.mdx index cd389644a..fba4ebbc6 100644 --- a/documentation/docs/graphql/authorization.mdx +++ b/documentation/docs/graphql/authorization.mdx @@ -249,11 +249,12 @@ For example you could define the subtasks with the `auth` option, only allowing When you need more control over authorization you can create a custom `Authorizer`. You may want to use a custom `Authorizer` if you need to use additional services to do authorization for a DTO. -The `Authorizer` interface requires two methods to be implemented +The `CustomAuthorizer` interface ensures two methods: - `authorize` - Should return a filter that should be used for all queries and mutations for the DTO. -- `authorizeRelation` - Should return a filter for the relation that will be used when querying the relation or - adding/removing relations to/from the DTO. +- `authorizeRelation` - Optionally modify the filter for the relation that will be used when querying the relation or + adding/removing relations to/from the DTO. If undefined is returned, the authorization filter of the relation DTO + will be used instead. In this example we'll create a simple authorizer for `SubTasks`. You can use this as a base to create a more complex authorizers that depends on other services. @@ -266,16 +267,16 @@ import { UserContext } from '../auth/auth.interfaces'; import { SubTaskDTO } from './dto/sub-task.dto'; @Injectable() -export class SubTaskAuthorizer implements Authorizer { +export class SubTaskAuthorizer implements CustomAuthorizer { authorize(context: UserContext): Promise> { return Promise.resolve({ ownerId: { eq: context.req.user.id } }); } - authorizeRelation(relationName: string, context: UserContext): Promise> { + authorizeRelation(relationName: string, context: UserContext): Promise | undefined> { if (relationName === 'todoItem') { return Promise.resolve({ ownerId: { eq: context.req.user.id } }); } - return Promise.resolve({}); + return Promise.resolve(undefined); } } ``` @@ -398,6 +399,11 @@ export class TodoItemResolver extends CRUDResolver(TodoItemDTO) { If you are extending the `CRUDResolver` directly be sure to [register your DTOs with the `NestjsQueryGraphQLModule`](./resolvers.mdx#crudresolver) ::: +:::important +When using `@InjectAuthorizer`, the injected Authorizer is not the CustomAuthorizer, but the DefaultCRUDAuthorizer that internally uses the CustomAuthorizer. +If you want to use the CustomAuthorizer directly, inject it with `@InjectCustomAuthorizer` instead. +::: + ## Authorize depending on operation Sometimes it might be necessary to perform different authorization based on the kind of operation an user wants to execute. diff --git a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts index 153ea261b..45cf179ca 100644 --- a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts +++ b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts @@ -3,7 +3,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Filter } from '@nestjs-query/core'; import { Injectable } from '@nestjs/common'; import { Authorizer, Relation, Authorize, UnPagedRelation } from '../../src'; -import { AuthorizationContext, OperationGroup, getAuthorizerToken } from '../../src/auth'; +import { + AuthorizationContext, + OperationGroup, + getAuthorizerToken, + getCustomAuthorizerToken, + CustomAuthorizer, +} from '../../src/auth'; import { createAuthorizerProviders } from '../../src/providers'; describe('createDefaultAuthorizer', () => { @@ -15,12 +21,12 @@ describe('createDefaultAuthorizer', () => { } @Injectable() - class RelationAuthorizer implements Authorizer { + class RelationAuthorizer implements CustomAuthorizer { authorize(context: UserContext): Promise> { return Promise.resolve({ authorizerOwnerId: { eq: context.user.id } }); } - authorizeRelation(): Promise> { + authorizeRelation(): Promise | undefined> { return Promise.reject(new Error('should not have called')); } } @@ -30,7 +36,11 @@ describe('createDefaultAuthorizer', () => { authorizerOwnerId!: number; } - @Authorize({ authorize: (ctx: UserContext) => ({ decoratorOwnerId: { eq: ctx.user.id } }) }) + @Authorize({ + authorize: (ctx: UserContext) => ({ + decoratorOwnerId: { eq: ctx.user.id }, + }), + }) class TestDecoratorRelation { decoratorOwnerId!: number; } @@ -59,6 +69,32 @@ describe('createDefaultAuthorizer', () => { ownerId!: number; } + @Injectable() + class TestWithAuthorizerAuthorizer implements CustomAuthorizer { + authorize(context: UserContext): Promise> { + return Promise.resolve({ ownerId: { eq: context.user.id } }); + } + + authorizeRelation(): Promise | undefined> { + return Promise.resolve(undefined); + } + } + + @Authorize(TestWithAuthorizerAuthorizer) + @Relation('relations', () => TestRelation, { + auth: { + authorize: (ctx: UserContext, authorizationContext?: AuthorizationContext) => + authorizationContext?.operationName === 'other' + ? { relationOwnerId: { neq: ctx.user.id } } + : { relationOwnerId: { eq: ctx.user.id } }, + }, + }) + @UnPagedRelation('unPagedDecoratorRelations', () => TestDecoratorRelation) + @Relation('authorizerRelation', () => RelationWithAuthorizer) + class TestWithAuthorizerDTO { + ownerId!: number; + } + beforeEach(async () => { testingModule = await Test.createTestingModule({ providers: [ @@ -68,6 +104,7 @@ describe('createDefaultAuthorizer', () => { RelationWithAuthorizer, TestDTO, TestNoAuthDTO, + TestWithAuthorizerDTO, ]), ], }).compile(); @@ -79,7 +116,12 @@ describe('createDefaultAuthorizer', () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); const filter = await authorizer.authorize( { user: { id: 2 } }, - { operationName: 'queryMany', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ ownerId: { eq: 2 } }); }); @@ -88,7 +130,12 @@ describe('createDefaultAuthorizer', () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); const filter = await authorizer.authorize( { user: { id: 2 } }, - { operationName: 'other', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'other', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ ownerId: { neq: 2 } }); }); @@ -97,7 +144,12 @@ describe('createDefaultAuthorizer', () => { const authorizer = testingModule.get>(getAuthorizerToken(TestNoAuthDTO)); const filter = await authorizer.authorize( { user: { id: 2 } }, - { operationName: 'queryMany', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({}); }); @@ -107,7 +159,12 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'unPagedDecoratorRelations', { user: { id: 2 } }, - { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ decoratorOwnerId: { eq: 2 } }); }); @@ -117,7 +174,12 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'relations', { user: { id: 2 } }, - { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ relationOwnerId: { eq: 2 } }); }); @@ -127,7 +189,12 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'relations', { user: { id: 2 } }, - { operationName: 'other', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'other', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ relationOwnerId: { neq: 2 } }); }); @@ -137,7 +204,12 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'authorizerRelation', { user: { id: 2 } }, - { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({ authorizerOwnerId: { eq: 2 } }); }); @@ -147,8 +219,125 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'unknownRelations', { user: { id: 2 } }, - { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, ); expect(filter).toEqual({}); }); + + it('should call authorizeRelation of authorizer and fallback to authorize decorator', async () => { + const authorizer = testingModule.get>(getAuthorizerToken(TestWithAuthorizerDTO)); + jest.spyOn(authorizer, 'authorizeRelation'); + const customAuthorizer = testingModule.get>( + getCustomAuthorizerToken(TestWithAuthorizerDTO), + ); + jest.spyOn(customAuthorizer, 'authorizeRelation'); + expect(customAuthorizer).toBeDefined(); + const filter = await authorizer.authorizeRelation( + 'unPagedDecoratorRelations', + { user: { id: 2 } }, + { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(filter).toEqual({ + decoratorOwnerId: { eq: 2 }, + }); + expect(customAuthorizer.authorizeRelation).toHaveBeenCalledWith( + 'unPagedDecoratorRelations', + { user: { id: 2 } }, + { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(authorizer.authorizeRelation).toHaveBeenCalledWith( + 'unPagedDecoratorRelations', + { user: { id: 2 } }, + { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + }); + + it('should call authorizeRelation of authorizer and fallback to custom authorizer of relation', async () => { + const authorizer = testingModule.get>(getAuthorizerToken(TestWithAuthorizerDTO)); + jest.spyOn(authorizer, 'authorizeRelation'); + const customAuthorizer = testingModule.get>( + getCustomAuthorizerToken(TestWithAuthorizerDTO), + ); + jest.spyOn(customAuthorizer, 'authorizeRelation'); + expect(customAuthorizer).toBeDefined(); + const relationAuthorizer = testingModule.get>( + getAuthorizerToken(RelationWithAuthorizer), + ); + jest.spyOn(relationAuthorizer, 'authorize'); + const customRelationAuthorizer = testingModule.get>( + getCustomAuthorizerToken(RelationWithAuthorizer), + ); + jest.spyOn(customRelationAuthorizer, 'authorize'); + const filter = await authorizer.authorizeRelation( + 'authorizerRelation', + { user: { id: 2 } }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(filter).toEqual({ + authorizerOwnerId: { eq: 2 }, + }); + expect(customAuthorizer.authorizeRelation).toHaveBeenCalledWith( + 'authorizerRelation', + { user: { id: 2 } }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(authorizer.authorizeRelation).toHaveBeenCalledWith( + 'authorizerRelation', + { user: { id: 2 } }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(relationAuthorizer.authorize).toHaveBeenCalledWith( + { user: { id: 2 } }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + expect(customRelationAuthorizer.authorize).toHaveBeenCalledWith( + { user: { id: 2 } }, + { + operationName: 'queryRelation', + operationGroup: OperationGroup.READ, + readonly: true, + many: true, + }, + ); + }); }); diff --git a/packages/query-graphql/src/auth/authorizer.ts b/packages/query-graphql/src/auth/authorizer.ts index 81bebadf8..e99182ba6 100644 --- a/packages/query-graphql/src/auth/authorizer.ts +++ b/packages/query-graphql/src/auth/authorizer.ts @@ -22,7 +22,19 @@ export interface AuthorizationContext { readonly many: boolean; } -export interface Authorizer { +export interface CustomAuthorizer { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any + authorize(context: any, authorizerContext: AuthorizationContext): Promise>; + + authorizeRelation?( + relationName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizerContext: AuthorizationContext, + ): Promise | undefined>; +} + +export interface Authorizer extends CustomAuthorizer { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any authorize(context: any, authorizerContext: AuthorizationContext): Promise>; @@ -31,5 +43,5 @@ export interface Authorizer { // eslint-disable-next-line @typescript-eslint/no-explicit-any context: any, authorizerContext: AuthorizationContext, - ): Promise>; + ): Promise>; } diff --git a/packages/query-graphql/src/auth/default-crud.authorizer.ts b/packages/query-graphql/src/auth/default-crud.authorizer.ts index f6b47064c..7461815a3 100644 --- a/packages/query-graphql/src/auth/default-crud.authorizer.ts +++ b/packages/query-graphql/src/auth/default-crud.authorizer.ts @@ -1,10 +1,10 @@ import { ModuleRef } from '@nestjs/core'; import { Class, Filter } from '@nestjs-query/core'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Inject, Optional } from '@nestjs/common'; import { getAuthorizer, getRelations } from '../decorators'; -import { getAuthorizerToken } from './tokens'; +import { getAuthorizerToken, getCustomAuthorizerToken } from './tokens'; import { ResolverRelation } from '../resolvers/relations'; -import { Authorizer, AuthorizationContext } from './authorizer'; +import { Authorizer, AuthorizationContext, CustomAuthorizer } from './authorizer'; export interface AuthorizerOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -23,28 +23,31 @@ const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer< export function createDefaultAuthorizer( DTOClass: Class, - opts: AuthorizerOptions, + opts?: CustomAuthorizer | AuthorizerOptions, // instance of class or authorizer options ): Class> { @Injectable() class DefaultAuthorizer implements Authorizer { - readonly authOptions: AuthorizerOptions = opts; + readonly authOptions?: AuthorizerOptions | CustomAuthorizer = opts; readonly relationsAuthorizers: Map | undefined>; - constructor(moduleRef: ModuleRef) { + private readonly relations: Map>; + + constructor( + private readonly moduleRef: ModuleRef, + @Optional() @Inject(getCustomAuthorizerToken(DTOClass)) private readonly customAuthorizer?: CustomAuthorizer, + ) { this.relationsAuthorizers = new Map | undefined>(); - this.relations.forEach((value, key) => { - if (value.auth) { - this.relationsAuthorizers.set(key, createRelationAuthorizer(value.auth)); - } else if (getAuthorizer(value.DTO)) { - this.relationsAuthorizers.set(key, moduleRef.get(getAuthorizerToken(value.DTO), { strict: false })); - } - }); + this.relations = this.getRelations(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { - return this.authOptions?.authorize(context, authorizationContext) ?? {}; + return ( + this.customAuthorizer?.authorize(context, authorizationContext) ?? + this.authOptions?.authorize(context, authorizationContext) ?? + {} + ); } async authorizeRelation( @@ -53,10 +56,34 @@ export function createDefaultAuthorizer( context: any, authorizationContext: AuthorizationContext, ): Promise> { + if (this.customAuthorizer && typeof this.customAuthorizer.authorizeRelation === 'function') { + const filterFromCustomAuthorizer = await this.customAuthorizer.authorizeRelation( + relationName, + context, + authorizationContext, + ); + if (filterFromCustomAuthorizer) return filterFromCustomAuthorizer; + } + this.addRelationAuthorizerIfNotExist(relationName); return this.relationsAuthorizers.get(relationName)?.authorize(context, authorizationContext) ?? {}; } - private get relations(): Map> { + private addRelationAuthorizerIfNotExist(relationName: string) { + if (!this.relationsAuthorizers.has(relationName)) { + const relation = this.relations.get(relationName); + if (!relation) return; + if (relation.auth) { + this.relationsAuthorizers.set(relationName, createRelationAuthorizer(relation.auth)); + } else if (getAuthorizer(relation.DTO)) { + this.relationsAuthorizers.set( + relationName, + this.moduleRef.get(getAuthorizerToken(relation.DTO), { strict: false }), + ); + } + } + } + + private getRelations(): Map> { const { many = {}, one = {} } = getRelations(DTOClass); const relationsMap = new Map>(); Object.keys(many).forEach((relation) => relationsMap.set(relation, many[relation])); diff --git a/packages/query-graphql/src/auth/tokens.ts b/packages/query-graphql/src/auth/tokens.ts index 86a68e359..5b2a07fd1 100644 --- a/packages/query-graphql/src/auth/tokens.ts +++ b/packages/query-graphql/src/auth/tokens.ts @@ -1,3 +1,4 @@ import { Class } from '@nestjs-query/core'; export const getAuthorizerToken = (DTOClass: Class): string => `${DTOClass.name}Authorizer`; +export const getCustomAuthorizerToken = (DTOClass: Class): string => `${DTOClass.name}CustomAuthorizer`; diff --git a/packages/query-graphql/src/decorators/authorizer.decorator.ts b/packages/query-graphql/src/decorators/authorizer.decorator.ts index 83281765e..59fddad63 100644 --- a/packages/query-graphql/src/decorators/authorizer.decorator.ts +++ b/packages/query-graphql/src/decorators/authorizer.decorator.ts @@ -1,20 +1,22 @@ import { Class, MetaValue, ValueReflector } from '@nestjs-query/core'; -import { AuthorizerOptions, createDefaultAuthorizer, Authorizer } from '../auth'; -import { AUTHORIZER_KEY } from './constants'; +import { AuthorizerOptions, createDefaultAuthorizer, Authorizer, CustomAuthorizer } from '../auth'; +import { AUTHORIZER_KEY, CUSTOM_AUTHORIZER_KEY } from './constants'; const reflector = new ValueReflector(AUTHORIZER_KEY); +const customAuthorizerReflector = new ValueReflector(CUSTOM_AUTHORIZER_KEY); export function Authorize( - optsOrAuthorizerOrClass: Class> | Authorizer | AuthorizerOptions, + optsOrAuthorizerOrClass: Class> | CustomAuthorizer | AuthorizerOptions, ) { return (DTOClass: Class): void => { - if ('authorize' in optsOrAuthorizerOrClass) { - if ('authorizeRelation' in optsOrAuthorizerOrClass) { - return reflector.set(DTOClass, optsOrAuthorizerOrClass); - } - return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass, optsOrAuthorizerOrClass)); + if (!('authorize' in optsOrAuthorizerOrClass)) { + // If the user provided a class, provide the custom authorizer and create a default authorizer that injects the custom authorizer + customAuthorizerReflector.set(DTOClass, optsOrAuthorizerOrClass); + return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass)); } - return reflector.set(DTOClass, optsOrAuthorizerOrClass); + return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass, optsOrAuthorizerOrClass)); }; } export const getAuthorizer = (DTOClass: Class): MetaValue>> => reflector.get(DTOClass); +export const getCustomAuthorizer = (DTOClass: Class): MetaValue>> => + customAuthorizerReflector.get(DTOClass); diff --git a/packages/query-graphql/src/decorators/constants.ts b/packages/query-graphql/src/decorators/constants.ts index ac09240a6..ea1d715fb 100644 --- a/packages/query-graphql/src/decorators/constants.ts +++ b/packages/query-graphql/src/decorators/constants.ts @@ -4,6 +4,7 @@ export const RELATION_KEY = 'nestjs-query:relation'; export const REFERENCE_KEY = 'nestjs-query:reference'; export const AUTHORIZER_KEY = 'nestjs-query:authorizer'; +export const CUSTOM_AUTHORIZER_KEY = 'nestjs-query:custom-authorizer'; export const KEY_SET_KEY = 'nestjs-query:key-set'; diff --git a/packages/query-graphql/src/decorators/index.ts b/packages/query-graphql/src/decorators/index.ts index 455d737b6..c7dcab592 100644 --- a/packages/query-graphql/src/decorators/index.ts +++ b/packages/query-graphql/src/decorators/index.ts @@ -31,6 +31,7 @@ export * from './decorator.utils'; export * from './hook-args.decorator'; export * from './authorizer.decorator'; export * from './inject-authorizer.decorator'; +export * from './inject-custom-authorizer.decorator'; export * from './key-set.decorator'; export * from './authorize-filter.decorator'; export * from './query-options.decorator'; diff --git a/packages/query-graphql/src/decorators/inject-custom-authorizer.decorator.ts b/packages/query-graphql/src/decorators/inject-custom-authorizer.decorator.ts new file mode 100644 index 000000000..2015786a7 --- /dev/null +++ b/packages/query-graphql/src/decorators/inject-custom-authorizer.decorator.ts @@ -0,0 +1,6 @@ +import { Class } from '@nestjs-query/core'; +import { Inject } from '@nestjs/common'; +import { getCustomAuthorizerToken } from '../auth'; + +export const InjectCustomAuthorizer = (DTOClass: Class): ParameterDecorator => + Inject(getCustomAuthorizerToken(DTOClass)); diff --git a/packages/query-graphql/src/index.ts b/packages/query-graphql/src/index.ts index 0e27a0a2a..3d8745b14 100644 --- a/packages/query-graphql/src/index.ts +++ b/packages/query-graphql/src/index.ts @@ -26,6 +26,7 @@ export { BeforeQueryMany, BeforeFindOne, InjectAuthorizer, + InjectCustomAuthorizer, Authorize, AuthorizerFilter, RelationAuthorizerFilter, @@ -42,7 +43,7 @@ export { DTONamesOpts } from './common'; export { NestjsQueryGraphQLModule } from './module'; export { AutoResolverOpts } from './providers'; export { pubSubToken, GraphQLPubSub } from './subscription'; -export { Authorizer, AuthorizerOptions, AuthorizationContext, OperationGroup } from './auth'; +export { Authorizer, CustomAuthorizer, AuthorizerOptions, AuthorizationContext, OperationGroup } from './auth'; export { Hook, HookTypes, diff --git a/packages/query-graphql/src/providers/authorizer.provider.ts b/packages/query-graphql/src/providers/authorizer.provider.ts index 2780e5993..e6a6a91f9 100644 --- a/packages/query-graphql/src/providers/authorizer.provider.ts +++ b/packages/query-graphql/src/providers/authorizer.provider.ts @@ -1,7 +1,7 @@ import { Provider } from '@nestjs/common'; import { Class } from '@nestjs-query/core'; -import { createDefaultAuthorizer, getAuthorizerToken } from '../auth'; -import { getAuthorizer } from '../decorators'; +import { createDefaultAuthorizer, getAuthorizerToken, getCustomAuthorizerToken } from '../auth'; +import { getAuthorizer, getCustomAuthorizer } from '../decorators'; function createServiceProvider(DTOClass: Class): Provider { const token = getAuthorizerToken(DTOClass); @@ -13,5 +13,19 @@ function createServiceProvider(DTOClass: Class): Provider { return { provide: token, useClass: authorizer }; } +function createCustomAuthorizerProvider(DTOClass: Class): Provider | undefined { + const token = getCustomAuthorizerToken(DTOClass); + const customAuthorizer = getCustomAuthorizer(DTOClass); + if (customAuthorizer) { + return { provide: token, useClass: customAuthorizer }; + } + return undefined; +} + export const createAuthorizerProviders = (DTOClasses: Class[]): Provider[] => - DTOClasses.map((DTOClass) => createServiceProvider(DTOClass)); + DTOClasses.reduce((providers, DTOClass) => { + const p = createCustomAuthorizerProvider(DTOClass); + if (p) providers.push(p); + providers.push(createServiceProvider(DTOClass)); + return providers; + }, []);