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:
3
.env
3
.env
@@ -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
300
API.md
@@ -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
|
|
||||||
}'
|
|
||||||
141
CONTRIBUTING.md
141
CONTRIBUTING.md
@@ -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
157
README.md
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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:
|
|
||||||
146
health-check.md
146
health-check.md
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||

|
|
||||||
|
|
||||||
**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
|
|
||||||
: 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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/auth/auth/dto/refresh-token.dto.ts
Normal file
26
src/auth/auth/dto/refresh-token.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { IsString, IsNotEmpty } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RefreshTokenDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||||
|
description: 'Refresh token para renovar o access token',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RefreshTokenResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||||
|
description: 'Novo access token',
|
||||||
|
})
|
||||||
|
accessToken: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 28800,
|
||||||
|
description: 'Tempo de expiração em segundos',
|
||||||
|
})
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
53
src/auth/auth/dto/session.dto.ts
Normal file
53
src/auth/auth/dto/session.dto.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class SessionInfoDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'abc123def456',
|
||||||
|
description: 'ID da sessão',
|
||||||
|
})
|
||||||
|
sessionId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '192.168.1.100',
|
||||||
|
description: 'IP de origem da sessão',
|
||||||
|
})
|
||||||
|
ipAddress: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
description: 'User agent da sessão',
|
||||||
|
})
|
||||||
|
userAgent: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2025-09-16T17:30:00.000Z',
|
||||||
|
description: 'Data de criação da sessão',
|
||||||
|
})
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2025-09-16T17:30:00.000Z',
|
||||||
|
description: 'Última atividade da sessão',
|
||||||
|
})
|
||||||
|
lastActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: true,
|
||||||
|
description: 'Se é a sessão atual',
|
||||||
|
})
|
||||||
|
isCurrent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SessionsResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
type: [SessionInfoDto],
|
||||||
|
description: 'Lista de sessões ativas',
|
||||||
|
})
|
||||||
|
sessions: SessionInfoDto[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 3,
|
||||||
|
description: 'Total de sessões ativas',
|
||||||
|
})
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
49
src/auth/guards/rate-limiting.guard.ts
Normal file
49
src/auth/guards/rate-limiting.guard.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RateLimitingGuard implements CanActivate {
|
||||||
|
constructor(private readonly rateLimitingService: RateLimitingService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const ip = this.getClientIp(request);
|
||||||
|
|
||||||
|
const isAllowed = await this.rateLimitingService.isAllowed(ip);
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
const attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||||
|
data: null,
|
||||||
|
details: {
|
||||||
|
attempts: attemptInfo.attempts,
|
||||||
|
remainingTime: attemptInfo.remainingTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrai o IP real do cliente considerando proxies
|
||||||
|
* @param request Objeto de requisição
|
||||||
|
* @returns Endereço IP do cliente
|
||||||
|
*/
|
||||||
|
private getClientIp(request: any): string {
|
||||||
|
return (
|
||||||
|
request.headers['x-forwarded-for']?.split(',')[0] ||
|
||||||
|
request.headers['x-real-ip'] ||
|
||||||
|
request.connection?.remoteAddress ||
|
||||||
|
request.socket?.remoteAddress ||
|
||||||
|
request.ip ||
|
||||||
|
'127.0.0.1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,7 @@ export interface JwtPayload {
|
|||||||
storeId: string;
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
126
src/auth/services/rate-limiting.service.ts
Normal file
126
src/auth/services/rate-limiting.service.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
|
|
||||||
|
export interface RateLimitConfig {
|
||||||
|
maxAttempts: number;
|
||||||
|
windowMs: number;
|
||||||
|
blockDurationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RateLimitingService {
|
||||||
|
private readonly defaultConfig: RateLimitConfig = {
|
||||||
|
maxAttempts: 5, // 5 tentativas
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutos
|
||||||
|
blockDurationMs: 30 * 60 * 1000, // 30 minutos de bloqueio
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se o IP pode fazer uma tentativa de login
|
||||||
|
* @param ip Endereço IP do cliente
|
||||||
|
* @param config Configuração personalizada (opcional)
|
||||||
|
* @returns true se permitido, false se bloqueado
|
||||||
|
*/
|
||||||
|
async isAllowed(ip: string, config?: Partial<RateLimitConfig>): Promise<boolean> {
|
||||||
|
const finalConfig = { ...this.defaultConfig, ...config };
|
||||||
|
const key = this.buildAttemptKey(ip);
|
||||||
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|
||||||
|
// Verifica se está bloqueado
|
||||||
|
const isBlocked = await this.redis.get(blockKey);
|
||||||
|
if (isBlocked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conta tentativas na janela de tempo
|
||||||
|
const attempts = await this.redis.get<string>(key);
|
||||||
|
const attemptCount = attempts ? parseInt(attempts) : 0;
|
||||||
|
|
||||||
|
if (attemptCount >= finalConfig.maxAttempts) {
|
||||||
|
// Bloqueia o IP
|
||||||
|
await this.redis.set(blockKey, 'blocked', finalConfig.blockDurationMs / 1000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra uma tentativa de login
|
||||||
|
* @param ip Endereço IP do cliente
|
||||||
|
* @param success true se login foi bem-sucedido
|
||||||
|
* @param config Configuração personalizada (opcional)
|
||||||
|
*/
|
||||||
|
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> {
|
||||||
|
const finalConfig = { ...this.defaultConfig, ...config };
|
||||||
|
const key = this.buildAttemptKey(ip);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await this.redis.del(key);
|
||||||
|
} else {
|
||||||
|
const attempts = await this.redis.get<string>(key);
|
||||||
|
const attemptCount = attempts ? parseInt(attempts) + 1 : 1;
|
||||||
|
|
||||||
|
await this.redis.set(key, attemptCount.toString(), finalConfig.windowMs / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtém informações sobre tentativas de um IP
|
||||||
|
* @param ip Endereço IP do cliente
|
||||||
|
* @returns Informações sobre tentativas
|
||||||
|
*/
|
||||||
|
async getAttemptInfo(ip: string): Promise<{
|
||||||
|
attempts: number;
|
||||||
|
isBlocked: boolean;
|
||||||
|
remainingTime?: number;
|
||||||
|
}> {
|
||||||
|
const key = this.buildAttemptKey(ip);
|
||||||
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|
||||||
|
const attempts = await this.redis.get<string>(key);
|
||||||
|
const isBlocked = await this.redis.get(blockKey);
|
||||||
|
const ttl = await this.redis.ttl(blockKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
attempts: attempts ? parseInt(attempts) : 0,
|
||||||
|
isBlocked: !!isBlocked,
|
||||||
|
remainingTime: isBlocked ? ttl : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa tentativas de um IP (útil para testes ou admin)
|
||||||
|
* @param ip Endereço IP do cliente
|
||||||
|
*/
|
||||||
|
async clearAttempts(ip: string): Promise<void> {
|
||||||
|
const key = this.buildAttemptKey(ip);
|
||||||
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|
||||||
|
await this.redis.del(key);
|
||||||
|
await this.redis.del(blockKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a chave para armazenar tentativas
|
||||||
|
* @param ip Endereço IP
|
||||||
|
* @returns Chave para o Redis
|
||||||
|
*/
|
||||||
|
private buildAttemptKey(ip: string): string {
|
||||||
|
return `auth:rate_limit:attempts:${ip}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a chave para armazenar bloqueio
|
||||||
|
* @param ip Endereço IP
|
||||||
|
* @returns Chave para o Redis
|
||||||
|
*/
|
||||||
|
private buildBlockKey(ip: string): string {
|
||||||
|
return `auth:rate_limit:blocked:${ip}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/auth/services/refresh-token.service.ts
Normal file
173
src/auth/services/refresh-token.service.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Injectable, Inject, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { JwtPayload } from '../models/jwt-payload.model';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
export interface RefreshTokenData {
|
||||||
|
userId: number;
|
||||||
|
tokenId: string;
|
||||||
|
expiresAt: number;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RefreshTokenService {
|
||||||
|
private readonly REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 dias em segundos
|
||||||
|
private readonly MAX_REFRESH_TOKENS_PER_USER = 5; // Máximo 5 refresh tokens por usuário
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gera um novo refresh token para o usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @returns Refresh token
|
||||||
|
*/
|
||||||
|
async generateRefreshToken(userId: number): Promise<string> {
|
||||||
|
const tokenId = randomBytes(32).toString('hex');
|
||||||
|
const refreshToken = this.jwtService.sign(
|
||||||
|
{ userId, tokenId, type: 'refresh' },
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenData: RefreshTokenData = {
|
||||||
|
userId,
|
||||||
|
tokenId,
|
||||||
|
expiresAt: Date.now() + (this.REFRESH_TOKEN_TTL * 1000),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||||
|
await this.redis.set(key, tokenData, this.REFRESH_TOKEN_TTL);
|
||||||
|
|
||||||
|
// Limita o número de refresh tokens por usuário
|
||||||
|
await this.limitRefreshTokensPerUser(userId);
|
||||||
|
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida um refresh token e retorna os dados do usuário
|
||||||
|
* @param refreshToken Token de refresh
|
||||||
|
* @returns Dados do usuário se válido
|
||||||
|
*/
|
||||||
|
async validateRefreshToken(refreshToken: string): Promise<JwtPayload> {
|
||||||
|
try {
|
||||||
|
const decoded = this.jwtService.verify(refreshToken) as any;
|
||||||
|
|
||||||
|
if (decoded.type !== 'refresh') {
|
||||||
|
throw new UnauthorizedException('Token inválido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tokenId } = decoded;
|
||||||
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||||
|
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
||||||
|
|
||||||
|
if (!tokenData) {
|
||||||
|
throw new UnauthorizedException('Refresh token expirado ou inválido');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenData.expiresAt < Date.now()) {
|
||||||
|
await this.revokeRefreshToken(userId, tokenId);
|
||||||
|
throw new UnauthorizedException('Refresh token expirado');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
sellerId: 0,
|
||||||
|
storeId: '',
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
tokenId
|
||||||
|
} as JwtPayload;
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Refresh token inválido');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoga um refresh token específico
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param tokenId ID do token
|
||||||
|
*/
|
||||||
|
async revokeRefreshToken(userId: number, tokenId: string): Promise<void> {
|
||||||
|
const key = this.buildRefreshTokenKey(userId, tokenId);
|
||||||
|
await this.redis.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoga todos os refresh tokens de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
*/
|
||||||
|
async revokeAllRefreshTokens(userId: number): Promise<void> {
|
||||||
|
const pattern = this.buildRefreshTokenPattern(userId);
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await this.redis.del(...keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista todos os refresh tokens ativos de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @returns Lista de tokens ativos
|
||||||
|
*/
|
||||||
|
async getActiveRefreshTokens(userId: number): Promise<RefreshTokenData[]> {
|
||||||
|
const pattern = this.buildRefreshTokenPattern(userId);
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
const tokens: RefreshTokenData[] = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
||||||
|
if (tokenData && tokenData.expiresAt > Date.now()) {
|
||||||
|
tokens.push(tokenData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limita o número de refresh tokens por usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
*/
|
||||||
|
private async limitRefreshTokensPerUser(userId: number): Promise<void> {
|
||||||
|
const activeTokens = await this.getActiveRefreshTokens(userId);
|
||||||
|
|
||||||
|
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
||||||
|
// Remove os tokens mais antigos
|
||||||
|
const tokensToRemove = activeTokens
|
||||||
|
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
||||||
|
.map(token => token.tokenId);
|
||||||
|
|
||||||
|
for (const tokenId of tokensToRemove) {
|
||||||
|
await this.revokeRefreshToken(userId, tokenId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a chave para armazenar o refresh token
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param tokenId ID do token
|
||||||
|
* @returns Chave para o Redis
|
||||||
|
*/
|
||||||
|
private buildRefreshTokenKey(userId: number, tokenId: string): string {
|
||||||
|
return `auth:refresh_tokens:${userId}:${tokenId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói o padrão para buscar refresh tokens de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @returns Padrão para o Redis
|
||||||
|
*/
|
||||||
|
private buildRefreshTokenPattern(userId: number): string {
|
||||||
|
return `auth:refresh_tokens:${userId}:*`;
|
||||||
|
}
|
||||||
|
}
|
||||||
198
src/auth/services/session-management.service.ts
Normal file
198
src/auth/services/session-management.service.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
|
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
export interface SessionData {
|
||||||
|
sessionId: string;
|
||||||
|
userId: number;
|
||||||
|
ipAddress: string;
|
||||||
|
userAgent: string;
|
||||||
|
createdAt: number;
|
||||||
|
lastActivity: number;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionManagementService {
|
||||||
|
private readonly SESSION_TTL = 8 * 60 * 60; // 8 horas em segundos
|
||||||
|
private readonly MAX_SESSIONS_PER_USER = 5; // Máximo 5 sessões por usuário
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria uma nova sessão para o usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param ipAddress Endereço IP
|
||||||
|
* @param userAgent User agent
|
||||||
|
* @returns Dados da sessão criada
|
||||||
|
*/
|
||||||
|
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
||||||
|
const sessionId = randomBytes(16).toString('hex');
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const sessionData: SessionData = {
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
createdAt: now,
|
||||||
|
lastActivity: now,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = this.buildSessionKey(userId, sessionId);
|
||||||
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
|
|
||||||
|
// Limita o número de sessões por usuário
|
||||||
|
await this.limitSessionsPerUser(userId);
|
||||||
|
|
||||||
|
return sessionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualiza a última atividade de uma sessão
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param sessionId ID da sessão
|
||||||
|
*/
|
||||||
|
async updateSessionActivity(userId: number, sessionId: string): Promise<void> {
|
||||||
|
const key = this.buildSessionKey(userId, sessionId);
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
|
||||||
|
if (sessionData) {
|
||||||
|
sessionData.lastActivity = Date.now();
|
||||||
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista todas as sessões ativas de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param currentSessionId ID da sessão atual (opcional)
|
||||||
|
* @returns Lista de sessões ativas
|
||||||
|
*/
|
||||||
|
async getActiveSessions(userId: number, currentSessionId?: string): Promise<SessionData[]> {
|
||||||
|
const pattern = this.buildSessionPattern(userId);
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
const sessions: SessionData[] = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
if (sessionData && sessionData.isActive) {
|
||||||
|
// Marca se é a sessão atual
|
||||||
|
if (currentSessionId && sessionData.sessionId === currentSessionId) {
|
||||||
|
sessionData.isActive = true; // Mantém como ativa
|
||||||
|
}
|
||||||
|
sessions.push(sessionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.sort((a, b) => b.lastActivity - a.lastActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encerra uma sessão específica
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param sessionId ID da sessão
|
||||||
|
*/
|
||||||
|
async terminateSession(userId: number, sessionId: string): Promise<void> {
|
||||||
|
const key = this.buildSessionKey(userId, sessionId);
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
|
||||||
|
if (!sessionData) {
|
||||||
|
throw new NotFoundException('Sessão não encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData.isActive = false;
|
||||||
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encerra todas as sessões de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
*/
|
||||||
|
async terminateAllSessions(userId: number): Promise<void> {
|
||||||
|
const pattern = this.buildSessionPattern(userId);
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
if (sessionData) {
|
||||||
|
sessionData.isActive = false;
|
||||||
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encerra todas as sessões de um usuário exceto a atual
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param currentSessionId ID da sessão atual
|
||||||
|
*/
|
||||||
|
async terminateOtherSessions(userId: number, currentSessionId: string): Promise<void> {
|
||||||
|
const pattern = this.buildSessionPattern(userId);
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
if (sessionData && sessionData.sessionId !== currentSessionId) {
|
||||||
|
sessionData.isActive = false;
|
||||||
|
await this.redis.set(key, sessionData, this.SESSION_TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se uma sessão está ativa
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param sessionId ID da sessão
|
||||||
|
* @returns true se a sessão estiver ativa
|
||||||
|
*/
|
||||||
|
async isSessionActive(userId: number, sessionId: string): Promise<boolean> {
|
||||||
|
const key = this.buildSessionKey(userId, sessionId);
|
||||||
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
|
||||||
|
return sessionData ? sessionData.isActive : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limita o número de sessões por usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
*/
|
||||||
|
private async limitSessionsPerUser(userId: number): Promise<void> {
|
||||||
|
const activeSessions = await this.getActiveSessions(userId);
|
||||||
|
|
||||||
|
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
|
||||||
|
// Remove as sessões mais antigas
|
||||||
|
const sessionsToRemove = activeSessions
|
||||||
|
.slice(this.MAX_SESSIONS_PER_USER)
|
||||||
|
.map(session => session.sessionId);
|
||||||
|
|
||||||
|
for (const sessionId of sessionsToRemove) {
|
||||||
|
await this.terminateSession(userId, sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a chave para armazenar a sessão
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @param sessionId ID da sessão
|
||||||
|
* @returns Chave para o Redis
|
||||||
|
*/
|
||||||
|
private buildSessionKey(userId: number, sessionId: string): string {
|
||||||
|
return `auth:sessions:${userId}:${sessionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói o padrão para buscar sessões de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
* @returns Padrão para o Redis
|
||||||
|
*/
|
||||||
|
private buildSessionPattern(userId: number): string {
|
||||||
|
return `auth:sessions:${userId}:*`;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/auth/services/token-blacklist.service.ts
Normal file
103
src/auth/services/token-blacklist.service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { JwtPayload } from '../models/jwt-payload.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TokenBlacklistService {
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adiciona um token à blacklist
|
||||||
|
* @param token Token JWT a ser invalidado
|
||||||
|
* @param expiresIn Tempo de expiração do token em segundos
|
||||||
|
*/
|
||||||
|
async addToBlacklist(token: string, expiresIn?: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const decoded = this.jwtService.decode(token) as JwtPayload;
|
||||||
|
if (!decoded) {
|
||||||
|
throw new Error('Token inválido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blacklistKey = this.buildBlacklistKey(token);
|
||||||
|
const ttl = expiresIn || this.calculateTokenTTL(decoded);
|
||||||
|
|
||||||
|
await this.redis.set(blacklistKey, 'blacklisted', ttl);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Erro ao adicionar token à blacklist: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se um token está na blacklist
|
||||||
|
* @param token Token JWT a ser verificado
|
||||||
|
* @returns true se o token estiver blacklistado
|
||||||
|
*/
|
||||||
|
async isBlacklisted(token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const blacklistKey = this.buildBlacklistKey(token);
|
||||||
|
const result = await this.redis.get(blacklistKey);
|
||||||
|
return result === 'blacklisted';
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove um token da blacklist (útil para testes)
|
||||||
|
* @param token Token JWT a ser removido
|
||||||
|
*/
|
||||||
|
async removeFromBlacklist(token: string): Promise<void> {
|
||||||
|
const blacklistKey = this.buildBlacklistKey(token);
|
||||||
|
await this.redis.del(blacklistKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa todos os tokens blacklistados de um usuário
|
||||||
|
* @param userId ID do usuário
|
||||||
|
*/
|
||||||
|
async clearUserBlacklist(userId: number): Promise<void> {
|
||||||
|
const pattern = `auth:blacklist:${userId}:*`;
|
||||||
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await this.redis.del(...keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a chave para armazenar o token na blacklist
|
||||||
|
* @param token Token JWT
|
||||||
|
* @returns Chave para o Redis
|
||||||
|
*/
|
||||||
|
private buildBlacklistKey(token: string): string {
|
||||||
|
const decoded = this.jwtService.decode(token) as JwtPayload;
|
||||||
|
const tokenHash = this.hashToken(token);
|
||||||
|
return `auth:blacklist:${decoded.id}:${tokenHash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula o TTL do token baseado na expiração
|
||||||
|
* @param payload Payload do JWT
|
||||||
|
* @returns TTL em segundos
|
||||||
|
*/
|
||||||
|
private calculateTokenTTL(payload: JwtPayload): number {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const exp = payload.exp || (now + 8 * 60 * 60); // 8h padrão
|
||||||
|
return Math.max(0, exp - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gera um hash do token para usar como identificador único
|
||||||
|
* @param token Token JWT
|
||||||
|
* @returns Hash do token
|
||||||
|
*/
|
||||||
|
private hashToken(token: string): string {
|
||||||
|
const crypto = require('crypto');
|
||||||
|
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import { JwtPayload } from '../models/jwt-payload.model';
|
|||||||
import { UserRepository } from '../../auth/users/UserRepository';
|
import { 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 {
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
3
src/core/configs/cache/IRedisClient.ts
vendored
3
src/core/configs/cache/IRedisClient.ts
vendored
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
145
src/core/configs/cache/index.html
vendored
145
src/core/configs/cache/index.html
vendored
@@ -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<T>(key: string): Promise<T | null>;
|
|
||||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
||||||
del(key: string): Promise<void>;
|
|
||||||
}</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) => {
|
|
||||||
const redis = new Redis({
|
|
||||||
host: configService.get('REDIS_HOST', '10.1.1.109'),
|
|
||||||
port: configService.get('REDIS_PORT', 6379),
|
|
||||||
});
|
|
||||||
|
|
||||||
redis.on('error', (err) => {
|
|
||||||
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<T>(key: string): Promise<T | null> {
|
|
||||||
const data = await this.redis.get(key);
|
|
||||||
return data ? JSON.parse(data) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async set<T>(key: string, value: T, ttlSeconds = 300): Promise<void> {
|
|
||||||
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async del(key: string): Promise<void> {
|
|
||||||
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<T>('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>
|
|
||||||
18
src/core/configs/cache/redis-client.adapter.ts
vendored
18
src/core/configs/cache/redis-client.adapter.ts
vendored
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/core/configs/cache/redis.provider.ts
vendored
7
src/core/configs/cache/redis.provider.ts
vendored
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user