feat: implementar melhorias na autenticação
- Adicionar refresh tokens para renovação automática de tokens - Implementar controle de sessões simultâneas - Adicionar blacklist de tokens para logout seguro - Implementar rate limiting para proteção contra ataques - Melhorar detecção de IP e identificação de sessão atual - Adicionar endpoints para gerenciamento de sessões - Corrigir inconsistências na validação de usuário - Atualizar configuração Redis com nova conexão
This commit is contained in:
@@ -4,6 +4,11 @@ import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
UseGuards,
|
||||
Request,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
@@ -12,12 +17,22 @@ import { LoginResponseDto } from './dto/LoginResponseDto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { ResultModel } from 'src/core/models/result.model';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { RateLimitingGuard } from '../guards/rate-limiting.guard';
|
||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||
import { SessionManagementService } from '../services/session-management.service';
|
||||
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto';
|
||||
import { SessionsResponseDto } from './dto/session.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBody,
|
||||
ApiOkResponse,
|
||||
ApiUnauthorizedResponse,
|
||||
ApiBearerAuth,
|
||||
ApiTooManyRequestsResponse,
|
||||
ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@@ -26,9 +41,13 @@ export class AuthController {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly authService: AuthService,
|
||||
private readonly rateLimitingService: RateLimitingService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly sessionManagementService: SessionManagementService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
@UseGuards(RateLimitingGuard)
|
||||
@ApiOperation({ summary: 'Realiza login e retorna um token JWT' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
@ApiOkResponse({
|
||||
@@ -36,24 +55,44 @@ export class AuthController {
|
||||
type: LoginResponseDto,
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
|
||||
async login(@Body() dto: LoginDto): Promise<LoginResponseDto> {
|
||||
@ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' })
|
||||
async login(@Body() dto: LoginDto, @Request() req): Promise<LoginResponseDto> {
|
||||
const ip = this.getClientIp(req);
|
||||
|
||||
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
||||
const result = await this.commandBus.execute(command);
|
||||
|
||||
if (!result.success) {
|
||||
// Registra tentativa falhada
|
||||
await this.rateLimitingService.recordAttempt(ip, false);
|
||||
|
||||
throw new HttpException(
|
||||
new ResultModel(false, result.error, null, result.error),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
// Registra tentativa bem-sucedida (limpa contador)
|
||||
await this.rateLimitingService.recordAttempt(ip, true);
|
||||
|
||||
const user = result.data;
|
||||
const token = await this.authService.createToken(
|
||||
const userAgent = req.headers['user-agent'] || 'Unknown';
|
||||
|
||||
// Cria sessão para o usuário primeiro
|
||||
const session = await this.sessionManagementService.createSession(
|
||||
user.id,
|
||||
ip,
|
||||
userAgent,
|
||||
);
|
||||
|
||||
// Cria tokens de acesso e refresh com sessionId
|
||||
const tokenPair = await this.authService.createTokenPair(
|
||||
user.id,
|
||||
user.sellerId,
|
||||
user.name,
|
||||
user.email,
|
||||
user.storeId,
|
||||
session.sessionId,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -63,7 +102,124 @@ export class AuthController {
|
||||
username: user.name,
|
||||
storeId: user.storeId,
|
||||
email: user.email,
|
||||
token: token,
|
||||
accessToken: tokenPair.accessToken,
|
||||
refreshToken: tokenPair.refreshToken,
|
||||
expiresIn: tokenPair.expiresIn,
|
||||
sessionId: session.sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai o IP real do cliente considerando proxies
|
||||
* @param request Objeto de requisição
|
||||
* @returns Endereço IP do cliente
|
||||
*/
|
||||
private getClientIp(request: any): string {
|
||||
return (
|
||||
request.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
request.headers['x-real-ip'] ||
|
||||
request.connection?.remoteAddress ||
|
||||
request.socket?.remoteAddress ||
|
||||
request.ip ||
|
||||
'127.0.0.1'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Post('logout')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Realiza logout e invalida o token JWT' })
|
||||
@ApiOkResponse({ description: 'Logout realizado com sucesso' })
|
||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||
async logout(@Request() req): Promise<{ message: string }> {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
throw new HttpException(
|
||||
new ResultModel(false, 'Token não fornecido', null, 'Token não fornecido'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.authService.logout(token);
|
||||
|
||||
return {
|
||||
message: 'Logout realizado com sucesso',
|
||||
};
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@ApiOperation({ summary: 'Renova o access token usando refresh token' })
|
||||
@ApiBody({ type: RefreshTokenDto })
|
||||
@ApiOkResponse({
|
||||
description: 'Token renovado com sucesso',
|
||||
type: RefreshTokenResponseDto,
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Refresh token inválido ou expirado' })
|
||||
async refreshToken(@Body() dto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
|
||||
const result = await this.authService.refreshAccessToken(dto.refreshToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('sessions')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Lista todas as sessões ativas do usuário' })
|
||||
@ApiOkResponse({
|
||||
description: 'Lista de sessões ativas',
|
||||
type: SessionsResponseDto,
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
||||
const userId = req.user.id;
|
||||
const currentSessionId = req.user.sessionId; // ID da sessão atual
|
||||
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId);
|
||||
|
||||
return {
|
||||
sessions: sessions.map(session => ({
|
||||
sessionId: session.sessionId,
|
||||
ipAddress: session.ipAddress,
|
||||
userAgent: session.userAgent,
|
||||
createdAt: new Date(session.createdAt).toISOString(),
|
||||
lastActivity: new Date(session.lastActivity).toISOString(),
|
||||
isCurrent: session.sessionId === currentSessionId,
|
||||
})),
|
||||
total: sessions.length,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('sessions/:sessionId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Encerra uma sessão específica' })
|
||||
@ApiParam({ name: 'sessionId', description: 'ID da sessão a ser encerrada' })
|
||||
@ApiOkResponse({ description: 'Sessão encerrada com sucesso' })
|
||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||
async terminateSession(
|
||||
@Request() req,
|
||||
@Param('sessionId') sessionId: string,
|
||||
): Promise<{ message: string }> {
|
||||
const userId = req.user.id;
|
||||
await this.sessionManagementService.terminateSession(userId, sessionId);
|
||||
|
||||
return {
|
||||
message: 'Sessão encerrada com sucesso',
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('sessions')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Encerra todas as sessões do usuário' })
|
||||
@ApiOkResponse({ description: 'Todas as sessões encerradas com sucesso' })
|
||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||
async terminateAllSessions(@Request() req): Promise<{ message: string }> {
|
||||
const userId = req.user.id;
|
||||
await this.sessionManagementService.terminateAllSessions(userId);
|
||||
|
||||
return {
|
||||
message: 'Todas as sessões foram encerradas com sucesso',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import { AuthController } from './auth.controller';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthenticateUserHandler } from './commands/authenticate-user.service';
|
||||
import { TokenBlacklistService } from '../services/token-blacklist.service';
|
||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||
import { SessionManagementService } from '../services/session-management.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -29,7 +33,15 @@ import { AuthenticateUserHandler } from './commands/authenticate-user.service';
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
TokenBlacklistService,
|
||||
RateLimitingService,
|
||||
RefreshTokenService,
|
||||
SessionManagementService,
|
||||
AuthenticateUserHandler
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtPayload } from '../models/jwt-payload.model';
|
||||
import { UserRepository } from '../users/UserRepository';
|
||||
import { TokenBlacklistService } from '../services/token-blacklist.service';
|
||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -11,30 +13,97 @@ export class AuthService {
|
||||
private readonly usersService: UsersService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly tokenBlacklistService: TokenBlacklistService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
) {}
|
||||
|
||||
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string) {
|
||||
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
||||
const user: JwtPayload = {
|
||||
id: id,
|
||||
sellerId: sellerId,
|
||||
storeId: storeId,
|
||||
username: username,
|
||||
email: email,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
const options: JwtSignOptions = { expiresIn: '8h' };
|
||||
return this.jwtService.sign(user, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria tokens de acesso e refresh
|
||||
* @param id ID do usuário
|
||||
* @param sellerId ID do vendedor
|
||||
* @param username Nome de usuário
|
||||
* @param email Email do usuário
|
||||
* @param storeId ID da loja
|
||||
* @returns Objeto com access token e refresh token
|
||||
*/
|
||||
async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
||||
const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(id);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn: 8 * 60 * 60, // 8 horas em segundos
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renova o access token usando o refresh token
|
||||
* @param refreshToken Token de refresh
|
||||
* @returns Novo access token
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
const tokenData = await this.refreshTokenService.validateRefreshToken(refreshToken);
|
||||
|
||||
const user = await this.userRepository.findById(tokenData.id);
|
||||
if (!user || user.situacao === 'I' || user.dataDesligamento) {
|
||||
throw new UnauthorizedException('Usuário inválido ou inativo');
|
||||
}
|
||||
|
||||
const newAccessToken = await this.createToken(
|
||||
user.id,
|
||||
user.sellerId,
|
||||
user.name,
|
||||
user.email,
|
||||
user.storeId
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
expiresIn: 8 * 60 * 60, // 8 horas em segundos
|
||||
};
|
||||
}
|
||||
|
||||
async validateUser(payload: JwtPayload): Promise<JwtPayload | null> {
|
||||
const user = await this.userRepository.findById(payload.id);
|
||||
if (!user || !user.active) return null;
|
||||
if (!user || user.situacao === 'I' || user.dataDesligamento) return null;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
sellerId: user.sellerId,
|
||||
storeId: user.storeId,
|
||||
username: user.username,
|
||||
username: user.name, // Usando name como username para consistência
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza logout do usuário adicionando o token à blacklist
|
||||
* @param token Token JWT a ser invalidado
|
||||
*/
|
||||
async logout(token: string): Promise<void> {
|
||||
await this.tokenBlacklistService.addToBlacklist(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um token está blacklistado
|
||||
* @param token Token JWT a ser verificado
|
||||
* @returns true se o token estiver blacklistado
|
||||
*/
|
||||
async isTokenBlacklisted(token: string): Promise<boolean> {
|
||||
return this.tokenBlacklistService.isBlacklisted(token);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,8 @@ export class LoginResponseDto {
|
||||
@ApiProperty() username: string;
|
||||
@ApiProperty() storeId: string;
|
||||
@ApiProperty() email: string;
|
||||
@ApiProperty() token: string;
|
||||
@ApiProperty() accessToken: string;
|
||||
@ApiProperty() refreshToken: string;
|
||||
@ApiProperty() expiresIn: number;
|
||||
@ApiProperty() sessionId: string;
|
||||
}
|
||||
|
||||
26
src/auth/auth/dto/refresh-token.dto.ts
Normal file
26
src/auth/auth/dto/refresh-token.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
description: 'Refresh token para renovar o access token',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class RefreshTokenResponseDto {
|
||||
@ApiProperty({
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
description: 'Novo access token',
|
||||
})
|
||||
accessToken: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 28800,
|
||||
description: 'Tempo de expiração em segundos',
|
||||
})
|
||||
expiresIn: number;
|
||||
}
|
||||
53
src/auth/auth/dto/session.dto.ts
Normal file
53
src/auth/auth/dto/session.dto.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SessionInfoDto {
|
||||
@ApiProperty({
|
||||
example: 'abc123def456',
|
||||
description: 'ID da sessão',
|
||||
})
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '192.168.1.100',
|
||||
description: 'IP de origem da sessão',
|
||||
})
|
||||
ipAddress: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
description: 'User agent da sessão',
|
||||
})
|
||||
userAgent: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2025-09-16T17:30:00.000Z',
|
||||
description: 'Data de criação da sessão',
|
||||
})
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2025-09-16T17:30:00.000Z',
|
||||
description: 'Última atividade da sessão',
|
||||
})
|
||||
lastActivity: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: 'Se é a sessão atual',
|
||||
})
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export class SessionsResponseDto {
|
||||
@ApiProperty({
|
||||
type: [SessionInfoDto],
|
||||
description: 'Lista de sessões ativas',
|
||||
})
|
||||
sessions: SessionInfoDto[];
|
||||
|
||||
@ApiProperty({
|
||||
example: 3,
|
||||
description: 'Total de sessões ativas',
|
||||
})
|
||||
total: number;
|
||||
}
|
||||
49
src/auth/guards/rate-limiting.guard.ts
Normal file
49
src/auth/guards/rate-limiting.guard.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||
|
||||
@Injectable()
|
||||
export class RateLimitingGuard implements CanActivate {
|
||||
constructor(private readonly rateLimitingService: RateLimitingService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const ip = this.getClientIp(request);
|
||||
|
||||
const isAllowed = await this.rateLimitingService.isAllowed(ip);
|
||||
|
||||
if (!isAllowed) {
|
||||
const attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||
data: null,
|
||||
details: {
|
||||
attempts: attemptInfo.attempts,
|
||||
remainingTime: attemptInfo.remainingTime,
|
||||
},
|
||||
},
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai o IP real do cliente considerando proxies
|
||||
* @param request Objeto de requisição
|
||||
* @returns Endereço IP do cliente
|
||||
*/
|
||||
private getClientIp(request: any): string {
|
||||
return (
|
||||
request.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
request.headers['x-real-ip'] ||
|
||||
request.connection?.remoteAddress ||
|
||||
request.socket?.remoteAddress ||
|
||||
request.ip ||
|
||||
'127.0.0.1'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,7 @@ export interface JwtPayload {
|
||||
storeId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
exp?: number; // Timestamp de expiração do JWT
|
||||
sessionId?: string; // ID da sessão atual
|
||||
}
|
||||
|
||||
|
||||
126
src/auth/services/rate-limiting.service.ts
Normal file
126
src/auth/services/rate-limiting.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||
|
||||
export interface RateLimitConfig {
|
||||
maxAttempts: number;
|
||||
windowMs: number;
|
||||
blockDurationMs: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RateLimitingService {
|
||||
private readonly defaultConfig: RateLimitConfig = {
|
||||
maxAttempts: 5, // 5 tentativas
|
||||
windowMs: 15 * 60 * 1000, // 15 minutos
|
||||
blockDurationMs: 30 * 60 * 1000, // 30 minutos de bloqueio
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verifica se o IP pode fazer uma tentativa de login
|
||||
* @param ip Endereço IP do cliente
|
||||
* @param config Configuração personalizada (opcional)
|
||||
* @returns true se permitido, false se bloqueado
|
||||
*/
|
||||
async isAllowed(ip: string, config?: Partial<RateLimitConfig>): Promise<boolean> {
|
||||
const finalConfig = { ...this.defaultConfig, ...config };
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
|
||||
// Verifica se está bloqueado
|
||||
const isBlocked = await this.redis.get(blockKey);
|
||||
if (isBlocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conta tentativas na janela de tempo
|
||||
const attempts = await this.redis.get<string>(key);
|
||||
const attemptCount = attempts ? parseInt(attempts) : 0;
|
||||
|
||||
if (attemptCount >= finalConfig.maxAttempts) {
|
||||
// Bloqueia o IP
|
||||
await this.redis.set(blockKey, 'blocked', finalConfig.blockDurationMs / 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra uma tentativa de login
|
||||
* @param ip Endereço IP do cliente
|
||||
* @param success true se login foi bem-sucedido
|
||||
* @param config Configuração personalizada (opcional)
|
||||
*/
|
||||
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> {
|
||||
const finalConfig = { ...this.defaultConfig, ...config };
|
||||
const key = this.buildAttemptKey(ip);
|
||||
|
||||
if (success) {
|
||||
await this.redis.del(key);
|
||||
} else {
|
||||
const attempts = await this.redis.get<string>(key);
|
||||
const attemptCount = attempts ? parseInt(attempts) + 1 : 1;
|
||||
|
||||
await this.redis.set(key, attemptCount.toString(), finalConfig.windowMs / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações sobre tentativas de um IP
|
||||
* @param ip Endereço IP do cliente
|
||||
* @returns Informações sobre tentativas
|
||||
*/
|
||||
async getAttemptInfo(ip: string): Promise<{
|
||||
attempts: number;
|
||||
isBlocked: boolean;
|
||||
remainingTime?: number;
|
||||
}> {
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
|
||||
const attempts = await this.redis.get<string>(key);
|
||||
const isBlocked = await this.redis.get(blockKey);
|
||||
const ttl = await this.redis.ttl(blockKey);
|
||||
|
||||
return {
|
||||
attempts: attempts ? parseInt(attempts) : 0,
|
||||
isBlocked: !!isBlocked,
|
||||
remainingTime: isBlocked ? ttl : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa tentativas de um IP (útil para testes ou admin)
|
||||
* @param ip Endereço IP do cliente
|
||||
*/
|
||||
async clearAttempts(ip: string): Promise<void> {
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
|
||||
await this.redis.del(key);
|
||||
await this.redis.del(blockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói a chave para armazenar tentativas
|
||||
* @param ip Endereço IP
|
||||
* @returns Chave para o Redis
|
||||
*/
|
||||
private buildAttemptKey(ip: string): string {
|
||||
return `auth:rate_limit:attempts:${ip}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói a chave para armazenar bloqueio
|
||||
* @param ip Endereço IP
|
||||
* @returns Chave para o Redis
|
||||
*/
|
||||
private buildBlockKey(ip: string): string {
|
||||
return `auth:rate_limit:blocked:${ip}`;
|
||||
}
|
||||
}
|
||||
173
src/auth/services/refresh-token.service.ts
Normal file
173
src/auth/services/refresh-token.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
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';
|
||||
|
||||
export interface RefreshTokenData {
|
||||
userId: number;
|
||||
tokenId: string;
|
||||
expiresAt: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RefreshTokenService {
|
||||
private readonly REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 dias em segundos
|
||||
private readonly MAX_REFRESH_TOKENS_PER_USER = 5; // Máximo 5 refresh tokens por usuário
|
||||
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gera um novo refresh token para o usuário
|
||||
* @param userId ID do usuário
|
||||
* @returns Refresh token
|
||||
*/
|
||||
async generateRefreshToken(userId: number): Promise<string> {
|
||||
const tokenId = randomBytes(32).toString('hex');
|
||||
const refreshToken = this.jwtService.sign(
|
||||
{ userId, tokenId, type: 'refresh' },
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
const tokenData: RefreshTokenData = {
|
||||
userId,
|
||||
tokenId,
|
||||
expiresAt: Date.now() + (this.REFRESH_TOKEN_TTL * 1000),
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||
await this.redis.set(key, tokenData, this.REFRESH_TOKEN_TTL);
|
||||
|
||||
// Limita o número de refresh tokens por usuário
|
||||
await this.limitRefreshTokensPerUser(userId);
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida um refresh token e retorna os dados do usuário
|
||||
* @param refreshToken Token de refresh
|
||||
* @returns Dados do usuário se válido
|
||||
*/
|
||||
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 } = 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 < Date.now()) {
|
||||
await this.revokeRefreshToken(userId, tokenId);
|
||||
throw new UnauthorizedException('Refresh token expirado');
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
sellerId: 0,
|
||||
storeId: '',
|
||||
username: '',
|
||||
email: '',
|
||||
tokenId
|
||||
} as JwtPayload;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Refresh token inválido');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoga um refresh token específico
|
||||
* @param userId ID do usuário
|
||||
* @param tokenId ID do token
|
||||
*/
|
||||
async revokeRefreshToken(userId: number, tokenId: string): Promise<void> {
|
||||
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||
await this.redis.del(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoga todos os refresh tokens de um usuário
|
||||
* @param userId ID do usuário
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todos os refresh tokens ativos de um usuário
|
||||
* @param userId ID do usuário
|
||||
* @returns Lista de tokens ativos
|
||||
*/
|
||||
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 > Date.now()) {
|
||||
tokens.push(tokenData);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.sort((a, b) => b.createdAt - a.createdAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limita o número de refresh tokens por usuário
|
||||
* @param userId ID do usuário
|
||||
*/
|
||||
private async limitRefreshTokensPerUser(userId: number): Promise<void> {
|
||||
const activeTokens = await this.getActiveRefreshTokens(userId);
|
||||
|
||||
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
||||
// Remove os tokens mais antigos
|
||||
const tokensToRemove = activeTokens
|
||||
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
||||
.map(token => token.tokenId);
|
||||
|
||||
for (const tokenId of tokensToRemove) {
|
||||
await this.revokeRefreshToken(userId, tokenId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói a chave para armazenar o refresh token
|
||||
* @param userId ID do usuário
|
||||
* @param tokenId ID do token
|
||||
* @returns Chave para o Redis
|
||||
*/
|
||||
private buildRefreshTokenKey(userId: number, tokenId: string): string {
|
||||
return `auth:refresh_tokens:${userId}:${tokenId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói o padrão para buscar refresh tokens de um usuário
|
||||
* @param userId ID do usuário
|
||||
* @returns Padrão para o Redis
|
||||
*/
|
||||
private buildRefreshTokenPattern(userId: number): string {
|
||||
return `auth:refresh_tokens:${userId}:*`;
|
||||
}
|
||||
}
|
||||
198
src/auth/services/session-management.service.ts
Normal file
198
src/auth/services/session-management.service.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export interface SessionData {
|
||||
sessionId: string;
|
||||
userId: number;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
createdAt: number;
|
||||
lastActivity: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionManagementService {
|
||||
private readonly SESSION_TTL = 8 * 60 * 60; // 8 horas em segundos
|
||||
private readonly MAX_SESSIONS_PER_USER = 5; // Máximo 5 sessões por usuário
|
||||
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Cria uma nova sessão para o usuário
|
||||
* @param userId ID do usuário
|
||||
* @param ipAddress Endereço IP
|
||||
* @param userAgent User agent
|
||||
* @returns Dados da sessão criada
|
||||
*/
|
||||
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
||||
const sessionId = randomBytes(16).toString('hex');
|
||||
const now = Date.now();
|
||||
|
||||
const sessionData: SessionData = {
|
||||
sessionId,
|
||||
userId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
createdAt: now,
|
||||
lastActivity: now,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||
|
||||
// Limita o número de sessões por usuário
|
||||
await this.limitSessionsPerUser(userId);
|
||||
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza a última atividade de uma sessão
|
||||
* @param userId ID do usuário
|
||||
* @param sessionId ID da sessão
|
||||
*/
|
||||
async updateSessionActivity(userId: number, sessionId: string): Promise<void> {
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
|
||||
if (sessionData) {
|
||||
sessionData.lastActivity = Date.now();
|
||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todas as sessões ativas de um usuário
|
||||
* @param userId ID do usuário
|
||||
* @param currentSessionId ID da sessão atual (opcional)
|
||||
* @returns Lista de sessões ativas
|
||||
*/
|
||||
async getActiveSessions(userId: number, currentSessionId?: string): Promise<SessionData[]> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
const sessions: SessionData[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData && sessionData.isActive) {
|
||||
// Marca se é a sessão atual
|
||||
if (currentSessionId && sessionData.sessionId === currentSessionId) {
|
||||
sessionData.isActive = true; // Mantém como ativa
|
||||
}
|
||||
sessions.push(sessionData);
|
||||
}
|
||||
}
|
||||
|
||||
return sessions.sort((a, b) => b.lastActivity - a.lastActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encerra uma sessão específica
|
||||
* @param userId ID do usuário
|
||||
* @param sessionId ID da sessão
|
||||
*/
|
||||
async terminateSession(userId: number, sessionId: string): Promise<void> {
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
|
||||
if (!sessionData) {
|
||||
throw new NotFoundException('Sessão não encontrada');
|
||||
}
|
||||
|
||||
sessionData.isActive = false;
|
||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encerra todas as sessões de um usuário
|
||||
* @param userId ID do usuário
|
||||
*/
|
||||
async terminateAllSessions(userId: number): Promise<void> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData) {
|
||||
sessionData.isActive = false;
|
||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encerra todas as sessões de um usuário exceto a atual
|
||||
* @param userId ID do usuário
|
||||
* @param currentSessionId ID da sessão atual
|
||||
*/
|
||||
async terminateOtherSessions(userId: number, currentSessionId: string): Promise<void> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData && sessionData.sessionId !== currentSessionId) {
|
||||
sessionData.isActive = false;
|
||||
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma sessão está ativa
|
||||
* @param userId ID do usuário
|
||||
* @param sessionId ID da sessão
|
||||
* @returns true se a sessão estiver ativa
|
||||
*/
|
||||
async isSessionActive(userId: number, sessionId: string): Promise<boolean> {
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
|
||||
return sessionData ? sessionData.isActive : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limita o número de sessões por usuário
|
||||
* @param userId ID do usuário
|
||||
*/
|
||||
private async limitSessionsPerUser(userId: number): Promise<void> {
|
||||
const activeSessions = await this.getActiveSessions(userId);
|
||||
|
||||
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
|
||||
// Remove as sessões mais antigas
|
||||
const sessionsToRemove = activeSessions
|
||||
.slice(this.MAX_SESSIONS_PER_USER)
|
||||
.map(session => session.sessionId);
|
||||
|
||||
for (const sessionId of sessionsToRemove) {
|
||||
await this.terminateSession(userId, sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói a chave para armazenar a sessão
|
||||
* @param userId ID do usuário
|
||||
* @param sessionId ID da sessão
|
||||
* @returns Chave para o Redis
|
||||
*/
|
||||
private buildSessionKey(userId: number, sessionId: string): string {
|
||||
return `auth:sessions:${userId}:${sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói o padrão para buscar sessões de um usuário
|
||||
* @param userId ID do usuário
|
||||
* @returns Padrão para o Redis
|
||||
*/
|
||||
private buildSessionPattern(userId: number): string {
|
||||
return `auth:sessions:${userId}:*`;
|
||||
}
|
||||
}
|
||||
103
src/auth/services/token-blacklist.service.ts
Normal file
103
src/auth/services/token-blacklist.service.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Injectable, Inject } 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';
|
||||
|
||||
@Injectable()
|
||||
export class TokenBlacklistService {
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Adiciona um token à blacklist
|
||||
* @param token Token JWT a ser invalidado
|
||||
* @param expiresIn Tempo de expiração do token em segundos
|
||||
*/
|
||||
async addToBlacklist(token: string, expiresIn?: number): Promise<void> {
|
||||
try {
|
||||
const decoded = this.jwtService.decode(token) as JwtPayload;
|
||||
if (!decoded) {
|
||||
throw new Error('Token inválido');
|
||||
}
|
||||
|
||||
const blacklistKey = this.buildBlacklistKey(token);
|
||||
const ttl = expiresIn || this.calculateTokenTTL(decoded);
|
||||
|
||||
await this.redis.set(blacklistKey, 'blacklisted', ttl);
|
||||
} catch (error) {
|
||||
throw new Error(`Erro ao adicionar token à blacklist: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um token está na blacklist
|
||||
* @param token Token JWT a ser verificado
|
||||
* @returns true se o token estiver blacklistado
|
||||
*/
|
||||
async isBlacklisted(token: string): Promise<boolean> {
|
||||
try {
|
||||
const blacklistKey = this.buildBlacklistKey(token);
|
||||
const result = await this.redis.get(blacklistKey);
|
||||
return result === 'blacklisted';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um token da blacklist (útil para testes)
|
||||
* @param token Token JWT a ser removido
|
||||
*/
|
||||
async removeFromBlacklist(token: string): Promise<void> {
|
||||
const blacklistKey = this.buildBlacklistKey(token);
|
||||
await this.redis.del(blacklistKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todos os tokens blacklistados de um usuário
|
||||
* @param userId ID do usuário
|
||||
*/
|
||||
async clearUserBlacklist(userId: number): Promise<void> {
|
||||
const pattern = `auth:blacklist:${userId}:*`;
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói a chave para armazenar o token na blacklist
|
||||
* @param token Token JWT
|
||||
* @returns Chave para o Redis
|
||||
*/
|
||||
private buildBlacklistKey(token: string): string {
|
||||
const decoded = this.jwtService.decode(token) as JwtPayload;
|
||||
const tokenHash = this.hashToken(token);
|
||||
return `auth:blacklist:${decoded.id}:${tokenHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula o TTL do token baseado na expiração
|
||||
* @param payload Payload do JWT
|
||||
* @returns TTL em segundos
|
||||
*/
|
||||
private calculateTokenTTL(payload: JwtPayload): number {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const exp = payload.exp || (now + 8 * 60 * 60); // 8h padrão
|
||||
return Math.max(0, exp - now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera um hash do token para usar como identificador único
|
||||
* @param token Token JWT
|
||||
* @returns Hash do token
|
||||
*/
|
||||
private hashToken(token: string): string {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { JwtPayload } from '../models/jwt-payload.model';
|
||||
import { UserRepository } from '../../auth/users/UserRepository';
|
||||
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||
import { TokenBlacklistService } from '../services/token-blacklist.service';
|
||||
import { SessionManagementService } from '../services/session-management.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -14,6 +16,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly tokenBlacklistService: TokenBlacklistService,
|
||||
private readonly sessionManagementService: SessionManagementService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
@@ -21,13 +25,24 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
async validate(payload: JwtPayload, req: any) {
|
||||
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||
if (token && await this.tokenBlacklistService.isBlacklisted(token)) {
|
||||
throw new UnauthorizedException('Token foi invalidado');
|
||||
}
|
||||
|
||||
const sessionKey = this.buildSessionKey(payload.id);
|
||||
const cachedUser = await this.redis.get<any>(sessionKey);
|
||||
|
||||
if (cachedUser) {
|
||||
// await this.auditAccess(cachedUser);
|
||||
return cachedUser;
|
||||
return {
|
||||
id: cachedUser.id,
|
||||
sellerId: cachedUser.sellerId,
|
||||
storeId: cachedUser.storeId,
|
||||
username: cachedUser.name,
|
||||
email: cachedUser.email,
|
||||
name: cachedUser.name,
|
||||
};
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(payload.id);
|
||||
@@ -35,13 +50,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
throw new UnauthorizedException('Usuário inválido ou inativo');
|
||||
}
|
||||
|
||||
await this.redis.set(sessionKey, user, 60 * 60 * 8); // 8h
|
||||
|
||||
return {
|
||||
const userData = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
sellerId: user.sellerId,
|
||||
storeId: user.storeId,
|
||||
username: user.name,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
sessionId: payload.sessionId, // Inclui sessionId do token
|
||||
};
|
||||
|
||||
await this.redis.set(sessionKey, userData, 60 * 60 * 8);
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
private buildSessionKey(userId: number): string {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersService } from './users.service';
|
||||
import { UserRepository } from './UserRepository';
|
||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
import { ChangePasswordService } from './change-password.service';
|
||||
import { EmailService } from './email.service';
|
||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||
|
||||
|
||||
@Module({
|
||||
@@ -15,11 +16,12 @@ import { EmailService } from './email.service';
|
||||
providers: [
|
||||
UsersService,
|
||||
UserRepository,
|
||||
AuthenticateUserHandler,
|
||||
ResetPasswordService,
|
||||
ChangePasswordService,
|
||||
EmailService,
|
||||
AuthenticateUserHandler,
|
||||
AuthenticateUserCommand,
|
||||
],
|
||||
exports: [UsersService,UserRepository],
|
||||
exports: [UsersService, UserRepository],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
Reference in New Issue
Block a user