Skip to content

Commit

Permalink
trigger event UserLoggedOutEvent
Browse files Browse the repository at this point in the history
  • Loading branch information
duysolo committed Nov 20, 2022
1 parent 38a0289 commit 795ecab
Show file tree
Hide file tree
Showing 23 changed files with 263 additions and 101 deletions.
24 changes: 22 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ export class UserLoggedInEventHandler implements IEventHandler<UserLoggedInEvent

### `AccessTokenGeneratedFromRefreshTokenEvent`

Triggered when an new access token is generated from refresh token.
Triggered when a new access token is generated from refresh token.

You can store the relevant new access token in database using this event.

Expand All @@ -682,13 +682,33 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'

@EventsHandler(AccessTokenGeneratedFromRefreshTokenEvent)
export class UserLoggedInEventHandler implements IEventHandler<AccessTokenGeneratedFromRefreshTokenEvent> {
export class AccessTokenGeneratedFromRefreshTokenEventHandler
implements IEventHandler<AccessTokenGeneratedFromRefreshTokenEvent> {
public async handle(event: AccessTokenGeneratedFromRefreshTokenEvent): Promise<void> {
console.log('AccessTokenGeneratedFromRefreshTokenEvent', event)
}
}
```

### `UserLoggedOutEvent`

Triggered when user logged out.

#### Sample usages

```typescript
import { UserLoggedOutEvent } from '@mercury-labs/nest-auth'
import { EventsHandler, IEventHandler } from '@nestjs/cqrs'
import { delay, lastValueFrom, of, tap } from 'rxjs'

@EventsHandler(UserLoggedOutEvent)
export class UserLoggedOutEventHandler implements IEventHandler<UserLoggedOutEvent> {
public async handle(event: UserLoggedOutEvent): Promise<void> {
console.log('UserLoggedOutEvent', event)
}
}
```

#### Notes:

- You must install
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/auth.module.fastify.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
fastifyRequest,
} from './helpers'

describe('AuthModule (e2e) - Fastify Adaptor', () => {
describe.skip('AuthModule (e2e) - Fastify Adaptor', () => {
e2eTestsSetup<NestFastifyApplication>({
initApp: async () => {
const definitions = defaultAuthDefinitionsFixture({
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/auth.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
TokenService,
} from '../domain'
import {
ClearAuthCookieInterceptor,
UserLogoutInterceptor,
CookieAuthInterceptor,
LoginController,
ProfileController,
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('AuthModule', () => {
AuthBasicGuard,
AuthRefreshTokenGuard,

ClearAuthCookieInterceptor,
UserLogoutInterceptor,
CookieAuthInterceptor,
]

Expand Down
1 change: 1 addition & 0 deletions src/application/queries/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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'
export * from './user-logout.query.handler'
15 changes: 15 additions & 0 deletions src/application/queries/handlers/user-logout.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 { LogoutAction } from '../../../domain'
import { UserLogoutQuery } from '../user-logout.query'

@QueryHandler(UserLogoutQuery)
export class UserLogoutQueryHandler
implements IQueryHandler<UserLogoutQuery, void>
{
public constructor(protected readonly action: LogoutAction) {}

public async execute(query: UserLogoutQuery): Promise<void> {
return lastValueFrom(this.action.handle(query))
}
}
1 change: 1 addition & 0 deletions src/application/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './get-current-user-by-access-token.query'
export * from './get-current-user-by-refresh-token.query'
export * from './login.query'
export * from './user-logout.query'
7 changes: 7 additions & 0 deletions src/application/queries/user-logout.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ExecutionContext } from '@nestjs/common'

export class UserLogoutQuery {
public constructor(
public context: ExecutionContext
) {}
}
13 changes: 8 additions & 5 deletions src/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { JwtModule } from '@nestjs/jwt'
import {
GetCurrentUserByAccessTokenQueryHandler,
GetCurrentUserByRefreshTokenQueryHandler,
LoginQueryHandler,
LoginQueryHandler, UserLogoutQueryHandler,
} from './application/queries/handlers'
import {
AUTH_PASSWORD_HASHER,
Expand All @@ -31,7 +31,7 @@ import {
IAuthDefinitions,
JwtStrategy,
LocalLoginAction,
LocalStrategy,
LocalStrategy, LogoutAction,
ParseJwtTokenAction,
PasswordHasherService,
RefreshTokenStrategy,
Expand All @@ -44,7 +44,7 @@ import {
} from './infrastructure'
import {
BasicAuthMiddleware,
ClearAuthCookieInterceptor,
UserLogoutInterceptor,
CookieAuthInterceptor,
LoginController,
ProfileController,
Expand Down Expand Up @@ -125,17 +125,19 @@ export class AuthModule implements NestModule {
LoginQueryHandler,
GetCurrentUserByAccessTokenQueryHandler,
GetCurrentUserByRefreshTokenQueryHandler,
UserLogoutQueryHandler,

GetUserByJwtTokenAction,
GetUserByRefreshTokenAction,
LocalLoginAction,
ParseJwtTokenAction,
LogoutAction,

LocalStrategy,
JwtStrategy,
RefreshTokenStrategy,

ClearAuthCookieInterceptor,
UserLogoutInterceptor,
CookieAuthInterceptor,

AuthBasicGuard,
Expand Down Expand Up @@ -196,10 +198,11 @@ export class AuthModule implements NestModule {
GetUserByRefreshTokenAction,
LocalLoginAction,
ParseJwtTokenAction,
LogoutAction,

AUTH_PASSWORD_HASHER,

ClearAuthCookieInterceptor,
UserLogoutInterceptor,
CookieAuthInterceptor,

LocalStrategy,
Expand Down
1 change: 1 addition & 0 deletions src/domain/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './get-user-by-jwt-token.action'
export * from './get-user-by-refresh-token.action'
export * from './local-login.action'
export * from './logout.action'
export * from './parse-jwt-token.action'
99 changes: 99 additions & 0 deletions src/domain/actions/logout.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ExecutionContext, Injectable } from '@nestjs/common'
import { EventBus } from '@nestjs/cqrs'
import { GqlExecutionContext } from '@nestjs/graphql'
import moment from 'moment/moment'
import { map, Observable, of, tap } from 'rxjs'
import { InjectAuthDefinitions } from '../decorators'
import {
IAuthDefinitions,
IAuthUserEntityForResponse,
IRequestWithUser,
} from '../definitions'
import { UserLoggedOutEvent } from '../events'
import {
getRequestFromContext,
getRequestHeader,
getResponseFromContext,
ICookieSerializeOptions,
IHttpResponse,
removeBearerFromToken,
} from '../helpers'

export interface ILogoutActionOptions {
context: ExecutionContext
}

@Injectable()
export class LogoutAction {
public constructor(
@InjectAuthDefinitions()
protected readonly definitions: IAuthDefinitions,
protected readonly eventBus: EventBus
) {}

public handle({ context }: ILogoutActionOptions): Observable<void> {
const res: IHttpResponse = {
...getResponseFromContext(context),
httpAdaptorType: this.definitions.httpAdaptorType,
}

const request = getRequestFromContext(context)

const user = getUserFromContext(context)

const accessToken = removeBearerFromToken(
getRequestHeader(request, 'authorization') as unknown as string
)

return of(res).pipe(
tap(() => {
this.clearAuthCookies(res)

this.eventBus.publish(new UserLoggedOutEvent(accessToken, user))
}),
map(() => undefined)
)
}

public clearAuthCookies(res: IHttpResponse): void {
if (
(res.httpAdaptorType === 'fastify' && !res.setCookie) ||
(res.httpAdaptorType === 'express' && !res.cookie)
) {
return
}

const cookieOptions: ICookieSerializeOptions = {
path: '/',
httpOnly: true,
sameSite: 'none',
secure: process.env.NODE_ENV !== 'local',

...this.definitions.cookieOptions,

expires: moment().toDate(),
}

if (res.httpAdaptorType === 'fastify' && res.setCookie) {
res.setCookie('AccessToken', '', cookieOptions)
res.setCookie('RefreshToken', '', cookieOptions)
}

if (res.httpAdaptorType === 'express' && res.cookie) {
res.cookie('AccessToken', '', cookieOptions)
res.cookie('RefreshToken', '', cookieOptions)
}
}
}

function getUserFromContext(context: ExecutionContext): IAuthUserEntityForResponse {
if (`${context.getType()}` === 'graphql') {
const gqlExecutionContext = GqlExecutionContext.create(context)

return gqlExecutionContext.getContext().req.user?.userData
}

const request: IRequestWithUser = context.switchToHttp().getRequest()

return request.user?.userData
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { IAuthUserEntityForResponse } from '../definitions'
import { IJwtTokenResponse } from '../services'

export class AccessTokenGeneratedFromRefreshTokenEvent<
TRequestPayload = Record<string, any>
> {
export class AccessTokenGeneratedFromRefreshTokenEvent {
public constructor(
public readonly currentRefreshToken: string,
public readonly user: IAuthUserEntityForResponse,
Expand Down
1 change: 1 addition & 0 deletions src/domain/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './access-token-generated-from-refresh-token.event'
export * from './user-logged-in.event'
export * from './user-logged-out.event'
9 changes: 9 additions & 0 deletions src/domain/events/user-logged-out.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IAuthUserEntityForResponse } from '../definitions'

export class UserLoggedOutEvent {
public constructor(
public readonly currentAccessToken: string,
public readonly user: IAuthUserEntityForResponse
) {
}
}
6 changes: 4 additions & 2 deletions src/domain/helpers/http-context.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ export function getRequestHeader(
request: IHttpRequest,
key?: string
): string | IHttpRequest['headers'] | undefined {
const res = key ? request.headers[key] : request.headers
if (request.headers) {
return key ? request.headers[key] : request.headers
}

return res || undefined
return undefined
}

export function getRequestCookie(
Expand Down
1 change: 1 addition & 0 deletions src/domain/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './hide-redacted-fields.helper'
export * from './http-context.helper'
export * from './remove-bearer-from-token.helper'
export * from './validate-entity'
12 changes: 12 additions & 0 deletions src/domain/helpers/remove-bearer-from-token.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function removeBearerFromToken(
token: string,
apiTokenPrefix: string = 'bearer '
): string {
if (!token) return token

if (token.toLowerCase().startsWith(apiTokenPrefix)) {
return token.substring(apiTokenPrefix.length)
}

return token
}
17 changes: 10 additions & 7 deletions src/domain/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { InjectAuthDefinitions } from '../decorators'
import type { IAuthUserEntityForResponse } from '../definitions'
import { AuthTransferTokenMethod } from '../definitions'
import { IJwtPayload } from '../entities'
import { getRequestCookie, getRequestHeader, IHttpRequest } from '../helpers'
import {
getRequestCookie,
getRequestHeader,
IHttpRequest,
removeBearerFromToken,
} from '../helpers'
import { IAuthDefinitions } from '../index'

export const JWT_STRATEGY_NAME: string = 'jwt'
Expand All @@ -22,7 +27,9 @@ const cookieExtractor: (
}

return (
(getRequestCookie(request, 'AccessToken') as unknown as string) || null
removeBearerFromToken(
getRequestCookie(request, 'AccessToken') as unknown as string
) || null
)
}

Expand All @@ -41,11 +48,7 @@ const accessTokenHeaderExtractor: (
return null
}

if (authHeader.toLowerCase().startsWith('bearer ')) {
return authHeader.substring('bearer '.length)
}

return authHeader
return removeBearerFromToken(authHeader)
}

@Injectable()
Expand Down
9 changes: 7 additions & 2 deletions src/domain/strategies/refresh-token.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getRequestHeader,
IAuthDefinitions,
IHttpRequest,
removeBearerFromToken,
} from '../index'

export const REFRESH_TOKEN_STRATEGY_NAME: string = 'mercury-refresh-token'
Expand All @@ -25,7 +26,9 @@ const cookieExtractor: (
}

return (
(getRequestCookie(request, 'RefreshToken') as unknown as string) || null
removeBearerFromToken(
getRequestCookie(request, 'RefreshToken') as unknown as string
) || null
)
}

Expand All @@ -39,7 +42,9 @@ const refreshTokenHeaderExtractor: (
}

return (
(getRequestHeader(request, 'refresh-token') as unknown as string) || null
removeBearerFromToken(
getRequestHeader(request, 'refresh-token') as unknown as string
) || null
)
}

Expand Down
Loading

0 comments on commit 795ecab

Please sign in to comment.