diff --git a/.env b/.env index cdb4b67..d8396d3 100644 --- a/.env +++ b/.env @@ -12,3 +12,5 @@ POSTGRES_PASSWORD=ti POSTGRES_DB=ksdb + + diff --git a/nest-cli.json b/nest-cli.json index 56167b3..d7214eb 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,4 +1,7 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } } diff --git a/package-lock.json b/package-lock.json index 75bba56..68e83f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6292,7 +6292,6 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", - "license": "MIT", "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", diff --git a/src/auth/auth/auth.controller.ts b/src/auth/auth/auth.controller.ts index 58377f0..d874ba8 100644 --- a/src/auth/auth/auth.controller.ts +++ b/src/auth/auth/auth.controller.ts @@ -10,7 +10,19 @@ import { UsersService } from '../users/users.service'; import { LoginDto } from '../auth/dto/login.dto'; import { ResultModel } from 'src/core/models/result.model'; 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') export class AuthController { constructor( @@ -19,24 +31,25 @@ export class AuthController { ) {} @Post('login') + @ApiOperation({ summary: 'Realiza login e retorna um token JWT' }) @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' }) - async login(@Body() model: LoginDto): Promise { + async login(@Body() model: LoginDto): Promise { const result = await this.usersService.authenticate({ userName: model.username, password: model.password, }); - console.log('Resultado da autenticação:', result); - -if (!result.success) { - throw new HttpException( - new ResultModel(false, result.error, null, result.error), - HttpStatus.UNAUTHORIZED, - ); -} - + if (!result.success) { + throw new HttpException( + new ResultModel(false, result.error, null, result.error), + HttpStatus.UNAUTHORIZED, + ); + } const user = result.data; @@ -58,4 +71,4 @@ if (!result.success) { token: token, }; } -} +} \ No newline at end of file diff --git a/src/auth/auth/auth.module.ts b/src/auth/auth/auth.module.ts index 4c9d709..7a98a9f 100644 --- a/src/auth/auth/auth.module.ts +++ b/src/auth/auth/auth.module.ts @@ -1,26 +1,25 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { JwtModule, JwtService } from '@nestjs/jwt'; +import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; -import { UsersModule } from '../users/users.module'; +import { AuthService } from './auth.service'; 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({ imports: [ - UsersModule, - PassportModule.register({ - defaultStrategy: 'jwt', - }), JwtModule.register({ secret: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', - signOptions: { - expiresIn: 3600, - }, + signOptions: { expiresIn: '8h' }, }), + PassportModule.register({ defaultStrategy: 'jwt' }), + RedisModule, + UsersModule, + ], controllers: [AuthController], - providers: [AuthService,JwtStrategy], + providers: [AuthService, JwtStrategy], exports: [AuthService], }) export class AuthModule {} diff --git a/src/auth/auth/auth.service.ts b/src/auth/auth/auth.service.ts index 6269ec6..8fd5f2c 100644 --- a/src/auth/auth/auth.service.ts +++ b/src/auth/auth/auth.service.ts @@ -4,6 +4,8 @@ import { Injectable } from '@nestjs/common'; import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { UsersService } from '../users/users.service'; import { JwtPayload } from '../models/jwt-payload.model'; +import Redis from 'ioredis'; + diff --git a/src/auth/auth/dist/auth.service.js b/src/auth/auth/dist/auth.service.js deleted file mode 100644 index c62e0a4..0000000 --- a/src/auth/auth/dist/auth.service.js +++ /dev/null @@ -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; diff --git a/src/auth/auth/dto/LoginResponseDto.ts b/src/auth/auth/dto/LoginResponseDto.ts new file mode 100644 index 0000000..95dd6ee --- /dev/null +++ b/src/auth/auth/dto/LoginResponseDto.ts @@ -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; +} diff --git a/src/auth/doc/login.fluxo.html b/src/auth/doc/login.fluxo.html new file mode 100644 index 0000000..7f5f841 --- /dev/null +++ b/src/auth/doc/login.fluxo.html @@ -0,0 +1,118 @@ + + + + + + Documentação - Fluxo de Login + + + +

🔐 Fluxo de Login - Portal Juru API

+ +

📌 Rota de Login

+

URL: POST /api/v1/auth/login

+

Descrição: Autentica o usuário, valida regras de negócio e retorna um token JWT.

+

Acesso: Público

+ +

📤 Body (JSON)

+
{
+  "username": "joelson.r",
+  "password": "1010"
+}
+ +

✅ Resposta (200 OK)

+
{
+  "id": 1498,
+  "sellerId": 2013,
+  "name": "JOELSON DE BRITO RIBEIRO",
+  "username": "JOELSON DE BRITO RIBEIRO",
+  "storeId": "4",
+  "email": "JOELSON.R@JURUNENSE.COM.BR",
+  "token": "eyJhbGciOiJIUzI1NiIsInR5..."
+}
+ +

❌ Resposta (401 Unauthorized)

+
{
+  "success": false,
+  "message": "Usuário ou senha inválidos.",
+  "data": null,
+  "error": "Usuário ou senha inválidos."
+}
+ +

🧱 Camadas e Responsabilidades

+ + + + + + + + + +
CamadaResponsabilidade
AuthControllerRecebe requisição e coordena autenticação
UsersServiceOrquestra os casos de uso (login, reset, troca senha)
AuthenticateUserServiceExecuta lógica de autenticação e validações
UserRepositoryExecuta SQL diretamente no Oracle
AuthServiceGera o token JWT com os dados do usuário
JwtStrategyValida o token em rotas protegidas, usando Redis como cache
RedisClientAdapterWrapper de acesso ao Redis com interface genérica e TTL
+ +

🧊 Redis na Autenticação

+ + +

🔐 Proteção de Rotas

+

Rotas protegidas utilizam @UseGuards(JwtAuthGuard) e @ApiBearerAuth().

+

Header necessário:

+ Authorization: Bearer <token> + +

🚀 Melhorias Futuras

+ + +

Última atualização: 28/03/2025

+ + diff --git a/src/auth/strategies/jwt-strategy.ts b/src/auth/strategies/jwt-strategy.ts index 9bc4f60..6cc8e21 100644 --- a/src/auth/strategies/jwt-strategy.ts +++ b/src/auth/strategies/jwt-strategy.ts @@ -1,24 +1,43 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +// ✅ jwt.strategy.ts +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; 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() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private readonly authService: AuthService) { + constructor( + @Inject(RedisClientToken) private readonly redis: IRedisClient, + private readonly userRepository: UserRepository, + ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', + secretOrKey: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', }); } async validate(payload: JwtPayload) { - const user = await this.authService.validateUser(payload); - if (!user) { - throw new UnauthorizedException(); + const sessionKey = `session:${payload.id}`; + const user = await this.redis.get(sessionKey); + + 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; } } diff --git a/src/auth/users/UserRepository.ts b/src/auth/users/UserRepository.ts index a255d48..c6d93b9 100644 --- a/src/auth/users/UserRepository.ts +++ b/src/auth/users/UserRepository.ts @@ -5,7 +5,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; @Injectable() export class UserRepository { constructor( - @InjectDataSource('oracle') // especifica que queremos o DataSource da conexão 'oracle' + @InjectDataSource('oracle') private readonly dataSource: DataSource, ) {} @@ -61,4 +61,16 @@ export class UserRepository { const result = await this.dataSource.query(sql, [sellerId, passwordHash]); 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; + } } diff --git a/src/auth/users/users.module.ts b/src/auth/users/users.module.ts index 9dd389a..c7724c9 100644 --- a/src/auth/users/users.module.ts +++ b/src/auth/users/users.module.ts @@ -21,6 +21,6 @@ import { EmailService } from './email.service'; ChangePasswordService, EmailService, ], - exports: [UsersService], + exports: [UsersService,UserRepository], }) export class UsersModule {} diff --git a/src/core/configs/cache/index.html b/src/core/configs/cache/index.html new file mode 100644 index 0000000..86c714a --- /dev/null +++ b/src/core/configs/cache/index.html @@ -0,0 +1,145 @@ + + + + + + Documentação - Integração Redis + + + +

📦 Integração Redis com Abstração - Portal Juru API

+ +

🧱 Arquitetura

+

O projeto utiliza o Redis com uma interface genérica para garantir desacoplamento, facilidade de teste e reaproveitamento em múltiplos módulos.

+ +

🔌 Interface IRedisClient

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

🧩 Provider REDIS_CLIENT

+

Faz a conexão direta com o Redis usando a biblioteca ioredis e o ConfigService para pegar host e porta.

+
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],
+};
+ +

📦 RedisClientAdapter (Wrapper)

+

Classe que implementa IRedisClient e encapsula as operações de cache. É injetada em serviços via token.

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

🔗 Token e Provider

+

Token de injeção definido para o adapter:

+
export const RedisClientToken = 'RedisClientInterface';
+
+export const RedisClientAdapterProvider = {
+  provide: RedisClientToken,
+  useClass: RedisClientAdapter,
+};
+ +

📦 Módulo Global RedisModule

+

Torna o Redis disponível em toda a aplicação.

+
@Global()
+@Module({
+  imports: [ConfigModule],
+  providers: [RedisProvider, RedisClientAdapterProvider],
+  exports: [RedisProvider, RedisClientAdapterProvider],
+})
+export class RedisModule {}
+ +

🧠 Uso em Serviços

+

Injetando o cache no seu service:

+
constructor(
+  @Inject(RedisClientToken)
+  private readonly redisClient: IRedisClient
+) {}
+ +

Uso típico:

+
const data = await this.redisClient.get<T>('chave');
+if (!data) {
+  const result = await fetchFromDb();
+  await this.redisClient.set('chave', result, 3600);
+}
+ +

🧰 Boas práticas

+
    +
  • ✅ TTL por recurso (ex: produtos: 1h, lojas: 24h)
  • +
  • ✅ Nomear chaves com prefixos por domínio (ex: data-consult:sellers)
  • +
  • ✅ Centralizar helpers como getOrSetCache para evitar repetição
  • +
  • ✅ Usar JSON.stringify e JSON.parse no adapter
  • +
  • ✅ Marcar módulo como @Global() para acesso em toda a aplicação
  • +
+ +

Última atualização: 29/03/2025

+ + diff --git a/src/core/configs/cache/redis.module.ts b/src/core/configs/cache/redis.module.ts index 9c2afbe..e8240d0 100644 --- a/src/core/configs/cache/redis.module.ts +++ b/src/core/configs/cache/redis.module.ts @@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider'; providers: [RedisProvider, RedisClientAdapterProvider], exports: [RedisProvider, RedisClientAdapterProvider], }) -export class CacheModule {} \ No newline at end of file +export class RedisModule {} \ No newline at end of file diff --git a/src/core/configs/cache/redis.provider.ts b/src/core/configs/cache/redis.provider.ts index cfc2a90..23b13e7 100644 --- a/src/core/configs/cache/redis.provider.ts +++ b/src/core/configs/cache/redis.provider.ts @@ -1,21 +1,21 @@ -import { Provider } from '@nestjs/common'; -import Redis from 'ioredis'; -import { ConfigService } from '@nestjs/config'; + import { Provider } from '@nestjs/common'; + import Redis from 'ioredis'; + import { ConfigService } from '@nestjs/config'; -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), - // password: configService.get('REDIS_PASSWORD', ''), - }); + 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), + // password: configService.get('REDIS_PASSWORD', ''), + }); - redis.on('error', (err) => { - console.error('Erro ao conectar ao Redis:', err); - }); + redis.on('error', (err) => { + console.error('Erro ao conectar ao Redis:', err); + }); - return redis; - }, - inject: [ConfigService], -}; + return redis; + }, + inject: [ConfigService], + }; diff --git a/src/data-consult/data-consult.module.ts b/src/data-consult/data-consult.module.ts index d8e65b8..d00c750 100644 --- a/src/data-consult/data-consult.module.ts +++ b/src/data-consult/data-consult.module.ts @@ -4,13 +4,15 @@ import { DataConsultController } from './data-consult.controller'; import { DataConsultRepository } from './data-consult.repository'; import { LoggerModule } from 'src/Log/logger.module'; import { ConfigModule } from '@nestjs/config'; +import { RedisModule } from 'src/core/configs/cache/redis.module'; @Module({ - imports: [LoggerModule, ConfigModule], + imports: [LoggerModule, ConfigModule, RedisModule], controllers: [DataConsultController], providers: [ DataConsultService, DataConsultRepository, + ], }) export class DataConsultModule {} diff --git a/src/data-consult/data-consult.repository.ts b/src/data-consult/data-consult.repository.ts index fb7661f..056ac26 100644 --- a/src/data-consult/data-consult.repository.ts +++ b/src/data-consult/data-consult.repository.ts @@ -47,10 +47,12 @@ export class DataConsultRepository { FROM PCUSUARI WHERE PCUSUARI.DTTERMINO IS NULL AND PCUSUARI.TIPOVEND NOT IN ('P') + AND (PCUSUARI.BLOQUEIO IS NULL OR PCUSUARI.BLOQUEIO = 'N') ORDER BY PCUSUARI.NOME `; return this.executeQuery(sql); } + async findBillings(): Promise { const sql = ` diff --git a/src/data-consult/data-consult.service.ts b/src/data-consult/data-consult.service.ts index 396164d..d63e37d 100644 --- a/src/data-consult/data-consult.service.ts +++ b/src/data-consult/data-consult.service.ts @@ -6,13 +6,24 @@ import { BillingDto } from './dto/billing.dto'; import { CustomerDto } from './dto/customer.dto'; import { ProductDto } from './dto/product.dto'; 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() export class DataConsultService { + + private readonly SELLERS_CACHE_KEY = 'data-consult:sellers'; + private readonly SELLERS_TTL = 3600; // 1 hora + + constructor( private readonly repository: DataConsultRepository, + @Inject(RedisClientToken) private readonly redisClient: IRedisClient, @Inject('LoggerService') - private readonly logger: ILogger + private readonly logger: ILogger, ) {} /** @@ -29,9 +40,17 @@ export class DataConsultService { * @returns Array de SellerDto */ async sellers(): Promise { - this.logger.log('Buscando todos os vendedores'); - return this.repository.findSellers(); - } + this.logger.log('Buscando vendedores com cache Redis...'); + return getOrSetCache( + this.redisClient, + this.SELLERS_CACHE_KEY, + this.SELLERS_TTL, + async () => { + this.logger.log('Cache de vendedores vazio. Buscando no banco...'); + return this.repository.findSellers(); + } + ); + } /** * Obter todos os faturamentos diff --git a/src/data-consult/doc.html b/src/data-consult/doc.html new file mode 100644 index 0000000..ad58a2e --- /dev/null +++ b/src/data-consult/doc.html @@ -0,0 +1,114 @@ + + + + + + Documentação - DataConsult Module + + + +

📊 Módulo de Consultas - Portal Juru API

+ +

📌 Endpoints Disponíveis

+ +

🔹 GET /api/v1/data-consult/stores

+

Descrição: Lista todas as lojas.

+

Autenticação: JWT

+ Retorna: StoreDto[] + +

🔹 GET /api/v1/data-consult/sellers

+

Descrição: Lista todos os vendedores ativos.

+

Autenticação: JWT

+ Retorna: SellerDto[] + +

🔹 GET /api/v1/data-consult/billings

+

Descrição: Retorna todos os tipos de faturamento válidos.

+

Autenticação: JWT

+ Retorna: BillingDto[] + +

🔹 GET /api/v1/data-consult/customers/:filter

+

Descrição: Busca clientes por nome, código ou CPF/CNPJ.

+

Parâmetro: filter

+

Autenticação: JWT

+ Retorna: CustomerDto[] + +

🔹 GET /api/v1/data-consult/products/:filter

+

Descrição: Busca produtos por descrição, código ou código auxiliar.

+

Parâmetro: filter

+

Autenticação: Não requer

+ Retorna: ProductDto[] + +

🧱 Camadas e Responsabilidades

+ + + + + + +
CamadaResponsabilidade
DataConsultControllerRecebe as requisições HTTP e delega para o service
DataConsultServiceIntermediário entre controller e repositório; adiciona logs e trata exceções
DataConsultRepositoryExecuta queries SQL no Oracle via TypeORM
LoggerServiceRegistra logs de acesso, sucesso ou erro
+ +

📦 Detalhes Técnicos

+
    +
  • Utiliza Oracle como banco de dados com configuração customizada.
  • +
  • As buscas por cliente e produto realizam múltiplas tentativas de match (ex: por código, nome, etc.).
  • +
  • Repositório implementa queryRunner com liberação segura de conexão.
  • +
  • Camada de serviço registra log com ILogger em todas as operações.
  • +
  • Erros no endpoint products são tratados com HttpException.
  • +
+ +

🔐 Segurança

+

Endpoints protegidos utilizam @UseGuards(JwtAuthGuard) e @ApiBearerAuth().

+

Header necessário:

+ Authorization: Bearer <token> + +

🚀 Melhorias Futuras

+
    +
  • [🔹 ] Cache Redis para lojas, vendedores e faturamentos
  • +
  • [ ] Auditoria detalhada de acessos no logger
  • +
  • [ ] Paginação nas buscas de clientes e produtos
  • +
  • [ ] Endpoint para exportação dos dados em CSV
  • +
+ +

Última atualização: 29/03/2025

+ + diff --git a/src/logistic/dist/logistic.controller.js b/src/logistic/dist/logistic.controller.js deleted file mode 100644 index ba1591b..0000000 --- a/src/logistic/dist/logistic.controller.js +++ /dev/null @@ -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; diff --git a/src/orders/application/invoice.service.ts b/src/orders/application/invoice.service.ts new file mode 100644 index 0000000..6d341b4 --- /dev/null +++ b/src/orders/application/invoice.service.ts @@ -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 { + 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, + }; + } +} \ No newline at end of file diff --git a/src/orders/application/orders.service.ts b/src/orders/application/orders.service.ts new file mode 100644 index 0000000..639e13c --- /dev/null +++ b/src/orders/application/orders.service.ts @@ -0,0 +1,6 @@ +///← Orquestra operações de pedido + + +import { Injectable } from '@nestjs/common'; + + diff --git a/src/orders/dto/invoice.dto.ts b/src/orders/dto/invoice.dto.ts new file mode 100644 index 0000000..a14faa4 --- /dev/null +++ b/src/orders/dto/invoice.dto.ts @@ -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[]; + } + \ No newline at end of file diff --git a/src/orders/orders.controller.ts b/src/orders/orders.controller.ts index ccaa562..76f8ddb 100644 --- a/src/orders/orders.controller.ts +++ b/src/orders/orders.controller.ts @@ -7,20 +7,35 @@ https://docs.nestjs.com/controllers#controllers import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; 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 { - constructor( private readonly ordersService: OrdersService) {} + constructor( + private readonly ordersService: OrdersService, + private readonly invoiceService: InvoiceService, + ) {} + @Get('find') findOrders(@Query() query) { return this.ordersService.findOrders(query); } - @Get('invoice/:chavenfe') - findInvoice(@Param('chavenfe') chavenfe: string) { - return this.ordersService.findInvoice(chavenfe); + @Get(':chavenfe') + async getByChave(@Param('chavenfe') chavenfe: string): Promise { + return this.invoiceService.getInvoiceByChave(chavenfe); } @Get('itens/:id') diff --git a/src/orders/orders.module.ts b/src/orders/orders.module.ts index 53a041c..746ad4d 100644 --- a/src/orders/orders.module.ts +++ b/src/orders/orders.module.ts @@ -1,19 +1,27 @@ /* eslint-disable prettier/prettier */ /* 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 { 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({ - imports: [], - controllers: [ - OrdersController,], - providers: [ - OrdersService,], + imports: [ + ConfigModule, + TypeOrmModule, + ], + controllers: [ + OrdersController, + ], + providers: [ + OrdersService, + InvoiceService, + InvoiceRepository, + ], }) -export class OrdersModule { } +export class OrdersModule {} diff --git a/src/orders/repositories/invoice.repository.ts b/src/orders/repositories/invoice.repository.ts new file mode 100644 index 0000000..ee08c1d --- /dev/null +++ b/src/orders/repositories/invoice.repository.ts @@ -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(); + } + } +} diff --git a/src/orders/repositories/orders.repository.ts b/src/orders/repositories/orders.repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/cache.util.ts b/src/shared/cache.util.ts new file mode 100644 index 0000000..3c0dc29 --- /dev/null +++ b/src/shared/cache.util.ts @@ -0,0 +1,15 @@ +import { IRedisClient } from '../core/configs/cache/IRedisClient'; + +export async function getOrSetCache( + redisClient: IRedisClient, + key: string, + ttlSeconds: number, + fallback: () => Promise +): Promise { + const cached = await redisClient.get(key); + if (cached) return cached; + + const data = await fallback(); + await redisClient.set(key, data, ttlSeconds); + return data; +} diff --git a/tsconfig.json b/tsconfig.json index bf10a23..f564900 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "sourceMap": true, "outDir": "./dist", "baseUrl": "./", - "incremental": true + // "incremental": true } }