142 lines
5.1 KiB
TypeScript
Executable File
142 lines
5.1 KiB
TypeScript
Executable File
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 { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
|
|
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: getPermissionsFromRoles(user.roles),
|
|
},
|
|
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<number>("ACCESS_TOKEN_EXPIRE");
|
|
const refreshExpire = this.configService.getOrThrow<number>("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<number>("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");
|
|
}
|
|
}
|