import { Injectable, Inject, UnauthorizedException } from '@nestjs/common'; import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; import { IRedisClient } from '../../core/configs/cache/IRedisClient'; import { JwtService } from '@nestjs/jwt'; import { JwtPayload } from '../models/jwt-payload.model'; import { randomBytes } from 'crypto'; import { DateUtil } from 'src/shared/date.util'; export interface RefreshTokenData { userId: number; tokenId: string; sessionId?: string; expiresAt: number; createdAt: number; } @Injectable() export class RefreshTokenService { private readonly REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60; private readonly MAX_REFRESH_TOKENS_PER_USER = 5; constructor( @Inject(RedisClientToken) private readonly redis: IRedisClient, private readonly jwtService: JwtService, ) {} async generateRefreshToken( userId: number, sessionId?: string, ): Promise { const tokenId = randomBytes(32).toString('hex'); const refreshToken = this.jwtService.sign( { userId, tokenId, sessionId, type: 'refresh' }, { expiresIn: '7d' }, ); const tokenData: RefreshTokenData = { userId, tokenId, sessionId, expiresAt: DateUtil.nowTimestamp() + this.REFRESH_TOKEN_TTL * 1000, createdAt: DateUtil.nowTimestamp(), }; const key = this.buildRefreshTokenKey(userId, tokenId); await this.redis.set(key, tokenData, this.REFRESH_TOKEN_TTL); await this.limitRefreshTokensPerUser(userId); return refreshToken; } async validateRefreshToken(refreshToken: string): Promise { try { const decoded = this.jwtService.verify(refreshToken) as any; if (decoded.type !== 'refresh') { throw new UnauthorizedException('Token inválido'); } const { userId, tokenId, sessionId } = decoded; const key = this.buildRefreshTokenKey(userId, tokenId); const tokenData = await this.redis.get(key); if (!tokenData) { throw new UnauthorizedException('Refresh token expirado ou inválido'); } if (tokenData.expiresAt < DateUtil.nowTimestamp()) { await this.revokeRefreshToken(userId, tokenId); throw new UnauthorizedException('Refresh token expirado'); } return { id: userId, sellerId: 0, storeId: '', username: '', email: '', sessionId: sessionId || tokenData.sessionId, tokenId, } as JwtPayload; } catch (error) { throw new UnauthorizedException('Refresh token inválido'); } } async revokeRefreshToken(userId: number, tokenId: string): Promise { const key = this.buildRefreshTokenKey(userId, tokenId); await this.redis.del(key); } async revokeAllRefreshTokens(userId: number): Promise { const pattern = this.buildRefreshTokenPattern(userId); const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } } async getActiveRefreshTokens(userId: number): Promise { const pattern = this.buildRefreshTokenPattern(userId); const keys = await this.redis.keys(pattern); const tokens: RefreshTokenData[] = []; for (const key of keys) { const tokenData = await this.redis.get(key); if (tokenData && tokenData.expiresAt > DateUtil.nowTimestamp()) { tokens.push(tokenData); } } return tokens.sort((a, b) => b.createdAt - a.createdAt); } private async limitRefreshTokensPerUser(userId: number): Promise { const activeTokens = await this.getActiveRefreshTokens(userId); if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) { const tokensToRemove = activeTokens .slice(this.MAX_REFRESH_TOKENS_PER_USER) .map((token) => token.tokenId); for (const tokenId of tokensToRemove) { await this.revokeRefreshToken(userId, tokenId); } } } private buildRefreshTokenKey(userId: number, tokenId: string): string { return `auth:refresh_tokens:${userId}:${tokenId}`; } private buildRefreshTokenPattern(userId: number): string { return `auth:refresh_tokens:${userId}:*`; } }