Skip to content

Commit

Permalink
applied CQRS & DDD
Browse files Browse the repository at this point in the history
  • Loading branch information
duysolo committed Nov 16, 2022
1 parent be6ffd4 commit 4b446d6
Show file tree
Hide file tree
Showing 15 changed files with 158 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IJwtPayload } from '../../domain'

export class GetCurrentUserByAccessTokenQuery {
public constructor(public input: IJwtPayload) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class GetCurrentUserByRefreshTokenQuery {
public constructor(public token: string) {}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions src/application/queries/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -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'
15 changes: 15 additions & 0 deletions src/application/queries/handlers/login.query.handler.ts
Original file line number Diff line number Diff line change
@@ -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<LoginQuery, IAuthUserEntityForResponse> {
public constructor(protected readonly action: LocalLoginAction) {
}

public async execute(query: LoginQuery) {
return lastValueFrom(this.action.handle(query.input))
}
}
3 changes: 3 additions & 0 deletions src/application/queries/index.ts
Original file line number Diff line number Diff line change
@@ -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'
5 changes: 5 additions & 0 deletions src/application/queries/login.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AuthDto } from '../../domain'

export class LoginQuery {
public constructor(public input: AuthDto) {}
}
14 changes: 14 additions & 0 deletions src/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -117,6 +122,10 @@ export class AuthModule implements NestModule {

TokenService,

LoginQueryHandler,
GetCurrentUserByAccessTokenQueryHandler,
GetCurrentUserByRefreshTokenQueryHandler,

GetUserByJwtTokenAction,
LocalLoginAction,
ParseJwtTokenAction,
Expand Down Expand Up @@ -190,10 +199,15 @@ export class AuthModule implements NestModule {

ClearAuthCookieInterceptor,
CookieAuthInterceptor,

LocalStrategy,
JwtStrategy,
RefreshTokenStrategy,

LoginQueryHandler,
GetCurrentUserByAccessTokenQueryHandler,
GetCurrentUserByRefreshTokenQueryHandler,

HashingModule,
],
}
Expand Down
11 changes: 8 additions & 3 deletions src/domain/guards/__tests__/auth.jwt.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpStatus, UnauthorizedException } from '@nestjs/common'
import { TestingModule } from '@nestjs/testing/testing-module'
import { lastValueFrom, map } from 'rxjs'
import {
createTestingModule,
Expand All @@ -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: '[email protected]',
}

const app = await createTestingModule(defaultAuthDefinitionsFixture())
app = await createTestingModule(defaultAuthDefinitionsFixture())

await app.init()

const service = app.get(TokenService)

Expand All @@ -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(
Expand All @@ -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(
Expand Down
17 changes: 11 additions & 6 deletions src/domain/guards/__tests__/auth.local.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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(
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/guards/__tests__/auth.refresh-token.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe('AuthRefreshTokenGuard', () => {

const app = await createTestingModule(defaultAuthDefinitionsFixture())

await app.init()

const service = app.get(TokenService)

correctRefreshToken = await lastValueFrom(
Expand Down
15 changes: 6 additions & 9 deletions src/domain/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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([
Expand All @@ -67,10 +68,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
public async validate(
payload: IJwtPayload
): Promise<IAuthUserEntityForResponse | undefined> {
try {
return lastValueFrom(this.getUserByJwtTokenAction.handle(payload))
} catch (error) {
throw new UnauthorizedException()
}
return this.queryBus.execute(new GetCurrentUserByAccessTokenQuery(payload))
}
}
12 changes: 6 additions & 6 deletions src/domain/strategies/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -28,8 +28,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
username: string,
password: string
): Promise<IAuthUserEntityForResponse> {
return lastValueFrom(
this.loginAction.handle({
return this.queryBus.execute(
new LoginQuery({
...request.body,
username,
password,
Expand Down
34 changes: 9 additions & 25 deletions src/domain/strategies/refresh-token.strategy.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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()

Expand All @@ -72,26 +66,16 @@ export class RefreshTokenStrategy extends PassportStrategy(
public async authenticate(req: any): Promise<void> {
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<IAuthUserEntityForResponse> {
return lastValueFrom(
this.authRepository
.getAuthUserByUsername(payload.username)
.pipe(map(hideRedactedFields(this.authDefinitions.redactedFields)))
)
}
}

0 comments on commit 4b446d6

Please sign in to comment.