From 64788600b2d28cd9a0eed6e21b15aeef11468828 Mon Sep 17 00:00:00 2001 From: shancheas Date: Thu, 9 Feb 2023 17:29:32 +0700 Subject: [PATCH] feat: implement session to project --- src/core/sessions/constants.ts | 6 ++++ .../sessions/domain/entities/jwt.interface.ts | 4 +++ .../entities/user-sessions.interface.ts | 6 ++++ .../interceptors/refresh-token.interceptor.ts | 31 +++++++++++++++++++ src/core/sessions/domain/providers/user.ts | 26 ++++++++++++++++ .../domain/services/session.service.ts | 23 ++++++++++++++ src/core/sessions/domain/utils/jwt.helpers.ts | 22 +++++++++++++ src/core/sessions/index.ts | 11 +++++++ src/core/sessions/session.module.ts | 18 +++++++++++ 9 files changed, 147 insertions(+) create mode 100644 src/core/sessions/constants.ts create mode 100644 src/core/sessions/domain/entities/jwt.interface.ts create mode 100644 src/core/sessions/domain/entities/user-sessions.interface.ts create mode 100644 src/core/sessions/domain/interceptors/refresh-token.interceptor.ts create mode 100644 src/core/sessions/domain/providers/user.ts create mode 100644 src/core/sessions/domain/services/session.service.ts create mode 100644 src/core/sessions/domain/utils/jwt.helpers.ts create mode 100644 src/core/sessions/index.ts create mode 100644 src/core/sessions/session.module.ts diff --git a/src/core/sessions/constants.ts b/src/core/sessions/constants.ts new file mode 100644 index 0000000..d7aaa32 --- /dev/null +++ b/src/core/sessions/constants.ts @@ -0,0 +1,6 @@ +export const USER_SESSIONS = 'USER_SESSIONS'; +export const ACCESS_CONTROL = 'ACCESS_CONTROL'; + +export const JWT_SECRET = + process.env.JWT_SECRET ?? 'B9A8Y92wZwbGBHOcUaHykeQ6mNNKeTFt'; +export const JWT_EXPIRED = process.env.JWT_EXPIRED ?? '12h'; diff --git a/src/core/sessions/domain/entities/jwt.interface.ts b/src/core/sessions/domain/entities/jwt.interface.ts new file mode 100644 index 0000000..dd10a51 --- /dev/null +++ b/src/core/sessions/domain/entities/jwt.interface.ts @@ -0,0 +1,4 @@ +export type JWTToken = { + exp: number; + iat: number; +}; diff --git a/src/core/sessions/domain/entities/user-sessions.interface.ts b/src/core/sessions/domain/entities/user-sessions.interface.ts new file mode 100644 index 0000000..4f818e9 --- /dev/null +++ b/src/core/sessions/domain/entities/user-sessions.interface.ts @@ -0,0 +1,6 @@ +export interface UsersSession { + id: number; + username: string; + name: string; + roles: string[]; +} diff --git a/src/core/sessions/domain/interceptors/refresh-token.interceptor.ts b/src/core/sessions/domain/interceptors/refresh-token.interceptor.ts new file mode 100644 index 0000000..8ca8d3c --- /dev/null +++ b/src/core/sessions/domain/interceptors/refresh-token.interceptor.ts @@ -0,0 +1,31 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Scope, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { SessionService } from '../..'; +import { Response, Request } from 'express'; + +@Injectable({ scope: Scope.REQUEST }) +export class RefreshTokenInterceptor implements NestInterceptor { + constructor(protected readonly session: SessionService) {} + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const authorization = request.headers['authorization']; + + if (authorization) { + const [, token] = authorization.split(' '); + const refreshToken = this.session.refreshToken(token); + if (refreshToken) { + response.setHeader('ex-refresh-token', refreshToken); + response.setHeader('Access-Control-Expose-Headers', 'ex-refresh-token'); + } + } + + return next.handle(); + } +} diff --git a/src/core/sessions/domain/providers/user.ts b/src/core/sessions/domain/providers/user.ts new file mode 100644 index 0000000..0c1b980 --- /dev/null +++ b/src/core/sessions/domain/providers/user.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable, Request, Scope } from '@nestjs/common'; +import { UsersSession } from '../entities/user-sessions.interface'; +import { REQUEST } from '@nestjs/core'; +import { SessionService } from '../services/session.service'; + +@Injectable({ scope: Scope.REQUEST }) +export class UserProvider { + constructor( + @Inject(REQUEST) private readonly request: Request, + private readonly session: SessionService, + ) {} + get user(): UsersSession { + /** + * There is no Token validation here + * Because, the token should be available and active here + * + * If this function throw an error + * rather you trying to call user from function that use `@Unprotected` decorator + * or you forget to set scope to `Scope.REQUEST` from app.module. + * + * Please check the token validation at JWTGuard (core/domain/jwt.guard.ts) + */ + const [, token] = this.request.headers['authorization'].split(' '); + return this.session.verifyToken(token); + } +} diff --git a/src/core/sessions/domain/services/session.service.ts b/src/core/sessions/domain/services/session.service.ts new file mode 100644 index 0000000..59c2aa9 --- /dev/null +++ b/src/core/sessions/domain/services/session.service.ts @@ -0,0 +1,23 @@ +import { Injectable, Scope } from '@nestjs/common'; +import { UsersSession } from '../entities/user-sessions.interface'; +import { JwtService } from '@nestjs/jwt'; +import { JWTToken } from '../..'; +import { isTokenNearExpired } from '../utils/jwt.helpers'; + +@Injectable({ scope: Scope.REQUEST }) +export class SessionService { + constructor(private readonly jwt: JwtService) {} + createAccessToken(session: UsersSession): string { + return this.jwt.sign(session); + } + + verifyToken(token: string): UsersSession & JWTToken { + return this.jwt.verify(token); + } + + refreshToken(token: string): string | undefined { + const { exp, iat, ...user } = this.verifyToken(token); + const isNearExp = isTokenNearExpired(exp, iat); + return isNearExp ? this.createAccessToken(user) : null; + } +} diff --git a/src/core/sessions/domain/utils/jwt.helpers.ts b/src/core/sessions/domain/utils/jwt.helpers.ts new file mode 100644 index 0000000..9f1ce45 --- /dev/null +++ b/src/core/sessions/domain/utils/jwt.helpers.ts @@ -0,0 +1,22 @@ +/** + * + * @param exp JWT Token expire time + * @param iat JWT Token create time + * @param expTimeTolerance this constant tell when the token near expire or not + * + * this function will return true when the rest time (exp - now) + * is less than expire duration (exp - iat) * expTimeTolerance + * otherwise will return false + */ +export function isTokenNearExpired( + exp: number, + iat: number, + expTimeTolerance = 0.2, +): boolean { + const now = new Date().getTime() / 1000; + const expDuration = exp - iat; + const toleranceDuration = expDuration * expTimeTolerance; + const restDuration = exp - now; + + return restDuration < toleranceDuration; +} diff --git a/src/core/sessions/index.ts b/src/core/sessions/index.ts new file mode 100644 index 0000000..e0fde92 --- /dev/null +++ b/src/core/sessions/index.ts @@ -0,0 +1,11 @@ +export * from './constants'; +export * from './domain/providers/user'; + +export * from './domain/entities/user-sessions.interface'; +export * from './domain/entities/jwt.interface'; + +export * from './domain/services/session.service'; + +export * from './domain/interceptors/refresh-token.interceptor'; + +export * from './session.module'; diff --git a/src/core/sessions/session.module.ts b/src/core/sessions/session.module.ts new file mode 100644 index 0000000..ed947c9 --- /dev/null +++ b/src/core/sessions/session.module.ts @@ -0,0 +1,18 @@ +import { Global, Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { JWT_EXPIRED, JWT_SECRET } from '../../auth/constants'; +import { UserProvider } from './domain/providers/user'; +import { SessionService } from './domain/services/session.service'; + +@Global() +@Module({ + imports: [ + JwtModule.register({ + secret: JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRED }, + }), + ], + providers: [SessionService, UserProvider], + exports: [SessionService, UserProvider], +}) +export class SessionModule {}