feat: Implement initial NestJS API project structure including user authentication, order management, and basic configurations.
This commit is contained in:
commit
51d85aeb9a
|
|
@ -0,0 +1,56 @@
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start:dev",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "npm: start:dev",
|
||||||
|
"detail": "nest start --watch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuração do Banco de Dados Oracle
|
||||||
|
|
||||||
|
Este projeto utiliza o banco de dados Oracle. Para configurar a conexão, crie um arquivo `.env` na raiz do projeto com as seguintes variáveis:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Configurações do Banco Oracle
|
||||||
|
ORACLE_USER=seu_usuario
|
||||||
|
ORACLE_PASSWORD=sua_senha
|
||||||
|
ORACLE_CONNECTION_STRING=localhost:1521/seu_sid
|
||||||
|
ORACLE_LIB_DIR=/Seu/Dir/OracleAqui
|
||||||
|
|
||||||
|
# Porta da aplicação
|
||||||
|
PORT=3001
|
||||||
|
|
||||||
|
#Chave secreta JWT
|
||||||
|
JWT_SECRET=suaChaveAqui
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Importante:**
|
||||||
|
|
||||||
|
- Certifique-se de ter o Oracle Instant Client instalado
|
||||||
|
- O caminho `ORACLE_LIB_DIR` deve apontar para o diretório do Oracle Instant Client
|
||||||
|
- Se as variáveis de ambiente não estiverem configuradas, a aplicação iniciará sem conexão com o banco
|
||||||
|
|
||||||
|
## Endpoints Disponíveis
|
||||||
|
|
||||||
|
- `GET /` - Página inicial (retorna "Hello World!")
|
||||||
|
- `GET /autenticar` - Endpoint de autenticação (requer configuração do banco Oracle)
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"name": "controle-saida-loja",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.1.5",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.0",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.0",
|
||||||
|
"@types/oracledb": "^6.6.2",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"dotenv": "^17.2.1",
|
||||||
|
"oracledb": "^6.9.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.5",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@swc/cli": "^0.6.0",
|
||||||
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CustomJwtService } from '../jwt/jwt.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard {
|
||||||
|
constructor(private readonly jwtService: CustomJwtService) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
throw new UnauthorizedException('Token não fornecido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verifyToken(token);
|
||||||
|
|
||||||
|
request.user = {
|
||||||
|
id: payload.sub,
|
||||||
|
username: payload.username,
|
||||||
|
nome: payload.nome,
|
||||||
|
email: payload.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Token inválido ou expirado');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { CustomJwtService } from './jwt.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET || 'Jurunense@Athentic@User',
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: '48h',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [CustomJwtService],
|
||||||
|
exports: [CustomJwtService, PassportModule],
|
||||||
|
})
|
||||||
|
export class JwtAuthModule {}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { CustomJwtService, JwtPayload } from './jwt.service';
|
||||||
|
|
||||||
|
describe('CustomJwtService', () => {
|
||||||
|
let service: CustomJwtService;
|
||||||
|
let jwtService: jest.Mocked<JwtService>;
|
||||||
|
|
||||||
|
const mockJwtService = {
|
||||||
|
sign: jest.fn(),
|
||||||
|
verify: jest.fn(),
|
||||||
|
decode: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
CustomJwtService,
|
||||||
|
{
|
||||||
|
provide: JwtService,
|
||||||
|
useValue: mockJwtService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CustomJwtService>(CustomJwtService);
|
||||||
|
jwtService = module.get(JwtService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateToken', () => {
|
||||||
|
it('deve gerar um token JWT com sucesso', () => {
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
const expectedToken = 'mock.jwt.token';
|
||||||
|
|
||||||
|
jwtService.sign.mockReturnValue(expectedToken);
|
||||||
|
|
||||||
|
const result = service.generateToken(payload);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedToken);
|
||||||
|
expect(jwtService.sign).toHaveBeenCalledWith(payload);
|
||||||
|
expect(jwtService.sign).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar erro quando payload for null', () => {
|
||||||
|
expect(() => service.generateToken(null as any)).toThrow(
|
||||||
|
'Payload inválido para geração de token',
|
||||||
|
);
|
||||||
|
expect(jwtService.sign).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar erro quando payload for undefined', () => {
|
||||||
|
expect(() => service.generateToken(undefined as any)).toThrow(
|
||||||
|
'Payload inválido para geração de token',
|
||||||
|
);
|
||||||
|
expect(jwtService.sign).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar erro quando houver erro na geração do token', () => {
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
jwtService.sign.mockImplementation(() => {
|
||||||
|
throw new Error('Erro interno');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => service.generateToken(payload)).toThrow('Erro ao gerar token');
|
||||||
|
expect(jwtService.sign).toHaveBeenCalledWith(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verifyToken', () => {
|
||||||
|
it('deve verificar e retornar o payload de um token válido', () => {
|
||||||
|
const token = 'valid.jwt.token';
|
||||||
|
const expectedPayload: JwtPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
iat: 1234567890,
|
||||||
|
};
|
||||||
|
|
||||||
|
jwtService.verify.mockReturnValue(expectedPayload);
|
||||||
|
|
||||||
|
const result = service.verifyToken(token);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedPayload);
|
||||||
|
expect(jwtService.verify).toHaveBeenCalledWith(token);
|
||||||
|
expect(jwtService.verify).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token for vazio', () => {
|
||||||
|
expect(() => service.verifyToken('')).toThrow(UnauthorizedException);
|
||||||
|
expect(() => service.verifyToken('')).toThrow('Token não fornecido ou inválido');
|
||||||
|
expect(jwtService.verify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token for apenas espaços', () => {
|
||||||
|
expect(() => service.verifyToken(' ')).toThrow(UnauthorizedException);
|
||||||
|
expect(() => service.verifyToken(' ')).toThrow('Token não fornecido ou inválido');
|
||||||
|
expect(jwtService.verify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token for null', () => {
|
||||||
|
expect(() => service.verifyToken(null as any)).toThrow(UnauthorizedException);
|
||||||
|
expect(jwtService.verify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token for inválido', () => {
|
||||||
|
const token = 'invalid.jwt.token';
|
||||||
|
|
||||||
|
jwtService.verify.mockImplementation(() => {
|
||||||
|
throw new Error('invalid token');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
|
||||||
|
expect(() => service.verifyToken(token)).toThrow('Token inválido ou malformado');
|
||||||
|
expect(jwtService.verify).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token estiver expirado', () => {
|
||||||
|
const token = 'expired.jwt.token';
|
||||||
|
|
||||||
|
jwtService.verify.mockImplementation(() => {
|
||||||
|
throw new Error('jwt expired');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
|
||||||
|
expect(() => service.verifyToken(token)).toThrow('Token expirado');
|
||||||
|
expect(jwtService.verify).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando token for malformado', () => {
|
||||||
|
const token = 'malformed.jwt.token';
|
||||||
|
|
||||||
|
jwtService.verify.mockImplementation(() => {
|
||||||
|
throw new Error('jwt malformed');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
|
||||||
|
expect(() => service.verifyToken(token)).toThrow('Token inválido ou malformado');
|
||||||
|
expect(jwtService.verify).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decodeToken', () => {
|
||||||
|
it('deve decodificar um token JWT com sucesso', () => {
|
||||||
|
const token = 'mock.jwt.token';
|
||||||
|
const expectedPayload: JwtPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
iat: 1234567890,
|
||||||
|
};
|
||||||
|
|
||||||
|
jwtService.decode.mockReturnValue(expectedPayload);
|
||||||
|
|
||||||
|
const result = service.decodeToken(token);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedPayload);
|
||||||
|
expect(jwtService.decode).toHaveBeenCalledWith(token);
|
||||||
|
expect(jwtService.decode).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar null quando o token não puder ser decodificado', () => {
|
||||||
|
const token = 'invalid.token';
|
||||||
|
|
||||||
|
jwtService.decode.mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = service.decodeToken(token);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(jwtService.decode).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar null quando token for vazio', () => {
|
||||||
|
expect(service.decodeToken('')).toBeNull();
|
||||||
|
expect(jwtService.decode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar null quando token for apenas espaços', () => {
|
||||||
|
expect(service.decodeToken(' ')).toBeNull();
|
||||||
|
expect(jwtService.decode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar null quando token for null', () => {
|
||||||
|
expect(service.decodeToken(null as any)).toBeNull();
|
||||||
|
expect(jwtService.decode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve decodificar token mesmo quando inválido (sem validação)', () => {
|
||||||
|
const token = 'invalid.but.decodable.token';
|
||||||
|
const expectedPayload: JwtPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
jwtService.decode.mockReturnValue(expectedPayload);
|
||||||
|
|
||||||
|
const result = service.decodeToken(token);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedPayload);
|
||||||
|
expect(jwtService.decode).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar null quando houver erro na decodificação', () => {
|
||||||
|
const token = 'error.token';
|
||||||
|
|
||||||
|
jwtService.decode.mockImplementation(() => {
|
||||||
|
throw new Error('Decode error');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = service.decodeToken(token);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(jwtService.decode).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService as NestJwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: number;
|
||||||
|
username: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CustomJwtService {
|
||||||
|
constructor(private readonly jwtService: NestJwtService) {}
|
||||||
|
|
||||||
|
generateToken(payload: JwtPayload): string {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
throw new Error('Payload inválido para geração de token');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.jwtService.sign(payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Erro ao gerar token: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyToken(token: string): JwtPayload {
|
||||||
|
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
||||||
|
throw new UnauthorizedException('Token não fornecido ou inválido');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verify<JwtPayload>(token);
|
||||||
|
return payload;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.includes('expired')) {
|
||||||
|
throw new UnauthorizedException('Token expirado');
|
||||||
|
}
|
||||||
|
if (error.message.includes('invalid') || error.message.includes('malformed')) {
|
||||||
|
throw new UnauthorizedException('Token inválido ou malformado');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException('Token inválido');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeToken(token: string): JwtPayload | null {
|
||||||
|
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.decode<JwtPayload>(token);
|
||||||
|
return payload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Get,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import UserDTO from 'src/models/UserDTO';
|
||||||
|
import AuthResponseDTO from 'src/models/AuthResponseDTO';
|
||||||
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBody,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
ApiInternalServerErrorResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
// Interface para tipar o request com usuário
|
||||||
|
interface RequestWithUser extends Request {
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('user')
|
||||||
|
@Controller('user')
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Post('/autenticar')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Autenticar usuário',
|
||||||
|
description:
|
||||||
|
'Endpoint público para autenticação de usuários. Retorna um token JWT válido.',
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userName: { type: 'string', example: 'JOAO.SILVA' },
|
||||||
|
password: { type: 'string', example: '123456' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Dados de login do usuário',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Usuário autenticado com sucesso',
|
||||||
|
type: AuthResponseDTO,
|
||||||
|
})
|
||||||
|
@ApiUnauthorizedResponse({
|
||||||
|
description: 'Usuário ou senha inválidos',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 401 },
|
||||||
|
message: { type: 'string', example: 'Usuario ou senha invalidos' },
|
||||||
|
error: { type: 'string', example: 'Unauthorized' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiInternalServerErrorResponse({
|
||||||
|
description: 'Erro interno do servidor ou problema de conexão com banco',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 500 },
|
||||||
|
message: { type: 'string', example: 'Erro ao consultar view' },
|
||||||
|
error: { type: 'string', example: 'Internal Server Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async autenticarUsuario(@Body() user: UserDTO): Promise<AuthResponseDTO> {
|
||||||
|
return await this.userService.autenticarUsuario({
|
||||||
|
userName: user.userName.toUpperCase(),
|
||||||
|
password: user.password.toUpperCase(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/perfil')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Obter perfil do usuário',
|
||||||
|
description:
|
||||||
|
'Endpoint protegido que retorna os dados do usuário autenticado',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Perfil do usuário retornado com sucesso',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', example: 1 },
|
||||||
|
username: { type: 'string', example: 'JOAO.SILVA' },
|
||||||
|
nome: { type: 'string', example: 'João Silva' },
|
||||||
|
email: { type: 'string', example: 'joao.silva@empresa.com' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiUnauthorizedResponse({
|
||||||
|
description: 'Token JWT inválido ou ausente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 401 },
|
||||||
|
message: { type: 'string', example: 'Unauthorized' },
|
||||||
|
error: { type: 'string', example: 'Unauthorized' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
getProfile(@Request() req: RequestWithUser): {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
} {
|
||||||
|
return req.user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UserController } from './user.controller';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { JwtAuthModule } from '../jwt/jwt.module';
|
||||||
|
import { DatabaseModule } from '../../database/database.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [JwtAuthModule, DatabaseModule],
|
||||||
|
controllers: [UserController],
|
||||||
|
providers: [UserService],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import {
|
||||||
|
InternalServerErrorException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { CustomJwtService } from '../jwt/jwt.service';
|
||||||
|
import { DatabaseService } from '../../database/database.service';
|
||||||
|
import UserDTO from 'src/models/UserDTO';
|
||||||
|
import AuthResponseDTO from 'src/models/AuthResponseDTO';
|
||||||
|
|
||||||
|
describe('UserService', () => {
|
||||||
|
let service: UserService;
|
||||||
|
let jwtService: jest.Mocked<CustomJwtService>;
|
||||||
|
let databaseService: jest.Mocked<DatabaseService>;
|
||||||
|
|
||||||
|
const mockJwtService = {
|
||||||
|
generateToken: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDatabaseService = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
testConnection: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
UserService,
|
||||||
|
{
|
||||||
|
provide: CustomJwtService,
|
||||||
|
useValue: mockJwtService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DatabaseService,
|
||||||
|
useValue: mockDatabaseService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UserService>(UserService);
|
||||||
|
jwtService = module.get(CustomJwtService);
|
||||||
|
databaseService = module.get(DatabaseService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autenticarUsuario', () => {
|
||||||
|
const mockUser: UserDTO = {
|
||||||
|
userName: 'testuser',
|
||||||
|
password: 'password123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDbResult = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
USERNAME: 'testuser',
|
||||||
|
NOME: 'Test User',
|
||||||
|
EMAIL: 'test@example.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('deve autenticar usuário com sucesso e retornar token e dados do usuário', async () => {
|
||||||
|
const expectedToken = 'mock.jwt.token';
|
||||||
|
const expectedPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
databaseService.execute.mockResolvedValue(mockDbResult);
|
||||||
|
jwtService.generateToken.mockReturnValue(expectedToken);
|
||||||
|
|
||||||
|
const result: AuthResponseDTO = await service.autenticarUsuario(mockUser);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
token: expectedToken,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
userName: 'testuser',
|
||||||
|
nome: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
},
|
||||||
|
message: 'Autenticação realizada com sucesso',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('SELECT'),
|
||||||
|
[mockUser.userName, mockUser.password],
|
||||||
|
);
|
||||||
|
expect(jwtService.generateToken).toHaveBeenCalledWith(expectedPayload);
|
||||||
|
expect(jwtService.generateToken).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando usuário não for encontrado', async () => {
|
||||||
|
databaseService.execute.mockResolvedValue({ rows: [] });
|
||||||
|
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
UnauthorizedException,
|
||||||
|
);
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
'Usuario ou senha invalidos',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('SELECT'),
|
||||||
|
[mockUser.userName, mockUser.password],
|
||||||
|
);
|
||||||
|
expect(jwtService.generateToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar UnauthorizedException quando senha estiver incorreta', async () => {
|
||||||
|
databaseService.execute.mockResolvedValue({ rows: [] });
|
||||||
|
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
UnauthorizedException,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalled();
|
||||||
|
expect(jwtService.generateToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar InternalServerErrorException quando houver erro no banco de dados', async () => {
|
||||||
|
const dbError = new Error('Erro de conexão com o banco');
|
||||||
|
databaseService.execute.mockRejectedValue(dbError);
|
||||||
|
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
InternalServerErrorException,
|
||||||
|
);
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
'Erro ao consultar view: Erro de conexão com o banco',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalled();
|
||||||
|
expect(jwtService.generateToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar InternalServerErrorException quando houver erro desconhecido', async () => {
|
||||||
|
databaseService.execute.mockRejectedValue('Erro desconhecido');
|
||||||
|
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
InternalServerErrorException,
|
||||||
|
);
|
||||||
|
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
|
||||||
|
'Erro ao consultar view: Erro desconhecido',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalled();
|
||||||
|
expect(jwtService.generateToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve propagar UnauthorizedException quando já lançada', async () => {
|
||||||
|
databaseService.execute.mockResolvedValue({ rows: [] });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.autenticarUsuario(mockUser);
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(UnauthorizedException);
|
||||||
|
expect(error.message).toBe('Usuario ou senha invalidos');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(databaseService.execute).toHaveBeenCalled();
|
||||||
|
expect(jwtService.generateToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve mapear corretamente os dados do banco para o payload JWT', async () => {
|
||||||
|
const customDbResult = {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
ID: 999,
|
||||||
|
USERNAME: 'customuser',
|
||||||
|
NOME: 'Custom Name',
|
||||||
|
EMAIL: 'custom@example.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
databaseService.execute.mockResolvedValue(customDbResult);
|
||||||
|
jwtService.generateToken.mockReturnValue('custom.token');
|
||||||
|
|
||||||
|
await service.autenticarUsuario(mockUser);
|
||||||
|
|
||||||
|
expect(jwtService.generateToken).toHaveBeenCalledWith({
|
||||||
|
sub: 999,
|
||||||
|
username: 'customuser',
|
||||||
|
nome: 'Custom Name',
|
||||||
|
email: 'custom@example.com',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('testConnection', () => {
|
||||||
|
it('deve retornar mensagem de sucesso quando conexão for bem-sucedida', async () => {
|
||||||
|
const expectedMessage = 'Conexão com Oracle bem-sucedida!';
|
||||||
|
databaseService.testConnection.mockResolvedValue(expectedMessage);
|
||||||
|
|
||||||
|
const result = await service.testConnection();
|
||||||
|
|
||||||
|
expect(result).toBe(expectedMessage);
|
||||||
|
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve propagar erro quando houver falha na conexão', async () => {
|
||||||
|
const connectionError = new Error('Falha ao conectar no Oracle: timeout');
|
||||||
|
databaseService.testConnection.mockRejectedValue(connectionError);
|
||||||
|
|
||||||
|
await expect(service.testConnection()).rejects.toThrow(connectionError);
|
||||||
|
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve propagar erro genérico quando houver falha desconhecida', async () => {
|
||||||
|
const genericError = new Error('Erro genérico');
|
||||||
|
databaseService.testConnection.mockRejectedValue(genericError);
|
||||||
|
|
||||||
|
await expect(service.testConnection()).rejects.toThrow(genericError);
|
||||||
|
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import UserDTO from 'src/models/UserDTO';
|
||||||
|
import { CustomJwtService } from '../jwt/jwt.service';
|
||||||
|
import AuthResponseDTO from 'src/models/AuthResponseDTO';
|
||||||
|
import { DatabaseService } from '../../database/database.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: CustomJwtService,
|
||||||
|
private readonly databaseService: DatabaseService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async autenticarUsuario(user: UserDTO): Promise<AuthResponseDTO> {
|
||||||
|
try {
|
||||||
|
const result = await this.databaseService.execute(
|
||||||
|
`SELECT PCEMPR.matricula as id
|
||||||
|
,PCEMPR.usuariobd as userName
|
||||||
|
,PCEMPR.NOME AS nome
|
||||||
|
,PCEMPR.EMAIL AS email
|
||||||
|
FROM PCEMPR
|
||||||
|
WHERE
|
||||||
|
PCEMPR.USUARIOBD = :username AND PCEMPR.SENHABD = CRYPT(:password, USUARIOBD)`,
|
||||||
|
[user.userName, user.password],
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = result.rows as any[];
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
throw new UnauthorizedException('Usuario ou senha invalidos');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = users[0];
|
||||||
|
|
||||||
|
// Gerar token JWT - usando as chaves corretas do Oracle (maiúsculas)
|
||||||
|
const payload = {
|
||||||
|
sub: userData.ID,
|
||||||
|
username: userData.USERNAME,
|
||||||
|
nome: userData.NOME,
|
||||||
|
email: userData.EMAIL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = this.jwtService.generateToken(payload);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: userData.ID,
|
||||||
|
userName: userData.USERNAME,
|
||||||
|
nome: userData.NOME,
|
||||||
|
email: userData.EMAIL,
|
||||||
|
},
|
||||||
|
message: 'Autenticação realizada com sucesso',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Erro ao consultar view: ' +
|
||||||
|
(error instanceof Error ? error.message : String(error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<string> {
|
||||||
|
return await this.databaseService.testConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('app')
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Endpoint de teste',
|
||||||
|
description: 'Endpoint público para verificar se a API está funcionando',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'API funcionando corretamente',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Hello World!',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { UserModule } from './Auth/user/user.module';
|
||||||
|
import { OrdersModule } from './orders/orders.module';
|
||||||
|
import { DatabaseModule } from './database/database.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
DatabaseModule,
|
||||||
|
UserModule,
|
||||||
|
OrdersModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from './database.service';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiInternalServerErrorResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('database')
|
||||||
|
@Controller('database')
|
||||||
|
export class DatabaseController {
|
||||||
|
constructor(private readonly databaseService: DatabaseService) {}
|
||||||
|
|
||||||
|
@Get('test-connection')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Testar conexão com banco de dados',
|
||||||
|
description:
|
||||||
|
'Endpoint público para testar a conectividade com o banco Oracle',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Conexão testada com sucesso',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Conexão estabelecida com sucesso',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiInternalServerErrorResponse({
|
||||||
|
description: 'Erro na conexão com banco de dados',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: false },
|
||||||
|
message: { type: 'string', example: 'Erro ao conectar com banco' },
|
||||||
|
poolAvailable: { type: 'boolean', example: false },
|
||||||
|
poolStats: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async testConnection(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
poolAvailable?: boolean;
|
||||||
|
poolStats?: unknown;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const result = await this.databaseService.testConnection();
|
||||||
|
return { success: true, message: result };
|
||||||
|
} catch (error) {
|
||||||
|
const poolAvailable = await this.databaseService.isPoolAvailable();
|
||||||
|
const poolStats = await this.databaseService.getPoolStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
poolAvailable,
|
||||||
|
poolStats: poolStats as unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('pool-status')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Status do pool de conexões',
|
||||||
|
description:
|
||||||
|
'Endpoint público para verificar o status do pool de conexões do banco',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Status do pool retornado com sucesso',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
poolAvailable: { type: 'boolean', example: true },
|
||||||
|
poolStats: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async getPoolStatus(): Promise<{
|
||||||
|
poolAvailable: boolean;
|
||||||
|
poolStats: unknown;
|
||||||
|
}> {
|
||||||
|
const poolAvailable = await this.databaseService.isPoolAvailable();
|
||||||
|
const poolStats = await this.databaseService.getPoolStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
poolAvailable,
|
||||||
|
poolStats: poolStats as unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from './database.service';
|
||||||
|
import { DatabaseController } from './database.controller';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
controllers: [DatabaseController],
|
||||||
|
providers: [DatabaseService],
|
||||||
|
exports: [DatabaseService],
|
||||||
|
})
|
||||||
|
export class DatabaseModule {}
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import * as oracledb from 'oracledb';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DatabaseService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(DatabaseService.name);
|
||||||
|
private pool: oracledb.Pool | null = null;
|
||||||
|
private poolCreationPromise: Promise<oracledb.Pool> | null = null;
|
||||||
|
private lastActivityTime: number = 0;
|
||||||
|
private inactivityTimer: NodeJS.Timeout | null = null;
|
||||||
|
private readonly INACTIVITY_TIMEOUT = 20000; // 20 segundos
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const libDir = process.env.ORACLE_LIB_DIR || '/Users/felipe/instantClient';
|
||||||
|
this.logger.log(`Oracle libDir: ${libDir}`);
|
||||||
|
oracledb.initOracleClient({ libDir });
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('🚀 Inicializando pool de conexões Oracle...');
|
||||||
|
try {
|
||||||
|
await this.getOrCreatePool();
|
||||||
|
this.logger.log('✅ Pool Oracle inicializado com sucesso!');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'❌ Erro ao inicializar pool Oracle:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateEnvironment(): boolean {
|
||||||
|
if (
|
||||||
|
!process.env.ORACLE_USER ||
|
||||||
|
!process.env.ORACLE_PASSWORD ||
|
||||||
|
!process.env.ORACLE_CONNECTION_STRING
|
||||||
|
) {
|
||||||
|
this.logger.warn('⚠️ Variáveis de ambiente do Oracle não configuradas.');
|
||||||
|
this.logger.warn(' Configure as seguintes variáveis no arquivo .env:');
|
||||||
|
this.logger.warn(' - ORACLE_USER');
|
||||||
|
this.logger.warn(' - ORACLE_PASSWORD');
|
||||||
|
this.logger.warn(' - ORACLE_CONNECTION_STRING');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPool(): Promise<oracledb.Pool> {
|
||||||
|
if (!this.validateEnvironment()) {
|
||||||
|
throw new Error('Configurações do Oracle não encontradas');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pool = await oracledb.createPool({
|
||||||
|
user: process.env.ORACLE_USER,
|
||||||
|
password: process.env.ORACLE_PASSWORD,
|
||||||
|
connectString: process.env.ORACLE_CONNECTION_STRING,
|
||||||
|
poolMin: 1,
|
||||||
|
poolMax: 30,
|
||||||
|
poolIncrement: 1,
|
||||||
|
queueTimeout: 60000,
|
||||||
|
});
|
||||||
|
this.logger.log('✅ Pool Oracle criada com sucesso!');
|
||||||
|
return pool;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'❌ Erro ao criar pool Oracle:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
throw new Error('Falha ao criar pool Oracle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreatePool(): Promise<oracledb.Pool> {
|
||||||
|
if (this.pool) {
|
||||||
|
return this.pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.poolCreationPromise) {
|
||||||
|
return await this.poolCreationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.poolCreationPromise = this.createPool();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.pool = await this.poolCreationPromise;
|
||||||
|
this.logger.log('✅ Pool Oracle recriada com sucesso!');
|
||||||
|
return this.pool;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erro ao recriar pool Oracle:', error);
|
||||||
|
this.poolCreationPromise = null;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.poolCreationPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateInactivityTimer(): void {
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
|
||||||
|
if (this.inactivityTimer) {
|
||||||
|
clearTimeout(this.inactivityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inactivityTimer = setTimeout(() => {
|
||||||
|
this.closePoolIfInactive().catch((error) => {
|
||||||
|
this.logger.error(
|
||||||
|
'❌ Erro no timer de inatividade:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, this.INACTIVITY_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async closePoolIfInactive(): Promise<void> {
|
||||||
|
const timeSinceLastActivity = Date.now() - this.lastActivityTime;
|
||||||
|
|
||||||
|
if (timeSinceLastActivity >= this.INACTIVITY_TIMEOUT && this.pool) {
|
||||||
|
this.logger.log('🔄 Fechando pool por inatividade (20s)...');
|
||||||
|
try {
|
||||||
|
await this.pool.close(20);
|
||||||
|
this.pool = null;
|
||||||
|
this.logger.log('✅ Pool fechada por inatividade');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'❌ Erro ao fechar pool:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConnection(): Promise<oracledb.Connection> {
|
||||||
|
try {
|
||||||
|
const pool = await this.getOrCreatePool();
|
||||||
|
|
||||||
|
// Verifica se a pool está saudável
|
||||||
|
const isHealthy = await this.isPoolHealthy();
|
||||||
|
if (!isHealthy) {
|
||||||
|
this.logger.log('🔄 Pool não está saudável, recriando...');
|
||||||
|
this.pool = null;
|
||||||
|
const newPool = await this.getOrCreatePool();
|
||||||
|
this.updateInactivityTimer();
|
||||||
|
return await newPool.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateInactivityTimer();
|
||||||
|
return await pool.getConnection();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erro ao obter conexão da pool:', error);
|
||||||
|
// Se a pool estiver fechada, tenta recriar
|
||||||
|
if (this.pool === null) {
|
||||||
|
this.logger.log('🔄 Tentando recriar pool...');
|
||||||
|
this.pool = null;
|
||||||
|
const newPool = await this.getOrCreatePool();
|
||||||
|
this.updateInactivityTimer();
|
||||||
|
return await newPool.getConnection();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
sql: string,
|
||||||
|
binds: any[] = [],
|
||||||
|
options: oracledb.ExecuteOptions = {},
|
||||||
|
): Promise<any> {
|
||||||
|
let conn: oracledb.Connection | null = null;
|
||||||
|
try {
|
||||||
|
conn = await this.getConnection();
|
||||||
|
return await conn.execute(sql, binds, {
|
||||||
|
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||||
|
autoCommit: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erro ao executar query:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
try {
|
||||||
|
await conn.close();
|
||||||
|
} catch (closeError) {
|
||||||
|
this.logger.error('❌ Erro ao fechar conexão:', closeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const pool = await this.getOrCreatePool();
|
||||||
|
this.updateInactivityTimer();
|
||||||
|
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
await conn.execute('SELECT 1 FROM DUAL');
|
||||||
|
await conn.close();
|
||||||
|
return 'Conexão com Oracle bem-sucedida!';
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('❌ Erro no teste de conexão:', err);
|
||||||
|
throw new Error(
|
||||||
|
'Falha ao conectar no Oracle: ' +
|
||||||
|
(err instanceof Error ? err.message : String(err)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isPoolAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!this.pool) {
|
||||||
|
await this.getOrCreatePool();
|
||||||
|
}
|
||||||
|
return !!this.pool;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erro ao verificar disponibilidade da pool:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isPoolHealthy(): Promise<boolean> {
|
||||||
|
if (!this.pool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conn = await this.pool.getConnection();
|
||||||
|
await conn.execute('SELECT 1 FROM DUAL');
|
||||||
|
await conn.close();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Pool não está saudável:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async forceClosePool(): Promise<void> {
|
||||||
|
if (this.inactivityTimer) {
|
||||||
|
clearTimeout(this.inactivityTimer);
|
||||||
|
this.inactivityTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pool) {
|
||||||
|
this.logger.log('🔄 Fechando pool forçadamente...');
|
||||||
|
try {
|
||||||
|
await this.pool.close(20);
|
||||||
|
this.pool = null;
|
||||||
|
this.logger.log('✅ Pool fechada forçadamente');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'❌ Erro ao fechar pool:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPoolStats(): Promise<any> {
|
||||||
|
try {
|
||||||
|
// Tenta obter ou criar a pool se não existir
|
||||||
|
if (!this.pool) {
|
||||||
|
await this.getOrCreatePool();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.pool) {
|
||||||
|
return { status: 'closed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'open',
|
||||||
|
lastActivity: new Date(this.lastActivityTime).toISOString(),
|
||||||
|
timeSinceLastActivity: Date.now() - this.lastActivityTime,
|
||||||
|
inactivityTimeout: this.INACTIVITY_TIMEOUT,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erro ao obter estatísticas da pool:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Habilitar CORS
|
||||||
|
app.enableCors({
|
||||||
|
origin: true,
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Configuração do Swagger
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Controle Saída Loja API')
|
||||||
|
.setDescription('API para controle de saída de loja com autenticação JWT')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addTag('auth', 'Endpoints de autenticação')
|
||||||
|
.addTag('user', 'Endpoints de usuário')
|
||||||
|
.addTag('database', 'Endpoints de banco de dados')
|
||||||
|
.addTag('orders', 'Endpoints de pedidos/notas fiscais')
|
||||||
|
.addBearerAuth(
|
||||||
|
{
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
name: 'JWT',
|
||||||
|
description: 'Enter JWT token',
|
||||||
|
in: 'header',
|
||||||
|
},
|
||||||
|
'JWT-auth', // This name here is important for references
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api', app, document, {
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
|
await app.listen(process.env.PORT ?? 3001, '0.0.0.0');
|
||||||
|
console.log(`Server is running on port ${process.env.PORT ?? 3001}`);
|
||||||
|
console.log(
|
||||||
|
`Swagger documentation available at http://localhost:${process.env.PORT ?? 3001}/api`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('Recebido SIGINT. Encerrando aplicação...');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('Recebido SIGTERM. Encerrando aplicação...');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export default class AuthResponseDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Token JWT para autenticação',
|
||||||
|
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Dados do usuário autenticado',
|
||||||
|
example: {
|
||||||
|
id: 1,
|
||||||
|
userName: 'JOAO.SILVA',
|
||||||
|
nome: 'João Silva',
|
||||||
|
email: 'joao.silva@empresa.com',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
userName: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Mensagem de resposta da autenticação',
|
||||||
|
example: 'Autenticação realizada com sucesso',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export default class UserDTO {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID do usuário (opcional, gerado automaticamente)',
|
||||||
|
example: 1,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Nome de usuário para login',
|
||||||
|
example: 'JOAO.SILVA',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'userName é obrigatório' })
|
||||||
|
@IsString({ message: 'userName deve ser uma string' })
|
||||||
|
userName: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Nome completo do usuário',
|
||||||
|
example: 'João Silva',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
nome?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Email do usuário',
|
||||||
|
example: 'joao.silva@empresa.com',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Senha do usuário',
|
||||||
|
example: 'SENHA123',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'password é obrigatório' })
|
||||||
|
@IsString({ message: 'password deve ser uma string' })
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class EntregaDto {
|
||||||
|
@ApiProperty({ description: 'Número da transação de venda' })
|
||||||
|
@IsNumber()
|
||||||
|
NUMTRANSVENDA: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Documento do recebedor' })
|
||||||
|
@IsString()
|
||||||
|
DOCUMENTO: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Nome do recebedor' })
|
||||||
|
@IsString()
|
||||||
|
RECEBEDOR: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Imagens da entrega',
|
||||||
|
type: () => EntregaImagemDto,
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EntregaImagemDto)
|
||||||
|
IMAGENS: EntregaImagemDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EntregaImagemDto {
|
||||||
|
@ApiProperty({ description: 'Tipo de imagem', enum: ['RP', 'SIG'] })
|
||||||
|
@IsEnum(['RP', 'SIG'])
|
||||||
|
TIPO: 'RP' | 'SIG';
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'URL da imagem' })
|
||||||
|
@IsString()
|
||||||
|
URLIMAGEM: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export class Order {}
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
ForbiddenException,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Request,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBadGatewayResponse,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiForbiddenResponse,
|
||||||
|
ApiInternalServerErrorResponse,
|
||||||
|
ApiNotAcceptableResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
ApiUnprocessableEntityResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../Auth/guards/jwt-auth.guard';
|
||||||
|
import { EntregaDto } from './dto/orders.dto';
|
||||||
|
import { OrdersService } from './orders.service';
|
||||||
|
|
||||||
|
// Interface para tipar o request com usuário
|
||||||
|
interface RequestWithUser extends Request {
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('orders')
|
||||||
|
@Controller('orders')
|
||||||
|
export class OrdersController {
|
||||||
|
constructor(private readonly ordersService: OrdersService) {}
|
||||||
|
|
||||||
|
@Get('invoice/:chaveNota')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Obter nota fiscal',
|
||||||
|
description:
|
||||||
|
'Endpoint protegido para obter dados de uma nota fiscal específica. Requer autenticação JWT e permissões adequadas.',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'chaveNota',
|
||||||
|
description: 'Chave da nota fiscal',
|
||||||
|
example: '35240112345678901234567890123456789012345678',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Nota fiscal retornada com sucesso',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
chaveNota: {
|
||||||
|
type: 'string',
|
||||||
|
example: '35240112345678901234567890123456789012345678',
|
||||||
|
},
|
||||||
|
numeroNota: { type: 'string', example: '123456' },
|
||||||
|
// Outros campos da nota fiscal...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiNotAcceptableResponse({
|
||||||
|
description: 'Nota fiscal ja feito devolução',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 406 },
|
||||||
|
message: { type: 'string', example: 'Nota fiscal ja feito devolução' },
|
||||||
|
error: { type: 'string', example: 'Not Acceptable' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiUnprocessableEntityResponse({
|
||||||
|
description: 'Nota fiscal não é do tipo de Entrega',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 422 },
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Nota fiscal não é do tipo de Entrega',
|
||||||
|
},
|
||||||
|
error: { type: 'string', example: 'Unprocessable Entity' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiBadGatewayResponse({
|
||||||
|
description: 'Nota Fiscal entregue ao cliente!',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 502 },
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Nota Fiscal entregue ao cliente!',
|
||||||
|
},
|
||||||
|
error: { type: 'string', example: 'Bad Gateway' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiUnauthorizedResponse({
|
||||||
|
description: 'Token JWT inválido ou ausente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 401 },
|
||||||
|
message: { type: 'string', example: 'Unauthorized' },
|
||||||
|
error: { type: 'string', example: 'Unauthorized' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiForbiddenResponse({
|
||||||
|
description: 'Usuário sem permissão para acessar a nota fiscal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 403 },
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Você não tem permissão para acessar as nota fiscal',
|
||||||
|
},
|
||||||
|
error: { type: 'string', example: 'Forbidden' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiInternalServerErrorResponse({
|
||||||
|
description: 'Erro interno do servidor',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 500 },
|
||||||
|
message: { type: 'string', example: 'Erro interno do servidor' },
|
||||||
|
error: { type: 'string', example: 'Internal Server Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async getNotaFiscal(
|
||||||
|
@Param('chaveNota') chaveNota: string,
|
||||||
|
@Request() req: RequestWithUser,
|
||||||
|
): Promise<any> {
|
||||||
|
const userFromToken = req.user;
|
||||||
|
|
||||||
|
const permissao = await this.ordersService.getPermissao(
|
||||||
|
userFromToken.id,
|
||||||
|
991234,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userFromToken.id !== permissao.idUsuario) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Você não tem permissão para acessar as nota fiscal',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const notaFiscal = await this.ordersService.getNotaFiscal(chaveNota);
|
||||||
|
|
||||||
|
return notaFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('updateinvoice/:chaveNota')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiBody({
|
||||||
|
type: EntregaDto,
|
||||||
|
examples: {
|
||||||
|
'Exemplo endopint': {
|
||||||
|
value: {
|
||||||
|
NUMTRANSVENDA: 123456,
|
||||||
|
DOCUMENTO: '123123',
|
||||||
|
RECEBEDOR: 'FELIPE TESTE',
|
||||||
|
IMAGENS: [
|
||||||
|
{
|
||||||
|
TIPO: 'RP',
|
||||||
|
URLIMAGEM: 'https://testedeinsecaodeimagem.com/1234.jpeg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TIPO: 'SIG',
|
||||||
|
URLIMAGEM: 'https://testedeinsecaodeimagem.com/1234.jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Atualizar registro de nota fiscal',
|
||||||
|
description:
|
||||||
|
'Endpoint protegido para atualizar o status de uma nota fiscal. Requer autenticação JWT e permissões adequadas.',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'chaveNota',
|
||||||
|
description: 'Chave da nota fiscal',
|
||||||
|
example: '35240112345678901234567890123456789012345678',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Registro atualizado com sucesso',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
message: { type: 'string', example: 'Registro atualizado com sucesso' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiUnauthorizedResponse({
|
||||||
|
description: 'Token JWT inválido ou ausente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 401 },
|
||||||
|
message: { type: 'string', example: 'Unauthorized' },
|
||||||
|
error: { type: 'string', example: 'Unauthorized' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiForbiddenResponse({
|
||||||
|
description: 'Usuário sem permissão para atualizar a nota fiscal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 403 },
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Você não tem permissão para atualizar as nota fiscal',
|
||||||
|
},
|
||||||
|
error: { type: 'string', example: 'Forbidden' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiInternalServerErrorResponse({
|
||||||
|
description: 'Erro interno do servidor',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 500 },
|
||||||
|
message: { type: 'string', example: 'Erro interno do servidor' },
|
||||||
|
error: { type: 'string', example: 'Internal Server Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async updateRegistro(
|
||||||
|
@Param('chaveNota') chaveNota: string,
|
||||||
|
@Request() req: RequestWithUser,
|
||||||
|
@Body() entrega: EntregaDto,
|
||||||
|
): Promise<any> {
|
||||||
|
const permissao = await this.ordersService.getPermissao(
|
||||||
|
req.user.id,
|
||||||
|
991234,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (req.user.id !== permissao.idUsuario) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Você não tem permissão para atualizar as nota fiscal',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ordersService.updateRegistro(chaveNota, req.user.id, entrega);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { OrdersService } from './orders.service';
|
||||||
|
import { OrdersController } from './orders.controller';
|
||||||
|
import { JwtAuthModule } from '../Auth/jwt/jwt.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [JwtAuthModule],
|
||||||
|
controllers: [OrdersController],
|
||||||
|
providers: [OrdersService],
|
||||||
|
})
|
||||||
|
export class OrdersModule {}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
import {
|
||||||
|
BadGatewayException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
NotAcceptableException,
|
||||||
|
NotFoundException,
|
||||||
|
UnprocessableEntityException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../database/database.service';
|
||||||
|
import { EntregaDto } from './dto/orders.dto';
|
||||||
|
|
||||||
|
// Interfaces para tipagem dos dados
|
||||||
|
interface NotaFiscal {
|
||||||
|
CODFILIAL: string;
|
||||||
|
DTSAIDA: Date | string;
|
||||||
|
NUMPED: number;
|
||||||
|
CHAVENFE: string;
|
||||||
|
NUMNOTA: number;
|
||||||
|
NUMTRANSVENDA: number;
|
||||||
|
CODCLI: number;
|
||||||
|
CLIENTE: string;
|
||||||
|
CODUSUR: number;
|
||||||
|
NOME: string;
|
||||||
|
NUMITENS: number;
|
||||||
|
DTCANHOTO: Date | string | null;
|
||||||
|
DEVOL: string;
|
||||||
|
CONDVENDA: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Produto {
|
||||||
|
CODPROD: number;
|
||||||
|
DESCRICAO: string;
|
||||||
|
UNIDADE: string;
|
||||||
|
CODAUXILIAR: string;
|
||||||
|
MULTIPLO: number;
|
||||||
|
URLIMAGEM: string;
|
||||||
|
QT: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotaFiscalComItens {
|
||||||
|
CODFILIAL: string;
|
||||||
|
DTSAIDA: Date | string;
|
||||||
|
NUMPED: number;
|
||||||
|
CHAVENFE: string;
|
||||||
|
NUMNOTA: number;
|
||||||
|
NUMTRANSVENDA: number;
|
||||||
|
CODCLI: number;
|
||||||
|
CLIENTE: string;
|
||||||
|
CODUSUR: number;
|
||||||
|
NOME: string;
|
||||||
|
NUMITENS: number;
|
||||||
|
DTCANHOTO: Date | string | null;
|
||||||
|
itens: Produto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface para o resultado do banco de dados
|
||||||
|
interface DatabaseResult {
|
||||||
|
rows: any[];
|
||||||
|
rowsAffected?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface para o resultado da permissão
|
||||||
|
interface PermissaoResult {
|
||||||
|
CODUSUARIO: number;
|
||||||
|
CODROTINA: number;
|
||||||
|
ACESSO: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrdersService {
|
||||||
|
constructor(private readonly databaseService: DatabaseService) {}
|
||||||
|
|
||||||
|
async getNotaFiscal(chaveNota: string): Promise<NotaFiscalComItens> {
|
||||||
|
const sqlNotaFiscal = `
|
||||||
|
SELECT PCNFSAID.CODFILIAL,
|
||||||
|
TO_CHAR(PCNFSAID.DTSAIDA, 'DD/MM/YYYY') AS DTSAIDA,
|
||||||
|
PCNFSAID.NUMPED,
|
||||||
|
PCNFSAID.CHAVENFE,
|
||||||
|
PCNFSAID.NUMNOTA,
|
||||||
|
PCNFSAID.NUMTRANSVENDA,
|
||||||
|
PCNFSAID.CODCLI,
|
||||||
|
PCCLIENT.CLIENTE,
|
||||||
|
PCNFSAID.CODUSUR,
|
||||||
|
PCUSUARI.NOME,
|
||||||
|
PCNFSAID.NUMITENS,
|
||||||
|
TO_CHAR(PCNFSAID.DTCANHOTO, 'DD/MM/YYYY') AS DTCANHOTO,
|
||||||
|
NVL((SELECT 'S' FROM ESTPREDEVCLI WHERE ESTPREDEVCLI.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA and rownum=1),'N') DEVOL,
|
||||||
|
PCNFSAID.CONDVENDA
|
||||||
|
FROM PCNFSAID, PCCLIENT, PCUSUARI
|
||||||
|
WHERE PCNFSAID.CODCLI = PCCLIENT.CODCLI
|
||||||
|
AND PCNFSAID.CODUSUR = PCUSUARI.CODUSUR
|
||||||
|
AND PCNFSAID.DTCANCEL IS NULL
|
||||||
|
AND PCNFSAID.CHAVENFE=:chaveNota`;
|
||||||
|
|
||||||
|
const sqlProduto = `
|
||||||
|
SELECT PCMOV.CODPROD,
|
||||||
|
PCPRODUT.DESCRICAO,
|
||||||
|
PCPRODUT.UNIDADE,
|
||||||
|
PCPRODUT.CODAUXILIAR,
|
||||||
|
PCPRODUT.MULTIPLO,
|
||||||
|
PCPRODUT.URLIMAGEM,
|
||||||
|
SUM(PCMOV.QT) QT
|
||||||
|
FROM PCMOV, PCPRODUT
|
||||||
|
WHERE PCMOV.CODPROD = PCPRODUT.CODPROD
|
||||||
|
AND PCMOV.NUMTRANSVENDA = :transacao
|
||||||
|
GROUP BY
|
||||||
|
PCMOV.CODPROD,
|
||||||
|
PCPRODUT.DESCRICAO,
|
||||||
|
PCPRODUT.UNIDADE,
|
||||||
|
PCPRODUT.CODAUXILIAR,
|
||||||
|
PCPRODUT.MULTIPLO,
|
||||||
|
PCPRODUT.URLIMAGEM`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await this.databaseService.execute(sqlNotaFiscal, [
|
||||||
|
chaveNota,
|
||||||
|
])) as DatabaseResult;
|
||||||
|
|
||||||
|
if (!result || !result.rows || result.rows.length === 0) {
|
||||||
|
throw new NotFoundException('Nota Fiscal não encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
const notaFiscal = result.rows[0] as NotaFiscal;
|
||||||
|
|
||||||
|
if (notaFiscal.CONDVENDA !== 8) {
|
||||||
|
throw new UnprocessableEntityException(
|
||||||
|
'Nota fiscal não é do tipo de Entrega',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notaFiscal.DEVOL === 'S') {
|
||||||
|
throw new NotAcceptableException('Nota fiscal ja feito devolução');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notaFiscal.DTCANHOTO !== null) {
|
||||||
|
throw new BadGatewayException('Nota Fiscal entregue ao cliente!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const itensResult = (await this.databaseService.execute(sqlProduto, [
|
||||||
|
notaFiscal.NUMTRANSVENDA,
|
||||||
|
])) as DatabaseResult;
|
||||||
|
|
||||||
|
const itens = itensResult.rows as Produto[];
|
||||||
|
|
||||||
|
const notaFiscalComItens: NotaFiscalComItens = {
|
||||||
|
CODFILIAL: notaFiscal.CODFILIAL,
|
||||||
|
DTSAIDA: notaFiscal.DTSAIDA,
|
||||||
|
NUMPED: notaFiscal.NUMPED,
|
||||||
|
CHAVENFE: notaFiscal.CHAVENFE,
|
||||||
|
NUMNOTA: notaFiscal.NUMNOTA,
|
||||||
|
NUMTRANSVENDA: notaFiscal.NUMTRANSVENDA,
|
||||||
|
CODCLI: notaFiscal.CODCLI,
|
||||||
|
CLIENTE: notaFiscal.CLIENTE,
|
||||||
|
CODUSUR: notaFiscal.CODUSUR,
|
||||||
|
NOME: notaFiscal.NOME,
|
||||||
|
NUMITENS: notaFiscal.NUMITENS,
|
||||||
|
DTCANHOTO: notaFiscal.DTCANHOTO,
|
||||||
|
itens: itens.map((item) => {
|
||||||
|
return {
|
||||||
|
CODPROD: item.CODPROD,
|
||||||
|
DESCRICAO: item.DESCRICAO,
|
||||||
|
UNIDADE: item.UNIDADE,
|
||||||
|
CODAUXILIAR: item.CODAUXILIAR,
|
||||||
|
MULTIPLO: item.MULTIPLO,
|
||||||
|
QT: item.QT,
|
||||||
|
URLIMAGEM: item.URLIMAGEM
|
||||||
|
? item.URLIMAGEM.split(';')[0].replace(
|
||||||
|
'http://167.249.211.178:8001',
|
||||||
|
'http://10.1.1.191',
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return notaFiscalComItens;
|
||||||
|
} catch (error) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Erro ao buscar nota fiscal',
|
||||||
|
error instanceof Error ? error.message : 'Erro desconhecido',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPermissao(idUsuario: number, codRotina: number): Promise<any> {
|
||||||
|
const sqlPermissao = `
|
||||||
|
SELECT PCCONTRO.CODUSUARIO,
|
||||||
|
PCCONTRO.CODROTINA,
|
||||||
|
PCCONTRO.ACESSO
|
||||||
|
FROM PCCONTRO
|
||||||
|
WHERE PCCONTRO.CODUSUARIO = :idUsuario
|
||||||
|
AND PCCONTRO.CODROTINA = :codRotina
|
||||||
|
AND PCCONTRO.ACESSO = 'S'
|
||||||
|
`;
|
||||||
|
try {
|
||||||
|
const result = (await this.databaseService.execute(sqlPermissao, [
|
||||||
|
idUsuario,
|
||||||
|
codRotina,
|
||||||
|
])) as DatabaseResult;
|
||||||
|
|
||||||
|
if (!result || !result.rows || result.rows.length === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Usuario sem permissao para acessar notas fiscais',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissao = result.rows[0] as PermissaoResult;
|
||||||
|
|
||||||
|
return {
|
||||||
|
idUsuario: permissao.CODUSUARIO,
|
||||||
|
codRotina: permissao.CODROTINA,
|
||||||
|
acesso: permissao.ACESSO,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Erro ao buscar permissao',
|
||||||
|
error instanceof Error ? error.message : 'Erro desconhecido',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRegistro(
|
||||||
|
chaveNota: string,
|
||||||
|
codusuario: number,
|
||||||
|
entrega: EntregaDto,
|
||||||
|
): Promise<any> {
|
||||||
|
const sqlUpdate = `
|
||||||
|
UPDATE PCNFSAID
|
||||||
|
SET CODFUNCCANHOTO = :codusuario, DTCANHOTO = TRUNC(SYSDATE)
|
||||||
|
WHERE CHAVENFE = :chaveNota
|
||||||
|
`;
|
||||||
|
const sqlInsertEntrega = `INSERT INTO ESTENTREGAS (CODSAIDA, NUMTRANSVENDA, DATA, DOCUMENTORECEBEDOR, NOMERECEBEDOR)
|
||||||
|
VALUES (0, :NUMTRANSVENDA, SYSDATE, :DOCUMENTO, :RECEBEDOR)`;
|
||||||
|
|
||||||
|
const seleInsertEntregaImagem = `INSERT INTO ESTENTREGASIMAGENS (CODSAIDA, NUMTRANSVENDA, TIPO, URL)
|
||||||
|
VALUES (0,:NUMTRANSVENDA,:TIPO, :URLIMAGEM)`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await this.databaseService.execute(sqlUpdate, [
|
||||||
|
codusuario,
|
||||||
|
chaveNota,
|
||||||
|
])) as DatabaseResult;
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
// Verificar se algum registro foi afetado
|
||||||
|
if (result.rowsAffected === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Nenhum registro encontrado para atualizar',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultEntregas = (await this.databaseService.execute(
|
||||||
|
sqlInsertEntrega,
|
||||||
|
[entrega.NUMTRANSVENDA, entrega.DOCUMENTO, entrega.RECEBEDOR],
|
||||||
|
)) as DatabaseResult;
|
||||||
|
|
||||||
|
if (resultEntregas.rowsAffected === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Nenhum registro de entrega encontrado para inserir',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const imagem of entrega.IMAGENS) {
|
||||||
|
const resultImagem = (await this.databaseService.execute(
|
||||||
|
seleInsertEntregaImagem,
|
||||||
|
[entrega.NUMTRANSVENDA, imagem.TIPO, imagem.URLIMAGEM],
|
||||||
|
)) as DatabaseResult;
|
||||||
|
|
||||||
|
console.log(resultImagem);
|
||||||
|
|
||||||
|
if (resultImagem.rowsAffected === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Nenhum registro de imagem de entrega encontrado para inserir',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Registro atualizado com sucesso!',
|
||||||
|
success: true,
|
||||||
|
rowsAffected: result.rowsAffected,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Erro ao atualizar registro',
|
||||||
|
error instanceof Error ? error.message : 'Erro desconhecido',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue