Projeto em estado funcional sem modificações no layout
commit
0cc863139d
|
@ -0,0 +1,26 @@
|
|||
# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
|
||||
# Visit https://fossa.com to learn more
|
||||
|
||||
version: 2
|
||||
cli:
|
||||
server: https://app.fossa.com
|
||||
fetcher: custom
|
||||
project: https://github.com/canove/whaticket.git
|
||||
analyze:
|
||||
modules:
|
||||
- name: backend
|
||||
type: npm
|
||||
target: backend
|
||||
path: backend
|
||||
- name: backend
|
||||
type: npm
|
||||
target: backend
|
||||
path: backend
|
||||
- name: frontend
|
||||
type: npm
|
||||
target: frontend
|
||||
path: frontend
|
||||
- name: frontend
|
||||
type: npm
|
||||
target: frontend
|
||||
path: frontend
|
|
@ -0,0 +1,19 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 10
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 10
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- bug
|
||||
- enhancement
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
|
@ -0,0 +1 @@
|
|||
sonar.exclusions=frontend/src/translate/languages/*,**/__tests__/**/*
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 canove
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,441 @@
|
|||
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?business=VWW3BHW4AWHUY&item_name=Desenvolvimento+de+Software¤cy_code=BRL)
|
||||
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B21084%2Fgithub.com%2Fcanove%2Fwhaticket.svg?type=shield)](https://app.fossa.com/projects/custom%2B21084%2Fgithub.com%2Fcanove%2Fwhaticket?ref=badge_shield)
|
||||
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=canove_whaticket&metric=alert_status)](https://sonarcloud.io/dashboard?id=canove_whaticket)
|
||||
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=canove_whaticket&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=canove_whaticket)
|
||||
[![Discord Chat](https://img.shields.io/discord/784109818247774249.svg?logo=discord)](https://discord.gg/Dp2tTZRYHg)
|
||||
|
||||
# WhaTicket
|
||||
|
||||
**NOTE**: The new version of whatsapp-web.js required Node 14. Upgrade your installations to keep using it.
|
||||
|
||||
A _very simple_ Ticket System based on WhatsApp messages.
|
||||
|
||||
Backend uses [whatsapp-web.js](https://github.com/pedroslopez/whatsapp-web.js) to receive and send WhatsApp messages, create tickets from them and store all in a MySQL database.
|
||||
|
||||
Frontend is a full-featured multi-user _chat app_ bootstrapped with react-create-app and Material UI, that comunicates with backend using REST API and Websockets. It allows you to interact with contacts, tickets, send and receive WhatsApp messages.
|
||||
|
||||
**NOTE**: I can't guarantee you will not be blocked by using this method, although it has worked for me. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.
|
||||
|
||||
## Motivation
|
||||
|
||||
I'm a SysAdmin, and in my daily work, I do a lot of support through WhatsApp. Since WhatsApp Web doesn't allow multiple users, and 90% of our tickets comes from this channel, we created this to share same whatsapp account cross our team.
|
||||
|
||||
## How it works?
|
||||
|
||||
On every new message received in an associated WhatsApp, a new Ticket is created. Then, this ticket can be reached in a _queue_ on _Tickets_ page, where you can assign ticket to your yourself by _aceppting_ it, respond ticket message and eventually _resolve_ it.
|
||||
|
||||
Subsequent messages from same contact will be related to first **open/pending** ticket found.
|
||||
|
||||
If a contact sent a new message in less than 2 hours interval, and there is no ticket from this contact with **pending/open** status, the newest **closed** ticket will be reopen, instead of creating a new one.
|
||||
|
||||
## Screenshots
|
||||
|
||||
![](https://github.com/canove/whaticket/raw/master/images/whaticket-queues.gif)
|
||||
<img src="https://raw.githubusercontent.com/canove/whaticket/master/images/chat2.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/chat3.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/multiple-whatsapps2.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/contacts1.png" width="350">
|
||||
|
||||
## Features
|
||||
|
||||
- Have multiple users chating in same WhatsApp Number ✅
|
||||
- Connect to multiple WhatsApp accounts and receive all messages in one place ✅ 🆕
|
||||
- Create and chat with new contacts without touching cellphone ✅
|
||||
- Send and receive message ✅
|
||||
- Send media (images/audio/documents) ✅
|
||||
- Receive media (images/audio/video/documents) ✅
|
||||
|
||||
## Installation and Usage (Linux Ubuntu - Development)
|
||||
|
||||
Create Mysql Database using docker:
|
||||
_Note_: change MYSQL_DATABASE, MYSQL_PASSWORD, MYSQL_USER and MYSQL_ROOT_PASSWORD.
|
||||
|
||||
```bash
|
||||
docker run --name whaticketdb -e MYSQL_ROOT_PASSWORD=strongpassword -e MYSQL_DATABASE=whaticket -e MYSQL_USER=whaticket -e MYSQL_PASSWORD=whaticket --restart always -p 3306:3306 -d mariadb:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
|
||||
```
|
||||
|
||||
Install puppeteer dependencies:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libxshmfence-dev libgbm-dev wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
|
||||
```
|
||||
|
||||
Clone this repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/canove/whaticket/ whaticket
|
||||
```
|
||||
|
||||
Go to backend folder and create .env file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
Fill `.env` file with environment variables:
|
||||
|
||||
```bash
|
||||
NODE_ENV=DEVELOPMENT #it helps on debugging
|
||||
BACKEND_URL=http://localhost
|
||||
FRONTEND_URL=https://localhost:3000
|
||||
PROXY_PORT=8080
|
||||
PORT=8080
|
||||
|
||||
DB_HOST= #DB host IP, usually localhost
|
||||
DB_DIALECT=
|
||||
DB_USER=
|
||||
DB_PASS=
|
||||
DB_NAME=
|
||||
|
||||
JWT_SECRET=3123123213123
|
||||
JWT_REFRESH_SECRET=75756756756
|
||||
```
|
||||
|
||||
Install backend dependencies, build app, run migrations and seeds:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx sequelize db:migrate
|
||||
npx sequelize db:seed:all
|
||||
```
|
||||
|
||||
Start backend:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Open a second terminal, go to frontend folder and create .env file:
|
||||
|
||||
```bash
|
||||
nano .env
|
||||
REACT_APP_BACKEND_URL = http://localhost:8080/ # Your previous configured backend app URL.
|
||||
```
|
||||
|
||||
Start frontend app:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
- Go to http://your_server_ip:3000/signup
|
||||
- Create an user and login with it.
|
||||
- On the sidebard, go to _Connections_ page and create your first WhatsApp connection.
|
||||
- Wait for QR CODE button to appear, click it and read qr code.
|
||||
- Done. Every message received by your synced WhatsApp number will appear in Tickets List.
|
||||
|
||||
## Basic production deployment (Ubuntu 18.04 VPS)
|
||||
|
||||
All instructions below assumes you are NOT running as root, since it will give an error in puppeteer. So let's start creating a new user and granting sudo privileges to it:
|
||||
|
||||
```bash
|
||||
adduser deploy
|
||||
usermod -aG sudo deploy
|
||||
```
|
||||
|
||||
Now we can login with this new user:
|
||||
|
||||
```bash
|
||||
su deploy
|
||||
```
|
||||
|
||||
You'll need two subdomains forwarding to yours VPS ip to follow these instructions. We'll use `myapp.mydomain.com` to frontend and `api.mydomain.com` to backend in the following example.
|
||||
|
||||
Update all system packages:
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade
|
||||
```
|
||||
|
||||
Install node and confirm node command is available:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
Install docker and add you user to docker group:
|
||||
|
||||
```bash
|
||||
sudo apt install apt-transport-https ca-certificates curl software-properties-common
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
|
||||
sudo apt update
|
||||
sudo apt install docker-ce
|
||||
sudo systemctl status docker
|
||||
sudo usermod -aG docker ${USER}
|
||||
su - ${USER}
|
||||
```
|
||||
|
||||
Create Mysql Database using docker:
|
||||
_Note_: change MYSQL_DATABASE, MYSQL_PASSWORD, MYSQL_USER and MYSQL_ROOT_PASSWORD.
|
||||
|
||||
```bash
|
||||
docker run --name whaticketdb -e MYSQL_ROOT_PASSWORD=strongpassword -e MYSQL_DATABASE=whaticket -e MYSQL_USER=whaticket -e MYSQL_PASSWORD=whaticket --restart always -p 3306:3306 -d mariadb:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
|
||||
```
|
||||
|
||||
Clone this repository:
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
git clone https://github.com/canove/whaticket whaticket
|
||||
```
|
||||
|
||||
Create backend .env file and fill with details:
|
||||
|
||||
```bash
|
||||
cp whaticket/backend/.env.example whaticket/backend/.env
|
||||
nano whaticket/backend/.env
|
||||
```
|
||||
|
||||
```bash
|
||||
NODE_ENV=
|
||||
BACKEND_URL=https://api.mydomain.com #USE HTTPS HERE, WE WILL ADD SSL LATTER
|
||||
FRONTEND_URL=https://myapp.mydomain.com #USE HTTPS HERE, WE WILL ADD SSL LATTER, CORS RELATED!
|
||||
PROXY_PORT=443 #USE NGINX REVERSE PROXY PORT HERE, WE WILL CONFIGURE IT LATTER
|
||||
PORT=8080
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_DIALECT=
|
||||
DB_USER=
|
||||
DB_PASS=
|
||||
DB_NAME=
|
||||
|
||||
JWT_SECRET=3123123213123
|
||||
JWT_REFRESH_SECRET=75756756756
|
||||
```
|
||||
|
||||
Install puppeteer dependencies:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libxshmfence-dev libgbm-dev wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
|
||||
```
|
||||
|
||||
Install backend dependencies, build app, run migrations and seeds:
|
||||
|
||||
```bash
|
||||
cd whaticket/backend
|
||||
npm install
|
||||
npm run build
|
||||
npx sequelize db:migrate
|
||||
npx sequelize db:seed:all
|
||||
```
|
||||
|
||||
Start it with `npm start`, you should see: `Server started on port...` on console. Hit `CTRL + C` to exit.
|
||||
|
||||
Install pm2 **with sudo**, and start backend with it:
|
||||
|
||||
```bash
|
||||
sudo npm install -g pm2
|
||||
pm2 start dist/server.js --name whaticket-backend
|
||||
```
|
||||
|
||||
Make pm2 auto start afeter reboot:
|
||||
|
||||
```bash
|
||||
pm2 startup ubuntu -u `YOUR_USERNAME`
|
||||
```
|
||||
|
||||
Copy the last line outputed from previus command and run it, its something like:
|
||||
|
||||
```bash
|
||||
sudo env PATH=\$PATH:/usr/bin pm2 startup ubuntu -u YOUR_USERNAME --hp /home/YOUR_USERNAM
|
||||
```
|
||||
|
||||
Go to frontend folder and install dependencies:
|
||||
|
||||
```bash
|
||||
cd ../frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
Edit .env file and fill it with your backend address, it should look like this:
|
||||
|
||||
```bash
|
||||
REACT_APP_BACKEND_URL = https://api.mydomain.com/
|
||||
```
|
||||
|
||||
Build frontend app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Start frontend with pm2, and save pm2 process list to start automatically after reboot:
|
||||
|
||||
```bash
|
||||
pm2 start server.js --name whaticket-frontend
|
||||
pm2 save
|
||||
```
|
||||
|
||||
To check if it's running, run `pm2 list`, it should look like:
|
||||
|
||||
```bash
|
||||
deploy@ubuntu-whats:~$ pm2 list
|
||||
┌─────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
|
||||
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ . │ status │ cpu │ mem │ user │ watching │
|
||||
├─────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
|
||||
│ 1 │ whaticket-frontend │ default │ 0.1.0 │ fork │ 179249 │ 12D │ 0 │ online │ 0.3% │ 50.2mb │ deploy │ disabled │
|
||||
│ 6 │ whaticket-backend │ default │ 1.0.0 │ fork │ 179253 │ 12D │ 15 │ online │ 0.3% │ 118.5mb │ deploy │ disabled │
|
||||
└─────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
|
||||
|
||||
```
|
||||
|
||||
Install nginx:
|
||||
|
||||
```bash
|
||||
sudo apt install nginx
|
||||
```
|
||||
|
||||
Remove nginx default site:
|
||||
|
||||
```bash
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
```
|
||||
|
||||
Create a new nginx site to frontend app:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/whaticket-frontend
|
||||
```
|
||||
|
||||
Edit and fill it with this information, changing `server_name` to yours equivalent to `myapp.mydomain.com`:
|
||||
|
||||
```bash
|
||||
server {
|
||||
server_name myapp.mydomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3333;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create another one to backend api, changing `server_name` to yours equivalent to `api.mydomain.com`, and `proxy_pass` to your localhost backend node server URL:
|
||||
|
||||
```bash
|
||||
sudo cp /etc/nginx/sites-available/whaticket-frontend /etc/nginx/sites-available/whaticket-backend
|
||||
sudo nano /etc/nginx/sites-available/whaticket-backend
|
||||
```
|
||||
|
||||
```bash
|
||||
server {
|
||||
server_name api.mydomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
Create a symbolic links to enalbe nginx sites:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/whaticket-frontend /etc/nginx/sites-enabled
|
||||
sudo ln -s /etc/nginx/sites-available/whaticket-backend /etc/nginx/sites-enabled
|
||||
```
|
||||
|
||||
By default, nginx limit body size to 1MB, what isn't enough to some media uploads. Lets change it to 20MB adding a new line to config file:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/nginx.conf
|
||||
...
|
||||
http {
|
||||
...
|
||||
client_max_body_size 20M; # HANDLE BIGGER UPLOADS
|
||||
}
|
||||
```
|
||||
|
||||
Test nginx configuration and restart server:
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo service nginx restart
|
||||
```
|
||||
|
||||
Now, enable SSL (https) on your sites to use all app features like notifications and sending audio messages. A easy way to this is using Certbot:
|
||||
|
||||
Install certbot:
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:certbot/certbot
|
||||
sudo apt update
|
||||
sudo apt install python-certbot-nginx
|
||||
```
|
||||
|
||||
Enable SSL on nginx (Fill / Accept all information asked):
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx
|
||||
```
|
||||
|
||||
## Access Data
|
||||
|
||||
User: admin@whaticket.com
|
||||
Password: admin
|
||||
|
||||
## Upgrading
|
||||
|
||||
WhaTicket is a working in progress and we are adding new features frequently. To update your old installation and get all the new features, you can use a bash script like this:
|
||||
|
||||
**Note**: Always check the .env.example and adjust your .env file before upgrading, since some new variable may be added.
|
||||
|
||||
```bash
|
||||
nano updateWhaticket
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "Updating Whaticket, please wait."
|
||||
|
||||
cd ~
|
||||
cd whaticket
|
||||
git pull
|
||||
cd backend
|
||||
npm install
|
||||
rm -rf dist
|
||||
npm run build
|
||||
npx sequelize db:migrate
|
||||
npx sequelize db:seed
|
||||
cd ../frontend
|
||||
npm install
|
||||
rm -rf build
|
||||
npm run build
|
||||
pm2 restart all
|
||||
|
||||
echo "Update finished. Enjoy!"
|
||||
```
|
||||
|
||||
Make it executable and run it:
|
||||
|
||||
```bash
|
||||
chmod +x updateWhaticket
|
||||
./updateWhaticket
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This project helps you and you want to help keep it going? Buy me a coffee:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/canove" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 61px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
Para doações em BRL, utilize o Paypal:
|
||||
|
||||
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?business=VWW3BHW4AWHUY&item_name=Desenvolvimento+de+Software¤cy_code=BRL)
|
||||
|
||||
Any help and suggestions will be apreciated.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
I just started leaning Javascript a few months ago and this is my first project. It may have security issues and many bugs. I recommend using it only on local network.
|
||||
|
||||
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. The official WhatsApp website can be found at https://whatsapp.com. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners.
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -0,0 +1,14 @@
|
|||
NODE_ENV=
|
||||
BACKEND_URL=http://localhost
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
PROXY_PORT=8080
|
||||
PORT=8080
|
||||
|
||||
DB_DIALECT=
|
||||
DB_HOST=
|
||||
DB_USER=
|
||||
DB_PASS=
|
||||
DB_NAME=
|
||||
|
||||
JWT_SECRET=
|
||||
JWT_REFRESH_SECRET=
|
|
@ -0,0 +1,3 @@
|
|||
/*.js
|
||||
node_modules
|
||||
dist
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "_" }
|
||||
],
|
||||
"import/prefer-default-export": "off",
|
||||
"no-console": "off",
|
||||
"no-param-reassign": "off",
|
||||
"prettier/prettier": "error",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never"
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
1,
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
node_modules
|
||||
public/*
|
||||
dist
|
||||
!public/.gitkeep
|
||||
.env
|
||||
.env.test
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
|
||||
/src/config/sentry.js
|
||||
|
||||
# Ignore test-related files
|
||||
/coverage.data
|
||||
/coverage/
|
|
@ -0,0 +1,8 @@
|
|||
const { resolve } = require("path");
|
||||
|
||||
module.exports = {
|
||||
"config": resolve(__dirname, "dist", "config", "database.js"),
|
||||
"modules-path": resolve(__dirname, "dist", "models"),
|
||||
"migrations-path": resolve(__dirname, "dist", "database", "migrations"),
|
||||
"seeders-path": resolve(__dirname, "dist", "database", "seeds")
|
||||
};
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
bail: 1,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/tmp/jest_rs",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
collectCoverageFrom: ["<rootDir>/src/services/**/*.ts"],
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: "v8",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
coverageReporters: ["text", "lcov"],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: "ts-jest",
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ["**/__tests__/**/*.spec.ts"]
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"start": "nodemon dist/server.js",
|
||||
"dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts",
|
||||
"pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all",
|
||||
"test": "NODE_ENV=test jest",
|
||||
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.29.2",
|
||||
"@types/pino": "^6.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.16.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"http-graceful-shutdown": "^2.3.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"multer": "^1.4.2",
|
||||
"mysql2": "^2.2.5",
|
||||
"pg": "^8.4.1",
|
||||
"pino": "^6.9.0",
|
||||
"pino-pretty": "^4.3.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^5.22.3",
|
||||
"sequelize-cli": "^5.5.1",
|
||||
"sequelize-typescript": "^1.1.0",
|
||||
"socket.io": "^3.0.5",
|
||||
"whatsapp-web.js": "^1.15.3",
|
||||
"yup": "^0.32.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/bluebird": "^3.5.32",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.7",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/factory-girl": "^5.0.2",
|
||||
"@types/faker": "^5.1.3",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/multer": "^1.4.4",
|
||||
"@types/node": "^14.11.8",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@types/validator": "^13.1.0",
|
||||
"@types/yup": "^0.29.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-config-airbnb-base": "^14.2.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-import-resolver-typescript": "^2.3.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"factory-girl": "^5.0.4",
|
||||
"faker": "^5.1.0",
|
||||
"jest": "^26.6.0",
|
||||
"nodemon": "^2.0.4",
|
||||
"prettier": "^2.1.2",
|
||||
"supertest": "^5.0.0",
|
||||
"ts-jest": "^26.4.1",
|
||||
"ts-node-dev": "^1.0.0-pre.63",
|
||||
"typescript": "4.0.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
singleQuote: false,
|
||||
trailingComma: "none",
|
||||
arrowParens: "avoid"
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
declare namespace Express {
|
||||
export interface Request {
|
||||
user: { id: string; profile: string };
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
declare module "qrcode-terminal";
|
|
@ -0,0 +1,69 @@
|
|||
import faker from "faker";
|
||||
import AppError from "../../../errors/AppError";
|
||||
import AuthUserService from "../../../services/UserServices/AuthUserService";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("Auth", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be able to login with an existing user", async () => {
|
||||
const password = faker.internet.password();
|
||||
const email = faker.internet.email();
|
||||
|
||||
await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
const response = await AuthUserService({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
expect(response).toHaveProperty("token");
|
||||
});
|
||||
|
||||
it("should not be able to login with not registered email", async () => {
|
||||
try {
|
||||
await AuthUserService({
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(AppError);
|
||||
expect(err.statusCode).toBe(401);
|
||||
expect(err.message).toBe("ERR_INVALID_CREDENTIALS");
|
||||
}
|
||||
});
|
||||
|
||||
it("should not be able to login with incorret password", async () => {
|
||||
await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: "mail@test.com",
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
try {
|
||||
await AuthUserService({
|
||||
email: "mail@test.com",
|
||||
password: faker.internet.password()
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(AppError);
|
||||
expect(err.statusCode).toBe(401);
|
||||
expect(err.message).toBe("ERR_INVALID_CREDENTIALS");
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import faker from "faker";
|
||||
import AppError from "../../../errors/AppError";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("User", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be able to create a new user", async () => {
|
||||
const user = await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
expect(user).toHaveProperty("id");
|
||||
});
|
||||
|
||||
it("should not be able to create a user with duplicated email", async () => {
|
||||
await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: "teste@sameemail.com",
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
try {
|
||||
await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: "teste@sameemail.com",
|
||||
password: faker.internet.password()
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(AppError);
|
||||
expect(err.statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import faker from "faker";
|
||||
import AppError from "../../../errors/AppError";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import DeleteUserService from "../../../services/UserServices/DeleteUserService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("User", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be delete a existing user", async () => {
|
||||
const { id } = await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
expect(DeleteUserService(id)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("to throw an error if tries to delete a non existing user", async () => {
|
||||
expect(DeleteUserService(faker.random.number())).rejects.toBeInstanceOf(
|
||||
AppError
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import faker from "faker";
|
||||
import User from "../../../models/User";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import ListUsersService from "../../../services/UserServices/ListUsersService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("User", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be able to list users", async () => {
|
||||
await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
const response = await ListUsersService({
|
||||
pageNumber: 1
|
||||
});
|
||||
|
||||
expect(response).toHaveProperty("users");
|
||||
expect(response.users[0]).toBeInstanceOf(User);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import faker from "faker";
|
||||
import AppError from "../../../errors/AppError";
|
||||
import User from "../../../models/User";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import ShowUserService from "../../../services/UserServices/ShowUserService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("User", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be able to find a user", async () => {
|
||||
const newUser = await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
const user = await ShowUserService(newUser.id);
|
||||
|
||||
expect(user).toHaveProperty("id");
|
||||
expect(user).toBeInstanceOf(User);
|
||||
});
|
||||
|
||||
it("should not be able to find a inexisting user", async () => {
|
||||
expect(ShowUserService(faker.random.number())).rejects.toBeInstanceOf(
|
||||
AppError
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import faker from "faker";
|
||||
import AppError from "../../../errors/AppError";
|
||||
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||
import UpdateUserService from "../../../services/UserServices/UpdateUserService";
|
||||
import { disconnect, truncate } from "../../utils/database";
|
||||
|
||||
describe("User", () => {
|
||||
beforeEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await truncate();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnect();
|
||||
});
|
||||
|
||||
it("should be able to find a user", async () => {
|
||||
const newUser = await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
const updatedUser = await UpdateUserService({
|
||||
userId: newUser.id,
|
||||
userData: {
|
||||
name: "New name",
|
||||
email: "newmail@email.com"
|
||||
}
|
||||
});
|
||||
|
||||
expect(updatedUser).toHaveProperty("name", "New name");
|
||||
expect(updatedUser).toHaveProperty("email", "newmail@email.com");
|
||||
});
|
||||
|
||||
it("should not be able to updated a inexisting user", async () => {
|
||||
const userId = faker.random.number();
|
||||
const userData = {
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email()
|
||||
};
|
||||
|
||||
expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf(
|
||||
AppError
|
||||
);
|
||||
});
|
||||
|
||||
it("should not be able to updated an user with invalid data", async () => {
|
||||
const newUser = await CreateUserService({
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password()
|
||||
});
|
||||
|
||||
const userId = newUser.id;
|
||||
const userData = {
|
||||
name: faker.name.findName(),
|
||||
email: "test.worgn.email"
|
||||
};
|
||||
|
||||
expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf(
|
||||
AppError
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import database from "../../database";
|
||||
|
||||
const truncate = async (): Promise<void> => {
|
||||
await database.truncate({ force: true, cascade: true });
|
||||
};
|
||||
|
||||
const disconnect = async (): Promise<void> => {
|
||||
return database.connectionManager.close();
|
||||
};
|
||||
|
||||
export { truncate, disconnect };
|
|
@ -0,0 +1,43 @@
|
|||
import "./bootstrap";
|
||||
import "reflect-metadata";
|
||||
import "express-async-errors";
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
import "./database";
|
||||
import uploadConfig from "./config/upload";
|
||||
import AppError from "./errors/AppError";
|
||||
import routes from "./routes";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
Sentry.init({ dsn: process.env.SENTRY_DSN });
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: process.env.FRONTEND_URL
|
||||
})
|
||||
);
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
app.use("/public", express.static(uploadConfig.directory));
|
||||
app.use(routes);
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
app.use(async (err: Error, req: Request, res: Response, _: NextFunction) => {
|
||||
if (err instanceof AppError) {
|
||||
logger.warn(err);
|
||||
return res.status(err.statusCode).json({ error: err.message });
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
return res.status(500).json({ error: "Internal server error" });
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,5 @@
|
|||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: process.env.NODE_ENV === "test" ? ".env.test" : ".env"
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
secret: process.env.JWT_SECRET || "mysecret",
|
||||
expiresIn: "15m",
|
||||
refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret",
|
||||
refreshExpiresIn: "7d"
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
require("../bootstrap");
|
||||
|
||||
module.exports = {
|
||||
define: {
|
||||
charset: "utf8mb4",
|
||||
collate: "utf8mb4_bin"
|
||||
},
|
||||
dialect: process.env.DB_DIALECT || "mysql",
|
||||
timezone: "-03:00",
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
logging: false
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import path from "path";
|
||||
import multer from "multer";
|
||||
|
||||
const publicFolder = path.resolve(__dirname, "..", "..", "public");
|
||||
export default {
|
||||
directory: publicFolder,
|
||||
|
||||
storage: multer.diskStorage({
|
||||
destination: publicFolder,
|
||||
filename(req, file, cb) {
|
||||
const fileName = new Date().getTime() + path.extname(file.originalname);
|
||||
|
||||
return cb(null, fileName);
|
||||
}
|
||||
})
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
import * as Yup from "yup";
|
||||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
|
||||
import ListContactsService from "../services/ContactServices/ListContactsService";
|
||||
import CreateContactService from "../services/ContactServices/CreateContactService";
|
||||
import ShowContactService from "../services/ContactServices/ShowContactService";
|
||||
import UpdateContactService from "../services/ContactServices/UpdateContactService";
|
||||
import DeleteContactService from "../services/ContactServices/DeleteContactService";
|
||||
|
||||
import CheckContactNumber from "../services/WbotServices/CheckNumber"
|
||||
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
|
||||
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
type IndexQuery = {
|
||||
searchParam: string;
|
||||
pageNumber: string;
|
||||
};
|
||||
|
||||
interface ExtraInfo {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
interface ContactData {
|
||||
name: string;
|
||||
number: string;
|
||||
email?: string;
|
||||
extraInfo?: ExtraInfo[];
|
||||
}
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||
|
||||
const { contacts, count, hasMore } = await ListContactsService({
|
||||
searchParam,
|
||||
pageNumber
|
||||
});
|
||||
|
||||
return res.json({ contacts, count, hasMore });
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const newContact: ContactData = req.body;
|
||||
newContact.number = newContact.number.replace("-", "").replace(" ", "");
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
number: Yup.string()
|
||||
.required()
|
||||
.matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
|
||||
});
|
||||
|
||||
try {
|
||||
await schema.validate(newContact);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message);
|
||||
}
|
||||
|
||||
await CheckIsValidContact(newContact.number);
|
||||
const validNumber : any = await CheckContactNumber(newContact.number)
|
||||
|
||||
const profilePicUrl = await GetProfilePicUrl(validNumber);
|
||||
|
||||
let name = newContact.name
|
||||
let number = validNumber
|
||||
let email = newContact.email
|
||||
let extraInfo = newContact.extraInfo
|
||||
|
||||
const contact = await CreateContactService({
|
||||
name,
|
||||
number,
|
||||
email,
|
||||
extraInfo,
|
||||
profilePicUrl
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("contact", {
|
||||
action: "create",
|
||||
contact
|
||||
});
|
||||
|
||||
return res.status(200).json(contact);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { contactId } = req.params;
|
||||
|
||||
const contact = await ShowContactService(contactId);
|
||||
|
||||
return res.status(200).json(contact);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const contactData: ContactData = req.body;
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
name: Yup.string(),
|
||||
number: Yup.string().matches(
|
||||
/^\d+$/,
|
||||
"Invalid number format. Only numbers is allowed."
|
||||
)
|
||||
});
|
||||
|
||||
try {
|
||||
await schema.validate(contactData);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message);
|
||||
}
|
||||
|
||||
await CheckIsValidContact(contactData.number);
|
||||
|
||||
const { contactId } = req.params;
|
||||
|
||||
const contact = await UpdateContactService({ contactData, contactId });
|
||||
|
||||
const io = getIO();
|
||||
io.emit("contact", {
|
||||
action: "update",
|
||||
contact
|
||||
});
|
||||
|
||||
return res.status(200).json(contact);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { contactId } = req.params;
|
||||
|
||||
await DeleteContactService(contactId);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("contact", {
|
||||
action: "delete",
|
||||
contactId
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Contact deleted" });
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { Request, Response } from "express";
|
||||
import ImportContactsService from "../services/WbotServices/ImportContactsService";
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
await ImportContactsService();
|
||||
|
||||
return res.status(200).json({ message: "contacts imported" });
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
import { Request, Response } from "express";
|
||||
|
||||
import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead";
|
||||
import { getIO } from "../libs/socket";
|
||||
import Message from "../models/Message";
|
||||
|
||||
import ListMessagesService from "../services/MessageServices/ListMessagesService";
|
||||
import ShowTicketService from "../services/TicketServices/ShowTicketService";
|
||||
import DeleteWhatsAppMessage from "../services/WbotServices/DeleteWhatsAppMessage";
|
||||
import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia";
|
||||
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
|
||||
|
||||
type IndexQuery = {
|
||||
pageNumber: string;
|
||||
};
|
||||
|
||||
type MessageData = {
|
||||
body: string;
|
||||
fromMe: boolean;
|
||||
read: boolean;
|
||||
quotedMsg?: Message;
|
||||
};
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { ticketId } = req.params;
|
||||
const { pageNumber } = req.query as IndexQuery;
|
||||
|
||||
const { count, messages, ticket, hasMore } = await ListMessagesService({
|
||||
pageNumber,
|
||||
ticketId
|
||||
});
|
||||
|
||||
SetTicketMessagesAsRead(ticket);
|
||||
|
||||
return res.json({ count, messages, ticket, hasMore });
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { ticketId } = req.params;
|
||||
const { body, quotedMsg }: MessageData = req.body;
|
||||
const medias = req.files as Express.Multer.File[];
|
||||
|
||||
const ticket = await ShowTicketService(ticketId);
|
||||
|
||||
SetTicketMessagesAsRead(ticket);
|
||||
|
||||
if (medias) {
|
||||
await Promise.all(
|
||||
medias.map(async (media: Express.Multer.File) => {
|
||||
await SendWhatsAppMedia({ media, ticket });
|
||||
})
|
||||
);
|
||||
} else {
|
||||
await SendWhatsAppMessage({ body, ticket, quotedMsg });
|
||||
}
|
||||
|
||||
return res.send();
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { messageId } = req.params;
|
||||
|
||||
const message = await DeleteWhatsAppMessage(messageId);
|
||||
|
||||
const io = getIO();
|
||||
io.to(message.ticketId.toString()).emit("appMessage", {
|
||||
action: "update",
|
||||
message
|
||||
});
|
||||
|
||||
return res.send();
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
import CreateQueueService from "../services/QueueService/CreateQueueService";
|
||||
import DeleteQueueService from "../services/QueueService/DeleteQueueService";
|
||||
import ListQueuesService from "../services/QueueService/ListQueuesService";
|
||||
import ShowQueueService from "../services/QueueService/ShowQueueService";
|
||||
import UpdateQueueService from "../services/QueueService/UpdateQueueService";
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const queues = await ListQueuesService();
|
||||
|
||||
return res.status(200).json(queues);
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { name, color, greetingMessage } = req.body;
|
||||
|
||||
const queue = await CreateQueueService({ name, color, greetingMessage });
|
||||
|
||||
const io = getIO();
|
||||
io.emit("queue", {
|
||||
action: "update",
|
||||
queue
|
||||
});
|
||||
|
||||
return res.status(200).json(queue);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { queueId } = req.params;
|
||||
|
||||
const queue = await ShowQueueService(queueId);
|
||||
|
||||
return res.status(200).json(queue);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { queueId } = req.params;
|
||||
|
||||
const queue = await UpdateQueueService(queueId, req.body);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("queue", {
|
||||
action: "update",
|
||||
queue
|
||||
});
|
||||
|
||||
return res.status(201).json(queue);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { queueId } = req.params;
|
||||
|
||||
await DeleteQueueService(queueId);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("queue", {
|
||||
action: "delete",
|
||||
queueId: +queueId
|
||||
});
|
||||
|
||||
return res.status(200).send();
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
import * as Yup from "yup";
|
||||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
|
||||
import ListQuickAnswerService from "../services/QuickAnswerService/ListQuickAnswerService";
|
||||
import CreateQuickAnswerService from "../services/QuickAnswerService/CreateQuickAnswerService";
|
||||
import ShowQuickAnswerService from "../services/QuickAnswerService/ShowQuickAnswerService";
|
||||
import UpdateQuickAnswerService from "../services/QuickAnswerService/UpdateQuickAnswerService";
|
||||
import DeleteQuickAnswerService from "../services/QuickAnswerService/DeleteQuickAnswerService";
|
||||
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
type IndexQuery = {
|
||||
searchParam: string;
|
||||
pageNumber: string;
|
||||
};
|
||||
|
||||
interface QuickAnswerData {
|
||||
shortcut: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||
|
||||
const { quickAnswers, count, hasMore } = await ListQuickAnswerService({
|
||||
searchParam,
|
||||
pageNumber
|
||||
});
|
||||
|
||||
return res.json({ quickAnswers, count, hasMore });
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const newQuickAnswer: QuickAnswerData = req.body;
|
||||
|
||||
const QuickAnswerSchema = Yup.object().shape({
|
||||
shortcut: Yup.string().required(),
|
||||
message: Yup.string().required()
|
||||
});
|
||||
|
||||
try {
|
||||
await QuickAnswerSchema.validate(newQuickAnswer);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message);
|
||||
}
|
||||
|
||||
const quickAnswer = await CreateQuickAnswerService({
|
||||
...newQuickAnswer
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("quickAnswer", {
|
||||
action: "create",
|
||||
quickAnswer
|
||||
});
|
||||
|
||||
return res.status(200).json(quickAnswer);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { quickAnswerId } = req.params;
|
||||
|
||||
const quickAnswer = await ShowQuickAnswerService(quickAnswerId);
|
||||
|
||||
return res.status(200).json(quickAnswer);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const quickAnswerData: QuickAnswerData = req.body;
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
shortcut: Yup.string(),
|
||||
message: Yup.string()
|
||||
});
|
||||
|
||||
try {
|
||||
await schema.validate(quickAnswerData);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message);
|
||||
}
|
||||
|
||||
const { quickAnswerId } = req.params;
|
||||
|
||||
const quickAnswer = await UpdateQuickAnswerService({
|
||||
quickAnswerData,
|
||||
quickAnswerId
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("quickAnswer", {
|
||||
action: "update",
|
||||
quickAnswer
|
||||
});
|
||||
|
||||
return res.status(200).json(quickAnswer);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { quickAnswerId } = req.params;
|
||||
|
||||
await DeleteQuickAnswerService(quickAnswerId);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("quickAnswer", {
|
||||
action: "delete",
|
||||
quickAnswerId
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Quick Answer deleted" });
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import { Request, Response } from "express";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
import AuthUserService from "../services/UserServices/AuthUserService";
|
||||
import { SendRefreshToken } from "../helpers/SendRefreshToken";
|
||||
import { RefreshTokenService } from "../services/AuthServices/RefreshTokenService";
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const { token, serializedUser, refreshToken } = await AuthUserService({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
SendRefreshToken(res, refreshToken);
|
||||
|
||||
return res.status(200).json({
|
||||
token,
|
||||
user: serializedUser
|
||||
});
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const token: string = req.cookies.jrt;
|
||||
|
||||
if (!token) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
|
||||
const { user, newToken, refreshToken } = await RefreshTokenService(
|
||||
res,
|
||||
token
|
||||
);
|
||||
|
||||
SendRefreshToken(res, refreshToken);
|
||||
|
||||
return res.json({ token: newToken, user });
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
res.clearCookie("jrt");
|
||||
|
||||
return res.send();
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { Request, Response } from "express";
|
||||
|
||||
import { getIO } from "../libs/socket";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
import UpdateSettingService from "../services/SettingServices/UpdateSettingService";
|
||||
import ListSettingsService from "../services/SettingServices/ListSettingsService";
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (req.user.profile !== "admin") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
const settings = await ListSettingsService();
|
||||
|
||||
return res.status(200).json(settings);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
if (req.user.profile !== "admin") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
const { settingKey: key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
const setting = await UpdateSettingService({
|
||||
key,
|
||||
value
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("settings", {
|
||||
action: "update",
|
||||
setting
|
||||
});
|
||||
|
||||
return res.status(200).json(setting);
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
|
||||
import CreateTicketService from "../services/TicketServices/CreateTicketService";
|
||||
import DeleteTicketService from "../services/TicketServices/DeleteTicketService";
|
||||
import ListTicketsService from "../services/TicketServices/ListTicketsService";
|
||||
import ShowTicketService from "../services/TicketServices/ShowTicketService";
|
||||
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
|
||||
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
|
||||
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||
|
||||
type IndexQuery = {
|
||||
searchParam: string;
|
||||
pageNumber: string;
|
||||
status: string;
|
||||
date: string;
|
||||
showAll: string;
|
||||
withUnreadMessages: string;
|
||||
queueIds: string;
|
||||
};
|
||||
|
||||
interface TicketData {
|
||||
contactId: number;
|
||||
status: string;
|
||||
queueId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const {
|
||||
pageNumber,
|
||||
status,
|
||||
date,
|
||||
searchParam,
|
||||
showAll,
|
||||
queueIds: queueIdsStringified,
|
||||
withUnreadMessages
|
||||
} = req.query as IndexQuery;
|
||||
|
||||
const userId = req.user.id;
|
||||
|
||||
let queueIds: number[] = [];
|
||||
|
||||
if (queueIdsStringified) {
|
||||
queueIds = JSON.parse(queueIdsStringified);
|
||||
}
|
||||
|
||||
const { tickets, count, hasMore } = await ListTicketsService({
|
||||
searchParam,
|
||||
pageNumber,
|
||||
status,
|
||||
date,
|
||||
showAll,
|
||||
userId,
|
||||
queueIds,
|
||||
withUnreadMessages
|
||||
});
|
||||
|
||||
return res.status(200).json({ tickets, count, hasMore });
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { contactId, status, userId }: TicketData = req.body;
|
||||
|
||||
const ticket = await CreateTicketService({ contactId, status, userId });
|
||||
|
||||
const io = getIO();
|
||||
io.to(ticket.status).emit("ticket", {
|
||||
action: "update",
|
||||
ticket
|
||||
});
|
||||
|
||||
return res.status(200).json(ticket);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { ticketId } = req.params;
|
||||
|
||||
const contact = await ShowTicketService(ticketId);
|
||||
|
||||
return res.status(200).json(contact);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { ticketId } = req.params;
|
||||
const ticketData: TicketData = req.body;
|
||||
|
||||
const { ticket } = await UpdateTicketService({
|
||||
ticketData,
|
||||
ticketId
|
||||
});
|
||||
|
||||
if (ticket.status === "closed") {
|
||||
const whatsapp = await ShowWhatsAppService(ticket.whatsappId);
|
||||
|
||||
const { farewellMessage } = whatsapp;
|
||||
|
||||
if (farewellMessage) {
|
||||
await SendWhatsAppMessage({
|
||||
body: farewellMessage,
|
||||
ticket
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return res.status(200).json(ticket);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { ticketId } = req.params;
|
||||
|
||||
const ticket = await DeleteTicketService(ticketId);
|
||||
|
||||
const io = getIO();
|
||||
io.to(ticket.status)
|
||||
.to(ticketId)
|
||||
.to("notification")
|
||||
.emit("ticket", {
|
||||
action: "delete",
|
||||
ticketId: +ticketId
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "ticket deleted" });
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
|
||||
import CheckSettingsHelper from "../helpers/CheckSettings";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
import CreateUserService from "../services/UserServices/CreateUserService";
|
||||
import ListUsersService from "../services/UserServices/ListUsersService";
|
||||
import UpdateUserService from "../services/UserServices/UpdateUserService";
|
||||
import ShowUserService from "../services/UserServices/ShowUserService";
|
||||
import DeleteUserService from "../services/UserServices/DeleteUserService";
|
||||
|
||||
type IndexQuery = {
|
||||
searchParam: string;
|
||||
pageNumber: string;
|
||||
};
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||
|
||||
const { users, count, hasMore } = await ListUsersService({
|
||||
searchParam,
|
||||
pageNumber
|
||||
});
|
||||
|
||||
return res.json({ users, count, hasMore });
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { email, password, name, profile, queueIds } = req.body;
|
||||
|
||||
if (
|
||||
req.url === "/signup" &&
|
||||
(await CheckSettingsHelper("userCreation")) === "disabled"
|
||||
) {
|
||||
throw new AppError("ERR_USER_CREATION_DISABLED", 403);
|
||||
} else if (req.url !== "/signup" && req.user.profile !== "admin") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
const user = await CreateUserService({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
profile,
|
||||
queueIds
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("user", {
|
||||
action: "create",
|
||||
user
|
||||
});
|
||||
|
||||
return res.status(200).json(user);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { userId } = req.params;
|
||||
|
||||
const user = await ShowUserService(userId);
|
||||
|
||||
return res.status(200).json(user);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
if (req.user.profile !== "admin") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
const userData = req.body;
|
||||
|
||||
const user = await UpdateUserService({ userData, userId });
|
||||
|
||||
const io = getIO();
|
||||
io.emit("user", {
|
||||
action: "update",
|
||||
user
|
||||
});
|
||||
|
||||
return res.status(200).json(user);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { userId } = req.params;
|
||||
|
||||
if (req.user.profile !== "admin") {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
await DeleteUserService(userId);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("user", {
|
||||
action: "delete",
|
||||
userId
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "User deleted" });
|
||||
};
|
|
@ -0,0 +1,116 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getIO } from "../libs/socket";
|
||||
import { removeWbot } from "../libs/wbot";
|
||||
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
|
||||
|
||||
import CreateWhatsAppService from "../services/WhatsappService/CreateWhatsAppService";
|
||||
import DeleteWhatsAppService from "../services/WhatsappService/DeleteWhatsAppService";
|
||||
import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService";
|
||||
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
|
||||
|
||||
interface WhatsappData {
|
||||
name: string;
|
||||
queueIds: number[];
|
||||
greetingMessage?: string;
|
||||
farewellMessage?: string;
|
||||
status?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||
const whatsapps = await ListWhatsAppsService();
|
||||
|
||||
return res.status(200).json(whatsapps);
|
||||
};
|
||||
|
||||
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const {
|
||||
name,
|
||||
status,
|
||||
isDefault,
|
||||
greetingMessage,
|
||||
farewellMessage,
|
||||
queueIds
|
||||
}: WhatsappData = req.body;
|
||||
|
||||
const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({
|
||||
name,
|
||||
status,
|
||||
isDefault,
|
||||
greetingMessage,
|
||||
farewellMessage,
|
||||
queueIds
|
||||
});
|
||||
|
||||
StartWhatsAppSession(whatsapp);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("whatsapp", {
|
||||
action: "update",
|
||||
whatsapp
|
||||
});
|
||||
|
||||
if (oldDefaultWhatsapp) {
|
||||
io.emit("whatsapp", {
|
||||
action: "update",
|
||||
whatsapp: oldDefaultWhatsapp
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json(whatsapp);
|
||||
};
|
||||
|
||||
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
|
||||
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||
|
||||
return res.status(200).json(whatsapp);
|
||||
};
|
||||
|
||||
export const update = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
const whatsappData = req.body;
|
||||
|
||||
const { whatsapp, oldDefaultWhatsapp } = await UpdateWhatsAppService({
|
||||
whatsappData,
|
||||
whatsappId
|
||||
});
|
||||
|
||||
const io = getIO();
|
||||
io.emit("whatsapp", {
|
||||
action: "update",
|
||||
whatsapp
|
||||
});
|
||||
|
||||
if (oldDefaultWhatsapp) {
|
||||
io.emit("whatsapp", {
|
||||
action: "update",
|
||||
whatsapp: oldDefaultWhatsapp
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json(whatsapp);
|
||||
};
|
||||
|
||||
export const remove = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
|
||||
await DeleteWhatsAppService(whatsappId);
|
||||
removeWbot(+whatsappId);
|
||||
|
||||
const io = getIO();
|
||||
io.emit("whatsapp", {
|
||||
action: "delete",
|
||||
whatsappId: +whatsappId
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Whatsapp deleted." });
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import { Request, Response } from "express";
|
||||
import { getWbot } from "../libs/wbot";
|
||||
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
|
||||
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
|
||||
|
||||
const store = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||
|
||||
StartWhatsAppSession(whatsapp);
|
||||
|
||||
return res.status(200).json({ message: "Starting session." });
|
||||
};
|
||||
|
||||
const update = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
|
||||
const { whatsapp } = await UpdateWhatsAppService({
|
||||
whatsappId,
|
||||
whatsappData: { session: "" }
|
||||
});
|
||||
|
||||
StartWhatsAppSession(whatsapp);
|
||||
|
||||
return res.status(200).json({ message: "Starting session." });
|
||||
};
|
||||
|
||||
const remove = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { whatsappId } = req.params;
|
||||
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||
|
||||
const wbot = getWbot(whatsapp.id);
|
||||
|
||||
wbot.logout();
|
||||
|
||||
return res.status(200).json({ message: "Session disconnected." });
|
||||
};
|
||||
|
||||
export default { store, remove, update };
|
|
@ -0,0 +1,36 @@
|
|||
import { Sequelize } from "sequelize-typescript";
|
||||
import User from "../models/User";
|
||||
import Setting from "../models/Setting";
|
||||
import Contact from "../models/Contact";
|
||||
import Ticket from "../models/Ticket";
|
||||
import Whatsapp from "../models/Whatsapp";
|
||||
import ContactCustomField from "../models/ContactCustomField";
|
||||
import Message from "../models/Message";
|
||||
import Queue from "../models/Queue";
|
||||
import WhatsappQueue from "../models/WhatsappQueue";
|
||||
import UserQueue from "../models/UserQueue";
|
||||
import QuickAnswer from "../models/QuickAnswer";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const dbConfig = require("../config/database");
|
||||
// import dbConfig from "../config/database";
|
||||
|
||||
const sequelize = new Sequelize(dbConfig);
|
||||
|
||||
const models = [
|
||||
User,
|
||||
Contact,
|
||||
Ticket,
|
||||
Message,
|
||||
Whatsapp,
|
||||
ContactCustomField,
|
||||
Setting,
|
||||
Queue,
|
||||
WhatsappQueue,
|
||||
UserQueue,
|
||||
QuickAnswer
|
||||
];
|
||||
|
||||
sequelize.addModels(models);
|
||||
|
||||
export default sequelize;
|
|
@ -0,0 +1,39 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Users", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
passwordHash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Users");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Contacts", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
number: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
profilePicUrl: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Contacts");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Tickets", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: "pending",
|
||||
allowNull: false
|
||||
},
|
||||
lastMessage: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
contactId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Contacts", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Users", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE(6),
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE(6),
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Tickets");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Messages", {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
body: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
ack: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
read: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
mediaType: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
mediaUrl: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Users", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
},
|
||||
ticketId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Tickets", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE",
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE(6),
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE(6),
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Messages");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Whatsapps", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
session: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
qrcode: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
battery: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
plugged: {
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Whatsapps");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("ContactCustomFields", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
contactId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Contacts", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE",
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("ContactCustomFields");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Contacts", "email", {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: ""
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Contacts", "email");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "userId");
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "userId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Users", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "fromMe", {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "fromMe");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.changeColumn("Tickets", "lastMessage", {
|
||||
type: DataTypes.TEXT
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.changeColumn("Tickets", "lastMessage", {
|
||||
type: DataTypes.STRING
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Users", "profile", {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: "admin"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Users", "profile");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Settings", {
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Settings");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Whatsapps", "name", {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Whatsapps", "name");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Whatsapps", "default", {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Whatsapps", "default");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Tickets", "whatsappId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Whatsapps", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Tickets", "whatsappId");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.renameColumn("Whatsapps", "default", "isDefault");
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.renameColumn("Whatsapps", "isDefault", "default");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "isDeleted", {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "isDeleted");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Users", "tokenVersion", {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Users", "tokenVersion");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Tickets", "isGroup", {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Tickets", "isGroup");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Contacts", "isGroup", {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Contacts", "isGroup");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "contactId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Contacts", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "contactId");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "vcardContactId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Contacts", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "vcardContactId");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "vcardContactId");
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "vcardContactId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Contacts", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Whatsapps", "retries", {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Whatsapps", "retries");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Messages", "quotedMsgId", {
|
||||
type: DataTypes.STRING,
|
||||
references: { model: "Messages", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Messages", "quotedMsgId");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Tickets", "unreadMessages", {
|
||||
type: DataTypes.INTEGER
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Tickets", "unreadMessages");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("Queues", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
greetingMessage: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("Queues");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Tickets", "queueId", {
|
||||
type: DataTypes.INTEGER,
|
||||
references: { model: "Queues", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Tickets", "queueId");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("WhatsappQueues", {
|
||||
whatsappId: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true
|
||||
},
|
||||
queueId: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("WhatsappQueues");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("UserQueues", {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true
|
||||
},
|
||||
queueId: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("UserQueues");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Whatsapps", "greetingMessage", {
|
||||
type: DataTypes.TEXT
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Whatsapps", "greetingMessage");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.createTable("QuickAnswers", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
shortcut: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("QuickAnswers");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { QueryInterface, DataTypes } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.addColumn("Whatsapps", "farewellMessage", {
|
||||
type: DataTypes.TEXT
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.removeColumn("Whatsapps", "farewellMessage");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkInsert(
|
||||
"Settings",
|
||||
[
|
||||
{
|
||||
key: "userCreation",
|
||||
value: "enabled",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
],
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkDelete("Settings", {});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkInsert(
|
||||
"Users",
|
||||
[
|
||||
{
|
||||
name: "Administrador",
|
||||
email: "admin@whaticket.com",
|
||||
passwordHash: "$2a$08$WaEmpmFDD/XkDqorkpQ42eUZozOqRCPkPcTkmHHMyuTGUOkI8dHsq",
|
||||
profile: "admin",
|
||||
tokenVersion: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
],
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.bulkDelete("Users", {});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
class AppError {
|
||||
public readonly message: string;
|
||||
|
||||
public readonly statusCode: number;
|
||||
|
||||
constructor(message: string, statusCode = 400) {
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppError;
|
|
@ -0,0 +1,15 @@
|
|||
import { Op } from "sequelize";
|
||||
import AppError from "../errors/AppError";
|
||||
import Ticket from "../models/Ticket";
|
||||
|
||||
const CheckContactOpenTickets = async (contactId: number): Promise<void> => {
|
||||
const ticket = await Ticket.findOne({
|
||||
where: { contactId, status: { [Op.or]: ["open", "pending"] } }
|
||||
});
|
||||
|
||||
if (ticket) {
|
||||
throw new AppError("ERR_OTHER_OPEN_TICKET");
|
||||
}
|
||||
};
|
||||
|
||||
export default CheckContactOpenTickets;
|
|
@ -0,0 +1,16 @@
|
|||
import Setting from "../models/Setting";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
const CheckSettings = async (key: string): Promise<string> => {
|
||||
const setting = await Setting.findOne({
|
||||
where: { key }
|
||||
});
|
||||
|
||||
if (!setting) {
|
||||
throw new AppError("ERR_NO_SETTING_FOUND", 404);
|
||||
}
|
||||
|
||||
return setting.value;
|
||||
};
|
||||
|
||||
export default CheckSettings;
|
|
@ -0,0 +1,23 @@
|
|||
import { sign } from "jsonwebtoken";
|
||||
import authConfig from "../config/auth";
|
||||
import User from "../models/User";
|
||||
|
||||
export const createAccessToken = (user: User): string => {
|
||||
const { secret, expiresIn } = authConfig;
|
||||
|
||||
return sign(
|
||||
{ usarname: user.name, profile: user.profile, id: user.id },
|
||||
secret,
|
||||
{
|
||||
expiresIn
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const createRefreshToken = (user: User): string => {
|
||||
const { refreshSecret, refreshExpiresIn } = authConfig;
|
||||
|
||||
return sign({ id: user.id, tokenVersion: user.tokenVersion }, refreshSecret, {
|
||||
expiresIn: refreshExpiresIn
|
||||
});
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
interface Timeout {
|
||||
id: number;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
const timeouts: Timeout[] = [];
|
||||
|
||||
const findAndClearTimeout = (ticketId: number) => {
|
||||
if (timeouts.length > 0) {
|
||||
const timeoutIndex = timeouts.findIndex(timeout => timeout.id === ticketId);
|
||||
|
||||
if (timeoutIndex !== -1) {
|
||||
clearTimeout(timeouts[timeoutIndex].timeout);
|
||||
timeouts.splice(timeoutIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const debounce = (
|
||||
func: { (): Promise<void>; (...args: never[]): void },
|
||||
wait: number,
|
||||
ticketId: number
|
||||
) => {
|
||||
return function executedFunction(...args: never[]): void {
|
||||
const later = () => {
|
||||
findAndClearTimeout(ticketId);
|
||||
func(...args);
|
||||
};
|
||||
|
||||
findAndClearTimeout(ticketId);
|
||||
|
||||
const newTimeout = {
|
||||
id: ticketId,
|
||||
timeout: setTimeout(later, wait)
|
||||
};
|
||||
|
||||
timeouts.push(newTimeout);
|
||||
};
|
||||
};
|
||||
|
||||
export { debounce };
|
|
@ -0,0 +1,16 @@
|
|||
import AppError from "../errors/AppError";
|
||||
import Whatsapp from "../models/Whatsapp";
|
||||
|
||||
const GetDefaultWhatsApp = async (): Promise<Whatsapp> => {
|
||||
const defaultWhatsapp = await Whatsapp.findOne({
|
||||
where: { isDefault: true }
|
||||
});
|
||||
|
||||
if (!defaultWhatsapp) {
|
||||
throw new AppError("ERR_NO_DEF_WAPP_FOUND");
|
||||
}
|
||||
|
||||
return defaultWhatsapp;
|
||||
};
|
||||
|
||||
export default GetDefaultWhatsApp;
|
|
@ -0,0 +1,18 @@
|
|||
import { Client as Session } from "whatsapp-web.js";
|
||||
import { getWbot } from "../libs/wbot";
|
||||
import GetDefaultWhatsApp from "./GetDefaultWhatsApp";
|
||||
import Ticket from "../models/Ticket";
|
||||
|
||||
const GetTicketWbot = async (ticket: Ticket): Promise<Session> => {
|
||||
if (!ticket.whatsappId) {
|
||||
const defaultWhatsapp = await GetDefaultWhatsApp();
|
||||
|
||||
await ticket.$set("whatsapp", defaultWhatsapp);
|
||||
}
|
||||
|
||||
const wbot = getWbot(ticket.whatsappId);
|
||||
|
||||
return wbot;
|
||||
};
|
||||
|
||||
export default GetTicketWbot;
|
|
@ -0,0 +1,44 @@
|
|||
import { Message as WbotMessage } from "whatsapp-web.js";
|
||||
import Ticket from "../models/Ticket";
|
||||
import GetTicketWbot from "./GetTicketWbot";
|
||||
import AppError from "../errors/AppError";
|
||||
|
||||
export const GetWbotMessage = async (
|
||||
ticket: Ticket,
|
||||
messageId: string
|
||||
): Promise<WbotMessage> => {
|
||||
const wbot = await GetTicketWbot(ticket);
|
||||
|
||||
const wbotChat = await wbot.getChatById(
|
||||
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`
|
||||
);
|
||||
|
||||
let limit = 20;
|
||||
|
||||
const fetchWbotMessagesGradually = async (): Promise<void | WbotMessage> => {
|
||||
const chatMessages = await wbotChat.fetchMessages({ limit });
|
||||
|
||||
const msgFound = chatMessages.find(msg => msg.id.id === messageId);
|
||||
|
||||
if (!msgFound && limit < 100) {
|
||||
limit += 20;
|
||||
return fetchWbotMessagesGradually();
|
||||
}
|
||||
|
||||
return msgFound;
|
||||
};
|
||||
|
||||
try {
|
||||
const msgFound = await fetchWbotMessagesGradually();
|
||||
|
||||
if (!msgFound) {
|
||||
throw new Error("Cannot found message within 100 last messages");
|
||||
}
|
||||
|
||||
return msgFound;
|
||||
} catch (err) {
|
||||
throw new AppError("ERR_FETCH_WAPP_MSG");
|
||||
}
|
||||
};
|
||||
|
||||
export default GetWbotMessage;
|
|
@ -0,0 +1,5 @@
|
|||
import { Response } from "express";
|
||||
|
||||
export const SendRefreshToken = (res: Response, token: string): void => {
|
||||
res.cookie("jrt", token, { httpOnly: true });
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import Queue from "../models/Queue";
|
||||
import User from "../models/User";
|
||||
|
||||
interface SerializedUser {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
profile: string;
|
||||
queues: Queue[];
|
||||
}
|
||||
|
||||
export const SerializeUser = (user: User): SerializedUser => {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profile: user.profile,
|
||||
queues: user.queues
|
||||
};
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import Message from "../models/Message";
|
||||
import Ticket from "../models/Ticket";
|
||||
|
||||
const SerializeWbotMsgId = (ticket: Ticket, message: Message): string => {
|
||||
const serializedMsgId = `${message.fromMe}_${ticket.contact.number}@${
|
||||
ticket.isGroup ? "g" : "c"
|
||||
}.us_${message.id}`;
|
||||
|
||||
return serializedMsgId;
|
||||
};
|
||||
|
||||
export default SerializeWbotMsgId;
|
|
@ -0,0 +1,38 @@
|
|||
import { getIO } from "../libs/socket";
|
||||
import Message from "../models/Message";
|
||||
import Ticket from "../models/Ticket";
|
||||
import { logger } from "../utils/logger";
|
||||
import GetTicketWbot from "./GetTicketWbot";
|
||||
|
||||
const SetTicketMessagesAsRead = async (ticket: Ticket): Promise<void> => {
|
||||
await Message.update(
|
||||
{ read: true },
|
||||
{
|
||||
where: {
|
||||
ticketId: ticket.id,
|
||||
read: false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await ticket.update({ unreadMessages: 0 });
|
||||
|
||||
try {
|
||||
const wbot = await GetTicketWbot(ticket);
|
||||
await wbot.sendSeen(
|
||||
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Could not mark messages as read. Maybe whatsapp session disconnected? Err: ${err}`
|
||||
);
|
||||
}
|
||||
|
||||
const io = getIO();
|
||||
io.to(ticket.status).to("notification").emit("ticket", {
|
||||
action: "updateUnread",
|
||||
ticketId: ticket.id
|
||||
});
|
||||
};
|
||||
|
||||
export default SetTicketMessagesAsRead;
|
|
@ -0,0 +1,17 @@
|
|||
import Ticket from "../models/Ticket";
|
||||
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
|
||||
|
||||
const UpdateDeletedUserOpenTicketsStatus = async (
|
||||
tickets: Ticket[]
|
||||
): Promise<void> => {
|
||||
tickets.forEach(async t => {
|
||||
const ticketId = t.id.toString();
|
||||
|
||||
await UpdateTicketService({
|
||||
ticketData: { status: "pending" },
|
||||
ticketId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default UpdateDeletedUserOpenTicketsStatus;
|
|
@ -0,0 +1,44 @@
|
|||
import { Server as SocketIO } from "socket.io";
|
||||
import { Server } from "http";
|
||||
import AppError from "../errors/AppError";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
let io: SocketIO;
|
||||
|
||||
export const initIO = (httpServer: Server): SocketIO => {
|
||||
io = new SocketIO(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL
|
||||
}
|
||||
});
|
||||
|
||||
io.on("connection", socket => {
|
||||
logger.info("Client Connected");
|
||||
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", () => {
|
||||
logger.info("Client disconnected");
|
||||
});
|
||||
});
|
||||
return io;
|
||||
};
|
||||
|
||||
export const getIO = (): SocketIO => {
|
||||
if (!io) {
|
||||
throw new AppError("Socket IO not initialized");
|
||||
}
|
||||
return io;
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
import qrCode from "qrcode-terminal";
|
||||
import { Client } from "whatsapp-web.js";
|
||||
import { getIO } from "./socket";
|
||||
import Whatsapp from "../models/Whatsapp";
|
||||
import AppError from "../errors/AppError";
|
||||
import { logger } from "../utils/logger";
|
||||
import { handleMessage } from "../services/WbotServices/wbotMessageListener";
|
||||
|
||||
interface Session extends Client {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
const sessions: Session[] = [];
|
||||
|
||||
const syncUnreadMessages = async (wbot: Session) => {
|
||||
const chats = await wbot.getChats();
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const chat of chats) {
|
||||
if (chat.unreadCount > 0) {
|
||||
const unreadMessages = await chat.fetchMessages({
|
||||
limit: chat.unreadCount
|
||||
});
|
||||
|
||||
for (const msg of unreadMessages) {
|
||||
await handleMessage(msg, wbot);
|
||||
}
|
||||
|
||||
await chat.sendSeen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const io = getIO();
|
||||
const sessionName = whatsapp.name;
|
||||
let sessionCfg;
|
||||
|
||||
if (whatsapp && whatsapp.session) {
|
||||
sessionCfg = JSON.parse(whatsapp.session);
|
||||
}
|
||||
|
||||
const wbot: Session = new Client({
|
||||
session: sessionCfg,
|
||||
puppeteer: {
|
||||
executablePath: process.env.CHROME_BIN || undefined
|
||||
}
|
||||
});
|
||||
|
||||
wbot.initialize();
|
||||
|
||||
wbot.on("qr", async qr => {
|
||||
logger.info("Session:", sessionName);
|
||||
qrCode.generate(qr, { small: true });
|
||||
await whatsapp.update({ qrcode: qr, status: "qrcode", retries: 0 });
|
||||
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
|
||||
if (sessionIndex === -1) {
|
||||
wbot.id = whatsapp.id;
|
||||
sessions.push(wbot);
|
||||
}
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
});
|
||||
|
||||
wbot.on("authenticated", async session => {
|
||||
logger.info(`Session: ${sessionName} AUTHENTICATED`);
|
||||
await whatsapp.update({
|
||||
session: JSON.stringify(session)
|
||||
});
|
||||
});
|
||||
|
||||
wbot.on("auth_failure", async msg => {
|
||||
console.error(
|
||||
`Session: ${sessionName} AUTHENTICATION FAILURE! Reason: ${msg}`
|
||||
);
|
||||
|
||||
if (whatsapp.retries > 1) {
|
||||
await whatsapp.update({ session: "", retries: 0 });
|
||||
}
|
||||
|
||||
const retry = whatsapp.retries;
|
||||
await whatsapp.update({
|
||||
status: "DISCONNECTED",
|
||||
retries: retry + 1
|
||||
});
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
|
||||
reject(new Error("Error starting whatsapp session."));
|
||||
});
|
||||
|
||||
wbot.on("ready", async () => {
|
||||
logger.info(`Session: ${sessionName} READY`);
|
||||
|
||||
await whatsapp.update({
|
||||
status: "CONNECTED",
|
||||
qrcode: "",
|
||||
retries: 0
|
||||
});
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
|
||||
if (sessionIndex === -1) {
|
||||
wbot.id = whatsapp.id;
|
||||
sessions.push(wbot);
|
||||
}
|
||||
|
||||
wbot.sendPresenceAvailable();
|
||||
await syncUnreadMessages(wbot);
|
||||
|
||||
resolve(wbot);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getWbot = (whatsappId: number): Session => {
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
|
||||
|
||||
if (sessionIndex === -1) {
|
||||
throw new AppError("ERR_WAPP_NOT_INITIALIZED");
|
||||
}
|
||||
return sessions[sessionIndex];
|
||||
};
|
||||
|
||||
export const removeWbot = (whatsappId: number): void => {
|
||||
try {
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
|
||||
if (sessionIndex !== -1) {
|
||||
sessions[sessionIndex].destroy();
|
||||
sessions.splice(sessionIndex, 1);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { verify } from "jsonwebtoken";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import AppError from "../errors/AppError";
|
||||
import authConfig from "../config/auth";
|
||||
|
||||
interface TokenPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
profile: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
const isAuth = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
|
||||
const [, token] = authHeader.split(" ");
|
||||
|
||||
try {
|
||||
const decoded = verify(token, authConfig.secret);
|
||||
const { id, profile } = decoded as TokenPayload;
|
||||
|
||||
req.user = {
|
||||
id,
|
||||
profile
|
||||
};
|
||||
} catch (err) {
|
||||
throw new AppError(
|
||||
"Invalid token. We'll try to assign a new one on next request",
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export default isAuth;
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
AllowNull,
|
||||
Unique,
|
||||
Default,
|
||||
HasMany
|
||||
} from "sequelize-typescript";
|
||||
import ContactCustomField from "./ContactCustomField";
|
||||
import Ticket from "./Ticket";
|
||||
|
||||
@Table
|
||||
class Contact extends Model<Contact> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
number: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Default("")
|
||||
@Column
|
||||
email: string;
|
||||
|
||||
@Column
|
||||
profilePicUrl: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isGroup: boolean;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@HasMany(() => Ticket)
|
||||
tickets: Ticket[];
|
||||
|
||||
@HasMany(() => ContactCustomField)
|
||||
extraInfo: ContactCustomField[];
|
||||
}
|
||||
|
||||
export default Contact;
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
ForeignKey,
|
||||
BelongsTo
|
||||
} from "sequelize-typescript";
|
||||
import Contact from "./Contact";
|
||||
|
||||
@Table
|
||||
class ContactCustomField extends Model<ContactCustomField> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@Column
|
||||
value: string;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact)
|
||||
contact: Contact;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default ContactCustomField;
|
|
@ -0,0 +1,84 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
DataType,
|
||||
PrimaryKey,
|
||||
Default,
|
||||
BelongsTo,
|
||||
ForeignKey
|
||||
} from "sequelize-typescript";
|
||||
import Contact from "./Contact";
|
||||
import Ticket from "./Ticket";
|
||||
|
||||
@Table
|
||||
class Message extends Model<Message> {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
id: string;
|
||||
|
||||
@Default(0)
|
||||
@Column
|
||||
ack: number;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
read: boolean;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
fromMe: boolean;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
body: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
get mediaUrl(): string | null {
|
||||
if (this.getDataValue("mediaUrl")) {
|
||||
return `${process.env.BACKEND_URL}:${
|
||||
process.env.PROXY_PORT
|
||||
}/public/${this.getDataValue("mediaUrl")}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Column
|
||||
mediaType: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE(6))
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE(6))
|
||||
updatedAt: Date;
|
||||
|
||||
@ForeignKey(() => Message)
|
||||
@Column
|
||||
quotedMsgId: string;
|
||||
|
||||
@BelongsTo(() => Message, "quotedMsgId")
|
||||
quotedMsg: Message;
|
||||
|
||||
@ForeignKey(() => Ticket)
|
||||
@Column
|
||||
ticketId: number;
|
||||
|
||||
@BelongsTo(() => Ticket)
|
||||
ticket: Ticket;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact, "contactId")
|
||||
contact: Contact;
|
||||
}
|
||||
|
||||
export default Message;
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
AllowNull,
|
||||
Unique,
|
||||
BelongsToMany
|
||||
} from "sequelize-typescript";
|
||||
import User from "./User";
|
||||
import UserQueue from "./UserQueue";
|
||||
|
||||
import Whatsapp from "./Whatsapp";
|
||||
import WhatsappQueue from "./WhatsappQueue";
|
||||
|
||||
@Table
|
||||
class Queue extends Model<Queue> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
color: string;
|
||||
|
||||
@Column
|
||||
greetingMessage: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@BelongsToMany(() => Whatsapp, () => WhatsappQueue)
|
||||
whatsapps: Array<Whatsapp & { WhatsappQueue: WhatsappQueue }>;
|
||||
|
||||
@BelongsToMany(() => User, () => UserQueue)
|
||||
users: Array<User & { UserQueue: UserQueue }>;
|
||||
}
|
||||
|
||||
export default Queue;
|
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
DataType,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement
|
||||
} from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
class QuickAnswer extends Model<QuickAnswer> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
shortcut: string;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
message: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default QuickAnswer;
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey
|
||||
} from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
class Setting extends Model<Setting> {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
key: string;
|
||||
|
||||
@Column
|
||||
value: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default Setting;
|
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
ForeignKey,
|
||||
BelongsTo,
|
||||
HasMany,
|
||||
AutoIncrement,
|
||||
Default
|
||||
} from "sequelize-typescript";
|
||||
|
||||
import Contact from "./Contact";
|
||||
import Message from "./Message";
|
||||
import Queue from "./Queue";
|
||||
import User from "./User";
|
||||
import Whatsapp from "./Whatsapp";
|
||||
|
||||
@Table
|
||||
class Ticket extends Model<Ticket> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column({ defaultValue: "pending" })
|
||||
status: string;
|
||||
|
||||
@Column
|
||||
unreadMessages: number;
|
||||
|
||||
@Column
|
||||
lastMessage: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isGroup: boolean;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId: number;
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user: User;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact)
|
||||
contact: Contact;
|
||||
|
||||
@ForeignKey(() => Whatsapp)
|
||||
@Column
|
||||
whatsappId: number;
|
||||
|
||||
@BelongsTo(() => Whatsapp)
|
||||
whatsapp: Whatsapp;
|
||||
|
||||
@ForeignKey(() => Queue)
|
||||
@Column
|
||||
queueId: number;
|
||||
|
||||
@BelongsTo(() => Queue)
|
||||
queue: Queue;
|
||||
|
||||
@HasMany(() => Message)
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export default Ticket;
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
DataType,
|
||||
BeforeCreate,
|
||||
BeforeUpdate,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
Default,
|
||||
HasMany,
|
||||
BelongsToMany
|
||||
} from "sequelize-typescript";
|
||||
import { hash, compare } from "bcryptjs";
|
||||
import Ticket from "./Ticket";
|
||||
import Queue from "./Queue";
|
||||
import UserQueue from "./UserQueue";
|
||||
|
||||
@Table
|
||||
class User extends Model<User> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@Column
|
||||
email: string;
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
password: string;
|
||||
|
||||
@Column
|
||||
passwordHash: string;
|
||||
|
||||
@Default(0)
|
||||
@Column
|
||||
tokenVersion: number;
|
||||
|
||||
@Default("admin")
|
||||
@Column
|
||||
profile: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@HasMany(() => Ticket)
|
||||
tickets: Ticket[];
|
||||
|
||||
@BelongsToMany(() => Queue, () => UserQueue)
|
||||
queues: Queue[];
|
||||
|
||||
@BeforeUpdate
|
||||
@BeforeCreate
|
||||
static hashPassword = async (instance: User): Promise<void> => {
|
||||
if (instance.password) {
|
||||
instance.passwordHash = await hash(instance.password, 8);
|
||||
}
|
||||
};
|
||||
|
||||
public checkPassword = async (password: string): Promise<boolean> => {
|
||||
return compare(password, this.getDataValue("passwordHash"));
|
||||
};
|
||||
}
|
||||
|
||||
export default User;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue