140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
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<string> {
|
|
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<JwtPayload> {
|
|
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<RefreshTokenData>(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<void> {
|
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
|
await this.redis.del(key);
|
|
}
|
|
|
|
async revokeAllRefreshTokens(userId: number): Promise<void> {
|
|
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<RefreshTokenData[]> {
|
|
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<RefreshTokenData>(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<void> {
|
|
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}:*`;
|
|
}
|
|
}
|