diff --git a/src/common/constants/index.ts b/src/common/constants/index.ts index 8fb1921..4d775ff 100755 --- a/src/common/constants/index.ts +++ b/src/common/constants/index.ts @@ -1,4 +1,6 @@ // export const AUTH_THROTTLE = "AUTH_THROTTLE"; export const AUTH_THROTTLE_TTL = 5 * 60 * 1000; export const AUTH_THROTTLE_LIMIT = 5; +export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000; +export const AUTH__REFRESH_THROTTLE_LIMIT = 10; export const JWT_STRATEGY_NAME = "jwt_Strategy"; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 0490bcf..ee62845 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { RefreshTokenDto } from "./DTO/refresh-token.dto"; import { RequestOtpDto } from "./DTO/request-otp.dto"; import { VerifyOtpDto } from "./DTO/verify-otp.dto"; import { AuthService } from "./providers/auth.service"; -import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL } from "../../common/constants"; +import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../../common/constants"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; @@ -84,6 +84,7 @@ export class AuthController { return this.authService.changePassword(userId, changePasswordDto); } + @Throttle({ default: { limit: AUTH__REFRESH_THROTTLE_LIMIT, ttl: AUTH__REFRESH_THROTTLE_TTL } }) @ApiOperation({ summary: "refresh the user access token / refresh token" }) @HttpCode(HttpStatus.OK) @Post("refresh") diff --git a/src/modules/auth/providers/tokens.service.ts b/src/modules/auth/providers/tokens.service.ts index a82b79b..c158ce8 100755 --- a/src/modules/auth/providers/tokens.service.ts +++ b/src/modules/auth/providers/tokens.service.ts @@ -1,7 +1,8 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { Injectable, Logger, UnauthorizedException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import dayjs from "dayjs"; +import { DataSource, QueryRunner } from "typeorm"; import { AuthMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; @@ -10,77 +11,117 @@ import { ITokenPayload } from "../interfaces/IToken-payload"; @Injectable() export class TokensService { + private readonly logger = new Logger(TokensService.name); constructor( private readonly configService: ConfigService, private readonly jwtService: JwtService, private readonly refreshTokensRepository: RefreshTokensRepository, + private readonly dataSource: DataSource, ) {} // ----------- generate token ----------------- - async generateTokens(user: User) { - return this.generateAccessAndRefreshToken({ - id: user.id, - isAdmin: user.roles.some((r) => r.isAdmin), - permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), - }); + async generateTokens(user: User, queryRunner?: QueryRunner) { + return this.generateAccessAndRefreshToken( + { + id: user.id, + isAdmin: user.roles.some((r) => r.isAdmin), + permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + }, + queryRunner, + ); } //**************************************************************** */ - private async generateAccessAndRefreshToken(payload: ITokenPayload) { + private async generateAccessAndRefreshToken(payload: ITokenPayload, queryRunner?: QueryRunner) { const accessExpire = this.configService.getOrThrow("ACCESS_TOKEN_EXPIRE"); const refreshExpire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); const accessToken = this.jwtService.sign(payload, { expiresIn: `${accessExpire}m` }); - const refreshToken = this.jwtService.sign(payload, { expiresIn: `${refreshExpire}d` }); + const refreshToken = this.jwtService.sign({ id: payload.id }, { expiresIn: `${refreshExpire}d` }); - await this.storeRefreshToken(payload.id, refreshToken); + await this.storeRefreshToken(payload.id, refreshToken, queryRunner); return { - accessToken: { token: accessToken, expire: Date.now() + 1000 * 60 * 60 * accessExpire }, - refreshToken: { token: refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * refreshExpire }, + accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() }, + refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() }, }; } //**************************************************************** */ - async storeRefreshToken(userId: string, refreshToken: string) { - const refreshExpire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); - const expiresAt = dayjs().add(refreshExpire, "day").toDate(); + async storeRefreshToken(userId: string, refreshToken: string, queryRunner = this.dataSource.createQueryRunner()) { + let transactionStarted = false; + try { + if (!queryRunner.isTransactionActive) { + await queryRunner.connect(); + await queryRunner.startTransaction(); + transactionStarted = true; + } - const existingToken = await this.refreshTokensRepository.findOne({ - where: { user: { id: userId } }, - }); + const refreshExpire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); + const expiresAt = dayjs().add(refreshExpire, "day").toDate(); - if (existingToken) { - existingToken.token = refreshToken; - existingToken.expiresAt = expiresAt; - await this.refreshTokensRepository.save(existingToken); - } else { - const token = this.refreshTokensRepository.create({ - token: refreshToken, - user: { id: userId }, - expiresAt, + const existingToken = await queryRunner.manager.findOne(this.refreshTokensRepository.target, { + where: { user: { id: userId } }, }); - await this.refreshTokensRepository.save(token); + if (existingToken) { + existingToken.token = refreshToken; + existingToken.expiresAt = expiresAt; + await queryRunner.manager.save(this.refreshTokensRepository.target, existingToken); + // + } else { + const token = queryRunner.manager.create(this.refreshTokensRepository.target, { + token: refreshToken, + user: { id: userId }, + expiresAt, + }); + await queryRunner.manager.save(this.refreshTokensRepository.target, token); + } + + if (transactionStarted) await queryRunner.commitTransaction(); + } catch (error) { + this.logger.error(error); + if (transactionStarted) await queryRunner.rollbackTransaction(); + throw error; + } finally { + if (transactionStarted) await queryRunner.release(); } } /************************************************************ */ async refreshToken(oldRefreshToken: string) { - const token = await this.refreshTokensRepository.findOne({ - where: { token: oldRefreshToken }, - relations: { user: { roles: { permissions: true } } }, - }); + const queryRunner = this.dataSource.createQueryRunner(); - if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); - if (dayjs(token.expiresAt).isBefore(dayjs())) throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); + const token = await queryRunner.manager.findOne(this.refreshTokensRepository.target, { + where: { token: oldRefreshToken }, + relations: { user: { roles: { permissions: true } } }, + }); - await this.refreshTokensRepository.delete({ id: token.id }); + if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); - return await this.generateTokens(token.user); + if (dayjs(token.expiresAt).isBefore(dayjs())) { + await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id }); + throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); + } + + await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id }); + + const tokens = await this.generateTokens(token.user, queryRunner); + await queryRunner.commitTransaction(); + return tokens; + } catch (error) { + this.logger.error(error); + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } /************************************************************ */