chore: add files

main
Henrriky 2024-12-16 20:37:23 -03:00
commit aad59e55b0
34 changed files with 18800 additions and 0 deletions

4
.env.example 100644
View File

@ -0,0 +1,4 @@
HITPHONE_DESKTOP_SERVER_ORIGINS="http://172.31.187.195:4000"
API_KEY="ASDASDASDASD"
ENCRYPTION_KEY="abc123"
ENVIRONMENT="DEV"

26
.eslintrc.json 100644
View File

@ -0,0 +1,26 @@
{
"env": {
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"standard-with-typescript"
],
"rules": {
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"semi": ["error", "never"],
"comma-dangle": ["error", "never"]
}
}

61
.gitignore vendored 100644
View File

@ -0,0 +1,61 @@
# 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
cert-hitphone.key
cert-hitphone.pem
localhost-key.pem
localhost.pem

4
.prettierrc 100644
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

9
README.MD 100644
View File

@ -0,0 +1,9 @@
# Variáveis de ambiente necessárias
- `HITPHONE_DESKTOP_SERVER_ORIGINS`: Campo do cors utilizado pelo servidor electron criado no desktop do cliente Hitphone
- `API_KEY`: Chave de API utilizada pelo client.
# Arquivos necessários
- Arquivo `cert-hitphone.key` na raiz do projeto: Chave privada do certificado SSL
- Arquivo `cert-hitphone.pem` na raiz do projeto: Certificado público do certificado SSL

47
TODO.MD 100644
View File

@ -0,0 +1,47 @@
# Objetivo
- Implementar uma Inbox de API que tem suporte para o provedor de SMS InfoBIP
## Chatwoot -> HIT-SMS-API
- [ ] Rota de envio dos eventos oriundos do Chatwoot
- [ ] POST https://hit-sms-api.omnihit.app.br/api/chatwoot/webhook/sms/:provider_infobip/:apikey/:inbox_123
- [ ] Evento: message_created
- Parâmetros Requisição:
```json
{
"id": 0,
"content": "This is a incoming message from API Channel",
"created_at": "2020-08-30T15:43:04.000Z",
"message_type": "incoming",
"content_type": null,
"content_attributes": {},
"source_id": null,
"sender": {
"id": 0,
"name": "contact-name",
"avatar": "",
"type": "contact"
},
"inbox": {
"id": 0,
"name": "API Channel"
},
"conversation": {
"additional_attributes": null,
"channel": "Channel::Api",
"id": 0,
"inbox_id": 0,
"status": "open",
"agent_last_seen_at": 0,
"contact_last_seen_at": 0,
"timestamp": 0
},
"account": {
"id": 1,
"name": "API testing"
},
"event": "message_created"
}
```
-

4
api.http 100644
View File

@ -0,0 +1,4 @@
### Requisição para obter a configuração do servidor
GET http://localhost:3435/api/hub/config
Content-Type: application/json
X-API-KEY: ASDASDASDASD

View File

@ -0,0 +1,25 @@
services:
redis-sms-api:
container_name: redis-sms-api
hostname: redis-sms-api
image: redis
ports:
- "6389:6379"
restart: always
volumes:
- redis-data:/data
redis-commander-sms-api:
container_name: redis-commander-sms-api
hostname: redis-commander-sms-api
image: ghcr.io/joeferner/redis-commander:latest
#build: .
environment:
- REDIS_HOSTS=local:redis-sms-api:6379
ports:
- "8095:8081"
depends_on:
- redis-sms-api
volumes:
redis-data:
driver: local

View File

@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: 'hitphone-hub',
script: 'dist/main.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production'
}
}
]
}

8
nest-cli.json 100644
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11523
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

75
package.json 100644
View File

@ -0,0 +1,75 @@
{
"name": "hitphone-hub",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "npm run build && pm2 start ecosystem.config.js && pm2 reload hitphone-hub && pm2 save",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"lint": "eslint 'src/**/*.{ts,js}'",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
"@infobip-api/sdk": "^0.3.2",
"@nestjs/common": "^10.4.4",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"morgan": "^1.10.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/morgan": "^1.9.9",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"standard": "^17.1.2",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

6587
pnpm-lock.yaml 100644

File diff suppressed because it is too large Load Diff

24
src/app.module.ts 100644
View File

@ -0,0 +1,24 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { HubModule } from './hub/hub.module'
import { LoggerMiddleware } from './middlewares/logger.middleware'
import { SmsModule } from './sms/sms.module';
import { ChatwootModule } from './chatwoot/chatwoot.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true
}),
HubModule,
SmsModule,
ChatwootModule
],
providers: [HubModule]
})
export class AppModule implements NestModule {
configure (consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware)
.forRoutes('*')
}
}

View File

@ -0,0 +1,35 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'
import { ChatwootService } from './chatwoot.service'
@Controller('chatwoot')
export class ChatwootController {
constructor (private readonly chatwootService: ChatwootService) {}
@Post('/webhook/:sms/:provider/:apikey/:inbox')
create (
@Param() params: { sms: string, provider: 'infobip', apikey: string, inbox: string }
) {
}
@Get()
findAll () {
return this.chatwootService.findAll()
}
@Get(':id')
findOne (@Param('id') id: string) {
return this.chatwootService.findOne(+id)
}
@Patch(':id')
update (@Param('id') id: string, @Body() updateChatwootDto: UpdateChatwootDto) {
return this.chatwootService.update(+id, updateChatwootDto)
}
@Delete(':id')
remove (@Param('id') id: string) {
return this.chatwootService.remove(+id)
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ChatwootService } from './chatwoot.service';
import { ChatwootController } from './chatwoot.controller';
@Module({
controllers: [ChatwootController],
providers: [ChatwootService],
})
export class ChatwootModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ChatwootService } from './chatwoot.service';
describe('ChatwootService', () => {
let service: ChatwootService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ChatwootService],
}).compile();
service = module.get<ChatwootService>(ChatwootService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { CreateChatwootDto } from './dto/create-chatwoot.dto';
import { UpdateChatwootDto } from './dto/update-chatwoot.dto';
@Injectable()
export class ChatwootService {
create(createChatwootDto: CreateChatwootDto) {
return 'This action adds a new chatwoot';
}
findAll() {
return `This action returns all chatwoot`;
}
findOne(id: number) {
return `This action returns a #${id} chatwoot`;
}
update(id: number, updateChatwootDto: UpdateChatwootDto) {
return `This action updates a #${id} chatwoot`;
}
remove(id: number) {
return `This action removes a #${id} chatwoot`;
}
}

View File

@ -0,0 +1,18 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
import { Request } from 'express'
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate (context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>()
const apiKey = request.headers['x-api-key']
const validApiKey = process.env.API_KEY
if (apiKey && apiKey === validApiKey) {
return true
}
throw new UnauthorizedException('API key is invalid or missing')
}
}

View File

@ -0,0 +1,24 @@
import { Controller, Get, InternalServerErrorException, Logger, UseGuards } from '@nestjs/common'
import { HubService } from './hub.service'
import { ApiKeyGuard } from 'src/guards/api-key.guard'
@Controller('hub')
@UseGuards(ApiKeyGuard)
export class HubController {
constructor (private readonly hubService: HubService) { }
@Get('/config')
async findHitphoneDesktopServerConfig () {
try {
const hitphoneDesktopServerConfig = await this.hubService.findHitphoneDesktopServerConfig()
return {
...hitphoneDesktopServerConfig
}
} catch (error) {
Logger.error(`Error reading certificate or key file: ${error.message}`)
throw new InternalServerErrorException({
message: 'Error reading config informations'
})
}
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { HubService } from './hub.service'
import { HubController } from './hub.controller'
@Module({
controllers: [HubController],
providers: [HubService]
})
export class HubModule {}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import * as fs from 'fs/promises'
import * as path from 'path'
import encrypt from 'src/utils/encrypt'
@Injectable()
export class HubService {
constructor (private readonly configService: ConfigService) { }
async findHitphoneDesktopServerConfig () {
try {
const ENCRYPTION_KEY = this.configService.get<string>('ENCRYPTION_KEY')
const origin = this.configService.get<string>('HITPHONE_DESKTOP_SERVER_ORIGINS')
const certificatePem = await fs.readFile(path.join(__dirname, '../../cert-hitphone.pem'), 'utf8')
const certificateKey = await fs.readFile(path.join(__dirname, '../../cert-hitphone.key'), 'utf8')
if (!origin || !certificatePem || !certificateKey || !ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY, HITPHONE_DESKTOP_SERVER_ORIGINS, cert-hitphone.pem or cert-hitphone.key doesn't exist")
}
const originSplit = origin.split(',')
// return keys
const encryptedOriginReturnKey = encrypt('origin', ENCRYPTION_KEY)
const encryptedCertificateKeyReturnKey = encrypt('certificateKey', ENCRYPTION_KEY)
const encryptedCertificatePemReturnKey = encrypt('certificatePem', ENCRYPTION_KEY)
// return values
const originEncrypted = originSplit.map((origin) => {
return encrypt(origin, ENCRYPTION_KEY)
})
const encryptedCertificateKey = encrypt(certificateKey, ENCRYPTION_KEY)
const encryptedCertificatePem = encrypt(certificatePem, ENCRYPTION_KEY)
return {
[encryptedOriginReturnKey]: originEncrypted,
[encryptedCertificateKeyReturnKey]: encryptedCertificateKey,
[encryptedCertificatePemReturnKey]: encryptedCertificatePem
}
} catch (error) {
throw new Error('Error reading certificate or key file: ' + error.message)
}
}
}

15
src/main.ts 100644
View File

@ -0,0 +1,15 @@
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap () {
const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('api')
app.enableCors({
origin: '*'
})
await app.listen(3435)
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
bootstrap()

View File

@ -0,0 +1,49 @@
import { Injectable, NestMiddleware } from '@nestjs/common'
import * as morgan from 'morgan'
import { IncomingMessage, ServerResponse } from 'http'
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private readonly morganMiddleware: any
constructor () {
// Define os tokens personalizados
morgan.token('timestamp', () => {
const date = new Date()
return date.toLocaleString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour12: false
})
})
morgan.token('statusColor', (req: IncomingMessage, res: ServerResponse) => {
const status = res.headersSent ? res.statusCode : undefined
const color = status !== undefined
? (
status >= 500
? 31
: status >= 400
? 33
: status >= 300
? 36
: status >= 200
? 32
: 0
)
: 0
return `\x1b[${color}m${status}\x1b[0m`
})
this.morganMiddleware = morgan(':timestamp - :method :url :statusColor :response-time ms - :res[content-length]')
}
use (req: IncomingMessage, res: ServerResponse, next: () => void) {
this.morganMiddleware(req, res, next)
}
}

View File

@ -0,0 +1 @@
export class CreateSmDto {}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateSmDto } from './create-sm.dto';
export class UpdateSmDto extends PartialType(CreateSmDto) {}

View File

@ -0,0 +1 @@
export class Sm {}

View File

@ -0,0 +1,45 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, InternalServerErrorException, Logger } from '@nestjs/common'
import { SmsService } from './sms.service'
import { CreateSmDto } from './dto/create-sm.dto'
import { UpdateSmDto } from './dto/update-sm.dto'
@Controller('sms')
export class SmsController {
constructor (private readonly smsService: SmsService) {}
@Post('/:provider')
createSmsMessage (@Body() smsMessage: CreateSmDto) {
try {
const hitphoneDesktopServerConfig = await this.hubService.findHitphoneDesktopServerConfig()
return {
...hitphoneDesktopServerConfig
}
} catch (error) {
Logger.error(`Error reading certificate or key file: ${error.message}`)
throw new InternalServerErrorException({
message: 'Error reading config informations'
})
}
// return this.smsService.create(createSmDto)
}
@Get()
findAll () {
return this.smsService.findAll()
}
@Get(':id')
findOne (@Param('id') id: string) {
return this.smsService.findOne(+id)
}
@Patch(':id')
update (@Param('id') id: string, @Body() updateSmDto: UpdateSmDto) {
return this.smsService.update(+id, updateSmDto)
}
@Delete(':id')
remove (@Param('id') id: string) {
return this.smsService.remove(+id)
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { SmsService } from './sms.service'
import { SmsController } from './sms.controller'
@Module({
controllers: [SmsController],
providers: [SmsService]
})
export class SmsModule {}

View File

@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing'
import { SmsService } from './sms.service'
describe('SmsService', () => {
let service: SmsService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SmsService]
}).compile()
service = module.get<SmsService>(SmsService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

View File

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common'
import { CreateSmDto } from './dto/create-sm.dto'
import { UpdateSmDto } from './dto/update-sm.dto'
@Injectable()
export class SmsService {
create (createSmDto: CreateSmDto) {
return 'This action adds a new sm'
}
findAll () {
return 'This action returns all sms'
}
findOne (id: number) {
return `This action returns a #${id} sm`
}
update (id: number, updateSmDto: UpdateSmDto) {
return `This action updates a #${id} sm`
}
remove (id: number) {
return `This action removes a #${id} sm`
}
}

View File

@ -0,0 +1,10 @@
import * as CryptoJS from 'crypto-js'
function encrypt (string: string, ENCRYPTION_KEY: string): string {
const encryptedText = CryptoJS.AES.encrypt(string, ENCRYPTION_KEY)
const ciphertext = encryptedText.toString()
return ciphertext
}
export default encrypt

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
tsconfig.json 100644
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
}
}