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

3
.env
View File

@@ -31,8 +31,7 @@ THROTTLE_LIMIT=10
NODE_ENV=development NODE_ENV=development
ORACLE_CLIENT_LIB_DIR=C:\\instantclient_19_25 ORACLE_CLIENT_LIB_DIR=C:\\instantclient_23_9

300
API.md
View File

@@ -1,300 +0,0 @@
# Portal Juru API Documentation
## Índice
1. [Autenticação](#autenticação)
2. [Endpoints](#endpoints)
- [Consulta de Dados](#consulta-de-dados)
- [Pedidos e Pagamentos](#pedidos-e-pagamentos)
3. [Modelos de Dados](#modelos-de-dados)
4. [Exemplos de Uso](#exemplos-de-uso)
5. [Códigos de Erro](#códigos-de-erro)
## Autenticação
A API utiliza autenticação JWT. Para acessar os endpoints protegidos, inclua o token no header:
```
Authorization: Bearer seu-token-jwt
```
## Endpoints
### Consulta de Dados
#### Listar Lojas
```http
GET /api/v1/data-consult/stores
```
**Resposta**
```json
[
{
"id": "001",
"name": "Loja Principal",
"store": "001 - Loja Principal"
}
]
```
#### Listar Vendedores
```http
GET /api/v1/data-consult/sellers
```
**Resposta**
```json
[
{
"id": "001",
"name": "João Silva"
}
]
```
#### Consultar Faturamento
```http
GET /api/v1/data-consult/billings
```
**Resposta**
```json
[
{
"id": "001",
"date": "2024-04-02T10:00:00Z",
"total": 1000.00
}
]
```
#### Filtrar Clientes
```http
GET /api/v1/data-consult/customers/:filter
```
**Parâmetros**
- `filter`: Termo de busca (nome, documento, etc.)
**Resposta**
```json
[
{
"id": "001",
"name": "Maria Silva",
"document": "123.456.789-00"
}
]
```
#### Buscar Produtos
```http
GET /api/v1/data-consult/products/:filter
```
**Parâmetros**
- `filter`: Termo de busca (nome, código, etc.)
**Resposta**
```json
[
{
"id": "001",
"name": "Produto Exemplo",
"manufacturerCode": "ABC123"
}
]
```
### Pedidos e Pagamentos
#### Listar Pedidos da Loja
```http
GET /api/v1/orders-payment/orders/:id
```
**Parâmetros**
- `id`: ID da loja
**Resposta**
```json
[
{
"createDate": "2024-04-02T10:00:00Z",
"storeId": "001",
"orderId": 12345,
"customerId": "001",
"customerName": "João Silva",
"sellerId": "001",
"sellerName": "Maria Santos",
"billingId": "001",
"billingName": "Cartão de Crédito",
"planId": "001",
"planName": "3x sem juros",
"amount": 1000.00,
"installments": 3,
"amountPaid": 1000.00
}
]
```
#### Buscar Pedido Específico
```http
GET /api/v1/orders-payment/orders/:id/:orderId
```
**Parâmetros**
- `id`: ID da loja
- `orderId`: ID do pedido
**Resposta**
```json
{
"createDate": "2024-04-02T10:00:00Z",
"storeId": "001",
"orderId": 12345,
"customerId": "001",
"customerName": "João Silva",
"sellerId": "001",
"sellerName": "Maria Santos",
"billingId": "001",
"billingName": "Cartão de Crédito",
"planId": "001",
"planName": "3x sem juros",
"amount": 1000.00,
"installments": 3,
"amountPaid": 1000.00
}
```
#### Listar Pagamentos do Pedido
```http
GET /api/v1/orders-payment/payments/:id
```
**Parâmetros**
- `id`: ID do pedido
**Resposta**
```json
[
{
"orderId": 12345,
"payDate": "2024-04-02T10:00:00Z",
"card": "**** **** **** 1234",
"installments": 3,
"flagName": "VISA",
"type": "CREDITO",
"amount": 1000.00,
"userId": "001",
"nsu": "123456789",
"auth": "A12345"
}
]
```
#### Criar Pagamento
```http
POST /api/v1/orders-payment/payments/create
```
**Corpo da Requisição**
```json
{
"orderId": 12345,
"card": "**** **** **** 1234",
"auth": "A12345",
"nsu": "123456789",
"installments": 3,
"amount": 1000.00,
"flagName": "VISA",
"paymentType": "CREDITO",
"userId": 1
}
```
#### Criar Fatura
```http
POST /api/v1/orders-payment/invoice/create
```
**Corpo da Requisição**
```json
{
"orderId": 12345,
"userId": 1
}
```
## Modelos de Dados
### OrderDto
```typescript
interface OrderDto {
createDate: Date; // Data de criação do pedido
storeId: string; // ID da loja
orderId: number; // ID do pedido
customerId: string; // ID do cliente
customerName: string; // Nome do cliente
sellerId: string; // ID do vendedor
sellerName: string; // Nome do vendedor
billingId: string; // ID da forma de pagamento
billingName: string; // Nome da forma de pagamento
planId: string; // ID do plano de pagamento
planName: string; // Nome do plano de pagamento
amount: number; // Valor total do pedido
installments: number; // Número de parcelas
amountPaid: number; // Valor total pago
}
```
### PaymentDto
```typescript
interface PaymentDto {
orderId: number; // ID do pedido
payDate: Date; // Data do pagamento
card: string; // Número do cartão
installments: number; // Número de parcelas
flagName: string; // Nome da bandeira
type: string; // Tipo de pagamento
amount: number; // Valor do pagamento
userId: string; // ID do usuário
nsu: string; // NSU da transação
auth: string; // Código de autorização
}
```
## Códigos de Erro
| Código | Descrição |
|--------|-----------|
| 400 | Requisição inválida |
| 401 | Não autorizado |
| 403 | Acesso negado |
| 404 | Recurso não encontrado |
| 500 | Erro interno do servidor |
## Exemplos de Uso
### Exemplo de Requisição com cURL
```bash
# Listar pedidos de uma loja
curl -X GET "http://localhost:3000/api/v1/orders-payment/orders/001" \
-H "Authorization: Bearer seu-token-jwt"
# Criar novo pagamento
curl -X POST "http://localhost:3000/api/v1/orders-payment/payments/create" \
-H "Authorization: Bearer seu-token-jwt" \
-H "Content-Type: application/json" \
-d '{
"orderId": 12345,
"card": "**** **** **** 1234",
"auth": "A12345",
"nsu": "123456789",
"installments": 3,
"amount": 1000.00,
"flagName": "VISA",
"paymentType": "CREDITO",
"userId": 1
}'

View File

@@ -1,141 +0,0 @@
# Guia de Contribuição
Obrigado por considerar contribuir para o Portal Juru API! Este documento fornece um conjunto de diretrizes para contribuir com o projeto.
## Como Contribuir
1. Faça um fork do projeto
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
4. Push para a branch (`git push origin feature/AmazingFeature`)
5. Abra um Pull Request
## Padrões de Código
### TypeScript/JavaScript
- Use TypeScript para todo o código
- Siga o [style guide oficial do TypeScript](https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html)
- Use ESLint e Prettier para formatação
- Mantenha o código limpo e bem documentado
### NestJS
- Siga as [melhores práticas do NestJS](https://docs.nestjs.com/recipes/prisma)
- Use decorators apropriadamente
- Mantenha os módulos bem organizados
- Use DTOs para validação de dados
### Banco de Dados
- Use TypeORM para todas as operações de banco de dados
- Mantenha as queries SQL otimizadas
- Use transações quando necessário
- Documente as queries complexas
## Estrutura do Projeto
```
src/
├── core/ # Configurações e utilitários core
│ ├── configs/ # Configurações do projeto
│ ├── database/ # Configuração do banco de dados
│ └── constants/ # Constantes do sistema
├── data-consult/ # Módulo de consulta de dados
│ ├── dto/ # Data Transfer Objects
│ ├── controllers/ # Controladores
│ └── services/ # Serviços
├── orders-payment/ # Módulo de pedidos e pagamentos
│ ├── dto/ # Data Transfer Objects
│ ├── controllers/ # Controladores
│ └── services/ # Serviços
└── orders/ # Módulo de pedidos
├── modules/ # Módulos
├── controllers/ # Controladores
├── services/ # Serviços
└── repositories/ # Repositórios
```
## Convenções de Commits
Use o seguinte formato para commits:
```
<type>(<scope>): <subject>
<body>
<footer>
```
Tipos de commit:
- `feat`: Nova feature
- `fix`: Correção de bug
- `docs`: Mudanças na documentação
- `style`: Formatação, ponto e vírgula, etc.
- `refactor`: Refatoração de código
- `test`: Adição ou correção de testes
- `chore`: Atualização de tarefas, configuração, etc.
Exemplo:
```
feat(orders): adiciona endpoint para criar pedidos
- Implementa validação de dados
- Adiciona testes unitários
- Atualiza documentação
Closes #123
```
## Testes
- Mantenha a cobertura de testes acima de 80%
- Use Jest para testes
- Escreva testes unitários e de integração
- Execute todos os testes antes de submeter um PR
```bash
# Executar testes
npm run test
# Executar testes com cobertura
npm run test:cov
```
## Documentação
- Mantenha a documentação atualizada
- Use Swagger para documentação da API
- Documente todas as novas features
- Atualize o README quando necessário
## Pull Requests
1. Descreva claramente as mudanças
2. Inclua exemplos de uso quando relevante
3. Atualize a documentação
4. Certifique-se que todos os testes passam
5. Solicite revisão de pelo menos um mantenedor
## Issues
- Use o template apropriado
- Forneça informações detalhadas
- Inclua exemplos de reprodução quando possível
- Use labels apropriadas
## Código de Conduta
- Seja respeitoso
- Mantenha discussões construtivas
- Aceite críticas construtivas
- Ajude outros contribuidores
## Suporte
Se você tiver dúvidas ou precisar de ajuda:
- Abra uma issue
- Entre em contato com os mantenedores
- Consulte a documentação

157
README.md
View File

@@ -1,157 +0,0 @@
# Portal Juru API
API para gerenciamento de pedidos, pagamentos e consultas de dados do sistema Portal Juru.
## 🚀 Tecnologias
- [NestJS](https://nestjs.com/) - Framework Node.js
- [TypeORM](https://typeorm.io/) - ORM para banco de dados
- [Swagger](https://swagger.io/) - Documentação da API
- [Oracle Database](https://www.oracle.com/database/) - Banco de dados
- [Redis](https://redis.io/) - Cache em memória
## 📋 Pré-requisitos
- Node.js (v16 ou superior)
- Oracle Database
- Redis
- npm ou yarn
## 🔧 Instalação
1. Clone o repositório:
```bash
git clone https://github.com/seu-usuario/portaljuru-api.git
cd portaljuru-api
```
2. Instale as dependências:
```bash
npm install
# ou
yarn install
```
3. Configure as variáveis de ambiente:
```bash
cp .env.example .env
# Edite o arquivo .env com suas configurações
```
4. Inicie o servidor:
```bash
npm run start:dev
# ou
yarn start:dev
```
## 📚 Documentação da API
A documentação completa da API está disponível em `/api` quando o servidor estiver rodando.
### Endpoints Principais
#### Consulta de Dados
- `GET /api/v1/data-consult/stores` - Lista todas as lojas
- `GET /api/v1/data-consult/sellers` - Lista todos os vendedores
- `GET /api/v1/data-consult/billings` - Retorna informações de faturamento
- `GET /api/v1/data-consult/customers/:filter` - Filtra clientes
- `GET /api/v1/data-consult/products/:filter` - Busca produtos filtrados
#### Pedidos e Pagamentos
- `GET /api/v1/orders-payment/orders/:id` - Lista pedidos de uma loja
- `GET /api/v1/orders-payment/orders/:id/:orderId` - Busca pedido específico
- `GET /api/v1/orders-payment/payments/:id` - Lista pagamentos de um pedido
- `POST /api/v1/orders-payment/payments/create` - Cria novo pagamento
- `POST /api/v1/orders-payment/invoice/create` - Cria nova fatura
## 🛠️ Estrutura do Projeto
```
src/
├── core/ # Configurações e utilitários core
│ ├── configs/ # Configurações do projeto
│ ├── database/ # Configuração do banco de dados
│ └── constants/ # Constantes do sistema
├── data-consult/ # Módulo de consulta de dados
│ ├── dto/ # Data Transfer Objects
│ ├── controllers/ # Controladores
│ └── services/ # Serviços
├── orders-payment/ # Módulo de pedidos e pagamentos
│ ├── dto/ # Data Transfer Objects
│ ├── controllers/ # Controladores
│ └── services/ # Serviços
└── orders/ # Módulo de pedidos
├── modules/ # Módulos
├── controllers/ # Controladores
├── services/ # Serviços
└── repositories/ # Repositórios
```
## 🔐 Autenticação
A API utiliza autenticação JWT. Para acessar os endpoints protegidos, inclua o token no header:
```
Authorization: Bearer seu-token-jwt
```
## 📦 DTOs (Data Transfer Objects)
### OrderDto
```typescript
{
createDate: Date; // Data de criação do pedido
storeId: string; // ID da loja
orderId: number; // ID do pedido
customerId: string; // ID do cliente
customerName: string; // Nome do cliente
sellerId: string; // ID do vendedor
sellerName: string; // Nome do vendedor
billingId: string; // ID da forma de pagamento
billingName: string; // Nome da forma de pagamento
planId: string; // ID do plano de pagamento
planName: string; // Nome do plano de pagamento
amount: number; // Valor total do pedido
installments: number; // Número de parcelas
amountPaid: number; // Valor total pago
}
```
### PaymentDto
```typescript
{
orderId: number; // ID do pedido
payDate: Date; // Data do pagamento
card: string; // Número do cartão
installments: number; // Número de parcelas
flagName: string; // Nome da bandeira
type: string; // Tipo de pagamento
amount: number; // Valor do pagamento
userId: string; // ID do usuário
nsu: string; // NSU da transação
auth: string; // Código de autorização
}
```
## 🧪 Testes
Para executar os testes:
```bash
npm run test
# ou
yarn test
```
## 📝 Licença
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
## 🤝 Contribuição
1. Faça o fork do projeto
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
4. Push para a branch (`git push origin feature/AmazingFeature`)
5. Abra um Pull Request

View File

@@ -1,120 +0,0 @@
# Configuração de Pool de Conexões
Este documento descreve a configuração do pool de conexões implementada para os bancos de dados Oracle e PostgreSQL na aplicação.
## Visão Geral
O pool de conexões é uma técnica que mantém um conjunto de conexões abertas com o banco de dados, permitindo seu reuso entre diferentes requisições. Isso traz diversos benefícios:
- **Melhor performance**: Eliminação do overhead de abertura e fechamento de conexões
- **Melhor escalabilidade**: Gerenciamento eficiente do número máximo de conexões
- **Maior resiliência**: Tratamento de falhas de conexão e reconexão automática
- **Menor carga no banco de dados**: Menor número de operações de login/logout
## Configuração do Oracle
### Parâmetros Configuráveis
Os seguintes parâmetros podem ser configurados através do arquivo `.env`:
| Parâmetro | Descrição | Valor Padrão |
|-----------|-----------|--------------|
| `ORACLE_POOL_MIN` | Número mínimo de conexões no pool | 5 |
| `ORACLE_POOL_MAX` | Número máximo de conexões no pool | 20 |
| `ORACLE_POOL_INCREMENT` | Incremento no número de conexões quando necessário | 5 |
| `ORACLE_POOL_TIMEOUT` | Tempo máximo (ms) para obter uma conexão | 30000 |
| `ORACLE_POOL_IDLE_TIMEOUT` | Tempo máximo (ms) que uma conexão pode ficar inativa | 300000 |
### Configurações Adicionais
Além dos parâmetros acima, as seguintes configurações foram implementadas:
- **Statement Cache**: Cache de 30 statements para melhorar a performance de queries repetidas
- **Connection Class**: Identificador 'PORTALJURU' para rastrear conexões no banco
- **Pool Ping Interval**: Verificação a cada 60 segundos para manter conexões ativas
- **Enable Stats**: Habilitação de estatísticas para monitoramento do pool
## Configuração do PostgreSQL
### Parâmetros Configuráveis
Os seguintes parâmetros podem ser configurados através do arquivo `.env`:
| Parâmetro | Descrição | Valor Padrão |
|-----------|-----------|--------------|
| `POSTGRES_POOL_MIN` | Número mínimo de conexões no pool | 5 |
| `POSTGRES_POOL_MAX` | Número máximo de conexões no pool | 20 |
| `POSTGRES_POOL_IDLE_TIMEOUT` | Tempo máximo (ms) que uma conexão pode ficar inativa | 30000 |
| `POSTGRES_POOL_CONNECTION_TIMEOUT` | Tempo máximo (ms) para estabelecer uma conexão | 5000 |
| `POSTGRES_POOL_ACQUIRE_TIMEOUT` | Tempo máximo (ms) para obter uma conexão do pool | 60000 |
### Configurações Adicionais
Além dos parâmetros acima, as seguintes configurações foram implementadas:
- **Statement Timeout**: Limite de 10 segundos para execução de queries
- **Query Timeout**: Limite de 10 segundos para execução de queries
- **SSL**: Configuração automática baseada no ambiente (development/production)
- **Query Cache**: Cache de 60 segundos para resultados de consultas
## Validação de Valores
O sistema implementa validação rigorosa para garantir valores apropriados:
- **Conversão Explícita**: Todos os valores de configuração são explicitamente convertidos para números (parseInt)
- **Valores Mínimos**: Cada parâmetro tem um valor mínimo aplicado automaticamente
- poolMin: no mínimo 1
- poolMax: no mínimo poolMin + 1
- timeouts: no mínimo 1000ms
- **Validação de Relações**: O sistema garante que poolMax seja sempre maior que poolMin
- **Arredondamento**: Valores convertidos de milissegundos para segundos são arredondados (Math.floor)
Estas validações previnem erros comuns como:
- "NJS-007: invalid value for poolMax"
- Timeouts negativos ou muito baixos
- Problemas de conversão entre strings e números
## Boas Práticas
### Dimensionamento do Pool
O dimensionamento do pool de conexões depende da carga esperada:
1. **Fórmula Básica**: `connections = ((core_count * 2) + effective_spindle_count)`
2. **Ambiente Web**: Considere o número máximo de requisições concorrentes
3. **Regra 80-20**: O pool deve ser dimensionado para acomodar 80% da carga de pico
### Monitoramento
Para garantir o funcionamento adequado do pool, monitore:
- **Taxa de uso do pool**: Número de conexões ativas vs. total
- **Tempo de espera por conexão**: Tempo médio para obter uma conexão
- **Erros de timeout**: Número de falhas por timeout
- **Conexões mortas**: Conexões que falharam mas não foram removidas do pool
### Ajuste Fino
Para ambientes de produção, considere os seguintes ajustes:
1. **Tamanho do pool**: Ajuste baseado no número de requisições concorrentes
2. **Tempo de idle**: Reduza em ambientes com muitos usuários esporádicos
3. **Tempo de timeout**: Aumente em caso de operações mais longas
4. **Statement cache**: Aumente para aplicações com queries repetitivas
## Implementação no TypeORM
A configuração foi implementada nos arquivos:
- `src/core/configs/typeorm.oracle.config.ts`
- `src/core/configs/typeorm.postgres.config.ts`
Estas configurações são carregadas no início da aplicação e aplicadas a todas as conexões.
### Melhores Práticas para Diagnóstico
1. **Use o endpoint de health check**: Verifique estatísticas do pool em `/health/pool`
2. **Analise logs**: Procure por padrões de erro relacionados a conexões
3. **Monitore performance**: Observe tempos de resposta e correlacione com o uso do pool

View File

@@ -1,30 +0,0 @@
# Atualizações de Dependências
## Dependências Atualizadas
- `@nestjs/mapped-types`: de 1.0.0 para 2.1.0
- `@nestjs/swagger`: de 7.4.2 para 11.1.0
- `bullmq`: de 5.45.2 para 5.46.0
- `oracledb`: de 5.5.0 para 6.8.0
- `reflect-metadata`: de 0.1.14 para 0.2.2
## Dependências de Desenvolvimento Atualizadas
- `@types/node`: para 22.14.0
- `rimraf`: para 6.0.1
## Dependências Que Ainda Precisam Ser Atualizadas
- Pacotes de teste (Jest): A atualização do Jest (de 26.x para 29.x) requer uma migração significativa e pode quebrar testes existentes.
- Prettier e ESLint: Estes podem ser atualizados em uma fase posterior.
## Vulnerabilidades
- Ainda existem 18 vulnerabilidades (16 moderadas, 2 altas) relacionadas principalmente ao Jest, que é usado apenas para testes.
- Para resolver todas as vulnerabilidades, incluindo mudanças significativas, você poderia executar `npm audit fix --force`, mas isso poderia quebrar funcionalidades existentes.
## Próximos Passos Recomendados
1. Atualizar o Jest separadamente, fazendo os ajustes necessários nos testes
2. Atualizar o ESLint e Prettier para versões mais recentes
3. Verificar a compatibilidade entre as dependências atualizadas e as que não foram atualizadas
4. Considerar atualizações de TypeORM e outros pacotes específicos do domínio
## Observações
- Usamos `--legacy-peer-deps` para contornar conflitos de dependências. Isso pode mascarar incompatibilidades reais entre pacotes.
- Recomenda-se testar a aplicação extensivamente após essas atualizações.

View File

@@ -1,60 +0,0 @@
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./monitoring/prometheus/:/etc/prometheus/
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring-network
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/provisioning/:/etc/grafana/provisioning/
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- monitoring-network
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
restart: unless-stopped
volumes:
- ./monitoring/alertmanager/:/etc/alertmanager/
command:
- '--config.file=/etc/alertmanager/config.yml'
- '--storage.path=/alertmanager'
ports:
- "9093:9093"
networks:
- monitoring-network
networks:
monitoring-network:
driver: bridge
volumes:
prometheus_data:
grafana_data:

View File

@@ -1,146 +0,0 @@
# Health Check da API
## Descrição
O sistema de Health Check implementado permite monitorar a saúde da aplicação e seus componentes críticos, como bancos de dados, uso de memória e espaço em disco. Isso facilita a detecção precoce de problemas e ajuda a manter a estabilidade da aplicação.
## Endpoints Disponíveis
### Verificação Geral
- **URL**: `/health`
- **Método**: `GET`
- **Descrição**: Verifica a saúde geral da aplicação, incluindo:
- Status da API (ping)
- Uso de disco
- Uso de memória
- Conexões de banco de dados (Oracle e PostgreSQL)
### Verificação de Banco de Dados
- **URL**: `/health/db`
- **Método**: `GET`
- **Descrição**: Verifica apenas as conexões de banco de dados (Oracle e PostgreSQL)
### Verificação de Memória
- **URL**: `/health/memory`
- **Método**: `GET`
- **Descrição**: Verifica o uso de memória da aplicação (heap e RSS)
### Verificação de Disco
- **URL**: `/health/disk`
- **Método**: `GET`
- **Descrição**: Verifica o espaço disponível em disco
## Formato de Resposta
A resposta segue o formato padrão do Terminus:
```json
{
"status": "ok",
"info": {
"api": {
"status": "up"
},
"disk": {
"status": "up"
},
"memory_heap": {
"status": "up"
},
"oracle": {
"status": "up"
},
"postgres": {
"status": "up"
}
},
"error": {},
"details": {
"api": {
"status": "up"
},
"disk": {
"status": "up",
"freeBytes": 53687091200,
"usedBytes": 170573111296,
"totalBytes": 224260202496
},
"memory_heap": {
"status": "up",
"usedBytes": 45731840,
"thresholdBytes": 157286400
},
"oracle": {
"status": "up"
},
"postgres": {
"status": "up"
}
}
}
```
## Níveis de Status
- **up**: O componente está funcionando corretamente
- **down**: O componente está com problemas
- **ok**: Todos os componentes estão funcionando corretamente
- **error**: Um ou mais componentes estão com problemas
## Integração com Monitoramento
### Prometheus (Recomendado)
Para integrar com Prometheus, instale e configure o pacote `@willsoto/nestjs-prometheus`:
```bash
npm install @willsoto/nestjs-prometheus prom-client --save
```
E então adicione o PrometheusModule ao HealthModule:
```typescript
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
@Module({
imports: [
// ...
PrometheusModule.register(),
],
})
export class HealthModule {}
```
### Integração com Ferramentas de APM
Os health checks podem ser integrados com ferramentas de Application Performance Monitoring (APM) como:
- New Relic
- Datadog
- Dynatrace
- Grafana
## Configuração de Alertas
Recomenda-se configurar alertas para quando os health checks falharem:
1. **Alertas de Banco de Dados**: Notificação imediata para problemas em conexões de banco
2. **Alertas de Memória**: Alerta quando o uso de memória estiver próximo ao limite
3. **Alertas de Disco**: Alerta quando o espaço em disco estiver abaixo do limite seguro
## Uso em Kubernetes
Se estiver usando Kubernetes, você pode integrar esses health checks como Readiness e Liveness Probes:
```yaml
readinessProbe:
httpGet:
path: /health
port: 8066
initialDelaySeconds: 15
periodSeconds: 30
```

View File

@@ -1,179 +0,0 @@
# Implementação de Monitoramento e Alertas
## Visão Geral
Este documento descreve a implementação de um sistema completo de monitoramento e alertas para o Portal Jurunense API, baseado em Prometheus, Grafana e AlertManager. O sistema permite visualizar métricas de performance, configurar dashboards personalizados e receber alertas automáticos quando ocorrerem problemas.
## Componentes Implementados
### 1. Coleta de Métricas
#### Métricas da API
- **HTTP**: Requisições, duração, códigos de status
- **Recursos**: Uso de CPU, memória, disco
- **Banco de Dados**: Conexões de pool, duração de queries
- **Negócio**: Métricas específicas do domínio (pedidos, pagamentos, etc.)
#### Implementação
- Integração com `@willsoto/nestjs-prometheus`
- Endpoint `/metrics` exposto para scraping do Prometheus
- Interceptor para coleta automática de métricas HTTP
- Serviço personalizado para métricas de negócio
### 2. Dashboards Grafana
#### Dashboard Principal
![Dashboard Principal](https://via.placeholder.com/800x400?text=Portal+Jurunense+Main+Dashboard)
**Painéis Incluídos:**
- Visão geral da saúde da aplicação
- Taxa de requisições por segundo
- Latência das requisições (percentis)
- Uso de recursos (CPU, memória, disco)
- Taxa de erros HTTP
#### Dashboard de Banco de Dados
![Dashboard de Banco de Dados](
**Painéis Incluídos:**
- Uso do pool de conexões
- Tempo de resposta de queries
- Queries em execução
- Erros de banco de dados
- Saúde das conexões
#### Dashboard de Negócio
![Dashboard de Negócio]
**Painéis Incluídos:**
- Pedidos por hora/dia
- Taxa de conversão de pagamentos
- Erros de processamento
- Tempo de resposta de APIs externas
- Alertas ativos
### 3. Sistema de Alertas
#### Alertas Implementados
- **Disponibilidade**: Serviço fora do ar
- **Performance**: Tempo de resposta elevado
- **Recursos**: Uso elevado de memória, CPU ou disco
- **Banco de Dados**: Pool de conexões quase esgotado
- **Negócio**: Erros de integração com serviços externos
#### Canais de Notificação
- **Email**: Para alertas não críticos e relatórios diários
- **Slack**: Para alertas em tempo real, com cores baseadas na severidade
- **Microsoft Teams**: Alternativa para organizações que usam Teams
- **SMS/Chamada** (opcional): Para alertas críticos fora do horário comercial
### 4. Arquitetura de Monitoramento
```
┌─────────────┐ ┌────────────┐ ┌─────────────┐
│ API │ │ Prometheus │ │ Grafana │
│ (/metrics) │───▶│ (Coleta) │───▶│ (Dashboards)│
└─────────────┘ └────────────┘ └─────────────┘
│ ▲
▼ │
┌────────────┐ ┌─────────────┐
│AlertManager│───▶│ Notificações│
│ (Alertas) │ │ (Email/Slack)│
└────────────┘ └─────────────┘
```
## Configuração e Uso
### Iniciando o Sistema de Monitoramento
```bash
# Iniciar todos os serviços de monitoramento
docker-compose -f docker-compose.monitoring.yml up -d
# Verificar status dos serviços
docker-compose -f docker-compose.monitoring.yml ps
# Visualizar logs
docker-compose -f docker-compose.monitoring.yml logs -f
```
### Acessando as Interfaces
- **Prometheus**: http://localhost:9090
- **Grafana**: http://localhost:3000 (admin/admin)
- **AlertManager**: http://localhost:9093
### Customizando Alertas
1. Edite o arquivo `monitoring/prometheus/rules/portaljuru_alerts.yml`
2. Adicione ou modifique regras de alerta usando a sintaxe PromQL
3. Recarregue a configuração: `curl -X POST http://localhost:9090/-/reload`
### Customizando Dashboards
1. Acesse o Grafana: http://localhost:3000
2. Faça login com as credenciais padrão (admin/admin)
3. Navegue até Dashboards > Browse
4. Clone um dashboard existente ou crie um novo
5. Use o editor visual para adicionar painéis e consultas
## Integração com APM (Application Performance Monitoring)
Além do monitoramento baseado em métricas, implementamos integração com ferramentas de APM para rastreamento distribuído e profiling:
### Jaeger para Rastreamento Distribuído
- **Endpoint**: http://localhost:16686
- **Features**:
- Visualização de traces completos de requisições
- Análise de gargalos em calls entre serviços
- Rastreamento de erros e exceções
### Configuração de Rastreamento Distribuído
O código abaixo foi adicionado para habilitar o rastreamento:
```typescript
// Trecho simplificado. O código completo está no módulo de saúde.
import { TracingModule } from './tracing.module';
@Module({
imports: [
TracingModule.forRoot({
serviceName: 'portaljuru-api',
samplingRate: 0.3,
}),
// ...
],
})
export class AppModule {}
```
## Monitoramento Proativo
Implementamos monitoramento proativo através de:
1. **Health Checks Periódicos**: Verificações automáticas a cada 5 minutos
2. **Alertas Preditivos**: Baseados em tendências anômalas de métricas
3. **Relatórios Diários**: Resumo automático enviado diariamente por email
4. **Página de Status**: Disponível em `/health/status` para usuários finais
## Boas Práticas
1. **Métricas Relevantes**: Foco em métricas que refletem a experiência do usuário
2. **Alertas Acionáveis**: Somente alertar em situações que precisam de ação humana
3. **Redução de Ruído**: Agrupamento e correlação de alertas para evitar fadiga
4. **Documentation as Code**: Dashboards e regras de alerta versionadas no git
5. **Runbooks**: Documentação de resposta para cada tipo de alerta
## Próximos Passos
1. **Expandir Métricas de Negócio**: Adicionar KPIs específicos para cada domínio
2. **Machine Learning**: Implementar detecção de anomalias baseada em ML
3. **Logs Centralizados**: Integrar com ELK Stack para correlação logs-métricas
4. **SLOs e SLIs**: Definir e monitorar objetivos de nível de serviço
5. **Automação de Remediação**: Scripts para resposta automática a problemas comuns
## Conclusão
O sistema de monitoramento implementado proporciona visibilidade completa sobre a saúde e performance do Portal Jurunense API. Com dashboards intuitivos e alertas precisos, a equipe pode detectar e resolver problemas rapidamente, reduzindo o tempo médio de recuperação (MTTR) e melhorando a confiabilidade do serviço.

View File

@@ -1,59 +0,0 @@
# Melhorias de Segurança Implementadas
## 1. Proteção de Informações Sensíveis
- **Variáveis de Ambiente**: Removidas credenciais expostas do arquivo `.env` e substituídas por valores genéricos.
- **Gitignore**: Atualizado para garantir que arquivos `.env` não sejam acidentalmente versionados.
- **Exemplo de ENV**: Criado arquivo `.env.example` como modelo sem credenciais reais para ser versionado.
## 2. Proteção Contra Ataques de Força Bruta
- **Rate Limiting**: Implementado middleware `RateLimiterMiddleware` para limitar o número de requisições por IP e rota.
- **Throttler Integrado**: Configurado corretamente o ThrottlerModule do NestJS para rate limiting baseado em rotas.
- **Headers de Rate Limit**: Adicionados cabeçalhos HTTP para informar o cliente sobre limites de requisição.
- **Configuração Flexível**: Os limites de rate limiting são configuráveis via variáveis de ambiente (`THROTTLE_TTL` e `THROTTLE_LIMIT`).
- **Aplicação Seletiva**: Aplicado principalmente em rotas sensíveis como autenticação e usuários.
## 3. Validação e Sanitização de Dados
- **Validadores Personalizados**: Criados decorators `IsSanitized` e `IsSecureId` para validação rigorosa de entradas.
- **Middleware de Sanitização**: Implementado `RequestSanitizerMiddleware` para limpar automaticamente todos os dados de entrada.
- **Proteção Contra Injeções**: Adicionadas verificações contra SQL Injection, NoSQL Injection e XSS.
- **Validação Global**: Configuração de `ValidationPipe` com opções mais rigorosas no arquivo `main.ts`.
## 4. Cabeçalhos HTTP Seguros
- **Helmet**: Adicionado o middleware Helmet para configurar cabeçalhos HTTP seguros.
- **CORS Restritivo**: Configuração mais rigorosa para CORS, limitando origens em produção.
- **Headers de Segurança**: Adicionados headers para proteção contra XSS, clickjacking e sniffing.
## 5. Recomendações Adicionais
- **Ambiente de Produção**: A configuração de segurança diferencia entre desenvolvimento e produção.
- **Mensagens de Erro**: Desativação de mensagens de erro detalhadas em produção para evitar vazamento de informações.
- **Autenticação**: Configuração de tempo de expiração do JWT mais adequada para balancear segurança e experiência.
## Como Usar
Os novos recursos de segurança são aplicados automaticamente na aplicação. Para utilizar os validadores personalizados em DTOs, importe-os assim:
```typescript
import { IsSanitized, IsSecureId } from '../common/validators/sanitize.validator';
export class ExampleDto {
@IsSecureId()
id: string;
@IsSanitized()
@IsString()
content: string;
}
```
## Próximos Passos Recomendados
1. Implementar auditoria de segurança regular
2. Configurar autenticação de dois fatores
3. Realizar análise estática de código para buscar vulnerabilidades adicionais
4. Implementar verificação de força de senha
5. Adicionar proteção contra CSRF para operações sensíveis

View File

@@ -4,6 +4,11 @@ import {
HttpException, HttpException,
HttpStatus, HttpStatus,
Post, Post,
Get,
Delete,
UseGuards,
Request,
Param,
} from '@nestjs/common'; } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
@@ -12,12 +17,22 @@ import { LoginResponseDto } from './dto/LoginResponseDto';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { ResultModel } from 'src/core/models/result.model'; import { ResultModel } from 'src/core/models/result.model';
import { AuthService } from './auth.service'; 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 { import {
ApiTags, ApiTags,
ApiOperation, ApiOperation,
ApiBody, ApiBody,
ApiOkResponse, ApiOkResponse,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
ApiBearerAuth,
ApiTooManyRequestsResponse,
ApiParam,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
@ApiTags('Auth') @ApiTags('Auth')
@@ -26,9 +41,13 @@ export class AuthController {
constructor( constructor(
private readonly commandBus: CommandBus, private readonly commandBus: CommandBus,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly rateLimitingService: RateLimitingService,
private readonly refreshTokenService: RefreshTokenService,
private readonly sessionManagementService: SessionManagementService,
) {} ) {}
@Post('login') @Post('login')
@UseGuards(RateLimitingGuard)
@ApiOperation({ summary: 'Realiza login e retorna um token JWT' }) @ApiOperation({ summary: 'Realiza login e retorna um token JWT' })
@ApiBody({ type: LoginDto }) @ApiBody({ type: LoginDto })
@ApiOkResponse({ @ApiOkResponse({
@@ -36,24 +55,44 @@ export class AuthController {
type: LoginResponseDto, type: LoginResponseDto,
}) })
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' }) @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 command = new AuthenticateUserCommand(dto.username, dto.password);
const result = await this.commandBus.execute(command); const result = await this.commandBus.execute(command);
if (!result.success) { if (!result.success) {
// Registra tentativa falhada
await this.rateLimitingService.recordAttempt(ip, false);
throw new HttpException( throw new HttpException(
new ResultModel(false, result.error, null, result.error), new ResultModel(false, result.error, null, result.error),
HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED,
); );
} }
// Registra tentativa bem-sucedida (limpa contador)
await this.rateLimitingService.recordAttempt(ip, true);
const user = result.data; 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.id,
user.sellerId, user.sellerId,
user.name, user.name,
user.email, user.email,
user.storeId, user.storeId,
session.sessionId,
); );
return { return {
@@ -63,7 +102,124 @@ export class AuthController {
username: user.name, username: user.name,
storeId: user.storeId, storeId: user.storeId,
email: user.email, 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 { CqrsModule } from '@nestjs/cqrs';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthenticateUserHandler } from './commands/authenticate-user.service'; 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({ @Module({
imports: [ imports: [
@@ -29,7 +33,15 @@ import { AuthenticateUserHandler } from './commands/authenticate-user.service';
UsersModule, UsersModule,
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService, JwtStrategy], providers: [
AuthService,
JwtStrategy,
TokenBlacklistService,
RateLimitingService,
RefreshTokenService,
SessionManagementService,
AuthenticateUserHandler
],
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} 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 { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { JwtPayload } from '../models/jwt-payload.model'; import { JwtPayload } from '../models/jwt-payload.model';
import { UserRepository } from '../users/UserRepository'; import { UserRepository } from '../users/UserRepository';
import { TokenBlacklistService } from '../services/token-blacklist.service';
import { RefreshTokenService } from '../services/refresh-token.service';
@Injectable() @Injectable()
@@ -11,30 +13,97 @@ export class AuthService {
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly userRepository: UserRepository, 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 = { const user: JwtPayload = {
id: id, id: id,
sellerId: sellerId, sellerId: sellerId,
storeId: storeId, storeId: storeId,
username: username, username: username,
email: email, email: email,
sessionId: sessionId,
}; };
const options: JwtSignOptions = { expiresIn: '8h' }; const options: JwtSignOptions = { expiresIn: '8h' };
return this.jwtService.sign(user, options); 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> { async validateUser(payload: JwtPayload): Promise<JwtPayload | null> {
const user = await this.userRepository.findById(payload.id); const user = await this.userRepository.findById(payload.id);
if (!user || !user.active) return null; if (!user || user.situacao === 'I' || user.dataDesligamento) return null;
return { return {
id: user.id, id: user.id,
sellerId: user.sellerId, sellerId: user.sellerId,
storeId: user.storeId, storeId: user.storeId,
username: user.username, username: user.name, // Usando name como username para consistência
email: user.email, 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() username: string;
@ApiProperty() storeId: string; @ApiProperty() storeId: string;
@ApiProperty() email: 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; storeId: string;
username: string; username: string;
email: 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 { UserRepository } from '../../auth/users/UserRepository';
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../../core/configs/cache/IRedisClient'; import { IRedisClient } from '../../core/configs/cache/IRedisClient';
import { TokenBlacklistService } from '../services/token-blacklist.service';
import { SessionManagementService } from '../services/session-management.service';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
@@ -14,6 +16,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
@Inject(RedisClientToken) private readonly redis: IRedisClient, @Inject(RedisClientToken) private readonly redis: IRedisClient,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly tokenBlacklistService: TokenBlacklistService,
private readonly sessionManagementService: SessionManagementService,
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 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 sessionKey = this.buildSessionKey(payload.id);
const cachedUser = await this.redis.get<any>(sessionKey); const cachedUser = await this.redis.get<any>(sessionKey);
if (cachedUser) { if (cachedUser) {
// await this.auditAccess(cachedUser); return {
return cachedUser; 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); 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'); throw new UnauthorizedException('Usuário inválido ou inativo');
} }
await this.redis.set(sessionKey, user, 60 * 60 * 8); // 8h const userData = {
return {
id: user.id, id: user.id,
name: user.name, sellerId: user.sellerId,
storeId: user.storeId,
username: user.name,
email: user.email, 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 { private buildSessionKey(userId: number): string {

View File

@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { UserRepository } from './UserRepository'; import { UserRepository } from './UserRepository';
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
import { ResetPasswordService } from './reset-password.service'; import { ResetPasswordService } from './reset-password.service';
import { ChangePasswordService } from './change-password.service'; import { ChangePasswordService } from './change-password.service';
import { EmailService } from './email.service'; import { EmailService } from './email.service';
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
@Module({ @Module({
@@ -15,10 +16,11 @@ import { EmailService } from './email.service';
providers: [ providers: [
UsersService, UsersService,
UserRepository, UserRepository,
AuthenticateUserHandler,
ResetPasswordService, ResetPasswordService,
ChangePasswordService, ChangePasswordService,
EmailService, EmailService,
AuthenticateUserHandler,
AuthenticateUserCommand,
], ],
exports: [UsersService, UserRepository], exports: [UsersService, UserRepository],
}) })

View File

@@ -2,5 +2,8 @@ export interface IRedisClient {
get<T>(key: string): Promise<T | null>; get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>; set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
del(key: string): 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); await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
} }
async del(key: string): Promise<void> { async del(key: string): Promise<void>;
await this.redis.del(key); 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', provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
const redis = new Redis({ const redis = new Redis({
host: configService.get<string>('REDIS_HOST', 'redis-17317.crce181.sa-east-1-2.ec2.redns.redis-cloud.com'), host: configService.get<string>('REDIS_HOST', '10.1.1.124'),
port: configService.get<number>('REDIS_PORT', 17317), port: configService.get<number>('REDIS_PORT', 6379),
username: configService.get<string>('REDIS_USERNAME', 'default' ), password: configService.get<string>('REDIS_PASSWORD', '1234'),
password: configService.get<string>('REDIS_PASSWORD', 'd8sVttpJdNxrWjYRK43QGAKzEt3I8HVc'),
}); });
redis.on('error', (err) => { 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 }); oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
// Definir a estratégia de pool padrão para Oracle // 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 { 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 poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20')); const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5')); const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000')); const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000')); const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'));
// Validação de valores mínimos
const validPoolMin = Math.max(1, poolMin); const validPoolMin = Math.max(1, poolMin);
const validPoolMax = Math.max(validPoolMin + 1, poolMax); const validPoolMax = Math.max(validPoolMin + 1, poolMax);
const validPoolIncrement = Math.max(1, poolIncrement); const validPoolIncrement = Math.max(1, poolIncrement);
// Certifique-se de que poolMax é maior que poolMin
if (validPoolMax <= validPoolMin) { if (validPoolMax <= validPoolMin) {
console.warn('Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1'); 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', logging: config.get('NODE_ENV') === 'development',
entities: [__dirname + '/../**/*.entity.{ts,js}'], entities: [__dirname + '/../**/*.entity.{ts,js}'],
extra: { extra: {
// Configurações de pool
poolMin: validPoolMin, poolMin: validPoolMin,
poolMax: validPoolMax, poolMax: validPoolMax,
poolIncrement: validPoolIncrement, poolIncrement: validPoolIncrement,

View File

@@ -4,8 +4,13 @@
https://docs.nestjs.com/controllers#controllers 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') @Controller('api/v1/crm/reason')
export class ReasonTableController { export class ReasonTableController {

View File

@@ -51,6 +51,8 @@ export class DataConsultController {
return this.dataConsultService.customers(filter); return this.dataConsultService.customers(filter);
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('products/:filter') @Get('products/:filter')
@ApiOperation({ summary: 'Busca produtos filtrados' }) @ApiOperation({ summary: 'Busca produtos filtrados' })
@ApiParam({ name: 'filter', description: 'Filtro de busca' }) @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 { import {
HealthCheck, HealthCheck,
HealthCheckService, HealthCheckService,
@@ -9,7 +9,8 @@ import {
import { TypeOrmHealthIndicator } from './indicators/typeorm.health'; import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health'; import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
import { ConfigService } from '@nestjs/config'; 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'; import * as os from 'os';
@ApiTags('Health Check') @ApiTags('Health Check')
@@ -26,10 +27,11 @@ export class HealthController {
private dbPoolStats: DbPoolStatsIndicator, private dbPoolStats: DbPoolStatsIndicator,
private configService: ConfigService, private configService: ConfigService,
) { ) {
// Define o caminho correto para o disco, baseado no sistema operacional
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/'; this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get() @Get()
@HealthCheck() @HealthCheck()
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' }) @ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
@@ -59,6 +61,8 @@ export class HealthController {
]); ]);
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('db') @Get('db')
@HealthCheck() @HealthCheck()
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' }) @ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
@@ -69,6 +73,8 @@ export class HealthController {
]); ]);
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('memory') @Get('memory')
@HealthCheck() @HealthCheck()
@ApiOperation({ summary: 'Verificar uso de memória' }) @ApiOperation({ summary: 'Verificar uso de memória' })
@@ -79,6 +85,8 @@ export class HealthController {
]); ]);
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('disk') @Get('disk')
@HealthCheck() @HealthCheck()
@ApiOperation({ summary: 'Verificar espaço em disco' }) @ApiOperation({ summary: 'Verificar espaço em disco' })
@@ -97,6 +105,8 @@ export class HealthController {
]); ]);
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('pool') @Get('pool')
@HealthCheck() @HealthCheck()
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' }) @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 { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { OrdersPaymentService } from './orders-payment.service'; import { OrdersPaymentService } from './orders-payment.service';
import { OrderDto } from './dto/order.dto'; import { OrderDto } from './dto/order.dto';
import { PaymentDto } from './dto/payment.dto'; import { PaymentDto } from './dto/payment.dto';
import { CreatePaymentDto } from './dto/create-payment.dto'; import { CreatePaymentDto } from './dto/create-payment.dto';
import { CreateInvoiceDto } from './dto/create-invoice.dto'; import { CreateInvoiceDto } from './dto/create-invoice.dto';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@ApiTags('Orders Payment') @ApiTags('Orders Payment')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment') @Controller('api/v1/orders-payment')
export class OrdersPaymentController { export class OrdersPaymentController {