import { randomBytes } from "node:crypto"; 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"; import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository"; import { ITokenPayload } from "../interfaces/IToken-payload"; import { ResellerType } from "../DTO/verify-reseller-otp.dto"; @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, 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, ); } // ----------------------------- async generateTokensForReseller(user: User, resellerType: ResellerType, queryRunner?: QueryRunner) { return this.generateAccessAndRefreshToken( { id: user.id, isAdmin: false, resellerType, permissions: [], }, queryRunner, ); } //**************************************************************** */ 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 = await this.generateRefreshToken(); await this.storeRefreshToken(payload.id, refreshToken, queryRunner); return { accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() }, refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() }, }; } //**************************************************************** */ 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 refreshExpire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); const expiresAt = dayjs().add(refreshExpire, "day").toDate(); 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 queryRunner = this.dataSource.createQueryRunner(); try { await queryRunner.connect(); await queryRunner.startTransaction(); const token = await queryRunner.manager.findOne(this.refreshTokensRepository.target, { where: { token: oldRefreshToken }, relations: { user: { roles: { permissions: true } } }, }); if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); 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(); } } /************************************************************ */ async invalidateRefreshToken(userId: string) { await this.refreshTokensRepository.delete({ user: { id: userId } }); this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`); } private async generateRefreshToken() { return randomBytes(32).toString("hex"); } }