feat: Added new feature
commit
6b68602472
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
**/backend/node_modules/
|
||||
**/frontend/node_modules/
|
||||
|
||||
**/backend/.env
|
||||
**/frontend/.env
|
||||
|
||||
**/backend/public/jsonfiles
|
||||
**/backend/public/uploads
|
||||
|
||||
/node_modules
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx commitlint --edit $1
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx commitlint --edit
|
|
@ -0,0 +1 @@
|
|||
web: node app.js
|
|
@ -0,0 +1,73 @@
|
|||
require('dotenv').config()
|
||||
require('express-async-errors')
|
||||
|
||||
|
||||
const { socketIO } = require('./utils')
|
||||
const { initIO, getIO } = socketIO
|
||||
|
||||
// express
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const session = require('express-session')
|
||||
|
||||
// rest of the packages
|
||||
const morgan = require('morgan')
|
||||
// const fileUpload = require('express-fileupload')
|
||||
|
||||
const rateLimiter = require('express-rate-limit')
|
||||
|
||||
// Swagger
|
||||
const swaggerUI = require('swagger-ui-express')
|
||||
const YAML = require('yamljs')
|
||||
const swaggerDocument = YAML.load('./swagger.yaml')
|
||||
|
||||
const helmet = require('helmet')
|
||||
const xss = require('xss-clean')
|
||||
const cors = require('cors')
|
||||
|
||||
// routers
|
||||
const crmRouter = require('./routes/crmRoute')
|
||||
|
||||
const notFoundMiddlware = require('./middleware/not-found')
|
||||
const errorHandlerMiddleware = require('./middleware/error-handler')
|
||||
|
||||
//middleware
|
||||
app.set('trust proxy', 1)
|
||||
app.use(rateLimiter({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 60,
|
||||
}))
|
||||
|
||||
// Use a session to keep track of client ID
|
||||
app.use(session({
|
||||
secret: Math.random().toString(36).substring(2),
|
||||
resave: false,
|
||||
saveUninitialized: true
|
||||
}))
|
||||
|
||||
// Security packages
|
||||
app.use(helmet())
|
||||
app.use(cors())
|
||||
app.use(xss())
|
||||
|
||||
app.use(morgan('tiny'))
|
||||
app.use(express.json())
|
||||
|
||||
// app.use(fileUpload())
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('<h1>Sentiment API</h1><a href="/api-docs">Documentation</a>')
|
||||
})
|
||||
|
||||
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument))
|
||||
|
||||
app.use('/api/v1/crm', crmRouter)
|
||||
|
||||
app.use(notFoundMiddlware)
|
||||
app.use(errorHandlerMiddleware)
|
||||
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
const server = app.listen(port, console.log(`Listening on port: ${port}...`))
|
||||
|
||||
initIO(server)
|
|
@ -0,0 +1,231 @@
|
|||
|
||||
const path = require('path')
|
||||
const { StatusCodes } = require("http-status-codes")
|
||||
const { createCRMContact,
|
||||
sendMessageSocket,
|
||||
mustContainProperties,
|
||||
journaling,
|
||||
findProperty,
|
||||
journalingRequest,
|
||||
createContact,
|
||||
lookupContactByPhone,
|
||||
templateValidator } = require('../utils')
|
||||
const fs = require("fs")
|
||||
const { URL } = require('url')
|
||||
const { oauth2 } = require('../utils')
|
||||
const { exchangeForTokens } = oauth2
|
||||
const Company = require('../models/Company')
|
||||
const CRM = require('../models/CRM')
|
||||
const CustomError = require('../errors')
|
||||
|
||||
const contactCreate = async (req, res) => {
|
||||
|
||||
const { companyId, crmFirstName, crmLastName, crmPhone, crmEmail } = req.body
|
||||
|
||||
mustContainProperties(req, ['companyId', 'crmPhone',])
|
||||
|
||||
await createCRMContact(companyId, crmFirstName, crmPhone, crmEmail, crmLastName)
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
|
||||
}
|
||||
|
||||
const deleteCrm = async (req, res) => {
|
||||
|
||||
const { companyId, crmBaseURL } = req.body
|
||||
|
||||
mustContainProperties(req, ['companyId', 'crmBaseURL',])
|
||||
|
||||
const crm = await CRM.findOne({
|
||||
companyId, crmBaseURL: new URL(crmBaseURL.trim()).hostname
|
||||
})
|
||||
|
||||
if (crm) await crm.deleteOne()
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
}
|
||||
|
||||
const deleteCompany = async (req, res) => {
|
||||
|
||||
const { companyId } = req.body
|
||||
|
||||
mustContainProperties(req, ['companyId',])
|
||||
|
||||
const company = await Company.findOne({ companyId, })
|
||||
|
||||
if (company) await company.deleteOne()
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
}
|
||||
|
||||
const callJournaling = async (req, res) => {
|
||||
|
||||
const { companyId, operation, crmPhone, crmAgent, crmCallDuration } = req.body
|
||||
|
||||
mustContainProperties(req, ['companyId', 'operation', 'crmPhone', 'crmAgent',])
|
||||
|
||||
if (operation == 'inboundAnsweredCall' && !crmCallDuration)
|
||||
throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is inboundAnsweredCall`)
|
||||
if (operation == 'outboundAsweredCall' && !crmCallDuration)
|
||||
throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is outboundAsweredCall`)
|
||||
|
||||
await journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration,)
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
}
|
||||
|
||||
const install = async (req, res) => {
|
||||
|
||||
const { authUrl, companyId } = req.query
|
||||
|
||||
// Store the authUrl in the session
|
||||
req.session.authUrl = authUrl
|
||||
|
||||
res.redirect(authUrl)
|
||||
}
|
||||
|
||||
const oauthCallBack = async (req, res) => {
|
||||
|
||||
const { code } = req.query
|
||||
|
||||
// Retrieve the stored authUrl from the session
|
||||
const storedAuthUrl = req.session.authUrl
|
||||
|
||||
const parsedUrl = new URL(storedAuthUrl)
|
||||
const clientId = parsedUrl.searchParams.get('client_id')
|
||||
|
||||
if (code) {
|
||||
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
|
||||
crmOauth = crmOauth.toObject()
|
||||
|
||||
const authCodeProof = {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: crmOauth.crm.authentication.crmClientId,
|
||||
client_secret: crmOauth.crm.authentication.crmClientSecret,
|
||||
redirect_uri: process.env.URL_OAUTH_CALLBACK,
|
||||
code
|
||||
}
|
||||
|
||||
// Exchange the authorization code for an access token and refresh token
|
||||
await exchangeForTokens(crmOauth, authCodeProof)
|
||||
|
||||
const url = `${process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT}?clientId=${encodeURIComponent(clientId)}`
|
||||
|
||||
return res.redirect(url)
|
||||
// return res.redirect(process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT)
|
||||
}
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
}
|
||||
|
||||
const testTemplate = async (req, res) => {
|
||||
|
||||
const { clientId, companyId } = req.body
|
||||
|
||||
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, testing: true })
|
||||
|
||||
if (!crmOauth)
|
||||
return res.status(StatusCodes.OK).send()
|
||||
|
||||
crmOauth = crmOauth.toObject()
|
||||
|
||||
const { crmPhoneTest } = crmOauth.crm.authentication
|
||||
|
||||
await templateValidator(crmPhoneTest, crmOauth.crm, companyId)
|
||||
|
||||
crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
|
||||
crmOauth.testing = false
|
||||
await crmOauth.save()
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
|
||||
}
|
||||
|
||||
const uploadCrmConfig = async (req, res) => {
|
||||
|
||||
const { companyId, } = req.body
|
||||
|
||||
mustContainProperties(req, ['companyId',])
|
||||
|
||||
if (!req?.file)
|
||||
throw new CustomError.BadRequestError(`The crm property file must be provided`)
|
||||
|
||||
const file = req.file
|
||||
|
||||
let newCrm = fs.readFileSync(file.path, "utf8")
|
||||
|
||||
newCrm = JSON.parse(newCrm)
|
||||
|
||||
const index = newCrm.crmRest.findIndex(rest => rest?.createContactRecord)
|
||||
|
||||
const crmBaseURL = new URL(newCrm.crmRest[index].createContactRecord.request.url.trim()).hostname
|
||||
|
||||
let company = await Company.findOne({ companyId })
|
||||
|
||||
if (!company) {
|
||||
company = await Company.create({ companyId })
|
||||
}
|
||||
|
||||
let crm = await CRM.findOne({ companyId, crmBaseURL })
|
||||
|
||||
if (crm) {
|
||||
crm.testing = true
|
||||
crm.crm = newCrm
|
||||
await crm.save()
|
||||
}
|
||||
else {
|
||||
crm = await CRM.create({
|
||||
crmBaseURL,
|
||||
companyId: company.companyId,
|
||||
company,
|
||||
testing: true,
|
||||
crm: newCrm
|
||||
})
|
||||
}
|
||||
|
||||
fs.unlinkSync(file.path)
|
||||
|
||||
const { type, crmClientId, crmClientSecret, crmScopes, crmPhoneTest } = crm.crm.authentication
|
||||
|
||||
if (type == 'oauth2') {
|
||||
|
||||
const { request, body, response } = findProperty(crm.crm.crmRest, 'authorizationEndpoint')
|
||||
|
||||
let { url } = request
|
||||
|
||||
url = url.replace('crmClientId', crmClientId)
|
||||
.replace('crmClientSecret', crmClientSecret)
|
||||
.replace('crmScopes', crmScopes)
|
||||
.replace('crmRedirectURI', process.env.URL_OAUTH_CALLBACK)
|
||||
|
||||
|
||||
const authUrl = `http://localhost:6001/api/v1/crm/install?authUrl=${encodeURIComponent(url)}&companyId=${encodeURIComponent(companyId)}`
|
||||
console.log('--------> authUrl: ', authUrl)
|
||||
|
||||
return res.status(StatusCodes.OK).json({ url: authUrl })
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
await templateValidator(crmPhoneTest, newCrm, companyId)
|
||||
|
||||
crm.testing = false
|
||||
await crm.save()
|
||||
}
|
||||
|
||||
res.status(StatusCodes.OK).send()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
contactCreate,
|
||||
uploadCrmConfig,
|
||||
callJournaling,
|
||||
oauthCallBack,
|
||||
install,
|
||||
deleteCrm,
|
||||
deleteCompany,
|
||||
testTemplate
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const mongoose = require('mongoose')
|
||||
|
||||
const connectDB = async () => {
|
||||
await mongoose.connect(process.env.DB_MONGO_URL, { dbName: process.env.DB_MONGO_NAME })
|
||||
}
|
||||
|
||||
connectDB().catch((err) => console.log(err))
|
||||
module.exports = mongoose
|
|
@ -0,0 +1,11 @@
|
|||
const { StatusCodes } = require('http-status-codes');
|
||||
const CustomAPIError = require('./custom-api');
|
||||
|
||||
class BadRequestError extends CustomAPIError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.statusCode = StatusCodes.BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BadRequestError;
|
|
@ -0,0 +1,7 @@
|
|||
class CustomAPIError extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomAPIError
|
|
@ -0,0 +1,16 @@
|
|||
const CustomAPIError = require('./custom-api');
|
||||
const UnauthenticatedError = require('./unauthenticated');
|
||||
const NotFoundError = require('./not-found');
|
||||
const BadRequestError = require('./bad-request');
|
||||
const UnauthorizedError = require('./unauthorized');
|
||||
const InternalServerError = require('./internal-server-error');
|
||||
|
||||
|
||||
module.exports = {
|
||||
CustomAPIError,
|
||||
UnauthenticatedError,
|
||||
NotFoundError,
|
||||
BadRequestError,
|
||||
UnauthorizedError,
|
||||
InternalServerError
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
const { StatusCodes } = require('http-status-codes')
|
||||
const CustomAPIError = require('./custom-api')
|
||||
|
||||
class NotFoundError extends CustomAPIError {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
this.statusCode = StatusCodes.INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotFoundError
|
|
@ -0,0 +1,11 @@
|
|||
const { StatusCodes } = require('http-status-codes');
|
||||
const CustomAPIError = require('./custom-api');
|
||||
|
||||
class NotFoundError extends CustomAPIError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.statusCode = StatusCodes.NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotFoundError;
|
|
@ -0,0 +1,11 @@
|
|||
const { StatusCodes } = require('http-status-codes');
|
||||
const CustomAPIError = require('./custom-api');
|
||||
|
||||
class UnauthenticatedError extends CustomAPIError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.statusCode = StatusCodes.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UnauthenticatedError;
|
|
@ -0,0 +1,11 @@
|
|||
const { StatusCodes } = require('http-status-codes');
|
||||
const CustomAPIError = require('./custom-api');
|
||||
|
||||
class UnauthorizedError extends CustomAPIError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.statusCode = StatusCodes.FORBIDDEN;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UnauthorizedError;
|
|
@ -0,0 +1,27 @@
|
|||
const CustomError = require('../errors')
|
||||
|
||||
const authorization = async (req, res, next) => {
|
||||
|
||||
const authHeader = req.headers.authorization
|
||||
|
||||
if (!authHeader) {
|
||||
throw new CustomError.BadRequestError('Authorization not found into header!')
|
||||
}
|
||||
|
||||
const [, token] = authHeader.split(" ");
|
||||
|
||||
if (!token) {
|
||||
throw new CustomError.BadRequestError('Authorization token not found into header!')
|
||||
}
|
||||
|
||||
if (token != process.env.TOKEN){
|
||||
throw new CustomError.UnauthorizedError('Authorization token Invalid')
|
||||
}
|
||||
|
||||
next()
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authorization,
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
const { StatusCodes } = require('http-status-codes');
|
||||
const errorHandlerMiddleware = (err, req, res, next) => {
|
||||
let customError = {
|
||||
// set default
|
||||
statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
msg: err.message || 'Something went wrong try again later',
|
||||
};
|
||||
if (err.name === 'ValidationError') {
|
||||
customError.msg = Object.values(err.errors)
|
||||
.map((item) => item.message)
|
||||
.join(',');
|
||||
customError.statusCode = 400;
|
||||
}
|
||||
if (err.code && err.code === 11000) {
|
||||
customError.msg = `Duplicate value entered for ${Object.keys(
|
||||
err.keyValue
|
||||
)} field, please choose another value`;
|
||||
customError.statusCode = 400;
|
||||
}
|
||||
if (err.name === 'CastError') {
|
||||
customError.msg = `No item found with id : ${err.value}`;
|
||||
customError.statusCode = 404;
|
||||
}
|
||||
|
||||
return res.status(customError.statusCode).json({ msg: customError.msg });
|
||||
};
|
||||
|
||||
module.exports = errorHandlerMiddleware;
|
|
@ -0,0 +1,3 @@
|
|||
const notFound = (req, res) => res.status(404).send('Route does not exist')
|
||||
|
||||
module.exports = notFound
|
|
@ -0,0 +1,134 @@
|
|||
const mongoose = require('../db/connect')
|
||||
|
||||
const { Schema } = mongoose
|
||||
|
||||
|
||||
// Define sub-schemas
|
||||
const RequestSchema = new Schema({
|
||||
requestContentType: {
|
||||
type: String,
|
||||
enum: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'empty', 'none'],
|
||||
default: 'application/json'
|
||||
},
|
||||
requestEncoding: {
|
||||
type: String,
|
||||
enum: ['Json', 'UrlEncoded', 'empty'],
|
||||
default: 'Json'
|
||||
},
|
||||
requestType: {
|
||||
type: String,
|
||||
enum: ['Post', 'Get', 'empty'],
|
||||
default: 'Post'
|
||||
},
|
||||
responseType: {
|
||||
type: String,
|
||||
enum: ['Json', 'XML', 'empty'],
|
||||
default: 'Json'
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const CallsSchema = new Schema({
|
||||
inboundAnsweredCall: Object,
|
||||
inboundMissedCall: Object,
|
||||
outboundAnsweredCall: Object,
|
||||
outboundUnansweredCall: Object
|
||||
})
|
||||
|
||||
const CallJournalingSchema = new Schema({
|
||||
request: RequestSchema,
|
||||
calls: [CallsSchema],
|
||||
response: Object
|
||||
})
|
||||
|
||||
// Define main schema
|
||||
const CRMRestSchema = new Schema({
|
||||
authorizationEndpoint: {
|
||||
request: RequestSchema
|
||||
},
|
||||
tokenEndpoint: {
|
||||
request: RequestSchema,
|
||||
body: Object,
|
||||
response: Object
|
||||
},
|
||||
createContactRecord: {
|
||||
request: RequestSchema,
|
||||
body: Object,
|
||||
response: Object
|
||||
},
|
||||
lookupContactByPhone: {
|
||||
request: RequestSchema,
|
||||
response: Object
|
||||
},
|
||||
callJournaling: CallJournalingSchema
|
||||
})
|
||||
|
||||
const AuthenticationSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['basic', 'bearer', 'oauth2'],
|
||||
default: 'bearer'
|
||||
},
|
||||
userName: String,
|
||||
passWord: String,
|
||||
token: String,
|
||||
crmClientId: String,
|
||||
crmClientSecret: String,
|
||||
crmScopes: String,
|
||||
crmOAuthRefreshToken: String,
|
||||
crmOAuthToken: String,
|
||||
crmPhoneTest: String
|
||||
})
|
||||
|
||||
|
||||
|
||||
const crmContactSchema = new Schema({
|
||||
crmBaseURL: {
|
||||
type: String
|
||||
},
|
||||
companyId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
company: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'company',
|
||||
required: true
|
||||
},
|
||||
testing: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
crm: {
|
||||
authentication: AuthenticationSchema,
|
||||
crmRest: [CRMRestSchema]
|
||||
}
|
||||
|
||||
}, { timestamps: true },
|
||||
)
|
||||
|
||||
crmContactSchema.virtual('crm_contacts', {
|
||||
ref: 'CRM_Contact',
|
||||
localField: '_id',
|
||||
foreignField: 'crm',
|
||||
justOne: false,
|
||||
// match: { rating: 4 }
|
||||
})
|
||||
|
||||
|
||||
crmContactSchema.pre('deleteOne', { document: true, query: false }, async function () {
|
||||
await this.model('CRM_Contact').deleteMany({ crm: this._id })
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const CRM = mongoose.model('CRM', crmContactSchema)
|
||||
|
||||
module.exports = CRM
|
|
@ -0,0 +1,31 @@
|
|||
const mongoose = require('../db/connect')
|
||||
|
||||
const { Schema } = mongoose
|
||||
|
||||
const crmContactSchema = new Schema({
|
||||
companyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
crm: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'crm',
|
||||
required: true
|
||||
},
|
||||
crmBaseURL: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contactId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
}, { timestamps: true })
|
||||
|
||||
const CRM_Contact = mongoose.model('CRM_Contact', crmContactSchema)
|
||||
|
||||
module.exports = CRM_Contact
|
|
@ -0,0 +1,46 @@
|
|||
const mongoose = require('../db/connect')
|
||||
|
||||
const { Schema } = mongoose
|
||||
|
||||
const crmCompany = new Schema({
|
||||
|
||||
companyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
})
|
||||
|
||||
crmCompany.virtual('crms', {
|
||||
ref: 'CRM',
|
||||
localField: '_id',
|
||||
foreignField: 'company',
|
||||
justOne: false,
|
||||
// match: { rating: 4 }
|
||||
})
|
||||
|
||||
crmCompany.pre('deleteOne', { document: true, query: false }, async function () {
|
||||
const crms = await this.model('CRM').find({ company: this._id })
|
||||
for (const crm of crms) {
|
||||
await crm.deleteOne() // Delete each CRM
|
||||
|
||||
// Delete associated CRM_Contacts
|
||||
const crmContacts = await this.model('CRM_Contact').find({ crm: crm._id })
|
||||
for (const crmContact of crmContacts) {
|
||||
await crmContact.deleteOne()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const Company = mongoose.model('Company', crmCompany)
|
||||
|
||||
module.exports = Company
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "crm_integrator",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon app.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-rate-limit": "^5.4.1",
|
||||
"express-session": "^1.17.3",
|
||||
"flat": "^3.0.1",
|
||||
"helmet": "^4.6.0",
|
||||
"http-status-codes": "^2.1.4",
|
||||
"ioredis": "^5.3.2",
|
||||
"luxon": "^3.4.4",
|
||||
"mongoose": "^7.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"socket.io": "^4.7.2",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"xss-clean": "^0.1.1",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.9"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const { authorization, } = require('../middleware/authentication')
|
||||
const { contactCreate, testTemplate, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany } = require('../controllers/crmController')
|
||||
const { fileUpload } = require("../utils")
|
||||
|
||||
router.route('/create-contact').post(authorization, contactCreate)
|
||||
router.route('/call-journaling').post(authorization, callJournaling)
|
||||
router.route('/upload').post(fileUpload.single('crm'), authorization, uploadCrmConfig)
|
||||
router.route('/delete').post(authorization, deleteCrm)
|
||||
router.route('/delete-company').post(authorization, deleteCompany)
|
||||
router.route('/oauth-callback').get(oauthCallBack)
|
||||
router.route('/install').get(install)
|
||||
router.route('/test').post(testTemplate)
|
||||
|
||||
module.exports = router
|
|
@ -0,0 +1,204 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Natural Language API Google
|
||||
description: This API describes the endpoints and parameters to use resources from Google Cloud API.
|
||||
contact: {}
|
||||
version: '1.0'
|
||||
servers:
|
||||
- url: http://localhost:6001/api/v1/nl
|
||||
variables: {}
|
||||
paths:
|
||||
/upload-audio-to-transcript:
|
||||
post:
|
||||
tags:
|
||||
- Speech to text async
|
||||
summary: Speech to text job
|
||||
operationId: Speechtotextjob
|
||||
parameters: []
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
encoding: {}
|
||||
schema:
|
||||
required:
|
||||
- audio
|
||||
type: object
|
||||
properties:
|
||||
audio:
|
||||
type: string
|
||||
format: binary
|
||||
languageCode:
|
||||
type: string
|
||||
description: 'If not provided, the default will be: pt-BR'
|
||||
example: pt-BR
|
||||
required: false
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security:
|
||||
- bearer: []
|
||||
/query-job-status:
|
||||
get:
|
||||
tags:
|
||||
- Speech to text async
|
||||
summary: Speech to text job process
|
||||
operationId: Speechtotextjobprocess
|
||||
parameters:
|
||||
- name: operationName
|
||||
in: query
|
||||
description: 'The job id returned after uploading the audio file that will be transcribed.'
|
||||
required: true
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 2993135803178989324
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security:
|
||||
- bearer: []
|
||||
/speech-to-text:
|
||||
post:
|
||||
tags:
|
||||
- Speech to text sync
|
||||
summary: Speech to text
|
||||
operationId: Speechtotext
|
||||
parameters: []
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
encoding: {}
|
||||
schema:
|
||||
required:
|
||||
- audio
|
||||
type: object
|
||||
properties:
|
||||
audio:
|
||||
type: string
|
||||
format: binary
|
||||
languageCode:
|
||||
description: 'If not provided, the default will be: pt-BR'
|
||||
type: string
|
||||
example: pt-BR
|
||||
required: false
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security:
|
||||
- bearer: []
|
||||
/sentiment:
|
||||
post:
|
||||
tags:
|
||||
- Sentiment
|
||||
summary: Get sentiment
|
||||
operationId: Getsentiment
|
||||
parameters: []
|
||||
requestBody:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- example:
|
||||
text: Toda vez a mesma coisa ja to cansado de ficar ligando pra resolver esses problemas de conexão!
|
||||
example:
|
||||
text: Toda vez a mesma coisa ja to cansado de ficar ligando pra resolver esses problemas de conexão!
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security:
|
||||
- bearer: []
|
||||
/text-to-speech:
|
||||
get:
|
||||
tags:
|
||||
- Text to speech
|
||||
summary: Text to speech
|
||||
operationId: Texttospeech
|
||||
parameters:
|
||||
- name: text
|
||||
in: query
|
||||
description: ''
|
||||
required: true
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
example: Vela branca na enxurrada la vou eu de léo em léo, se o navio é pequeno do tamanho de um chapeu, não importa a volta ao mundo, é viagem de brinquedo em um barquinho de papel.
|
||||
- name: voice_name
|
||||
in: query
|
||||
description: 'Ex: pt-BR-Wavenet-C'
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
example:
|
||||
- name: voice_gender
|
||||
in: query
|
||||
description: 'Ex: FEMALE'
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
example:
|
||||
- name: languageCode
|
||||
in: query
|
||||
description: 'Ex: pt-BR'
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
example:
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security:
|
||||
- bearer: []
|
||||
/voice-config:
|
||||
get:
|
||||
tags:
|
||||
- Text to speech
|
||||
summary: Get voice config
|
||||
operationId: Getvoiceconfig
|
||||
parameters:
|
||||
- name: languageCode
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
example: pt-Br
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
headers: {}
|
||||
deprecated: false
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
security: []
|
||||
tags:
|
||||
- name: Speech to text async
|
||||
- name: Speech to text sync
|
||||
- name: Text to speech
|
||||
- name: Sentiment
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
function convertToIntegerIfNumber(str) {
|
||||
if (/^\d+$/.test(str)) {
|
||||
return parseInt(str, 10)
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = convertToIntegerIfNumber
|
|
@ -0,0 +1,25 @@
|
|||
const lookupContactByPhone = require('./lookupCRMContactByPhone')
|
||||
const createContact = require('./createContact')
|
||||
const loadCRM = require('./loadCRM')
|
||||
|
||||
async function createCRMContact(companyId, crmFirstName, crmPhone, crmEmail = '', crmLastName = '',) {
|
||||
|
||||
const crmFiles = await loadCRM(companyId)
|
||||
|
||||
for (const crmConfig of crmFiles) {
|
||||
|
||||
const { crmRest: rest, authentication } = crmConfig.crm
|
||||
|
||||
const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId)
|
||||
|
||||
if (contact.exist) continue
|
||||
|
||||
await createContact(companyId, rest, authentication, crmPhone, crmFirstName, crmLastName, crmEmail,)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = createCRMContact
|
||||
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
const flatten = require('flat')
|
||||
const unflatten = require('flat').unflatten
|
||||
const axios = require('axios')
|
||||
const CRM_Contact = require('../models/CRM_Contact')
|
||||
const { URL } = require('url')
|
||||
const findProperty = require('./findProperty')
|
||||
const CRM = require('../models/CRM')
|
||||
const requestConfigHeader = require('./requestConfigHeader')
|
||||
const sendMessageSocket = require('./sendMessageSocket')
|
||||
|
||||
async function createContact(companyId, rest, authentication, crmPhone, crmFirstName = '', crmLastName = '', crmEmail = '', test = {}) {
|
||||
let { request, body, response } = findProperty(rest, 'createContactRecord')
|
||||
|
||||
const { requestContentType, requestEncoding, requestType, responseType, url } = request
|
||||
|
||||
body = flatten(body)
|
||||
|
||||
const mapping = {
|
||||
crmFirstName,
|
||||
crmLastName,
|
||||
crmPhone,
|
||||
crmEmail
|
||||
}
|
||||
|
||||
for (const prop in body) {
|
||||
if (mapping.hasOwnProperty(body[prop])) {
|
||||
const variable = mapping[body[prop]]
|
||||
if (variable) {
|
||||
body[prop] = variable
|
||||
} else {
|
||||
if (body[prop] == 'crmLastName' && !crmLastName) {
|
||||
body[prop] = 'unnamed'
|
||||
}
|
||||
else
|
||||
delete body[prop]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body = unflatten(body)
|
||||
|
||||
const { type, userName, passWord, token, crmClientId } = authentication
|
||||
|
||||
//url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = ''
|
||||
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body)
|
||||
|
||||
if (test?.testing) {
|
||||
msg = `Tentanto criar contato de numero ${crmPhone} no crm`
|
||||
sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } })
|
||||
}
|
||||
|
||||
let { data } = await axios(config)
|
||||
|
||||
data = flatten(data)
|
||||
|
||||
let auxContactId
|
||||
|
||||
for (const prop in data) {
|
||||
|
||||
const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '')
|
||||
|
||||
if (_prop == response?.id?.trim()) {
|
||||
auxContactId = data[prop]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (auxContactId && !test?.testing) {
|
||||
const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname })
|
||||
await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: crmPhone })
|
||||
}
|
||||
|
||||
return { exist: true, contactId: auxContactId, phone: crmPhone }
|
||||
|
||||
}
|
||||
|
||||
module.exports = createContact
|
|
@ -0,0 +1,13 @@
|
|||
const fs = require('fs')
|
||||
|
||||
function createDir(path) {
|
||||
fs.mkdir(path, { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating directory:', err)
|
||||
} else {
|
||||
console.log('Directory created successfully')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createDir
|
|
@ -0,0 +1,8 @@
|
|||
const fs = require("fs")
|
||||
|
||||
const crmCompany = (pathFile) => {
|
||||
const crmRest = fs.readFileSync(pathFile, "utf8")
|
||||
return JSON.parse(crmRest)
|
||||
}
|
||||
|
||||
module.exports = crmCompany
|
|
@ -0,0 +1,54 @@
|
|||
const { DateTime } = require('luxon')
|
||||
|
||||
function dateTime() {
|
||||
const brazilTimeZone = 'America/Sao_Paulo'
|
||||
const currentDateBrazil = DateTime.now().setZone(brazilTimeZone) // Get current date and time in Brazil timezone
|
||||
|
||||
const formattedDateTime = currentDateBrazil.toFormat("yyyy-MM-dd'T'HH:mm:ssZZ") // Format to ISO 8601
|
||||
|
||||
console.log('FORMATTED DATE TIME: ', formattedDateTime)
|
||||
|
||||
return formattedDateTime
|
||||
|
||||
}
|
||||
|
||||
function secondsFormat(seconds, format = '') {
|
||||
|
||||
seconds = parseInt(seconds, 10)
|
||||
|
||||
switch (format) {
|
||||
case 'hh:mm':
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const formattedHours = String(hours).padStart(2, '0')
|
||||
const formattedMinutes = String(minutes).padStart(2, '0')
|
||||
console.log(`formate hours: ${formattedHours}:${formattedMinutes}`)
|
||||
return `${formattedHours}:${formattedMinutes}`
|
||||
case 'milliseconds':
|
||||
return seconds * 1000
|
||||
|
||||
}
|
||||
|
||||
return seconds
|
||||
|
||||
}
|
||||
|
||||
function getPastDateTimeFromSeconds(currentTime, secondsToSubtract) {
|
||||
|
||||
const currentDateTime = DateTime.fromISO(currentTime)
|
||||
|
||||
// Subtract seconds from the current date and time
|
||||
const pastDateTime = currentDateTime.minus({ seconds: secondsToSubtract })
|
||||
|
||||
// Extract timezone offset from the current time
|
||||
const timezoneOffset = currentDateTime.toFormat('ZZ')
|
||||
|
||||
// Format the past date and time in the desired format manually
|
||||
const formattedPastDateTime = pastDateTime.toFormat(`yyyy-MM-dd'T'HH:mm:ss${timezoneOffset}`)
|
||||
|
||||
return formattedPastDateTime
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = { dateTime, secondsFormat, getPastDateTimeFromSeconds }
|
|
@ -0,0 +1,14 @@
|
|||
const fs = require('fs')
|
||||
|
||||
function dirExist(path) {
|
||||
try {
|
||||
fs.accessSync(path, fs.constants.F_OK)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Directory does not exist')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports = dirExist
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
const multer = require('multer')
|
||||
const path = require('path')
|
||||
const CustomError = require('../errors')
|
||||
const fs = require('fs')
|
||||
const dirExist = require('./dirExist')
|
||||
const createDir = require('./createDir')
|
||||
|
||||
//Destination to store the file
|
||||
const fileStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
|
||||
const dir = path.join(process.cwd(), 'public', 'jsonfiles')
|
||||
|
||||
if (!dirExist(dir)) {
|
||||
createDir(dir)
|
||||
}
|
||||
|
||||
cb(null, dir)
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, Date.now() + String(Math.floor(Math.random() * 1000)) + path.extname(file.originalname))
|
||||
}
|
||||
})
|
||||
|
||||
const fileUpload = multer({
|
||||
storage: fileStorage,
|
||||
fileFilter(req, file, cb) {
|
||||
|
||||
if (!file.originalname.match(/\.(json)$/i)) {
|
||||
return cb(new CustomError.BadRequestError(`'Invalid file type. Send only .json file!`))
|
||||
|
||||
}
|
||||
cb(undefined, true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
module.exports = fileUpload
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
function findProperty(array, property) {
|
||||
|
||||
const index = array.findIndex((item) => {
|
||||
return item[property] !== undefined
|
||||
})
|
||||
|
||||
return array[index][property]
|
||||
}
|
||||
|
||||
module.exports = findProperty
|
|
@ -0,0 +1,50 @@
|
|||
// const { createJWT, isTokenValid, attachCookiesToResponse } = require('./jwt')
|
||||
// const createTokenUser = require('./createTokenUser')
|
||||
// const checkPermissions = require('./checkPermissions')
|
||||
|
||||
|
||||
const fileUpload = require('./fileUpload')
|
||||
const loadCRM = require('./loadCRM')
|
||||
const crmCompany = require('./crmCompany')
|
||||
const createCRMContact = require('./createCRMContact')
|
||||
const lookupCRMContactByPhone = require('./lookupCRMContactByPhone')
|
||||
const dirExist = require('./dirExist')
|
||||
const createDir = require('./createDir')
|
||||
const loadJsonFiles = require('./loadJsonFiles')
|
||||
const journaling = require('./journaling')
|
||||
const convertToIntegerIfNumber = require('./convertToIntegerIfNumber')
|
||||
const createContact = require('./createContact')
|
||||
const oauth2 = require('./oauth2')
|
||||
const redisClient = require('./redisClient')
|
||||
const findProperty = require('./findProperty')
|
||||
const dateTime = require('./dateTime')
|
||||
const mustContainProperties = require('./mustContainProperties')
|
||||
const journalingRequest = require('./journalingRequest')
|
||||
const lookupContactByPhone = require('./lookupCRMContactByPhone')
|
||||
const socketIO = require('./socketIO')
|
||||
const sendMessageSocket = require('./sendMessageSocket')
|
||||
const templateValidator = require('./templateValidator')
|
||||
|
||||
module.exports = {
|
||||
fileUpload,
|
||||
loadCRM,
|
||||
crmCompany,
|
||||
createCRMContact,
|
||||
lookupCRMContactByPhone,
|
||||
dirExist,
|
||||
createDir,
|
||||
loadJsonFiles,
|
||||
journaling,
|
||||
convertToIntegerIfNumber,
|
||||
createContact,
|
||||
oauth2,
|
||||
redisClient,
|
||||
findProperty,
|
||||
dateTime,
|
||||
mustContainProperties,
|
||||
journalingRequest,
|
||||
lookupContactByPhone,
|
||||
socketIO,
|
||||
sendMessageSocket,
|
||||
templateValidator
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
const loadCRM = require('./loadCRM')
|
||||
const lookupContactByPhone = require('./lookupCRMContactByPhone')
|
||||
const createContact = require('./createContact')
|
||||
const findProperty = require('./findProperty')
|
||||
|
||||
const journalingRequest = require('./journalingRequest')
|
||||
|
||||
async function journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration = 0) {
|
||||
|
||||
const crmFiles = await loadCRM(companyId)
|
||||
|
||||
for (const crmConfig of crmFiles) {
|
||||
|
||||
const { crmRest: rest, authentication } = crmConfig.crm
|
||||
|
||||
let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId)
|
||||
|
||||
if (!contact.exist) {
|
||||
contact = await createContact(companyId, rest, authentication, crmPhone)
|
||||
}
|
||||
|
||||
let { request, calls, response } = findProperty(rest, 'callJournaling')
|
||||
|
||||
let body = findProperty(calls, operation)
|
||||
|
||||
await journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, rest)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = journaling
|
||||
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
const { dateTime, secondsFormat, getPastDateTimeFromSeconds } = require('./dateTime')
|
||||
const requestConfigHeader = require('./requestConfigHeader')
|
||||
const flatten = require('flat')
|
||||
const unflatten = require('flat').unflatten
|
||||
const axios = require('axios')
|
||||
const path = require('path')
|
||||
const convertToIntegerIfNumber = require('./convertToIntegerIfNumber')
|
||||
const sendMessageSocket = require('./sendMessageSocket')
|
||||
|
||||
async function journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, test = {}) {
|
||||
const { requestContentType, requestEncoding, requestType, responseType, url } = request
|
||||
|
||||
body = flatten(body)
|
||||
|
||||
let ignore = []
|
||||
|
||||
for (let key in body) {
|
||||
|
||||
const k = Object.keys(body).find(k => {
|
||||
if (!ignore.includes(k) && k.includes('._prop') && k.replace('._prop', '._type') ==
|
||||
key.replace('._prop', '._type')) {
|
||||
ignore.push(key)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (k) {
|
||||
const type = body[k.replace('._prop', '._type')]
|
||||
const format = body[k.replace('._prop', '._format')]
|
||||
|
||||
const newKey = k.replace('._prop', '').replace('._type', '').replace('._format', '')
|
||||
|
||||
if (body[key] == 'crmCallDuration') {
|
||||
switch (format) {
|
||||
case 'hh:mm':
|
||||
body[newKey] = secondsFormat(crmCallDuration, 'hh:mm')
|
||||
break
|
||||
case 'milliseconds':
|
||||
body[newKey] = secondsFormat(crmCallDuration, 'milliseconds')
|
||||
break
|
||||
case 'seconds':
|
||||
body[newKey] = secondsFormat(crmCallDuration, 'seconds')
|
||||
break
|
||||
}
|
||||
}
|
||||
else if (body[key] == 'crmCallDateTime') {
|
||||
switch (format) {
|
||||
case 'ISO8601':
|
||||
body[newKey] = crmCallDuration ? getPastDateTimeFromSeconds(dateTime(), crmCallDuration) : dateTime()
|
||||
break
|
||||
}
|
||||
}
|
||||
else if (body[key] == 'crmContactId') {
|
||||
body[newKey] = contact.contactId
|
||||
}
|
||||
else if (body[key] == 'crmAgent') {
|
||||
body[newKey] = crmAgent
|
||||
}
|
||||
else if (body[key] == 'crmPhone') {
|
||||
body[newKey] = crmPhone
|
||||
}
|
||||
|
||||
const property = body[newKey] ? body[newKey] : body[key]
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
body[newKey] = convertToIntegerIfNumber(property)
|
||||
break
|
||||
case 'string':
|
||||
body[newKey] = `${property}`
|
||||
break
|
||||
case 'boolean':
|
||||
body[newKey] = Boolean(property)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch (body[key]) {
|
||||
case 'crmPhone':
|
||||
body[key] = crmPhone
|
||||
break
|
||||
case 'crmContactId':
|
||||
body[key] = contact.contactId
|
||||
break
|
||||
case 'crmAgent':
|
||||
body[key] = crmAgent
|
||||
break
|
||||
case 'crmCallDuration':
|
||||
body[key] = crmCallDuration
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const data = unflatten(body)
|
||||
|
||||
const { type, userName, passWord, token, crmClientId } = authentication
|
||||
|
||||
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data)
|
||||
|
||||
if (test?.testing && test?.companyId && test?.msg) {
|
||||
sendMessageSocket({ companyId: test.companyId, status: 'processing', data: { request: config, msg: test.msg } })
|
||||
}
|
||||
// console.log('JOURNALING CONFIG: ', config)
|
||||
|
||||
const res = await axios(config)
|
||||
}
|
||||
|
||||
module.exports = journalingRequest
|
|
@ -0,0 +1,10 @@
|
|||
const CRM = require('../models/CRM')
|
||||
|
||||
async function loadCRM(companyId) {
|
||||
|
||||
const crm = await CRM.find({ companyId, enabled: true, testing: false }).select('crm').lean()
|
||||
|
||||
return crm
|
||||
}
|
||||
|
||||
module.exports = loadCRM
|
|
@ -0,0 +1,20 @@
|
|||
const fs = require('fs').promises
|
||||
const path = require('path')
|
||||
|
||||
async function loadJSONFiles(dirPath) {
|
||||
const files = await fs.readdir(dirPath)
|
||||
const jsonFiles = files.filter(file => file.endsWith('.json'))
|
||||
|
||||
const filePromises = jsonFiles.map(async file => {
|
||||
const filePath = path.join(dirPath, file)
|
||||
const data = await fs.readFile(filePath, 'utf8')
|
||||
return { crm: JSON.parse(data), path: filePath }
|
||||
})
|
||||
|
||||
const jsonDataArray = await Promise.all(filePromises)
|
||||
|
||||
return jsonDataArray
|
||||
|
||||
}
|
||||
|
||||
module.exports = loadJSONFiles
|
|
@ -0,0 +1,71 @@
|
|||
const axios = require('axios')
|
||||
const flatten = require('flat')
|
||||
const CustomError = require('../errors')
|
||||
const CRM_Contact = require('../models/CRM_Contact')
|
||||
const CRM = require('../models/CRM')
|
||||
|
||||
const { URL } = require('url')
|
||||
const { getAccessToken } = require('./oauth2')
|
||||
const findProperty = require('./findProperty')
|
||||
const requestConfigHeader = require('./requestConfigHeader')
|
||||
const sendMessageSocket = require('./sendMessageSocket')
|
||||
|
||||
async function lookupContactByPhone(rest, authentication, crmPhone, companyId, test = {}) {
|
||||
|
||||
let { request, body, response } = findProperty(rest, 'lookupContactByPhone')
|
||||
|
||||
let { requestContentType, requestEncoding, requestType, responseType, url } = request
|
||||
|
||||
|
||||
const { type, userName, passWord, token, crmClientId } = authentication
|
||||
|
||||
const crmInfo = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, phone: crmPhone })
|
||||
|
||||
if (crmInfo) return { exist: true, contactId: crmInfo.contactId, phone: crmPhone }
|
||||
|
||||
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId)
|
||||
|
||||
if (test?.testing){
|
||||
let msg = `Tentanto checar se o contato de numero ${crmPhone} existe no crm`
|
||||
sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } })
|
||||
}
|
||||
|
||||
let { data } = await axios(config)
|
||||
|
||||
data = flatten(data)
|
||||
|
||||
let auxPhone
|
||||
let auxContactId
|
||||
|
||||
for (const prop in data) {
|
||||
|
||||
const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '')
|
||||
|
||||
if (_prop == response?.phone?.trim()) {
|
||||
auxPhone = data[prop].replace('+', '')
|
||||
}
|
||||
|
||||
if (_prop == response?.id?.trim()) {
|
||||
auxContactId = data[prop]
|
||||
}
|
||||
|
||||
if (auxPhone && auxContactId) break
|
||||
|
||||
}
|
||||
|
||||
if (auxPhone) {
|
||||
|
||||
if (auxPhone && auxContactId) {
|
||||
const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname })
|
||||
await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: auxPhone })
|
||||
}
|
||||
|
||||
return { exist: true, contactId: auxContactId, phone: crmPhone }
|
||||
}
|
||||
|
||||
return { exit: false }
|
||||
}
|
||||
|
||||
module.exports = lookupContactByPhone
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
const CustomError = require('../errors')
|
||||
|
||||
function mustContainProperties(req, requiredProperties) {
|
||||
|
||||
const missingProperties = requiredProperties.filter(prop => !req.body[prop])
|
||||
|
||||
if (missingProperties.length > 0) {
|
||||
throw new CustomError.BadRequestError(`Missing properties: ${missingProperties.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mustContainProperties
|
|
@ -0,0 +1,75 @@
|
|||
const qs = require('qs')
|
||||
const axios = require('axios')
|
||||
const findProperty = require('./findProperty')
|
||||
const CRM = require('../models/CRM')
|
||||
|
||||
const { set, get, del } = require('./redisClient')
|
||||
|
||||
const exchangeForTokens = async (crmOauth, exchangeProof) => {
|
||||
|
||||
const { request, body, response } = findProperty(crmOauth.crm.crmRest, 'tokenEndpoint')
|
||||
|
||||
let { requestContentType, requestEncoding, requestType, responseType, url } = request
|
||||
|
||||
let config = {
|
||||
method: requestType,
|
||||
url,
|
||||
data: qs.stringify(exchangeProof)
|
||||
}
|
||||
|
||||
if (requestContentType != 'none') {
|
||||
config = {
|
||||
...config, headers: {
|
||||
'Content-Type': requestContentType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await axios(config)
|
||||
|
||||
const { refresh_token, access_token, token_type, expires_in } = data
|
||||
|
||||
await set(crmOauth.crm.authentication.crmClientId, access_token, Math.round(expires_in * 0.75))
|
||||
|
||||
crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': crmOauth.crm.authentication.crmClientId })
|
||||
|
||||
if (refresh_token) {
|
||||
crmOauth.crm.authentication.crmOAuthRefreshToken = refresh_token
|
||||
}
|
||||
crmOauth.crm.authentication.crmOAuthToken = access_token
|
||||
await crmOauth.save()
|
||||
|
||||
return access_token
|
||||
|
||||
}
|
||||
|
||||
const refreshAccessToken = async (clientId) => {
|
||||
|
||||
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
|
||||
crmOauth = crmOauth.toObject()
|
||||
|
||||
const refreshTokenProof = {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: crmOauth.crm.authentication.crmClientId,
|
||||
client_secret: crmOauth.crm.authentication.crmClientSecret,
|
||||
redirect_uri: process.env.URL_OAUTH_CALLBACK,
|
||||
refresh_token: crmOauth.crm.authentication.crmOAuthRefreshToken
|
||||
}
|
||||
return await exchangeForTokens(crmOauth, refreshTokenProof)
|
||||
}
|
||||
|
||||
const getAccessToken = async (clientId) => {
|
||||
|
||||
if (!await get(clientId)) {
|
||||
console.log('Refreshing expired access token')
|
||||
await refreshAccessToken(clientId)
|
||||
}
|
||||
return await get(clientId)
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
exchangeForTokens,
|
||||
refreshAccessToken,
|
||||
getAccessToken
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
const Redis = require("ioredis")
|
||||
const redis = new Redis(process.env.REDIS_URI)
|
||||
|
||||
// Function to set a token with expiration
|
||||
async function set(key, value, expirationInSeconds) {
|
||||
await redis.set(key, value, 'EX', expirationInSeconds)
|
||||
console.log(`Token ${key} set successfully with expiration of ${expirationInSeconds} seconds!`)
|
||||
}
|
||||
|
||||
// Function to get a token
|
||||
async function get(key) {
|
||||
const token = await redis.get(key)
|
||||
if (token === null) {
|
||||
console.log('Token not found')
|
||||
} else {
|
||||
console.log(`Token for ${key}: ${token}`)
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
// Function to delete a token
|
||||
async function del(key) {
|
||||
const deletedCount = await redis.del(key)
|
||||
if (deletedCount === 1) {
|
||||
console.log(`Token ${key} deleted successfully!`)
|
||||
} else {
|
||||
console.log('Token not found')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
set, get, del
|
||||
}
|
||||
|
||||
// // Example usage
|
||||
// const tokenKey = 'userToken'
|
||||
// const tokenValue = 'exampleTokenValue'
|
||||
// const expirationSeconds = 300; // 300 seconds expiration (5 minutes)
|
||||
|
||||
// (async () => {
|
||||
// await setTokenWithExpiration(tokenKey, tokenValue, expirationSeconds)
|
||||
// await getToken(tokenKey)
|
||||
// await updateToken(tokenKey, 'newTokenValue')
|
||||
// await getToken(tokenKey)
|
||||
// await deleteToken(tokenKey)
|
||||
// await getToken(tokenKey) // Checking if token is deleted
|
||||
// redis.quit() // Close Redis connection
|
||||
// })()
|
|
@ -0,0 +1,49 @@
|
|||
const { getAccessToken } = require('./oauth2')
|
||||
|
||||
async function requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '') {
|
||||
let config = {}
|
||||
|
||||
url = url.replace('crmPhone', crmPhone)
|
||||
let commonConfig = {
|
||||
method: requestType,
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': requestContentType,
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
commonConfig = { ...commonConfig, data }
|
||||
}
|
||||
|
||||
if (type === 'basic') {
|
||||
const auth = Buffer.from(`${userName}:${passWord}`).toString('base64')
|
||||
config = {
|
||||
...commonConfig,
|
||||
headers: {
|
||||
...commonConfig.headers,
|
||||
'Authorization': `Basic ${auth}`,
|
||||
}
|
||||
}
|
||||
} else if (type === 'bearer') {
|
||||
config = {
|
||||
...commonConfig,
|
||||
headers: {
|
||||
...commonConfig.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
}
|
||||
} else if (type === 'oauth2') {
|
||||
const accessToken = await getAccessToken(crmClientId)
|
||||
config = {
|
||||
...commonConfig,
|
||||
headers: {
|
||||
...commonConfig.headers,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
module.exports = requestConfigHeader
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
const { getIO } = require('./socketIO')
|
||||
|
||||
function sendMessageSocket({ companyId, action, status, data }) {
|
||||
|
||||
// console.log(`*** ${msg} ***`)
|
||||
|
||||
const io = getIO()
|
||||
io.to(`company_${companyId.toString()}`).emit("crm_upload", {
|
||||
action,
|
||||
status,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = sendMessageSocket
|
|
@ -0,0 +1,121 @@
|
|||
|
||||
|
||||
const SocketIO = require('socket.io')
|
||||
let io
|
||||
|
||||
const initIO = (httpServer) => {
|
||||
io = SocketIO(httpServer, {
|
||||
cors: {
|
||||
origin: 'http://localhost:3000'
|
||||
},
|
||||
maxHttpBufferSize: 1e8
|
||||
})
|
||||
|
||||
io.on("connection", socket => {
|
||||
console.log('CLIENT CONNECTED')
|
||||
|
||||
socket.on("companySession", (companyId) => {
|
||||
console.log(`A client joined a companySession channel: ${companyId}`)
|
||||
socket.join(`company_${companyId}`)
|
||||
});
|
||||
|
||||
// socket.on("joinWhatsSession", (whatsappId: string) => {
|
||||
// logger.info(`A client joined a joinWhatsSession channel: ${whatsappId}`)
|
||||
// socket.join(`session_${whatsappId}`)
|
||||
// })
|
||||
|
||||
// socket.on("message_from_client", () => {
|
||||
// socket.emit("message_from_server", "Sent an event from the server!")
|
||||
// })
|
||||
|
||||
// socket.on("message_create", async (data: any) => {
|
||||
// handleMessage(data.msg, data)
|
||||
// })
|
||||
|
||||
// socket.on("media_uploaded", async (data: any) => {
|
||||
// handleMessage(data.msg, data)
|
||||
// })
|
||||
|
||||
// socket.on("message_ack", async (data: any) => {
|
||||
// handleMsgAck(data.id, data.ack)
|
||||
// })
|
||||
|
||||
|
||||
// socket.on("joinChatBox", (ticketId: string) => {
|
||||
// logger.info("A client joined a ticket channel")
|
||||
// socket.join(ticketId)
|
||||
// })
|
||||
|
||||
// socket.on("joinNotification", () => {
|
||||
// logger.info("A client joined notification channel")
|
||||
// socket.join("notification")
|
||||
// })
|
||||
|
||||
// socket.on("joinTickets", (status: string) => {
|
||||
// logger.info(`A client joined to ${status} tickets channel.`)
|
||||
// socket.join(status)
|
||||
// })
|
||||
|
||||
socket.on("disconnect", (data) => {
|
||||
console.log(`Client disconnected socket: ${data}`)
|
||||
})
|
||||
|
||||
// socket.on("disconnecting", async () => {
|
||||
// console.log("socket.rooms: ", socket.rooms) // the Set contains at least the socket ID
|
||||
|
||||
// let rooms = socket.rooms
|
||||
|
||||
// console.log("rooms: ", rooms, " | rooms.size: ", rooms.size)
|
||||
|
||||
// if (rooms && rooms.size == 1) return
|
||||
// if (rooms && rooms.size == 2 && ![...rooms][1].startsWith("session_"))
|
||||
// return
|
||||
|
||||
// let whatsappIds: any = await Whatsapp.findAll({
|
||||
// attributes: ["id"],
|
||||
// raw: true
|
||||
// })
|
||||
|
||||
// if (whatsappIds && whatsappIds.length > 0) {
|
||||
// whatsappIds = whatsappIds.map((e: any) => `${e.id}`)
|
||||
|
||||
// console.log(
|
||||
// "whatsappIds whatsappIds whatsappIds whatsappIds whatsappIds: ",
|
||||
// whatsappIds
|
||||
// )
|
||||
|
||||
// if (
|
||||
// rooms &&
|
||||
// rooms.size == 2 &&
|
||||
// [...rooms][1].startsWith("session_") &&
|
||||
// whatsappIds.includes([...rooms][1].replace("session_", ""))
|
||||
// ) {
|
||||
// console.log([...rooms][1])
|
||||
|
||||
// let whatsappId = [...rooms][1].replace("session_", "")
|
||||
|
||||
// const whatsapp = await Whatsapp.findByPk(whatsappId, {})
|
||||
|
||||
// if (whatsapp) {
|
||||
// await whatsapp.update({ status: "OPENING" })
|
||||
|
||||
// io.emit("whatsappSession", {
|
||||
// action: "update",
|
||||
// session: whatsapp
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
})
|
||||
return io
|
||||
}
|
||||
|
||||
const getIO = () => {
|
||||
if (!io) {
|
||||
throw new AppError("Socket IO not initialized")
|
||||
}
|
||||
return io
|
||||
}
|
||||
|
||||
module.exports = { initIO, getIO }
|
|
@ -0,0 +1,63 @@
|
|||
const lookupContactByPhone = require('./lookupCRMContactByPhone')
|
||||
const createContact = require('./createContact')
|
||||
const sendMessageSocket = require('./sendMessageSocket')
|
||||
const findProperty = require('./findProperty')
|
||||
const journalingRequest = require('./journalingRequest')
|
||||
|
||||
async function templateValidator(crmPhoneTest, crm, companyId) {
|
||||
|
||||
let phoneTest = crmPhoneTest ? crmPhoneTest : '5511900112233'
|
||||
|
||||
let contact = await lookupContactByPhone(
|
||||
rest = crm.crmRest,
|
||||
authentication = crm.authentication,
|
||||
crmPhone = phoneTest,
|
||||
companyId,
|
||||
test = { testing: true })
|
||||
|
||||
if (!contact.exist) {
|
||||
|
||||
console.log('O CONTATO SER CRIADO')
|
||||
|
||||
contact = await createContact(companyId,
|
||||
rest = crm.crmRest,
|
||||
authentication = crm.authentication,
|
||||
crmPhone = phoneTest,
|
||||
crmFirstName = '',
|
||||
crmLastName = '',
|
||||
crmEmail = '',
|
||||
test = { testing: true })
|
||||
|
||||
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
|
||||
|
||||
}
|
||||
else {
|
||||
msg = `Contato de numero ${phoneTest} já existe no crm`
|
||||
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
|
||||
|
||||
}
|
||||
|
||||
let { request, calls, response } = findProperty(crm.crmRest, 'callJournaling')
|
||||
|
||||
for (const operation of calls) {
|
||||
|
||||
msg = `Tentando inserir registro de chamada(call journal) ${Object.keys(operation)[0]} para o contato ${phoneTest}`
|
||||
|
||||
await journalingRequest(request,
|
||||
body = operation[Object.keys(operation)[0]],
|
||||
crmCallDuration = "900",
|
||||
contact,
|
||||
crmAgent = "2245",
|
||||
crmPhone = phoneTest,
|
||||
authentication = crm.authentication,
|
||||
test = { testing: true, msg, companyId })
|
||||
|
||||
msg = `Registro de chamada inserido com sucesso ${Object.keys(operation)[0]} para o contato ${phoneTest}`
|
||||
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
|
||||
}
|
||||
|
||||
sendMessageSocket({ companyId, status: 'finished', data: { crmPhoneTest: phoneTest } })
|
||||
|
||||
}
|
||||
|
||||
module.exports = templateValidator
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "crm-upload-template-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-scripts": "5.0.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -0,0 +1,13 @@
|
|||
import Title from './components/Title'
|
||||
import UploadFile from './components/UploadFile'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main>
|
||||
<Title />
|
||||
<UploadFile />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
|
@ -0,0 +1,10 @@
|
|||
const Title = () => {
|
||||
return (
|
||||
<div className='title'>
|
||||
<h1>
|
||||
<span>CRM</span> upload template
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Title;
|
|
@ -0,0 +1,204 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import socketIOClient from 'socket.io-client'
|
||||
import Modal from 'react-modal'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
Modal.setAppElement('#root')
|
||||
|
||||
const UploadFile = () => {
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const [payload, setPayload] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [status, setStatus] = useState('')
|
||||
const [errorResponse, setErrorResponse] = useState('')
|
||||
const [crmPhoneTest, setCrmPhoneTest] = useState('')
|
||||
|
||||
const [file, setFile] = useState(null)
|
||||
const [uploadStatus, setUploadStatus] = useState('')
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const selectedFile = e.target.files[0]
|
||||
setFile(selectedFile)
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) {
|
||||
alert('Please select a file.')
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('crm', file)
|
||||
formData.append('companyId', process.env.REACT_APP_COMPANY_ID)
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL_API}/api/v1/crm/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': 'Bearer 2ivck10D3o9qAZi0pkKudVDl9bdEVXY2s8gdxZ0jYgL1DZWTgDz6wDiIjlWgYmJtVOoqf0b42ZTLBRrfo8WoAaScRsujz3jQUNXdchSg0o43YilZGmVhheGJNAeIQRknHEll4nRJ7avcFgmDGoYbEey7TSC8EHS4Z3gzeufYYSfnKNDBwwzBURIQrTOxYFe3tBHsGOzwnuD2lU5tnEx7tr2XRO4zRNYeNY4lMBOFM0mRuyAe4kuqTrKXmJ8As200'
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
|
||||
const { url } = response.data
|
||||
|
||||
console.log('-----> url: ', url)
|
||||
if (url)
|
||||
window.location.href = url
|
||||
|
||||
setUploadStatus('File uploaded successfully!')
|
||||
} else {
|
||||
setUploadStatus('Failed to upload file.')
|
||||
|
||||
console.log('error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
setUploadStatus('Error uploading file.')
|
||||
|
||||
setStatus('error')
|
||||
setErrorResponse(error.response.data.msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log('process.env.REACT_APP_COMPANY_ID: ', process.env.REACT_APP_COMPANY_ID)
|
||||
|
||||
const socket = socketIOClient(process.env.REACT_APP_URL_API)
|
||||
|
||||
const currentURL = window.location.href
|
||||
const urlObject = new URL(currentURL)
|
||||
const clientId = urlObject.searchParams.get('clientId')
|
||||
console.log('clientId: ', clientId)
|
||||
|
||||
if (clientId) {
|
||||
|
||||
const postData = async () => {
|
||||
try {
|
||||
await axios.post(`${process.env.REACT_APP_URL_API}/api/v1/crm/test`, {
|
||||
clientId,
|
||||
companyId: process.env.REACT_APP_COMPANY_ID,
|
||||
})
|
||||
} catch (error) {
|
||||
setStatus('error')
|
||||
console.error('-----> Error:', error)
|
||||
console.log('TEST: ', error.response.data.msg)
|
||||
setErrorResponse(error.response.data.msg)
|
||||
}
|
||||
}
|
||||
|
||||
postData()
|
||||
|
||||
}
|
||||
|
||||
socket.emit('companySession', process.env.REACT_APP_COMPANY_ID)
|
||||
|
||||
socket.on('crm_upload', (data) => {
|
||||
console.log('Received data from server:', data)
|
||||
|
||||
setStatus(data?.status)
|
||||
|
||||
setMessage(data?.data?.msg)
|
||||
|
||||
if (data?.data?.request) {
|
||||
setPayload(data.data.request, null, 2)
|
||||
}
|
||||
|
||||
if (data?.data?.crmPhoneTest) {
|
||||
setCrmPhoneTest(data.data.crmPhoneTest)
|
||||
}
|
||||
setIsModalOpen(true)
|
||||
})
|
||||
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleAfterOpenModal = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="centered">
|
||||
<input type="file" onChange={handleFileChange} />
|
||||
<button onClick={handleUpload}>Upload</button>
|
||||
{uploadStatus && <p>{uploadStatus}</p>}
|
||||
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={handleAfterOpenModal}
|
||||
contentLabel="Example Modal"
|
||||
>
|
||||
<div className="modal-title">
|
||||
|
||||
{status === 'finished' && <h2>Template validado com sucesso</h2>}
|
||||
{status === 'error' && <h2>Template com erro no processo abaixo</h2>}
|
||||
{status === 'processing' && <h2>Testando template. Aguarde...</h2>}
|
||||
|
||||
</div>
|
||||
|
||||
<div className="error-message">
|
||||
{status === 'error' && (
|
||||
<p>
|
||||
{errorResponse}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="success-message">
|
||||
{status === 'finished' && (
|
||||
<p>
|
||||
Verifique no seu CRM os dados inseridos para o contato de teste: {crmPhoneTest}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="icon-container">
|
||||
{status === 'finished' &&
|
||||
<div className="checkmark-container">
|
||||
<FontAwesomeIcon icon={faCheckCircle} size="5x" color="green" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{status === 'finished' ? <></> : <> <MessageComponent title={message} />{payload ? <PayloadComponent formattedJSON={payload} /> : <></>}</>}
|
||||
|
||||
<button className="close-button" onClick={closeModal}>Close Modal</button>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageComponent = ({ title }) => {
|
||||
return (
|
||||
<div>
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PayloadComponent = ({ formattedJSON }) => {
|
||||
return (
|
||||
<div>
|
||||
<pre>{JSON.stringify(formattedJSON, null, 2)}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default UploadFile
|
|
@ -0,0 +1,344 @@
|
|||
*,
|
||||
::after,
|
||||
::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* fonts */
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&family=Montserrat&display=swap'); */
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
} /*16px*/
|
||||
|
||||
:root {
|
||||
/* colors */
|
||||
--primary-100: #e2e0ff;
|
||||
--primary-200: #c1beff;
|
||||
--primary-300: #a29dff;
|
||||
--primary-400: #837dff;
|
||||
--primary-500: #645cff;
|
||||
--primary-600: #504acc;
|
||||
--primary-700: #3c3799;
|
||||
--primary-800: #282566;
|
||||
--primary-900: #141233;
|
||||
|
||||
/* grey */
|
||||
--grey-50: #f8fafc;
|
||||
--grey-100: #f1f5f9;
|
||||
--grey-200: #e2e8f0;
|
||||
--grey-300: #cbd5e1;
|
||||
--grey-400: #94a3b8;
|
||||
--grey-500: #64748b;
|
||||
--grey-600: #475569;
|
||||
--grey-700: #334155;
|
||||
--grey-800: #1e293b;
|
||||
--grey-900: #0f172a;
|
||||
/* rest of the colors */
|
||||
--black: #222;
|
||||
--white: #fff;
|
||||
--red-light: #f8d7da;
|
||||
--red-dark: #842029;
|
||||
--green-light: #d1e7dd;
|
||||
--green-dark: #0f5132;
|
||||
|
||||
/* fonts */
|
||||
/* --headingFont: 'Roboto', sans-serif;
|
||||
--bodyFont: 'Nunito', sans-serif; */
|
||||
--small-text: 0.875rem;
|
||||
--extra-small-text: 0.7em;
|
||||
/* rest of the vars */
|
||||
--backgroundColor: var(--grey-50);
|
||||
--textColor: var(--grey-900);
|
||||
--borderRadius: 0.25rem;
|
||||
--letterSpacing: 1px;
|
||||
--transition: 0.3s ease-in-out all;
|
||||
--max-width: 1120px;
|
||||
--fixed-width: 600px;
|
||||
|
||||
/* box shadow*/
|
||||
--shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--backgroundColor);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.75;
|
||||
color: var(--textColor);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
max-width: 40em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-family: var(--headingFont);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
letter-spacing: var(--letterSpacing);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 3.052rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.441rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.953rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.563rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
small,
|
||||
.text-small {
|
||||
font-size: var(--small-text);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
/* buttons */
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
color: var(--white);
|
||||
background: var(--primary-500);
|
||||
border: transparent;
|
||||
border-radius: var(--borderRadius);
|
||||
letter-spacing: var(--letterSpacing);
|
||||
padding: 0.375rem 0.75rem;
|
||||
box-shadow: var(--shadow-1);
|
||||
transition: var(--transition);
|
||||
text-transform: capitalize;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn:hover {
|
||||
background: var(--primary-700);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
.btn-hipster {
|
||||
color: var(--primary-500);
|
||||
background: var(--primary-200);
|
||||
}
|
||||
.btn-hipster:hover {
|
||||
color: var(--primary-200);
|
||||
background: var(--primary-700);
|
||||
}
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* alerts */
|
||||
.alert {
|
||||
padding: 0.375rem 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-color: transparent;
|
||||
border-radius: var(--borderRadius);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: var(--red-dark);
|
||||
background: var(--red-light);
|
||||
}
|
||||
.alert-success {
|
||||
color: var(--green-dark);
|
||||
background: var(--green-light);
|
||||
}
|
||||
/* form */
|
||||
|
||||
.form {
|
||||
width: 90vw;
|
||||
max-width: var(--fixed-width);
|
||||
background: var(--white);
|
||||
border-radius: var(--borderRadius);
|
||||
box-shadow: var(--shadow-2);
|
||||
padding: 2rem 2.5rem;
|
||||
margin: 3rem auto;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: var(--small-text);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: capitalize;
|
||||
letter-spacing: var(--letterSpacing);
|
||||
}
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--borderRadius);
|
||||
background: var(--backgroundColor);
|
||||
border: 1px solid var(--grey-200);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
height: 7rem;
|
||||
}
|
||||
::placeholder {
|
||||
font-family: inherit;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
.form-alert {
|
||||
color: var(--red-dark);
|
||||
letter-spacing: var(--letterSpacing);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
/* alert */
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border: 5px solid var(--grey-400);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary-500);
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
.loading {
|
||||
margin: 0 auto;
|
||||
}
|
||||
/* title */
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.title-underline {
|
||||
background: var(--primary-500);
|
||||
width: 10rem;
|
||||
height: 0.25rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.title span {
|
||||
color: var(--primary-500);
|
||||
}
|
||||
/*
|
||||
===============
|
||||
Axios Tutorial
|
||||
===============
|
||||
*/
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 90vw;
|
||||
max-width: var(--fixed-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dad-joke {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50vh; /* Set the height of the container to the full viewport height */
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
margin-bottom: 0px; /* Adjust spacing as needed */
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkmark-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* margin-bottom: 20px; */
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff; /* Example color - Change as needed */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #0056b3; /* Example color - Change as needed */
|
||||
}
|
||||
|
||||
/* Style for the modal */
|
||||
.custom-modal {
|
||||
border: 10px solid #000; /* Black border with 10px thickness */
|
||||
pointer-events: auto;
|
||||
/* Other modal styles here */
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "api-crm",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Adriano <adriano08andrade@hotmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"husky": "^8.0.3"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue