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:
Joelson
2025-09-16 18:17:37 -03:00
parent 055f138e5a
commit 21c3225c52
33 changed files with 1061 additions and 1375 deletions

View File

@@ -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',
};
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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;
}

View 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;
}

View 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;
}

View 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'
);
}
}

View File

@@ -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
}

View 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}`;
}
}

View 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}:*`;
}
}

View 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}:*`;
}
}

View 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);
}
}

View File

@@ -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 {

View File

@@ -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 {}

View File

@@ -2,5 +2,8 @@ export interface IRedisClient {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
del(key: string): Promise<void>;
del(...keys: string[]): Promise<void>;
keys(pattern: string): Promise<string[]>;
ttl(key: string): Promise<number>;
}

View File

@@ -1,145 +0,0 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documentação - Integração Redis</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9f9f9;
color: #333;
line-height: 1.6;
padding: 2rem;
}
h1, h2, h3 {
color: #007acc;
}
code, pre {
background-color: #eee;
padding: 1rem;
border-radius: 4px;
display: block;
white-space: pre-wrap;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
th, td {
border: 1px solid #ddd;
padding: 0.75rem;
}
th {
background-color: #007acc;
color: white;
}
.tag {
display: inline-block;
background: #007acc;
color: white;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.85rem;
}
</style>
</head>
<body>
<h1>📦 Integração Redis com Abstração - Portal Juru API</h1>
<h2>🧱 Arquitetura</h2>
<p>O projeto utiliza o Redis com uma interface genérica para garantir desacoplamento, facilidade de teste e reaproveitamento em múltiplos módulos.</p>
<h3>🔌 Interface IRedisClient</h3>
<pre><code>export interface IRedisClient {
get&lt;T&gt;(key: string): Promise&lt;T | null&gt;;
set&lt;T&gt;(key: string, value: T, ttlSeconds?: number): Promise&lt;void&gt;;
del(key: string): Promise&lt;void&gt;;
}</code></pre>
<h3>🧩 Provider REDIS_CLIENT</h3>
<p>Faz a conexão direta com o Redis usando a biblioteca <code>ioredis</code> e o <code>ConfigService</code> para pegar host e porta.</p>
<pre><code>export const RedisProvider: Provider = {
provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) =&gt; {
const redis = new Redis({
host: configService.get('REDIS_HOST', '10.1.1.109'),
port: configService.get('REDIS_PORT', 6379),
});
redis.on('error', (err) =&gt; {
console.error('Erro ao conectar ao Redis:', err);
});
return redis;
},
inject: [ConfigService],
};</code></pre>
<h3>📦 RedisClientAdapter (Wrapper)</h3>
<p>Classe que implementa <code>IRedisClient</code> e encapsula as operações de cache. É injetada em serviços via token.</p>
<pre><code>@Injectable()
export class RedisClientAdapter implements IRedisClient {
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
async get&lt;T&gt;(key: string): Promise&lt;T | null&gt; {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set&lt;T&gt;(key: string, value: T, ttlSeconds = 300): Promise&lt;void&gt; {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async del(key: string): Promise&lt;void&gt; {
await this.redis.del(key);
}
}</code></pre>
<h3>🔗 Token e Provider</h3>
<p>Token de injeção definido para o adapter:</p>
<pre><code>export const RedisClientToken = 'RedisClientInterface';
export const RedisClientAdapterProvider = {
provide: RedisClientToken,
useClass: RedisClientAdapter,
};</code></pre>
<h3>📦 Módulo Global RedisModule</h3>
<p>Torna o Redis disponível em toda a aplicação.</p>
<pre><code>@Global()
@Module({
imports: [ConfigModule],
providers: [RedisProvider, RedisClientAdapterProvider],
exports: [RedisProvider, RedisClientAdapterProvider],
})
export class RedisModule {}</code></pre>
<h2>🧠 Uso em Serviços</h2>
<p>Injetando o cache no seu service:</p>
<pre><code>constructor(
@Inject(RedisClientToken)
private readonly redisClient: IRedisClient
) {}</code></pre>
<p>Uso típico:</p>
<pre><code>const data = await this.redisClient.get&lt;T&gt;('chave');
if (!data) {
const result = await fetchFromDb();
await this.redisClient.set('chave', result, 3600);
}</code></pre>
<h2>🧰 Boas práticas</h2>
<ul>
<li>✅ TTL por recurso (ex: produtos: 1h, lojas: 24h)</li>
<li>✅ Nomear chaves com prefixos por domínio (ex: <code>data-consult:sellers</code>)</li>
<li>✅ Centralizar helpers como <code>getOrSetCache</code> para evitar repetição</li>
<li>✅ Usar <code>JSON.stringify</code> e <code>JSON.parse</code> no adapter</li>
<li>✅ Marcar módulo como <code>@Global()</code> para acesso em toda a aplicação</li>
</ul>
<p><strong>Última atualização:</strong> 29/03/2025</p>
</body>
</html>

View File

@@ -18,7 +18,21 @@ export class RedisClientAdapter implements IRedisClient {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async del(key: string): Promise<void> {
await this.redis.del(key);
async del(key: string): Promise<void>;
async del(...keys: string[]): Promise<void>;
async del(keyOrKeys: string | string[]): Promise<void> {
if (Array.isArray(keyOrKeys)) {
await this.redis.del(...keyOrKeys);
} else {
await this.redis.del(keyOrKeys);
}
}
async keys(pattern: string): Promise<string[]> {
return this.redis.keys(pattern);
}
async ttl(key: string): Promise<number> {
return this.redis.ttl(key);
}
}

View File

@@ -6,10 +6,9 @@
provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) => {
const redis = new Redis({
host: configService.get<string>('REDIS_HOST', 'redis-17317.crce181.sa-east-1-2.ec2.redns.redis-cloud.com'),
port: configService.get<number>('REDIS_PORT', 17317),
username: configService.get<string>('REDIS_USERNAME', 'default' ),
password: configService.get<string>('REDIS_PASSWORD', 'd8sVttpJdNxrWjYRK43QGAKzEt3I8HVc'),
host: configService.get<string>('REDIS_HOST', '10.1.1.124'),
port: configService.get<number>('REDIS_PORT', 6379),
password: configService.get<string>('REDIS_PASSWORD', '1234'),
});
redis.on('error', (err) => {

View File

@@ -4,7 +4,6 @@ import * as oracledb from 'oracledb';
// Inicializar o cliente Oracle
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
// Definir a estratégia de pool padrão para Oracle
@@ -14,19 +13,16 @@ oracledb.poolIncrement = 1; // incremental de conexões
export function createOracleConfig(config: ConfigService): DataSourceOptions {
// Obter configurações de ambiente ou usar valores padrão
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'));
// Validação de valores mínimos
const validPoolMin = Math.max(1, poolMin);
const validPoolMax = Math.max(validPoolMin + 1, poolMax);
const validPoolIncrement = Math.max(1, poolIncrement);
// Certifique-se de que poolMax é maior que poolMin
if (validPoolMax <= validPoolMin) {
console.warn('Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1');
}
@@ -40,7 +36,6 @@ export function createOracleConfig(config: ConfigService): DataSourceOptions {
logging: config.get('NODE_ENV') === 'development',
entities: [__dirname + '/../**/*.entity.{ts,js}'],
extra: {
// Configurações de pool
poolMin: validPoolMin,
poolMax: validPoolMax,
poolIncrement: validPoolIncrement,

View File

@@ -4,8 +4,13 @@
https://docs.nestjs.com/controllers#controllers
*/
import { Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@ApiTags('CRM - Reason Table')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('api/v1/crm/reason')
export class ReasonTableController {

View File

@@ -51,6 +51,8 @@ export class DataConsultController {
return this.dataConsultService.customers(filter);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('products/:filter')
@ApiOperation({ summary: 'Busca produtos filtrados' })
@ApiParam({ name: 'filter', description: 'Filtro de busca' })

View File

@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
@@ -9,7 +9,8 @@ import {
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
import { ConfigService } from '@nestjs/config';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import * as os from 'os';
@ApiTags('Health Check')
@@ -26,10 +27,11 @@ export class HealthController {
private dbPoolStats: DbPoolStatsIndicator,
private configService: ConfigService,
) {
// Define o caminho correto para o disco, baseado no sistema operacional
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get()
@HealthCheck()
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
@@ -59,6 +61,8 @@ export class HealthController {
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('db')
@HealthCheck()
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
@@ -69,6 +73,8 @@ export class HealthController {
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('memory')
@HealthCheck()
@ApiOperation({ summary: 'Verificar uso de memória' })
@@ -79,6 +85,8 @@ export class HealthController {
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('disk')
@HealthCheck()
@ApiOperation({ summary: 'Verificar espaço em disco' })
@@ -97,6 +105,8 @@ export class HealthController {
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('pool')
@HealthCheck()
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' })

View File

@@ -1,12 +1,15 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { OrdersPaymentService } from './orders-payment.service';
import { OrderDto } from './dto/order.dto';
import { PaymentDto } from './dto/payment.dto';
import { CreatePaymentDto } from './dto/create-payment.dto';
import { CreateInvoiceDto } from './dto/create-invoice.dto';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@ApiTags('Orders Payment')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment')
export class OrdersPaymentController {