swagger configurado na rota chavenfe

This commit is contained in:
unknown
2025-03-29 15:06:21 -03:00
parent 5e4ef04b4a
commit 32bd7c14b3
29 changed files with 709 additions and 241 deletions

2
.env
View File

@@ -12,3 +12,5 @@ POSTGRES_PASSWORD=ti
POSTGRES_DB=ksdb POSTGRES_DB=ksdb

View File

@@ -1,4 +1,7 @@
{ {
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src" "sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
} }

1
package-lock.json generated
View File

@@ -6292,7 +6292,6 @@
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz",
"integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@ioredis/commands": "^1.1.1", "@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",

View File

@@ -10,7 +10,19 @@ import { UsersService } from '../users/users.service';
import { LoginDto } from '../auth/dto/login.dto'; import { LoginDto } from '../auth/dto/login.dto';
import { ResultModel } from 'src/core/models/result.model'; import { ResultModel } from 'src/core/models/result.model';
import { ApiBody, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { ApiBody, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { ApiResponse } from '@nestjs/swagger';
import { ApiOperation } from '@nestjs/swagger';
import { ApiBearerAuth } from '@nestjs/swagger';
import { ApiCreatedResponse } from '@nestjs/swagger';
import { ApiBadRequestResponse } from '@nestjs/swagger';
import { ApiNotFoundResponse } from '@nestjs/swagger';
import { ApiInternalServerErrorResponse } from '@nestjs/swagger';
import { ApiForbiddenResponse } from '@nestjs/swagger';
import { LoginResponseDto } from './dto/LoginResponseDto';
@ApiTags('Auth')
@Controller('api/v1/auth') @Controller('api/v1/auth')
export class AuthController { export class AuthController {
constructor( constructor(
@@ -19,24 +31,25 @@ export class AuthController {
) {} ) {}
@Post('login') @Post('login')
@ApiOperation({ summary: 'Realiza login e retorna um token JWT' })
@ApiBody({ type: LoginDto }) @ApiBody({ type: LoginDto })
@ApiOkResponse({ description: 'Login realizado com sucesso' }) @ApiOkResponse({
description: 'Login realizado com sucesso',
type: LoginResponseDto,
})
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' }) @ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
async login(@Body() model: LoginDto): Promise<any> { async login(@Body() model: LoginDto): Promise<LoginResponseDto> {
const result = await this.usersService.authenticate({ const result = await this.usersService.authenticate({
userName: model.username, userName: model.username,
password: model.password, password: model.password,
}); });
console.log('Resultado da autenticação:', result); if (!result.success) {
throw new HttpException(
if (!result.success) { new ResultModel(false, result.error, null, result.error),
throw new HttpException( HttpStatus.UNAUTHORIZED,
new ResultModel(false, result.error, null, result.error), );
HttpStatus.UNAUTHORIZED, }
);
}
const user = result.data; const user = result.data;

View File

@@ -1,26 +1,25 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller'; import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module'; import { AuthService } from './auth.service';
import { JwtStrategy } from '../strategies/jwt-strategy'; import { JwtStrategy } from '../strategies/jwt-strategy';
import { RedisModule } from 'src/core/configs/cache/redis.module';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
@Module({ @Module({
imports: [ imports: [
UsersModule,
PassportModule.register({
defaultStrategy: 'jwt',
}),
JwtModule.register({ JwtModule.register({
secret: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', secret: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9',
signOptions: { signOptions: { expiresIn: '8h' },
expiresIn: 3600,
},
}), }),
PassportModule.register({ defaultStrategy: 'jwt' }),
RedisModule,
UsersModule,
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService,JwtStrategy], providers: [AuthService, JwtStrategy],
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -4,6 +4,8 @@ import { Injectable } 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 Redis from 'ioredis';

View File

@@ -1,82 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
exports.__esModule = true;
exports.AuthService = void 0;
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
var common_1 = require("@nestjs/common");
var AuthService = /** @class */ (function () {
function AuthService(usersService, jwtService) {
this.usersService = usersService;
this.jwtService = jwtService;
}
AuthService.prototype.createToken = function (id, sellerId, username, email, storeId) {
return __awaiter(this, void 0, void 0, function () {
var user, options;
return __generator(this, function (_a) {
user = {
id: id,
sellerId: sellerId,
storeId: storeId,
username: username,
email: email
};
options = { expiresIn: '8h' };
return [2 /*return*/, this.jwtService.sign(user, options)];
});
});
};
AuthService.prototype.validateUser = function (payload) {
return __awaiter(this, void 0, Promise, function () {
return __generator(this, function (_a) {
return [2 /*return*/, payload];
});
});
};
AuthService = __decorate([
common_1.Injectable()
], AuthService);
return AuthService;
}());
exports.AuthService = AuthService;

View File

@@ -0,0 +1,12 @@
// login-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class LoginResponseDto {
@ApiProperty() id: number;
@ApiProperty() sellerId: number;
@ApiProperty() name: string;
@ApiProperty() username: string;
@ApiProperty() storeId: string;
@ApiProperty() email: string;
@ApiProperty() token: string;
}

View File

@@ -0,0 +1,118 @@
<!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 - Fluxo de Login</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>🔐 Fluxo de Login - Portal Juru API</h1>
<h2>📌 Rota de Login</h2>
<p><strong>URL:</strong> <code>POST /api/v1/auth/login</code></p>
<p><strong>Descrição:</strong> Autentica o usuário, valida regras de negócio e retorna um token JWT.</p>
<p><strong>Acesso:</strong> Público</p>
<h3>📤 Body (JSON)</h3>
<pre>{
"username": "joelson.r",
"password": "1010"
}</pre>
<h3>✅ Resposta (200 OK)</h3>
<pre>{
"id": 1498,
"sellerId": 2013,
"name": "JOELSON DE BRITO RIBEIRO",
"username": "JOELSON DE BRITO RIBEIRO",
"storeId": "4",
"email": "JOELSON.R@JURUNENSE.COM.BR",
"token": "eyJhbGciOiJIUzI1NiIsInR5..."
}</pre>
<h3>❌ Resposta (401 Unauthorized)</h3>
<pre>{
"success": false,
"message": "Usuário ou senha inválidos.",
"data": null,
"error": "Usuário ou senha inválidos."
}</pre>
<h2>🧱 Camadas e Responsabilidades</h2>
<table>
<tr><th>Camada</th><th>Responsabilidade</th></tr>
<tr><td>AuthController</td><td>Recebe requisição e coordena autenticação</td></tr>
<tr><td>UsersService</td><td>Orquestra os casos de uso (login, reset, troca senha)</td></tr>
<tr><td>AuthenticateUserService</td><td>Executa lógica de autenticação e validações</td></tr>
<tr><td>UserRepository</td><td>Executa SQL diretamente no Oracle</td></tr>
<tr><td>AuthService</td><td>Gera o token JWT com os dados do usuário</td></tr>
<tr><td>JwtStrategy</td><td>Valida o token em rotas protegidas, usando Redis como cache</td></tr>
<tr><td>RedisClientAdapter</td><td>Wrapper de acesso ao Redis com interface genérica e TTL</td></tr>
</table>
<h2>🧊 Redis na Autenticação</h2>
<ul>
<li><strong>Chave:</strong> <code>session:{userId}</code></li>
<li><strong>Valor:</strong> JSON serializado do usuário autenticado</li>
<li><strong>TTL:</strong> 8 horas</li>
<li><strong>Fallback:</strong> Se o cache não existir, consulta ao banco</li>
<li><strong>Auditoria:</strong> espaço reservado para log de acesso</li>
</ul>
<h2>🔐 Proteção de Rotas</h2>
<p>Rotas protegidas utilizam <code>@UseGuards(JwtAuthGuard)</code> e <code>@ApiBearerAuth()</code>.</p>
<p><strong>Header necessário:</strong></p>
<code>Authorization: Bearer &lt;token&gt;</code>
<h2>🚀 Melhorias Futuras</h2>
<ul>
<li>[ ] Blacklist de tokens para logout</li>
<li>[ ] Log de auditoria</li>
<li>[ ] Refresh Token</li>
<li>[ ] Controle de permissões/roles</li>
</ul>
<p><strong>Última atualização:</strong> 28/03/2025</p>
</body>
</html>

View File

@@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ // ✅ jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from '../models/jwt-payload.model'; import { JwtPayload } from '../models/jwt-payload.model';
import { AuthService } from '../auth/auth.service'; import { UserRepository } from '../../auth/users/UserRepository';
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) { constructor(
@Inject(RedisClientToken) private readonly redis: IRedisClient,
private readonly userRepository: UserRepository,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', secretOrKey: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9',
@@ -15,10 +20,24 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
} }
async validate(payload: JwtPayload) { async validate(payload: JwtPayload) {
const user = await this.authService.validateUser(payload); const sessionKey = `session:${payload.id}`;
if (!user) { const user = await this.redis.get<any>(sessionKey);
throw new UnauthorizedException();
if (user) {
// Audit log placeholder
// await this.auditAccess(user);
return user;
} }
return user;
const userDb = await this.userRepository.findById(payload.id);
if (!userDb || userDb.situacao === 'I' || userDb.dataDesligamento) {
throw new UnauthorizedException('Usuário inválido ou inativo');
}
await this.redis.set(sessionKey, userDb, 60 * 60 * 8);
// Audit fallback
// await this.auditAccess(userDb, 'fallback');
return userDb;
} }
} }

View File

@@ -5,7 +5,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
@Injectable() @Injectable()
export class UserRepository { export class UserRepository {
constructor( constructor(
@InjectDataSource('oracle') // especifica que queremos o DataSource da conexão 'oracle' @InjectDataSource('oracle')
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
) {} ) {}
@@ -61,4 +61,16 @@ export class UserRepository {
const result = await this.dataSource.query(sql, [sellerId, passwordHash]); const result = await this.dataSource.query(sql, [sellerId, passwordHash]);
return result[0] || null; return result[0] || null;
} }
async findById(id: number) {
const sql = `
SELECT MATRICULA AS "id", NOME AS "name", CODUSUR AS "sellerId",
CODFILIAL AS "storeId", EMAIL AS "email",
DTDEMISSAO as "dataDesligamento", SITUACAO as "situacao"
FROM PCEMPR
WHERE MATRICULA = :1
`;
const result = await this.dataSource.query(sql, [id]);
return result[0] || null;
}
} }

View File

@@ -21,6 +21,6 @@ import { EmailService } from './email.service';
ChangePasswordService, ChangePasswordService,
EmailService, EmailService,
], ],
exports: [UsersService], exports: [UsersService,UserRepository],
}) })
export class UsersModule {} export class UsersModule {}

145
src/core/configs/cache/index.html vendored Normal file
View File

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

View File

@@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider';
providers: [RedisProvider, RedisClientAdapterProvider], providers: [RedisProvider, RedisClientAdapterProvider],
exports: [RedisProvider, RedisClientAdapterProvider], exports: [RedisProvider, RedisClientAdapterProvider],
}) })
export class CacheModule {} export class RedisModule {}

View File

@@ -1,21 +1,21 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
export const RedisProvider: Provider = { export const RedisProvider: Provider = {
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', '10.1.1.109'), host: configService.get<string>('REDIS_HOST', '10.1.1.109'),
port: configService.get<number>('REDIS_PORT', 6379), port: configService.get<number>('REDIS_PORT', 6379),
// password: configService.get<string>('REDIS_PASSWORD', ''), // password: configService.get<string>('REDIS_PASSWORD', ''),
}); });
redis.on('error', (err) => { redis.on('error', (err) => {
console.error('Erro ao conectar ao Redis:', err); console.error('Erro ao conectar ao Redis:', err);
}); });
return redis; return redis;
}, },
inject: [ConfigService], inject: [ConfigService],
}; };

View File

@@ -4,13 +4,15 @@ import { DataConsultController } from './data-consult.controller';
import { DataConsultRepository } from './data-consult.repository'; import { DataConsultRepository } from './data-consult.repository';
import { LoggerModule } from 'src/Log/logger.module'; import { LoggerModule } from 'src/Log/logger.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { RedisModule } from 'src/core/configs/cache/redis.module';
@Module({ @Module({
imports: [LoggerModule, ConfigModule], imports: [LoggerModule, ConfigModule, RedisModule],
controllers: [DataConsultController], controllers: [DataConsultController],
providers: [ providers: [
DataConsultService, DataConsultService,
DataConsultRepository, DataConsultRepository,
], ],
}) })
export class DataConsultModule {} export class DataConsultModule {}

View File

@@ -47,11 +47,13 @@ export class DataConsultRepository {
FROM PCUSUARI FROM PCUSUARI
WHERE PCUSUARI.DTTERMINO IS NULL WHERE PCUSUARI.DTTERMINO IS NULL
AND PCUSUARI.TIPOVEND NOT IN ('P') AND PCUSUARI.TIPOVEND NOT IN ('P')
AND (PCUSUARI.BLOQUEIO IS NULL OR PCUSUARI.BLOQUEIO = 'N')
ORDER BY PCUSUARI.NOME ORDER BY PCUSUARI.NOME
`; `;
return this.executeQuery<SellerDto[]>(sql); return this.executeQuery<SellerDto[]>(sql);
} }
async findBillings(): Promise<BillingDto[]> { async findBillings(): Promise<BillingDto[]> {
const sql = ` const sql = `
SELECT PCCOB.CODCOB as "id", SELECT PCCOB.CODCOB as "id",

View File

@@ -6,13 +6,24 @@ import { BillingDto } from './dto/billing.dto';
import { CustomerDto } from './dto/customer.dto'; import { CustomerDto } from './dto/customer.dto';
import { ProductDto } from './dto/product.dto'; import { ProductDto } from './dto/product.dto';
import { ILogger } from '../Log/ILogger'; import { ILogger } from '../Log/ILogger';
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../core/configs/cache/IRedisClient';
import { getOrSetCache } from '../shared/cache.util';
@Injectable() @Injectable()
export class DataConsultService { export class DataConsultService {
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
private readonly SELLERS_TTL = 3600; // 1 hora
constructor( constructor(
private readonly repository: DataConsultRepository, private readonly repository: DataConsultRepository,
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
@Inject('LoggerService') @Inject('LoggerService')
private readonly logger: ILogger private readonly logger: ILogger,
) {} ) {}
/** /**
@@ -29,8 +40,16 @@ export class DataConsultService {
* @returns Array de SellerDto * @returns Array de SellerDto
*/ */
async sellers(): Promise<SellerDto[]> { async sellers(): Promise<SellerDto[]> {
this.logger.log('Buscando todos os vendedores'); this.logger.log('Buscando vendedores com cache Redis...');
return this.repository.findSellers(); return getOrSetCache<SellerDto[]>(
this.redisClient,
this.SELLERS_CACHE_KEY,
this.SELLERS_TTL,
async () => {
this.logger.log('Cache de vendedores vazio. Buscando no banco...');
return this.repository.findSellers();
}
);
} }
/** /**

114
src/data-consult/doc.html Normal file
View File

@@ -0,0 +1,114 @@
<!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 - DataConsult Module</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>📊 Módulo de Consultas - Portal Juru API</h1>
<h2>📌 Endpoints Disponíveis</h2>
<h3>🔹 GET /api/v1/data-consult/stores</h3>
<p><strong>Descrição:</strong> Lista todas as lojas.</p>
<p><strong>Autenticação:</strong> <span class="tag">JWT</span></p>
<code>Retorna: StoreDto[]</code>
<h3>🔹 GET /api/v1/data-consult/sellers</h3>
<p><strong>Descrição:</strong> Lista todos os vendedores ativos.</p>
<p><strong>Autenticação:</strong> <span class="tag">JWT</span></p>
<code>Retorna: SellerDto[]</code>
<h3>🔹 GET /api/v1/data-consult/billings</h3>
<p><strong>Descrição:</strong> Retorna todos os tipos de faturamento válidos.</p>
<p><strong>Autenticação:</strong> <span class="tag">JWT</span></p>
<code>Retorna: BillingDto[]</code>
<h3>🔹 GET /api/v1/data-consult/customers/:filter</h3>
<p><strong>Descrição:</strong> Busca clientes por nome, código ou CPF/CNPJ.</p>
<p><strong>Parâmetro:</strong> <code>filter</code></p>
<p><strong>Autenticação:</strong> <span class="tag">JWT</span></p>
<code>Retorna: CustomerDto[]</code>
<h3>🔹 GET /api/v1/data-consult/products/:filter</h3>
<p><strong>Descrição:</strong> Busca produtos por descrição, código ou código auxiliar.</p>
<p><strong>Parâmetro:</strong> <code>filter</code></p>
<p><strong>Autenticação:</strong> Não requer</p>
<code>Retorna: ProductDto[]</code>
<h2>🧱 Camadas e Responsabilidades</h2>
<table>
<tr><th>Camada</th><th>Responsabilidade</th></tr>
<tr><td>DataConsultController</td><td>Recebe as requisições HTTP e delega para o service</td></tr>
<tr><td>DataConsultService</td><td>Intermediário entre controller e repositório; adiciona logs e trata exceções</td></tr>
<tr><td>DataConsultRepository</td><td>Executa queries SQL no Oracle via TypeORM</td></tr>
<tr><td>LoggerService</td><td>Registra logs de acesso, sucesso ou erro</td></tr>
</table>
<h2>📦 Detalhes Técnicos</h2>
<ul>
<li>Utiliza <strong>Oracle</strong> como banco de dados com configuração customizada.</li>
<li>As buscas por cliente e produto realizam múltiplas tentativas de match (ex: por código, nome, etc.).</li>
<li>Repositório implementa <code>queryRunner</code> com liberação segura de conexão.</li>
<li>Camada de serviço registra log com <code>ILogger</code> em todas as operações.</li>
<li>Erros no endpoint <code>products</code> são tratados com <code>HttpException</code>.</li>
</ul>
<h2>🔐 Segurança</h2>
<p>Endpoints protegidos utilizam <code>@UseGuards(JwtAuthGuard)</code> e <code>@ApiBearerAuth()</code>.</p>
<p><strong>Header necessário:</strong></p>
<code>Authorization: Bearer &lt;token&gt;</code>
<h2>🚀 Melhorias Futuras</h2>
<ul>
<li>[🔹 ] Cache Redis para lojas, vendedores e faturamentos</li>
<li>[ ] Auditoria detalhada de acessos no logger</li>
<li>[ ] Paginação nas buscas de clientes e produtos</li>
<li>[ ] Endpoint para exportação dos dados em CSV</li>
</ul>
<p><strong>Última atualização:</strong> 29/03/2025</p>
</body>
</html>

View File

@@ -1,79 +0,0 @@
"use strict";
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/controllers#controllers
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
exports.__esModule = true;
exports.LogisticController = void 0;
var common_1 = require("@nestjs/common");
var jwt_auth_guard_1 = require("src/auth/guards/jwt-auth.guard"); // ajuste o caminho se necessário
var swagger_1 = require("@nestjs/swagger");
var LogisticController = /** @class */ (function () {
function LogisticController(logisticService) {
this.logisticService = logisticService;
}
LogisticController.prototype.getExpedicao = function () {
return this.logisticService.getExpedicao();
};
LogisticController.prototype.getEmployee = function () {
return this.logisticService.getEmployee();
};
LogisticController.prototype.getDelivery = function (placa) {
return this.logisticService.getDeliveries(placa);
};
LogisticController.prototype.getStatusCar = function (placa) {
return this.logisticService.getStatusCar(placa);
};
LogisticController.prototype.createOutCar = function (data) {
return this.logisticService.createCarOut(data);
};
LogisticController.prototype.createinCar = function (data) {
return this.logisticService.createCarIn(data);
};
__decorate([
common_1.Get('expedicao'),
swagger_1.ApiOperation({ summary: 'Retorna informações de expedição' })
], LogisticController.prototype, "getExpedicao");
__decorate([
common_1.Get('employee'),
swagger_1.ApiOperation({ summary: 'Retorna lista de funcionários' })
], LogisticController.prototype, "getEmployee");
__decorate([
common_1.Get('deliveries/:placa'),
swagger_1.ApiOperation({ summary: 'Retorna entregas por placa' }),
__param(0, common_1.Param('placa'))
], LogisticController.prototype, "getDelivery");
__decorate([
common_1.Get('status-car/:placa'),
swagger_1.ApiOperation({ summary: 'Retorna status do veículo por placa' }),
__param(0, common_1.Param('placa'))
], LogisticController.prototype, "getStatusCar");
__decorate([
common_1.Post('create'),
swagger_1.ApiOperation({ summary: 'Registra saída de veículo' }),
__param(0, common_1.Body())
], LogisticController.prototype, "createOutCar");
__decorate([
common_1.Post('return-car'),
swagger_1.ApiOperation({ summary: 'Registra retorno de veículo' }),
__param(0, common_1.Body())
], LogisticController.prototype, "createinCar");
LogisticController = __decorate([
swagger_1.ApiTags('Logística'),
swagger_1.ApiBearerAuth(),
common_1.UseGuards(jwt_auth_guard_1.JwtAuthGuard),
common_1.Controller('api/v1/logistic')
], LogisticController);
return LogisticController;
}());
exports.LogisticController = LogisticController;

View File

@@ -0,0 +1,21 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InvoiceRepository } from '../repositories/invoice.repository';
import { InvoiceDto } from '../dto/invoice.dto';
@Injectable()
export class InvoiceService {
constructor(private readonly invoiceRepo: InvoiceRepository) {}
async getInvoiceByChave(chavenfe: string): Promise<InvoiceDto> {
const invoice = await this.invoiceRepo.findInvoiceByChave(chavenfe);
if (!invoice) {
throw new NotFoundException('Nota fiscal não localizada.');
}
const itens = await this.invoiceRepo.findInvoiceItems(invoice.transactionId);
return {
...invoice,
itens,
};
}
}

View File

@@ -0,0 +1,6 @@
///← Orquestra operações de pedido
import { Injectable } from '@nestjs/common';

View File

@@ -0,0 +1,25 @@
export class InvoiceItemDto {
productId: number;
productName: string;
package: string;
qt: number;
ean: string;
multiple: number;
productType: string;
image: string | null;
}
export class InvoiceDto {
storeId: number;
invoiceDate: Date;
orderId: number;
invoiceId: number;
transactionId: number;
customerId: number;
customer: string;
sellerId: number;
sellerName: string;
itensQt: number;
itens: InvoiceItemDto[];
}

View File

@@ -7,20 +7,35 @@ https://docs.nestjs.com/controllers#controllers
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { OrdersService } from './orders.service'; import { OrdersService } from './orders.service';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { UseGuards } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { InvoiceService } from './application/invoice.service';
import { InvoiceDto } from './dto/invoice.dto';
@Controller('api/v1/orders')
@ApiTags('Invoice')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Controller('api/v1/invoice')
export class OrdersController { export class OrdersController {
constructor( private readonly ordersService: OrdersService) {} constructor(
private readonly ordersService: OrdersService,
private readonly invoiceService: InvoiceService,
) {}
@Get('find') @Get('find')
findOrders(@Query() query) { findOrders(@Query() query) {
return this.ordersService.findOrders(query); return this.ordersService.findOrders(query);
} }
@Get('invoice/:chavenfe') @Get(':chavenfe')
findInvoice(@Param('chavenfe') chavenfe: string) { async getByChave(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
return this.ordersService.findInvoice(chavenfe); return this.invoiceService.getInvoiceByChave(chavenfe);
} }
@Get('itens/:id') @Get('itens/:id')

View File

@@ -1,19 +1,27 @@
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrdersService } from './orders.service';
import { InvoiceRepository } from './repositories/invoice.repository';
import { InvoiceService } from './application/invoice.service';
import { OrdersController } from './orders.controller';
@Module({ @Module({
imports: [], imports: [
controllers: [ ConfigModule,
OrdersController,], TypeOrmModule,
providers: [ ],
OrdersService,], controllers: [
OrdersController,
],
providers: [
OrdersService,
InvoiceService,
InvoiceRepository,
],
}) })
export class OrdersModule { } export class OrdersModule {}

View File

@@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
@Injectable()
export class InvoiceRepository {
constructor(
@InjectDataSource() private readonly dataSource: DataSource,
) {}
async findInvoiceByChave(chavenfe: string) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `
SELECT
pcnfsaid.codfilial AS "storeId",
pcnfsaid.dtsaida AS "invoiceDate",
pcnfsaid.numped AS "orderId",
pcnfsaid.numnota AS "invoiceId",
pcnfsaid.numtransvenda AS "transactionId",
pcnfsaid.codcli AS "customerId",
pcclient.cliente AS "customer",
pcnfsaid.codusur AS "sellerId",
pcusuari.nome AS "sellerName",
(
SELECT SUM(pcmov.qt)
FROM pcmov
WHERE pcmov.numtransvenda = pcnfsaid.numtransvenda
) AS "itensQt",
NULL AS "itens"
FROM pcnfsaid, pcclient, pcusuari
WHERE
pcnfsaid.codcli = pcclient.codcli
AND pcnfsaid.codusur = pcusuari.codusur
AND pcnfsaid.chavenfe = :chavenfe
`;
return await queryRunner.manager.query(sql, [chavenfe]);
} finally {
await queryRunner.release();
}
}
async findInvoiceItems(transactionId: number) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `
SELECT
pcmov.codprod AS "productId",
pcprodut.descricao AS "productName",
pcprodut.embalagem AS "package",
pcmov.qt AS "qt",
pcprodut.codauxiliar AS "ean",
pcprodut.multiplo AS "multiple",
pcprodut.tipoproduto AS "productType",
REPLACE(
CASE
WHEN INSTR(PCPRODUT.URLIMAGEM, ';') > 0 THEN
SUBSTR(PCPRODUT.URLIMAGEM, 1, INSTR(PCPRODUT.URLIMAGEM, ';') - 1)
WHEN PCPRODUT.URLIMAGEM IS NOT NULL THEN
PCPRODUT.URLIMAGEM
ELSE NULL
END,
'167.249.211.178:8001',
'10.1.1.191'
) AS "image"
FROM pcmov, pcprodut
WHERE
pcmov.codprod = pcprodut.codprod
AND pcmov.numtransvenda = :transactionId
`;
return await queryRunner.manager.query(sql, [transactionId]);
} finally {
await queryRunner.release();
}
}
}

15
src/shared/cache.util.ts Normal file
View File

@@ -0,0 +1,15 @@
import { IRedisClient } from '../core/configs/cache/IRedisClient';
export async function getOrSetCache<T>(
redisClient: IRedisClient,
key: string,
ttlSeconds: number,
fallback: () => Promise<T>
): Promise<T> {
const cached = await redisClient.get<T>(key);
if (cached) return cached;
const data = await fallback();
await redisClient.set<T>(key, data, ttlSeconds);
return data;
}

View File

@@ -10,6 +10,6 @@
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"incremental": true // "incremental": true
} }
} }