From 4b446d629c2c07ca9041552d047f70be2c4042cf Mon Sep 17 00:00:00 2001 From: duysolo Date: Wed, 16 Nov 2022 22:56:13 +0700 Subject: [PATCH] applied CQRS & DDD --- .../get-current-user-by-access-token.query.ts | 5 +++ ...get-current-user-by-refresh-token.query.ts | 3 ++ ...rent-user-by-access-token.query.handler.ts | 27 ++++++++++++ ...ent-user-by-refresh-token.query.handler.ts | 41 +++++++++++++++++++ src/application/queries/handlers/index.ts | 3 ++ .../queries/handlers/login.query.handler.ts | 15 +++++++ src/application/queries/index.ts | 3 ++ src/application/queries/login.query.ts | 5 +++ src/auth.module.ts | 14 +++++++ .../guards/__tests__/auth.jwt.guard.spec.ts | 11 +++-- .../guards/__tests__/auth.local.guard.spec.ts | 17 +++++--- .../auth.refresh-token.guard.spec.ts | 2 + src/domain/strategies/jwt.strategy.ts | 15 +++---- src/domain/strategies/local.strategy.ts | 12 +++--- .../strategies/refresh-token.strategy.ts | 34 ++++----------- 15 files changed, 158 insertions(+), 49 deletions(-) create mode 100644 src/application/queries/get-current-user-by-access-token.query.ts create mode 100644 src/application/queries/get-current-user-by-refresh-token.query.ts create mode 100644 src/application/queries/handlers/get-current-user-by-access-token.query.handler.ts create mode 100644 src/application/queries/handlers/get-current-user-by-refresh-token.query.handler.ts create mode 100644 src/application/queries/handlers/index.ts create mode 100644 src/application/queries/handlers/login.query.handler.ts create mode 100644 src/application/queries/index.ts create mode 100644 src/application/queries/login.query.ts diff --git a/src/application/queries/get-current-user-by-access-token.query.ts b/src/application/queries/get-current-user-by-access-token.query.ts new file mode 100644 index 0000000..809b8ee --- /dev/null +++ b/src/application/queries/get-current-user-by-access-token.query.ts @@ -0,0 +1,5 @@ +import { IJwtPayload } from '../../domain' + +export class GetCurrentUserByAccessTokenQuery { + public constructor(public input: IJwtPayload) {} +} diff --git a/src/application/queries/get-current-user-by-refresh-token.query.ts b/src/application/queries/get-current-user-by-refresh-token.query.ts new file mode 100644 index 0000000..90ad1fb --- /dev/null +++ b/src/application/queries/get-current-user-by-refresh-token.query.ts @@ -0,0 +1,3 @@ +export class GetCurrentUserByRefreshTokenQuery { + public constructor(public token: string) {} +} diff --git a/src/application/queries/handlers/get-current-user-by-access-token.query.handler.ts b/src/application/queries/handlers/get-current-user-by-access-token.query.handler.ts new file mode 100644 index 0000000..84a4832 --- /dev/null +++ b/src/application/queries/handlers/get-current-user-by-access-token.query.handler.ts @@ -0,0 +1,27 @@ +import { UnauthorizedException } from '@nestjs/common' +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' +import { lastValueFrom } from 'rxjs' +import { + GetUserByJwtTokenAction, + IAuthUserEntityForResponse, +} from '../../../domain' +import { GetCurrentUserByAccessTokenQuery } from '../get-current-user-by-access-token.query' + +@QueryHandler(GetCurrentUserByAccessTokenQuery) +export class GetCurrentUserByAccessTokenQueryHandler + implements + IQueryHandler< + GetCurrentUserByAccessTokenQuery, + IAuthUserEntityForResponse | undefined + > +{ + public constructor(protected readonly action: GetUserByJwtTokenAction) {} + + public async execute(query: GetCurrentUserByAccessTokenQuery) { + try { + return await lastValueFrom(this.action.handle(query.input)) + } catch (error) { + throw new UnauthorizedException() + } + } +} diff --git a/src/application/queries/handlers/get-current-user-by-refresh-token.query.handler.ts b/src/application/queries/handlers/get-current-user-by-refresh-token.query.handler.ts new file mode 100644 index 0000000..6f3d4bc --- /dev/null +++ b/src/application/queries/handlers/get-current-user-by-refresh-token.query.handler.ts @@ -0,0 +1,41 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' +import { lastValueFrom, map } from 'rxjs' +import { + AuthRepository, + hideRedactedFields, + IAuthDefinitions, + IAuthUserEntityForResponse, + InjectAuthDefinitions, + TokenService, +} from '../../../domain' +import { GetCurrentUserByRefreshTokenQuery } from '../get-current-user-by-refresh-token.query' + +@QueryHandler(GetCurrentUserByRefreshTokenQuery) +export class GetCurrentUserByRefreshTokenQueryHandler + implements + IQueryHandler< + GetCurrentUserByRefreshTokenQuery, + IAuthUserEntityForResponse | undefined + > +{ + public constructor( + @InjectAuthDefinitions() + protected readonly authDefinitions: IAuthDefinitions, + protected readonly authRepository: AuthRepository, + protected readonly jwtService: TokenService + ) {} + + public async execute(query: GetCurrentUserByRefreshTokenQuery) { + const jwtPayload = query.token + ? this.jwtService.decodeRefreshToken(query.token) + : undefined + + return jwtPayload + ? await lastValueFrom( + this.authRepository + .getAuthUserByUsername(jwtPayload.username) + .pipe(map(hideRedactedFields(this.authDefinitions.redactedFields))) + ) + : undefined + } +} diff --git a/src/application/queries/handlers/index.ts b/src/application/queries/handlers/index.ts new file mode 100644 index 0000000..0fd62fc --- /dev/null +++ b/src/application/queries/handlers/index.ts @@ -0,0 +1,3 @@ +export * from './get-current-user-by-access-token.query.handler' +export * from './get-current-user-by-refresh-token.query.handler' +export * from './login.query.handler' diff --git a/src/application/queries/handlers/login.query.handler.ts b/src/application/queries/handlers/login.query.handler.ts new file mode 100644 index 0000000..8121e1b --- /dev/null +++ b/src/application/queries/handlers/login.query.handler.ts @@ -0,0 +1,15 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' +import { lastValueFrom } from 'rxjs' +import { IAuthUserEntityForResponse, LocalLoginAction } from '../../../domain' +import { LoginQuery } from '../login.query' + +@QueryHandler(LoginQuery) +export class LoginQueryHandler + implements IQueryHandler { + public constructor(protected readonly action: LocalLoginAction) { + } + + public async execute(query: LoginQuery) { + return lastValueFrom(this.action.handle(query.input)) + } +} diff --git a/src/application/queries/index.ts b/src/application/queries/index.ts new file mode 100644 index 0000000..f66bc62 --- /dev/null +++ b/src/application/queries/index.ts @@ -0,0 +1,3 @@ +export * from './get-current-user-by-access-token.query' +export * from './get-current-user-by-refresh-token.query' +export * from './login.query' diff --git a/src/application/queries/login.query.ts b/src/application/queries/login.query.ts new file mode 100644 index 0000000..494fe00 --- /dev/null +++ b/src/application/queries/login.query.ts @@ -0,0 +1,5 @@ +import { AuthDto } from '../../domain' + +export class LoginQuery { + public constructor(public input: AuthDto) {} +} diff --git a/src/auth.module.ts b/src/auth.module.ts index decad7c..603ad7d 100644 --- a/src/auth.module.ts +++ b/src/auth.module.ts @@ -12,6 +12,11 @@ import { import { APP_GUARD, Reflector } from '@nestjs/core' import { CqrsModule } from '@nestjs/cqrs' import { JwtModule } from '@nestjs/jwt' +import { + GetCurrentUserByAccessTokenQueryHandler, + GetCurrentUserByRefreshTokenQueryHandler, + LoginQueryHandler, +} from './application/queries/handlers' import { AUTH_PASSWORD_HASHER, AuthBasicGuard, @@ -117,6 +122,10 @@ export class AuthModule implements NestModule { TokenService, + LoginQueryHandler, + GetCurrentUserByAccessTokenQueryHandler, + GetCurrentUserByRefreshTokenQueryHandler, + GetUserByJwtTokenAction, LocalLoginAction, ParseJwtTokenAction, @@ -190,10 +199,15 @@ export class AuthModule implements NestModule { ClearAuthCookieInterceptor, CookieAuthInterceptor, + LocalStrategy, JwtStrategy, RefreshTokenStrategy, + LoginQueryHandler, + GetCurrentUserByAccessTokenQueryHandler, + GetCurrentUserByRefreshTokenQueryHandler, + HashingModule, ], } diff --git a/src/domain/guards/__tests__/auth.jwt.guard.spec.ts b/src/domain/guards/__tests__/auth.jwt.guard.spec.ts index 85112a3..699530c 100644 --- a/src/domain/guards/__tests__/auth.jwt.guard.spec.ts +++ b/src/domain/guards/__tests__/auth.jwt.guard.spec.ts @@ -1,4 +1,5 @@ import { HttpStatus, UnauthorizedException } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing/testing-module' import { lastValueFrom, map } from 'rxjs' import { createTestingModule, @@ -12,13 +13,17 @@ import { generateExecutionContextForJwtStrategy } from './helpers/test-guard.hel describe('AuthJwtGuard', () => { let correctAccessToken: string + let app: TestingModule + beforeAll(async () => { const userInfo: IAuthUserEntityForResponse = { id: '123456', username: 'sample-user@gmail.com', } - const app = await createTestingModule(defaultAuthDefinitionsFixture()) + app = await createTestingModule(defaultAuthDefinitionsFixture()) + + await app.init() const service = app.get(TokenService) @@ -30,7 +35,7 @@ describe('AuthJwtGuard', () => { }) it('should allow user to continue when accessToken is correct', async function () { - const guard = new AuthJwtGuard() + const guard = app.get(AuthJwtGuard) expect( await guard.canActivate( @@ -40,7 +45,7 @@ describe('AuthJwtGuard', () => { }) it('should not allow user to continue when accessToken is invalid', async function () { - const guard = new AuthJwtGuard() + const guard = app.get(AuthJwtGuard) try { await guard.canActivate( diff --git a/src/domain/guards/__tests__/auth.local.guard.spec.ts b/src/domain/guards/__tests__/auth.local.guard.spec.ts index 98af073..a544c89 100644 --- a/src/domain/guards/__tests__/auth.local.guard.spec.ts +++ b/src/domain/guards/__tests__/auth.local.guard.spec.ts @@ -1,7 +1,8 @@ import { HttpStatus, UnauthorizedException } from '@nestjs/common' +import { TestingModule } from '@nestjs/testing/testing-module' import { createTestingModule, - defaultAuthDefinitionsFixture + defaultAuthDefinitionsFixture, } from '../../../__tests__/helpers' import { AuthLocalGuard } from '../auth.local.guard' import { generateExecutionContextForLocalAuth } from './helpers/test-guard.helper' @@ -14,12 +15,16 @@ describe('AuthLocalGuard', () => { password: 'testLogin@12345', } + let app: TestingModule + beforeAll(async () => { - await createTestingModule(defaultAuthDefinitionsFixture()) + app = await createTestingModule(defaultAuthDefinitionsFixture()) + + await app.init() }) - it('should allow user to login when credentials is correct', async function() { - const guard = new AuthLocalGuard() + it('should allow user to login when credentials is correct', async function () { + const guard = app.get(AuthLocalGuard) expect( await guard.canActivate( @@ -33,8 +38,8 @@ describe('AuthLocalGuard', () => { ).toBeTruthy() }) - it('should not allow user to continue when accessToken is invalid', async function() { - const guard = new AuthLocalGuard() + it('should not allow user to continue when accessToken is invalid', async function () { + const guard = app.get(AuthLocalGuard) try { await guard.canActivate( diff --git a/src/domain/guards/__tests__/auth.refresh-token.guard.spec.ts b/src/domain/guards/__tests__/auth.refresh-token.guard.spec.ts index 67e14e8..db1728d 100644 --- a/src/domain/guards/__tests__/auth.refresh-token.guard.spec.ts +++ b/src/domain/guards/__tests__/auth.refresh-token.guard.spec.ts @@ -20,6 +20,8 @@ describe('AuthRefreshTokenGuard', () => { const app = await createTestingModule(defaultAuthDefinitionsFixture()) + await app.init() + const service = app.get(TokenService) correctRefreshToken = await lastValueFrom( diff --git a/src/domain/strategies/jwt.strategy.ts b/src/domain/strategies/jwt.strategy.ts index a52ea3b..9b68d4c 100644 --- a/src/domain/strategies/jwt.strategy.ts +++ b/src/domain/strategies/jwt.strategy.ts @@ -1,13 +1,14 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common' +import { Injectable } from '@nestjs/common' +import { QueryBus } from '@nestjs/cqrs' import { PassportStrategy } from '@nestjs/passport' import { ExtractJwt, JwtFromRequestFunction, Strategy } from 'passport-jwt' -import { lastValueFrom } from 'rxjs' +import { GetCurrentUserByAccessTokenQuery } from '../../application/queries' import { InjectAuthDefinitions } from '../decorators' import type { IAuthUserEntityForResponse } from '../definitions' import { AuthTransferTokenMethod } from '../definitions' import { IJwtPayload } from '../entities' import { getRequestCookie, getRequestHeader, IHttpRequest } from '../helpers' -import { GetUserByJwtTokenAction, IAuthDefinitions } from '../index' +import { IAuthDefinitions } from '../index' export const JWT_STRATEGY_NAME: string = 'jwt' @@ -52,7 +53,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) { public constructor( @InjectAuthDefinitions() protected readonly authDefinitions: IAuthDefinitions, - protected readonly getUserByJwtTokenAction: GetUserByJwtTokenAction + protected readonly queryBus: QueryBus ) { super({ jwtFromRequest: ExtractJwt.fromExtractors([ @@ -67,10 +68,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) { public async validate( payload: IJwtPayload ): Promise { - try { - return lastValueFrom(this.getUserByJwtTokenAction.handle(payload)) - } catch (error) { - throw new UnauthorizedException() - } + return this.queryBus.execute(new GetCurrentUserByAccessTokenQuery(payload)) } } diff --git a/src/domain/strategies/local.strategy.ts b/src/domain/strategies/local.strategy.ts index 7ea5ff2..aa6b118 100644 --- a/src/domain/strategies/local.strategy.ts +++ b/src/domain/strategies/local.strategy.ts @@ -1,20 +1,20 @@ import { Injectable } from '@nestjs/common' +import { QueryBus } from '@nestjs/cqrs' import { PassportStrategy } from '@nestjs/passport' import { Strategy } from 'passport-local' -import { lastValueFrom } from 'rxjs' +import { LoginQuery } from '../../application/queries' import type { IAuthUserEntityForResponse } from '../definitions' import { InjectAuthDefinitions } from '../decorators' import { IAuthDefinitions } from '../index' -import { LocalLoginAction } from '../actions' export const LOCAL_STRATEGY_NAME: string = 'local' @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { public constructor( - protected readonly loginAction: LocalLoginAction, @InjectAuthDefinitions() - protected readonly authDefinitions: IAuthDefinitions + protected readonly authDefinitions: IAuthDefinitions, + protected readonly queryBus: QueryBus ) { super({ usernameField: authDefinitions.usernameField || 'username', @@ -28,8 +28,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) { username: string, password: string ): Promise { - return lastValueFrom( - this.loginAction.handle({ + return this.queryBus.execute( + new LoginQuery({ ...request.body, username, password, diff --git a/src/domain/strategies/refresh-token.strategy.ts b/src/domain/strategies/refresh-token.strategy.ts index 4c9735e..526ef75 100644 --- a/src/domain/strategies/refresh-token.strategy.ts +++ b/src/domain/strategies/refresh-token.strategy.ts @@ -1,22 +1,17 @@ import { HttpStatus, Injectable } from '@nestjs/common' +import { QueryBus } from '@nestjs/cqrs' import { PassportStrategy } from '@nestjs/passport' import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt' import { Strategy } from 'passport-strategy' -import { lastValueFrom, map } from 'rxjs' +import { GetCurrentUserByRefreshTokenQuery } from '../../application/queries' import { AuthTransferTokenMethod, getRequestCookie, getRequestHeader, - hideRedactedFields, - IAuthUserEntityForResponse, + IAuthDefinitions, IHttpRequest, - IJwtPayload, } from '../index' - -import { IAuthDefinitions } from '../index' import { InjectAuthDefinitions } from '../decorators' -import { AuthRepository } from '../repositories' -import { TokenService } from '../services' export const REFRESH_TOKEN_STRATEGY_NAME: string = 'mercury-refresh-token' @@ -58,8 +53,7 @@ export class RefreshTokenStrategy extends PassportStrategy( public constructor( @InjectAuthDefinitions() protected readonly authDefinitions: IAuthDefinitions, - protected readonly authRepository: AuthRepository, - protected readonly jwtService: TokenService + protected readonly queryBus: QueryBus ) { super() @@ -72,26 +66,16 @@ export class RefreshTokenStrategy extends PassportStrategy( public async authenticate(req: any): Promise { const token: string | any = this.jwtFromRequest(req) - const jwtPayload = token - ? this.jwtService.decodeRefreshToken(token) + const user = token + ? await this.queryBus.execute( + new GetCurrentUserByRefreshTokenQuery(token) + ) : undefined - const user = jwtPayload ? await this.validate(jwtPayload) : undefined - - if (!jwtPayload || !user) { + if (!user) { this.fail(HttpStatus.UNAUTHORIZED) } else { this.success(user) } } - - protected async validate( - payload: IJwtPayload - ): Promise { - return lastValueFrom( - this.authRepository - .getAuthUserByUsername(payload.username) - .pipe(map(hideRedactedFields(this.authDefinitions.redactedFields))) - ) - } }