diff --git a/README.md b/README.md
index bcf2b33..7f81173 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ 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 messagees.
+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.
@@ -22,13 +22,22 @@ If a contact sent a new message in less than 2 hours interval, and there is no t
## Screenshots
-
+
+
+## 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) ✅
## Demo
-**Note**: It's not a good idea to sync your whatsapp account is this demo enviroment, because all your received messages will be stored in database and will be accessible by everyone that access this URL and creates an account.
+**Note**: It's not a good idea to sync your WhatsApp account is this demo enviroment, because all your received messages will be stored in database and will be accessible by everyone that access this URL and creates an account.
-That said, theres not much to test without syncing an whatsapp account, since adding contacts or tickets simple throws an error if app is not synced with whatassp. I will create a better test enviroment in future.
+That said, theres not much to test without syncing an WhatsApp account, since adding contacts or tickets simple throws an error if app is not synced with WhatsApp.
Meanwhile, if you want to test it, remember to disconnect session and delete all tickets and contacts after your tests.
@@ -38,12 +47,12 @@ email: demo@demo.com
password: demo123
-It's online thanks to [@ramphy](https://github.com/ramphy), who provided a VPS for me to create these installation instructions.
+It's online thanks to [@ramphy](https://github.com/ramphy), who provided a VPS to creation of these installation instructions.
## Installation and Usage (Linux Ubuntu - Development)
Create Mysql Database using docker:
-_Note_: change dbname, username password.
+_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
@@ -57,9 +66,10 @@ sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service l
- Clone this repo
- On backend folder:
- - Copy .env.example to .env and fill it with database details
+ - Copy .env.example to .env and fill it with database details.
- Install dependecies: `npm install` (Only in the first time)
- Create database tables: `npx sequelize db:migrate` (Only in the first time)
+ - Fill database with initial values: `npx sequelize db:seed:all`
- Start backend: `npm start`
- In another terminal, on frontend folder:
- Copy .env.example to .env and fill it with backend URL (normally localhost:port)
@@ -67,12 +77,13 @@ sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service l
- Start frontend: `npm start`
- Go to http://your_server_ip:3000/signup
- Create an user and login with it.
-- On the sidebard, go to _Connection_ and read QRCode with your WhatsApp.
+- On the sidebard, go to _Connections_ 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)
-You need two subdomains forwarding to youts VPS ip to follow these instructions. We'll use `myapp.mydomain.com` and `api.mydomain.com` in examples.
+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. We'll also use an dedicated user with sudo privileges no deploy it (not root).
Update all system packages:
@@ -89,7 +100,7 @@ node -v
npm -v
```
-Install docker:
+Install docker and add you user to docker group:
```bash
sudo apt install apt-transport-https ca-certificates curl software-properties-common
@@ -98,20 +109,21 @@ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubun
sudo apt update
sudo apt install docker-ce
sudo systemctl status docker
-```
-
-Add current user to docker group:
-
-```bash
sudo usermod -aG docker \${USER}
su - \${USER}
```
-Create database container (Instructions in installation)
+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
```
@@ -124,44 +136,44 @@ nano whaticket/backend/.env
```bash
NODE_ENV=
-BACKEND_URL=https://api.mydomain.com
-PROXY_PORT=443
+BACKEND_URL=https://api.mydomain.com #USE HTTPS HERE, WE WILL ADD SSL LATTER
+PROXY_PORT=443 #USE NGINX REVERSE PROXY PORT HERE, WE WILL CONFIGURE IT LATTER
PORT=8080
-DB_HOST=
+DB_HOST=localhost
DB_USER=
DB_PASS=
DB_NAME=
```
-Install puppeteer dependencies(Instructions in installation)
+Install puppeteer dependencies:
-Install backend dependencies and run migrations:
+```bash
+sudo apt-get install -y 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, run migrations and seeds:
```bash
cd whaticket/backend
npm install
npx sequelize db:migrate
+npx sequelize db:seed:all
```
-Start backend to confirm its working, you should see: `Server started on port...` on console.
+Start it with `npm start`, you should see: `Server started on port...` on console. Hit `CTRL + C` to exit.
-Install pm2 **with sudo**:
+Install pm2 **with sudo**, and start backend with it:
```bash
sudo npm install -g pm2
-```
-
-Start backend with pm2:
-
-```bash
pm2 start src/app.js --name whaticket-backend
```
Make pm2 auto start afeter reboot:
```bash
-pm2 startup ubuntu -u YOUR_USERNAME
+pm2 startup ubuntu -u `YOUR_USERNAME`
```
Copy the last line outputed from previus command and run it, its something like:
@@ -170,10 +182,10 @@ Copy the last line outputed from previus command and run it, its something like:
sudo env PATH=\$PATH:/usr/bin pm2 startup ubuntu -u YOUR_USERNAME --hp /home/YOUR_USERNAM
```
-Now, lets prepare frontend:
+Go to frontend folder and install dependencies:
```bash
-cd ../whaticket/frontend
+cd ../frontend
npm install
```
@@ -189,16 +201,24 @@ Build frontend app:
npm run build
```
-Start frotend with pm2:
+Start frontend with pm2, and save pm2 process list to start automatically after reboot:
```bash
pm2 start server.js --name whaticket-frontend
+pm2 save
```
-Save pm2 process list to start automatically after reboot:
+To check if it's running, run `pm2 list`, it should look like:
```bash
-pm2 save
+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:
@@ -211,7 +231,6 @@ Remove nginx default site:
```bash
sudo rm /etc/nginx/sites-enabled/default
-sudo service nginx restart
```
Create a new nginx site to frontend app:
@@ -240,7 +259,7 @@ server {
}
```
-Create another one to backend api, changing `server_name` to yours equivalent to `api.mydomain.com`, and `proxy_pass` to you localhost backend node server URL:
+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
@@ -257,7 +276,7 @@ server {
}
```
-Create a symbolic link to enalbe nginx site:
+Create a symbolic links to enalbe nginx sites:
```bash
sudo ln -s /etc/nginx/sites-available/whaticket-frontend /etc/nginx/sites-enabled
@@ -285,14 +304,6 @@ Enable SSL on nginx (Accept all information asked):
sudo certbot --nginx
```
-## Features
-
-- Have multiple users chating in same WhatsApp Number ✅
-- Create and chat with new contacts without touching cellphone ✅
-- Send and receive message ✅
-- Send media (images/audio/documents) ✅
-- Receive media (images/audio/video/documents) ✅
-
## Contributing
Any help and suggestions are welcome!
diff --git a/backend/package.json b/backend/package.json
index 9bc51aa..cfe437b 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -22,7 +22,6 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
- "express-validator": "^6.5.0",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
@@ -30,7 +29,8 @@
"sequelize": "^6.3.4",
"socket.io": "^2.3.0",
"whatsapp-web.js": "^1.8.0",
- "youch": "^2.0.10"
+ "youch": "^2.0.10",
+ "yup": "^0.29.3"
},
"devDependencies": {
"nodemon": "^2.0.4",
diff --git a/backend/src/app.js b/backend/src/app.js
index 273d65a..e1b5df1 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -7,16 +7,12 @@ const cors = require("cors");
const multer = require("multer");
const Sentry = require("@sentry/node");
-const wBot = require("./libs/wbot");
+const { initWbot } = require("./libs/wbot");
const wbotMessageListener = require("./services/wbotMessageListener");
const wbotMonitor = require("./services/wbotMonitor");
+const Whatsapp = require("./models/Whatsapp");
-const MessagesRoutes = require("./routes/messages");
-const ContactsRoutes = require("./routes/contacts");
-const AuthRoutes = require("./routes/auth");
-const TicketsRoutes = require("./routes/tickets");
-const WhatsRoutes = require("./routes/whatsapp");
-const UsersRoutes = require("./routes/users");
+const Router = require("./router");
const app = express();
@@ -40,13 +36,7 @@ app.use(cors());
app.use(express.json());
app.use(multer({ storage: fileStorage }).single("media"));
app.use("/public", express.static(path.join(__dirname, "..", "public")));
-
-app.use("/auth", AuthRoutes);
-app.use(ContactsRoutes);
-app.use(TicketsRoutes);
-app.use(MessagesRoutes);
-app.use(WhatsRoutes);
-app.use(UsersRoutes);
+app.use(Router);
const io = require("./libs/socket").init(server);
io.on("connection", socket => {
@@ -66,13 +56,21 @@ io.on("connection", socket => {
});
});
-wBot
- .init()
- .then(({ dbSession }) => {
- wbotMessageListener();
- wbotMonitor(dbSession);
- })
- .catch(err => console.log(err));
+const startWhatsAppSessions = async () => {
+ const whatsapps = await Whatsapp.findAll();
+
+ if (whatsapps.length > 0) {
+ whatsapps.forEach(whatsapp => {
+ initWbot(whatsapp)
+ .then(() => {
+ wbotMessageListener(whatsapp);
+ wbotMonitor(whatsapp);
+ })
+ .catch(err => console.log(err));
+ });
+ }
+};
+startWhatsAppSessions();
app.use(Sentry.Handlers.errorHandler());
@@ -82,6 +80,6 @@ app.use(async (err, req, res, next) => {
console.log(err);
return res.status(500).json(errors);
}
- console.log(err);
+
return res.status(500).json({ error: "Internal server error" });
});
diff --git a/backend/src/config/database.js b/backend/src/config/database.js
index 654fd21..5fae881 100644
--- a/backend/src/config/database.js
+++ b/backend/src/config/database.js
@@ -12,4 +12,5 @@ module.exports = {
username: process.env.DB_USER,
password: process.env.DB_PASS,
logging: false,
+ seederStorage: "sequelize",
};
diff --git a/backend/src/controllers/ContactController.js b/backend/src/controllers/ContactController.js
index 2fc503a..268d5a3 100644
--- a/backend/src/controllers/ContactController.js
+++ b/backend/src/controllers/ContactController.js
@@ -2,13 +2,14 @@ const Sequelize = require("sequelize");
const { Op } = require("sequelize");
const Contact = require("../models/Contact");
+const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot");
exports.index = async (req, res) => {
- const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query;
+ const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = {
[Op.or]: [
@@ -23,7 +24,7 @@ exports.index = async (req, res) => {
],
};
- let limit = +rowsPerPage;
+ let limit = 20;
let offset = limit * (pageNumber - 1);
const { count, rows: contacts } = await Contact.findAndCountAll({
@@ -33,31 +34,42 @@ exports.index = async (req, res) => {
order: [["createdAt", "DESC"]],
});
- return res.json({ contacts, count });
+ const hasMore = count > offset + contacts.length;
+
+ return res.json({ contacts, count, hasMore });
};
exports.store = async (req, res) => {
- const wbot = getWbot();
+ const defaultWhatsapp = await Whatsapp.findOne({
+ where: { default: true },
+ });
+
+ if (!defaultWhatsapp) {
+ return res
+ .status(404)
+ .json({ error: "No default WhatsApp found. Check Connection page." });
+ }
+
+ const wbot = getWbot(defaultWhatsapp);
const io = getIO();
const newContact = req.body;
- let isValidNumber;
-
try {
- isValidNumber = await wbot.isRegisteredUser(`${newContact.number}@c.us`);
+ const isValidNumber = await wbot.isRegisteredUser(
+ `${newContact.number}@c.us`
+ );
+ if (!isValidNumber) {
+ return res
+ .status(400)
+ .json({ error: "The suplied number is not a valid Whatsapp number" });
+ }
} catch (err) {
- console.log("Could not check whatsapp contact. Is session details valid?");
+ console.log(err);
return res.status(500).json({
error: "Could not check whatsapp contact. Check connection page.",
});
}
- if (!isValidNumber) {
- return res
- .status(400)
- .json({ error: "The suplied number is not a valid Whatsapp number" });
- }
-
const profilePicUrl = await wbot.getProfilePicUrl(
`${newContact.number}@c.us`
);
@@ -74,26 +86,22 @@ exports.store = async (req, res) => {
contact: contact,
});
- res.status(200).json(contact);
+ return res.status(200).json(contact);
};
exports.show = async (req, res) => {
const { contactId } = req.params;
- const { id, name, number, email, extraInfo } = await Contact.findByPk(
- contactId,
- {
- include: "extraInfo",
- }
- );
-
- res.status(200).json({
- id,
- name,
- number,
- email,
- extraInfo,
+ const contact = await Contact.findByPk(contactId, {
+ include: "extraInfo",
+ attributes: ["id", "name", "number", "email"],
});
+
+ if (!contact) {
+ return res.status(404).json({ error: "No contact found with this id." });
+ }
+
+ return res.status(200).json(contact);
};
exports.update = async (req, res) => {
@@ -108,7 +116,7 @@ exports.update = async (req, res) => {
});
if (!contact) {
- return res.status(400).json({ error: "No contact found with this ID" });
+ return res.status(404).json({ error: "No contact found with this ID" });
}
if (updatedContact.extraInfo) {
@@ -138,7 +146,7 @@ exports.update = async (req, res) => {
contact: contact,
});
- res.status(200).json(contact);
+ return res.status(200).json(contact);
};
exports.delete = async (req, res) => {
@@ -148,7 +156,7 @@ exports.delete = async (req, res) => {
const contact = await Contact.findByPk(contactId);
if (!contact) {
- return res.status(400).json({ error: "No contact found with this ID" });
+ return res.status(404).json({ error: "No contact found with this ID" });
}
await contact.destroy();
@@ -158,5 +166,5 @@ exports.delete = async (req, res) => {
contactId: contactId,
});
- res.status(200).json({ message: "Contact deleted" });
+ return res.status(200).json({ message: "Contact deleted" });
};
diff --git a/backend/src/controllers/ImportPhoneContactsController.js b/backend/src/controllers/ImportPhoneContactsController.js
index b410c08..b24ee0d 100644
--- a/backend/src/controllers/ImportPhoneContactsController.js
+++ b/backend/src/controllers/ImportPhoneContactsController.js
@@ -1,12 +1,32 @@
const Contact = require("../models/Contact");
+const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
-const { getWbot, init } = require("../libs/wbot");
+const { getWbot, initWbot } = require("../libs/wbot");
exports.store = async (req, res, next) => {
- const io = getIO();
- const wbot = getWbot();
+ const defaultWhatsapp = await Whatsapp.findOne({
+ where: { default: true },
+ });
- const phoneContacts = await wbot.getContacts();
+ if (!defaultWhatsapp) {
+ return res
+ .status(404)
+ .json({ error: "No default WhatsApp found. Check Connection page." });
+ }
+
+ const io = getIO();
+ const wbot = getWbot(defaultWhatsapp);
+
+ let phoneContacts;
+
+ try {
+ phoneContacts = await wbot.getContacts();
+ } catch (err) {
+ console.log(err);
+ return res.status(500).json({
+ error: "Could not check whatsapp contact. Check connection page.",
+ });
+ }
await Promise.all(
phoneContacts.map(async ({ number, name }) => {
diff --git a/backend/src/controllers/MessageController.js b/backend/src/controllers/MessageController.js
index 62d9afc..33a04b7 100644
--- a/backend/src/controllers/MessageController.js
+++ b/backend/src/controllers/MessageController.js
@@ -1,6 +1,7 @@
const Message = require("../models/Message");
const Contact = require("../models/Contact");
const User = require("../models/User");
+const Whatsapp = require("../models/Whatsapp");
const Ticket = require("../models/Ticket");
const { getIO } = require("../libs/socket");
@@ -11,7 +12,7 @@ const { MessageMedia } = require("whatsapp-web.js");
const setMessagesAsRead = async ticket => {
const io = getIO();
- const wbot = getWbot();
+ const wbot = getWbot(ticket.whatsappId);
await Message.update(
{ read: true },
@@ -38,9 +39,6 @@ const setMessagesAsRead = async ticket => {
};
exports.index = async (req, res, next) => {
- // const wbot = getWbot();
- // const io = getIO();
-
const { ticketId } = req.params;
const { searchParam = "", pageNumber = 1 } = req.query;
@@ -107,7 +105,6 @@ exports.index = async (req, res, next) => {
};
exports.store = async (req, res, next) => {
- const wbot = getWbot();
const io = getIO();
const { ticketId } = req.params;
@@ -125,6 +122,26 @@ exports.store = async (req, res, next) => {
],
});
+ if (!ticket) {
+ return res.status(404).json({ error: "No ticket found with this ID" });
+ }
+
+ if (!ticket.whatsappId) {
+ const defaultWhatsapp = await Whatsapp.findOne({
+ where: { default: true },
+ });
+
+ if (!defaultWhatsapp) {
+ return res
+ .status(404)
+ .json({ error: "No default WhatsApp found. Check Connection page." });
+ }
+
+ await ticket.setWhatsapp(defaultWhatsapp);
+ }
+
+ const wbot = getWbot(ticket.whatsappId);
+
try {
if (media) {
const newMedia = MessageMedia.fromFilePath(req.file.path);
@@ -177,7 +194,7 @@ exports.store = async (req, res, next) => {
await setMessagesAsRead(ticket);
- return res.json({ newMessage, ticket });
+ return res.status(200).json({ newMessage, ticket });
}
return res
diff --git a/backend/src/controllers/SessionController.js b/backend/src/controllers/SessionController.js
index f8d22ff..e9afcb0 100644
--- a/backend/src/controllers/SessionController.js
+++ b/backend/src/controllers/SessionController.js
@@ -8,7 +8,7 @@ exports.store = async (req, res, next) => {
const user = await User.findOne({ where: { email: email } });
if (!user) {
- return res.status(401).json({ error: "No user found with this email" });
+ return res.status(404).json({ error: "No user found with this email" });
}
if (!(await user.checkPassword(password))) {
@@ -23,7 +23,10 @@ exports.store = async (req, res, next) => {
}
);
- return res
- .status(200)
- .json({ token: token, username: user.name, userId: user.id });
+ return res.status(200).json({
+ token: token,
+ username: user.name,
+ profile: user.profile,
+ userId: user.id,
+ });
};
diff --git a/backend/src/controllers/SettingController.js b/backend/src/controllers/SettingController.js
new file mode 100644
index 0000000..045a0ee
--- /dev/null
+++ b/backend/src/controllers/SettingController.js
@@ -0,0 +1,39 @@
+const Setting = require("../models/Setting");
+const { getIO } = require("../libs/socket");
+
+exports.index = async (req, res) => {
+ if (req.user.profile !== "admin") {
+ return res
+ .status(403)
+ .json({ error: "Only administrators can access this route." });
+ }
+
+ const settings = await Setting.findAll();
+
+ return res.status(200).json(settings);
+};
+
+exports.update = async (req, res) => {
+ if (req.user.profile !== "admin") {
+ return res
+ .status(403)
+ .json({ error: "Only administrators can access this route." });
+ }
+
+ const io = getIO();
+ const { settingKey } = req.params;
+ const setting = await Setting.findByPk(settingKey);
+
+ if (!setting) {
+ return res.status(404).json({ error: "No setting found with this ID" });
+ }
+
+ await setting.update(req.body);
+
+ io.emit("settings", {
+ action: "update",
+ setting,
+ });
+
+ return res.status(200).json(setting);
+};
diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js
index 5ae04ab..7fe06ae 100644
--- a/backend/src/controllers/TicketController.js
+++ b/backend/src/controllers/TicketController.js
@@ -4,6 +4,7 @@ const { startOfDay, endOfDay, parseISO } = require("date-fns");
const Ticket = require("../models/Ticket");
const Contact = require("../models/Contact");
const Message = require("../models/Message");
+const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
@@ -16,7 +17,7 @@ exports.index = async (req, res) => {
showAll,
} = req.query;
- const userId = req.userId;
+ const userId = req.user.id;
const limit = 20;
const offset = limit * (pageNumber - 1);
@@ -105,12 +106,23 @@ exports.index = async (req, res) => {
const hasMore = count > offset + tickets.length;
- return res.json({ count, tickets, hasMore });
+ return res.status(200).json({ count, tickets, hasMore });
};
exports.store = async (req, res) => {
const io = getIO();
- const ticket = await Ticket.create(req.body);
+
+ const defaultWhatsapp = await Whatsapp.findOne({
+ where: { default: true },
+ });
+
+ if (!defaultWhatsapp) {
+ return res
+ .status(404)
+ .json({ error: "No default WhatsApp found. Check Connection page." });
+ }
+
+ const ticket = await defaultWhatsapp.createTicket(req.body);
const contact = await ticket.getContact();
@@ -121,7 +133,7 @@ exports.store = async (req, res) => {
ticket: serializaedTicket,
});
- res.status(200).json(ticket);
+ return res.status(200).json(ticket);
};
exports.update = async (req, res) => {
@@ -139,7 +151,7 @@ exports.update = async (req, res) => {
});
if (!ticket) {
- return res.status(400).json({ error: "No ticket found with this ID" });
+ return res.status(404).json({ error: "No ticket found with this ID" });
}
await ticket.update(req.body);
@@ -149,7 +161,7 @@ exports.update = async (req, res) => {
ticket: ticket,
});
- res.status(200).json(ticket);
+ return res.status(200).json(ticket);
};
exports.delete = async (req, res) => {
@@ -169,5 +181,5 @@ exports.delete = async (req, res) => {
ticketId: ticket.id,
});
- res.status(200).json({ message: "ticket deleted" });
+ return res.status(200).json({ message: "ticket deleted" });
};
diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js
index fbbb94b..6dc0e0f 100644
--- a/backend/src/controllers/UserController.js
+++ b/backend/src/controllers/UserController.js
@@ -1,48 +1,161 @@
-const { validationResult } = require("express-validator");
+const Sequelize = require("sequelize");
+const Yup = require("yup");
+const { Op } = require("sequelize");
const User = require("../models/User");
+const Setting = require("../models/Setting");
+
+const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
- // const { searchParam = "", pageNumber = 1 } = req.query;
+ if (req.user.profile !== "admin") {
+ return res
+ .status(403)
+ .json({ error: "Only administrators can access this route." });
+ }
- const users = await User.findAll({ attributes: ["name", "id", "email"] });
+ const { searchParam = "", pageNumber = 1 } = req.query;
- return res.status(200).json(users);
+ const whereCondition = {
+ [Op.or]: [
+ {
+ name: Sequelize.where(
+ Sequelize.fn("LOWER", Sequelize.col("name")),
+ "LIKE",
+ "%" + searchParam.toLowerCase() + "%"
+ ),
+ },
+ { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } },
+ ],
+ };
+
+ let limit = 20;
+ let offset = limit * (pageNumber - 1);
+
+ const { count, rows: users } = await User.findAndCountAll({
+ attributes: ["name", "id", "email", "profile"],
+ where: whereCondition,
+ limit,
+ offset,
+ order: [["createdAt", "DESC"]],
+ });
+
+ const hasMore = count > offset + users.length;
+
+ return res.status(200).json({ users, count, hasMore });
};
exports.store = async (req, res, next) => {
- const errors = validationResult(req);
+ console.log(req.url);
+ const schema = Yup.object().shape({
+ name: Yup.string().required().min(2),
+ email: Yup.string()
+ .email()
+ .required()
+ .test(
+ "Check-email",
+ "An user with this email already exists",
+ async value => {
+ const userFound = await User.findOne({ where: { email: value } });
+ return !Boolean(userFound);
+ }
+ ),
+ password: Yup.string().required().min(5),
+ });
- if (!errors.isEmpty()) {
+ if (req.url === "/signup") {
+ const { value: userCreation } = await Setting.findByPk("userCreation");
+
+ if (userCreation === "disabled") {
+ return res
+ .status(403)
+ .json({ error: "User creation is disabled by administrator." });
+ }
+ } else if (req.user.profile !== "admin") {
return res
- .status(400)
- .json({ error: "Validation failed", data: errors.array() });
+ .status(403)
+ .json({ error: "Only administrators can create users." });
}
- const { name, id, email } = await User.create(req.body);
+ try {
+ await schema.validate(req.body);
+ } catch (err) {
+ return res.status(400).json({ error: err.message });
+ }
- res.status(201).json({ message: "User created!", userId: id });
+ const io = getIO();
+
+ const { name, id, email, profile } = await User.create(req.body);
+
+ io.emit("user", {
+ action: "create",
+ user: { name, id, email, profile },
+ });
+
+ return res.status(201).json({ message: "User created!", userId: id });
};
-exports.update = async (req, res) => {
+exports.show = async (req, res) => {
const { userId } = req.params;
const user = await User.findByPk(userId, {
- attributes: ["name", "id", "email"],
+ attributes: ["id", "name", "email", "profile"],
});
if (!user) {
res.status(400).json({ error: "No user found with this id." });
}
+ return res.status(200).json(user);
+};
+
+exports.update = async (req, res) => {
+ const schema = Yup.object().shape({
+ name: Yup.string().min(2),
+ email: Yup.string().email(),
+ password: Yup.string(),
+ });
+
+ if (req.user.profile !== "admin") {
+ return res
+ .status(403)
+ .json({ error: "Only administrators can edit users." });
+ }
+
+ await schema.validate(req.body);
+
+ const io = getIO();
+ const { userId } = req.params;
+
+ const user = await User.findByPk(userId, {
+ attributes: ["name", "id", "email", "profile"],
+ });
+
+ if (!user) {
+ res.status(404).json({ error: "No user found with this id." });
+ }
+
+ if (user.profile === "admin" && req.body.profile === "user") {
+ const adminUsers = await User.count({ where: { profile: "admin" } });
+ if (adminUsers <= 1) {
+ return res
+ .status(403)
+ .json({ error: "There must be at leat one admin user." });
+ }
+ }
+
await user.update(req.body);
- //todo, send socket IO to users channel.
+ io.emit("user", {
+ action: "update",
+ user: user,
+ });
- res.status(200).json(user);
+ return res.status(200).json(user);
};
exports.delete = async (req, res) => {
+ const io = getIO();
const { userId } = req.params;
const user = await User.findByPk(userId);
@@ -51,7 +164,18 @@ exports.delete = async (req, res) => {
res.status(400).json({ error: "No user found with this id." });
}
+ if (req.user.profile !== "admin") {
+ return res
+ .status(403)
+ .json({ error: "Only administrators can edit users." });
+ }
+
await user.destroy();
- res.status(200).json({ message: "User deleted" });
+ io.emit("user", {
+ action: "delete",
+ userId: userId,
+ });
+
+ return res.status(200).json({ message: "User deleted" });
};
diff --git a/backend/src/controllers/WhatsAppController.js b/backend/src/controllers/WhatsAppController.js
new file mode 100644
index 0000000..30fc5b3
--- /dev/null
+++ b/backend/src/controllers/WhatsAppController.js
@@ -0,0 +1,141 @@
+const Yup = require("yup");
+const Whatsapp = require("../models/Whatsapp");
+const { getIO } = require("../libs/socket");
+const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
+const wbotMessageListener = require("../services/wbotMessageListener");
+const wbotMonitor = require("../services/wbotMonitor");
+
+exports.index = async (req, res) => {
+ const whatsapps = await Whatsapp.findAll();
+
+ return res.status(200).json(whatsapps);
+};
+
+exports.store = async (req, res) => {
+ const schema = Yup.object().shape({
+ name: Yup.string().required().min(2),
+ default: Yup.boolean()
+ .required()
+ .test(
+ "Check-default",
+ "Only one default whatsapp is permited",
+ async value => {
+ if (value === true) {
+ const whatsappFound = await Whatsapp.findOne({
+ where: { default: true },
+ });
+ return !Boolean(whatsappFound);
+ } else return true;
+ }
+ ),
+ });
+
+ try {
+ await schema.validate(req.body);
+ } catch (err) {
+ return res.status(400).json({ error: err.message });
+ }
+
+ const io = getIO();
+
+ const whatsapp = await Whatsapp.create(req.body);
+
+ if (!whatsapp) {
+ return res.status(400).json({ error: "Cannot create whatsapp session." });
+ }
+
+ initWbot(whatsapp)
+ .then(() => {
+ wbotMessageListener(whatsapp);
+ wbotMonitor(whatsapp);
+ })
+ .catch(err => console.log(err));
+
+ io.emit("whatsapp", {
+ action: "update",
+ whatsapp: whatsapp,
+ });
+
+ return res.status(200).json(whatsapp);
+};
+
+exports.show = async (req, res) => {
+ const { whatsappId } = req.params;
+ const whatsapp = await Whatsapp.findByPk(whatsappId);
+
+ if (!whatsapp) {
+ return res.status(200).json({ message: "Session not found" });
+ }
+
+ return res.status(200).json(whatsapp);
+};
+
+exports.update = async (req, res) => {
+ const { whatsappId } = req.params;
+
+ const schema = Yup.object().shape({
+ name: Yup.string().required().min(2),
+ default: Yup.boolean()
+ .required()
+ .test(
+ "Check-default",
+ "Only one default whatsapp is permited",
+ async value => {
+ if (value === true) {
+ const whatsappFound = await Whatsapp.findOne({
+ where: { default: true },
+ });
+ if (whatsappFound) {
+ return !(whatsappFound.id !== +whatsappId);
+ } else {
+ return true;
+ }
+ } else return true;
+ }
+ ),
+ });
+
+ try {
+ await schema.validate(req.body);
+ } catch (err) {
+ return res.status(400).json({ error: err.message });
+ }
+
+ const io = getIO();
+
+ const whatsapp = await Whatsapp.findByPk(whatsappId);
+
+ if (!whatsapp) {
+ return res.status(404).json({ message: "Whatsapp not found" });
+ }
+
+ await whatsapp.update(req.body);
+
+ io.emit("whatsapp", {
+ action: "update",
+ whatsapp: whatsapp,
+ });
+
+ return res.status(200).json({ message: "Whatsapp updated" });
+};
+
+exports.delete = async (req, res) => {
+ const io = getIO();
+ const { whatsappId } = req.params;
+
+ const whatsapp = await Whatsapp.findByPk(whatsappId);
+
+ if (!whatsapp) {
+ return res.status(404).json({ message: "Whatsapp not found" });
+ }
+
+ await whatsapp.destroy();
+ removeWbot(whatsapp.id);
+
+ io.emit("whatsapp", {
+ action: "delete",
+ whatsappId: whatsapp.id,
+ });
+
+ return res.status(200).json({ message: "Whatsapp deleted." });
+};
diff --git a/backend/src/controllers/WhatsAppSessionController.js b/backend/src/controllers/WhatsAppSessionController.js
index 643d330..0bee528 100644
--- a/backend/src/controllers/WhatsAppSessionController.js
+++ b/backend/src/controllers/WhatsAppSessionController.js
@@ -1,45 +1,32 @@
-const Whatsapp = require("../models/Whatsapp");
-const { getIO } = require("../libs/socket");
-const { getWbot } = require("../libs/wbot");
+// const Whatsapp = require("../models/Whatsapp");
+// const { getIO } = require("../libs/socket");
+// const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
+// const wbotMessageListener = require("../services/wbotMessageListener");
+// const wbotMonitor = require("../services/wbotMonitor");
-exports.show = async (req, res) => {
- const { sessionId } = req.params;
- const dbSession = await Whatsapp.findByPk(sessionId);
+// exports.show = async (req, res) => {
+// const { whatsappId } = req.params;
+// const dbSession = await Whatsapp.findByPk(whatsappId);
- if (!dbSession) {
- return res.status(200).json({ message: "Session not found" });
- }
+// if (!dbSession) {
+// return res.status(200).json({ message: "Session not found" });
+// }
- return res.status(200).json(dbSession);
-};
-
-exports.delete = async (req, res) => {
- const wbot = getWbot();
- const io = getIO();
-
- const { sessionId } = req.params;
- const dbSession = await Whatsapp.findByPk(sessionId);
-
- if (!dbSession) {
- return res.status(200).json({ message: "Session not found" });
- }
-
- await dbSession.update({ session: "", status: "pending" });
- wbot.logout();
-
- io.emit("session", {
- action: "logout",
- session: dbSession,
- });
-
- return res.status(200).json({ message: "session disconnected" });
-};
-
-// exports.getContacts = async (req, res, next) => {
-// const io = getIO();
-// const wbot = getWbot();
-
-// const phoneContacts = await wbot.getContacts();
-
-// return res.status(200).json(phoneContacts);
+// return res.status(200).json(dbSession);
+// };
+
+// exports.delete = async (req, res) => {
+// const { whatsappId } = req.params;
+
+// const dbSession = await Whatsapp.findByPk(whatsappId);
+
+// if (!dbSession) {
+// return res.status(404).json({ message: "Session not found" });
+// }
+
+// const wbot = getWbot(dbSession.id);
+
+// wbot.logout();
+
+// return res.status(200).json({ message: "Session disconnected." });
// };
diff --git a/backend/src/database/index.js b/backend/src/database/index.js
index 4f2eef4..fe9956c 100644
--- a/backend/src/database/index.js
+++ b/backend/src/database/index.js
@@ -7,8 +7,17 @@ const Ticket = require("../models/Ticket");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
+const Setting = require("../models/Setting");
-const models = [User, Contact, Ticket, Message, Whatsapp, ContactCustomField];
+const models = [
+ User,
+ Contact,
+ Ticket,
+ Message,
+ Whatsapp,
+ ContactCustomField,
+ Setting,
+];
class Database {
constructor() {
diff --git a/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js b/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js
new file mode 100644
index 0000000..cb3e93b
--- /dev/null
+++ b/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Users", "profile", {
+ type: Sequelize.STRING,
+ allowNull: false,
+ defaultValue: "admin",
+ });
+ },
+
+ down: queryInterface => {
+ return queryInterface.removeColumn("Users", "profile");
+ },
+};
diff --git a/backend/src/database/migrations/20200903215941-create-settings.js b/backend/src/database/migrations/20200903215941-create-settings.js
new file mode 100644
index 0000000..ad82017
--- /dev/null
+++ b/backend/src/database/migrations/20200903215941-create-settings.js
@@ -0,0 +1,29 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Settings", {
+ key: {
+ type: Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false,
+ },
+ value: {
+ type: Sequelize.TEXT,
+ allowNull: false,
+ },
+ createdAt: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ },
+ updatedAt: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ },
+ });
+ },
+
+ down: queryInterface => {
+ return queryInterface.dropTable("Settings");
+ },
+};
diff --git a/backend/src/database/migrations/20200904220257-add-name-to-whatsapp.js b/backend/src/database/migrations/20200904220257-add-name-to-whatsapp.js
new file mode 100644
index 0000000..b2fefc2
--- /dev/null
+++ b/backend/src/database/migrations/20200904220257-add-name-to-whatsapp.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Whatsapps", "name", {
+ type: Sequelize.STRING,
+ allowNull: false,
+ unique: true,
+ });
+ },
+
+ down: queryInterface => {
+ return queryInterface.removeColumn("Whatsapps", "name");
+ },
+};
diff --git a/backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.js b/backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.js
new file mode 100644
index 0000000..57e03fc
--- /dev/null
+++ b/backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Whatsapps", "default", {
+ type: Sequelize.BOOLEAN,
+ allowNull: false,
+ defaultValue: false,
+ });
+ },
+
+ down: queryInterface => {
+ return queryInterface.removeColumn("Whatsapps", "default");
+ },
+};
diff --git a/backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.js b/backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.js
new file mode 100644
index 0000000..28f7c0c
--- /dev/null
+++ b/backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.js
@@ -0,0 +1,16 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Tickets", "whatsappId", {
+ type: Sequelize.INTEGER,
+ references: { model: "Whatsapps", key: "id" },
+ onUpdate: "CASCADE",
+ onDelete: "SET NULL",
+ });
+ },
+
+ down: queryInterface => {
+ return queryInterface.removeColumn("Tickets", "whatsappId");
+ },
+};
diff --git a/backend/src/database/seeds/20200824172424-create-contacts.js b/backend/src/database/seeds/20200824172424-create-contacts.js
deleted file mode 100644
index 54a55cd..0000000
--- a/backend/src/database/seeds/20200824172424-create-contacts.js
+++ /dev/null
@@ -1,48 +0,0 @@
-"use strict";
-
-module.exports = {
- up: (queryInterface, Sequelize) => {
- return queryInterface.bulkInsert(
- "Contacts",
- [
- {
- name: "Joana Doe",
- profilePicUrl:
- "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1834&q=80",
- number: 5512345678,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- name: "John Rulles",
- profilePicUrl:
- "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
- number: 5512345679,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- name: "Jonas Jhones",
- profilePicUrl:
- "https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
- number: 5512345680,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- name: "Julie June",
- profilePicUrl:
- "https://images.unsplash.com/photo-1493666438817-866a91353ca9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1049&q=80",
- number: 5512345681,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- ],
- {}
- );
- },
-
- down: (queryInterface, Sequelize) => {
- return queryInterface.bulkDelete("Contacts", null, {});
- },
-};
diff --git a/backend/src/database/seeds/20200824173654-create-tickets.js b/backend/src/database/seeds/20200824173654-create-tickets.js
deleted file mode 100644
index 83fd634..0000000
--- a/backend/src/database/seeds/20200824173654-create-tickets.js
+++ /dev/null
@@ -1,261 +0,0 @@
-"use strict";
-
-module.exports = {
- up: (queryInterface, Sequelize) => {
- return queryInterface.bulkInsert(
- "Tickets",
- [
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 3,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 4,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 2,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- status: "pending",
- lastMessage: "hello!",
- contactId: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- ],
- {}
- );
- },
-
- down: (queryInterface, Sequelize) => {
- return queryInterface.bulkDelete("Tickets", null, {});
- },
-};
diff --git a/backend/src/database/seeds/20200824174824-create-messages.js b/backend/src/database/seeds/20200824174824-create-messages.js
deleted file mode 100644
index 0037136..0000000
--- a/backend/src/database/seeds/20200824174824-create-messages.js
+++ /dev/null
@@ -1,148 +0,0 @@
-"use strict";
-
-module.exports = {
- up: (queryInterface, Sequelize) => {
- return queryInterface.bulkInsert(
- "Messages",
- [
- {
- id: "12312321342",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 1,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321313",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 1,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321314",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 1,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321315",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 1,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321316",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 5,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321355",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 5,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321318",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 5,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321319",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 5,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321399",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 11,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321391",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 11,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321392",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 3,
- ticketId: 11,
- fromMe: true,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- {
- id: "12312321393",
- body:
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
- ack: 0,
- ticketId: 11,
- fromMe: false,
- read: 1,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- ],
- {}
- );
- },
-
- down: (queryInterface, Sequelize) => {
- return queryInterface.bulkDelete("Messages", null, {});
- },
-};
diff --git a/backend/src/database/seeds/20200904070004-create-default-settings.js b/backend/src/database/seeds/20200904070004-create-default-settings.js
new file mode 100644
index 0000000..73dfccb
--- /dev/null
+++ b/backend/src/database/seeds/20200904070004-create-default-settings.js
@@ -0,0 +1,22 @@
+"use strict";
+
+module.exports = {
+ up: queryInterface => {
+ return queryInterface.bulkInsert(
+ "Settings",
+ [
+ {
+ key: "userCreation",
+ value: "enabled",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ ],
+ {}
+ );
+ },
+
+ down: queryInterface => {
+ return queryInterface.bulkDelete("Settings", null, {});
+ },
+};
diff --git a/backend/src/libs/wbot.js b/backend/src/libs/wbot.js
index ff62ade..86ee3a0 100644
--- a/backend/src/libs/wbot.js
+++ b/backend/src/libs/wbot.js
@@ -3,78 +3,108 @@ const { Client } = require("whatsapp-web.js");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
-let wbot;
+let sessions = [];
module.exports = {
- init: async () => {
- let sessionCfg;
+ initWbot: async whatsapp => {
+ try {
+ const io = getIO();
+ const sessionName = whatsapp.name;
+ let sessionCfg;
- const [dbSession] = await Whatsapp.findOrCreate({
- where: { id: 1 },
- defaults: {
- id: 1,
- },
- });
- if (dbSession && dbSession.session) {
- sessionCfg = JSON.parse(dbSession.session);
+ if (whatsapp && whatsapp.session) {
+ sessionCfg = JSON.parse(whatsapp.session);
+ }
+
+ const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
+ if (sessionIndex !== -1) {
+ sessions[sessionIndex].destroy();
+ sessions.splice(sessionIndex, 1);
+ }
+
+ const wbot = new Client({
+ session: sessionCfg,
+ restartOnAuthFail: true,
+ });
+ wbot.initialize();
+
+ wbot.on("qr", async qr => {
+ console.log("Session:", sessionName);
+
+ qrCode.generate(qr, { small: true });
+
+ await whatsapp.update({ qrcode: qr, status: "qrcode" });
+
+ io.emit("whatsappSession", {
+ action: "update",
+ session: whatsapp,
+ });
+ });
+
+ wbot.on("authenticated", async session => {
+ console.log("Session:", sessionName, "AUTHENTICATED");
+
+ await whatsapp.update({
+ session: JSON.stringify(session),
+ status: "authenticated",
+ });
+
+ io.emit("whatsappSession", {
+ action: "update",
+ session: whatsapp,
+ });
+ });
+
+ wbot.on("auth_failure", async msg => {
+ console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg);
+
+ await whatsapp.update({ session: "" });
+ });
+
+ wbot.on("ready", async () => {
+ console.log("Session:", sessionName, "READY");
+
+ await whatsapp.update({
+ status: "CONNECTED",
+ qrcode: "",
+ });
+
+ io.emit("whatsappSession", {
+ action: "update",
+ session: whatsapp,
+ });
+
+ wbot.sendPresenceAvailable();
+ });
+
+ wbot.id = whatsapp.id;
+ sessions.push(wbot);
+ } catch (err) {
+ console.log(err);
}
- wbot = new Client({
- session: sessionCfg,
- restartOnAuthFail: true,
- });
- wbot.initialize();
- wbot.on("qr", async qr => {
- qrCode.generate(qr, { small: true });
- await dbSession.update({ id: 1, qrcode: qr, status: "disconnected" });
- getIO().emit("session", {
- action: "update",
- qr: qr,
- session: dbSession,
- });
- });
- wbot.on("authenticated", async session => {
- console.log("AUTHENTICATED");
- await dbSession.update({
- id: 1,
- session: JSON.stringify(session),
- status: "authenticated",
- });
- getIO().emit("session", {
- action: "authentication",
- session: dbSession,
- });
- });
- wbot.on("auth_failure", async msg => {
- console.error("AUTHENTICATION FAILURE", msg);
- await Whatsapp.update({ session: "" }, { where: { id: 1 } });
- });
- wbot.on("ready", async () => {
- console.log("READY");
- await dbSession.update(
- { status: "CONNECTED", qrcode: "" },
- { where: { id: 1 } }
- );
- // const chats = await wbot.getChats(); // pega as mensagens nao lidas (recebidas quando o bot estava offline)
- // let unreadMessages; // todo > salvar isso no DB pra mostrar no frontend
- // for (let chat of chats) {
- // if (chat.unreadCount > 0) {
- // unreadMessages = await chat.fetchMessages({
- // limit: chat.unreadCount,
- // });
- // }
- // }
-
- // console.log(unreadMessages);
- wbot.sendPresenceAvailable();
- });
- return { wbot, dbSession };
+ return null;
},
- getWbot: () => {
- if (!wbot) {
- throw new Error("Wbot not initialized");
+ getWbot: whatsappId => {
+ const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
+
+ if (sessionIndex === -1) {
+ console.log("This Wbot session is not initialized");
+ return null;
+ }
+ return sessions[sessionIndex];
+ },
+
+ removeWbot: whatsappId => {
+ try {
+ const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
+ if (sessionIndex !== -1) {
+ sessions[sessionIndex].destroy();
+ sessions.splice(sessionIndex, 1);
+ }
+ } catch (err) {
+ console.log(err);
}
- return wbot;
},
};
diff --git a/backend/src/middleware/is-auth.js b/backend/src/middleware/is-auth.js
index 6bfaabc..596d737 100644
--- a/backend/src/middleware/is-auth.js
+++ b/backend/src/middleware/is-auth.js
@@ -1,5 +1,7 @@
const jwt = require("jsonwebtoken");
+const util = require("util");
+const User = require("../models/User");
const authConfig = require("../config/auth");
module.exports = async (req, res, next) => {
@@ -11,12 +13,24 @@ module.exports = async (req, res, next) => {
const [, token] = authHeader.split(" ");
- jwt.verify(token, authConfig.secret, (error, result) => {
- if (error) {
- return res.status(401).json({ error: "Invalid token" });
+ try {
+ const decoded = await util.promisify(jwt.verify)(token, authConfig.secret);
+
+ const user = await User.findByPk(decoded.userId, {
+ attributes: ["id", "name", "profile", "email"],
+ });
+
+ if (!user) {
+ return res
+ .status(401)
+ .json({ error: "The token corresponding user does not exists." });
}
- req.userId = result.userId;
- // todo >> find user in DB and store in req.user to use latter, or throw an error if user not exists anymore
- next();
- });
+
+ req.user = user;
+
+ return next();
+ } catch (err) {
+ console.log(err);
+ return res.status(401).json({ error: "Invalid Token" });
+ }
};
diff --git a/backend/src/models/Setting.js b/backend/src/models/Setting.js
new file mode 100644
index 0000000..fe4168a
--- /dev/null
+++ b/backend/src/models/Setting.js
@@ -0,0 +1,24 @@
+const Sequelize = require("sequelize");
+
+class Setting extends Sequelize.Model {
+ static init(sequelize) {
+ super.init(
+ {
+ key: {
+ type: Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false,
+ unique: true,
+ },
+ value: { type: Sequelize.TEXT, allowNull: false },
+ },
+ {
+ sequelize,
+ }
+ );
+
+ return this;
+ }
+}
+
+module.exports = Setting;
diff --git a/backend/src/models/Ticket.js b/backend/src/models/Ticket.js
index 5882527..771eba5 100644
--- a/backend/src/models/Ticket.js
+++ b/backend/src/models/Ticket.js
@@ -39,6 +39,10 @@ class Ticket extends Sequelize.Model {
static associate(models) {
this.belongsTo(models.Contact, { foreignKey: "contactId", as: "contact" });
this.belongsTo(models.User, { foreignKey: "userId", as: "user" });
+ this.belongsTo(models.Whatsapp, {
+ foreignKey: "whatsappId",
+ as: "whatsapp",
+ });
this.hasMany(models.Message, { foreignKey: "ticketId", as: "messages" });
}
}
diff --git a/backend/src/models/User.js b/backend/src/models/User.js
index d474366..375f129 100644
--- a/backend/src/models/User.js
+++ b/backend/src/models/User.js
@@ -7,6 +7,7 @@ class User extends Sequelize.Model {
{
name: { type: Sequelize.STRING },
password: { type: Sequelize.VIRTUAL },
+ profile: { type: Sequelize.STRING, defaultValue: "admin" },
passwordHash: { type: Sequelize.STRING },
email: { type: Sequelize.STRING },
},
diff --git a/backend/src/models/Whatsapp.js b/backend/src/models/Whatsapp.js
index d60604a..7d77f17 100644
--- a/backend/src/models/Whatsapp.js
+++ b/backend/src/models/Whatsapp.js
@@ -6,9 +6,15 @@ class Whatsapp extends Sequelize.Model {
{
session: { type: Sequelize.TEXT },
qrcode: { type: Sequelize.TEXT },
+ name: { type: Sequelize.STRING, unique: true, allowNull: false },
status: { type: Sequelize.STRING },
battery: { type: Sequelize.STRING },
plugged: { type: Sequelize.BOOLEAN },
+ default: {
+ type: Sequelize.BOOLEAN,
+ defaultValue: false,
+ allowNull: false,
+ },
},
{
sequelize,
@@ -17,6 +23,10 @@ class Whatsapp extends Sequelize.Model {
return this;
}
+
+ static associate(models) {
+ this.hasMany(models.Ticket, { foreignKey: "whatsappId", as: "tickets" });
+ }
}
module.exports = Whatsapp;
diff --git a/backend/src/router/index.js b/backend/src/router/index.js
new file mode 100644
index 0000000..094ceb3
--- /dev/null
+++ b/backend/src/router/index.js
@@ -0,0 +1,21 @@
+const express = require("express");
+
+const AuthRoutes = require("./routes/auth");
+const TicketsRoutes = require("./routes/tickets");
+const MessagesRoutes = require("./routes/messages");
+const ContactsRoutes = require("./routes/contacts");
+const WhatsRoutes = require("./routes/whatsapp");
+const UsersRoutes = require("./routes/users");
+const SettingsRoutes = require("./routes/settings");
+
+const routes = express.Router();
+
+routes.use("/auth", AuthRoutes);
+routes.use(TicketsRoutes);
+routes.use(MessagesRoutes);
+routes.use(ContactsRoutes);
+routes.use(WhatsRoutes);
+routes.use(UsersRoutes);
+routes.use(SettingsRoutes);
+
+module.exports = routes;
diff --git a/backend/src/router/routes/auth.js b/backend/src/router/routes/auth.js
new file mode 100644
index 0000000..930d1e6
--- /dev/null
+++ b/backend/src/router/routes/auth.js
@@ -0,0 +1,16 @@
+const express = require("express");
+const SessionController = require("../../controllers/SessionController");
+const UserController = require("../../controllers/UserController");
+const isAuth = require("../../middleware/is-auth");
+
+const routes = express.Router();
+
+routes.post("/signup", UserController.store);
+
+routes.post("/login", SessionController.store);
+
+routes.get("/check", isAuth, (req, res) => {
+ res.status(200).json({ authenticated: true });
+});
+
+module.exports = routes;
diff --git a/backend/src/routes/contacts.js b/backend/src/router/routes/contacts.js
similarity index 67%
rename from backend/src/routes/contacts.js
rename to backend/src/router/routes/contacts.js
index 6b22706..1d9bfa2 100644
--- a/backend/src/routes/contacts.js
+++ b/backend/src/router/routes/contacts.js
@@ -1,8 +1,8 @@
const express = require("express");
-const isAuth = require("../middleware/is-auth");
+const isAuth = require("../../middleware/is-auth");
-const ContactController = require("../controllers/ContactController");
-const ImportPhoneContactsController = require("../controllers/ImportPhoneContactsController");
+const ContactController = require("../../controllers/ContactController");
+const ImportPhoneContactsController = require("../../controllers/ImportPhoneContactsController");
const routes = express.Router();
diff --git a/backend/src/routes/messages.js b/backend/src/router/routes/messages.js
similarity index 63%
rename from backend/src/routes/messages.js
rename to backend/src/router/routes/messages.js
index 0a88edc..e48298b 100644
--- a/backend/src/routes/messages.js
+++ b/backend/src/router/routes/messages.js
@@ -1,7 +1,7 @@
const express = require("express");
-const isAuth = require("../middleware/is-auth");
+const isAuth = require("../../middleware/is-auth");
-const MessageController = require("../controllers/MessageController");
+const MessageController = require("../../controllers/MessageController");
const routes = express.Router();
diff --git a/backend/src/router/routes/settings.js b/backend/src/router/routes/settings.js
new file mode 100644
index 0000000..fc0fe65
--- /dev/null
+++ b/backend/src/router/routes/settings.js
@@ -0,0 +1,14 @@
+const express = require("express");
+const isAuth = require("../../middleware/is-auth");
+
+const SettingController = require("../../controllers/SettingController");
+
+const routes = express.Router();
+
+routes.get("/settings", isAuth, SettingController.index);
+
+// routes.get("/settings/:settingKey", isAuth, SettingsController.show);
+
+routes.put("/settings/:settingKey", isAuth, SettingController.update);
+
+module.exports = routes;
diff --git a/backend/src/routes/tickets.js b/backend/src/router/routes/tickets.js
similarity index 73%
rename from backend/src/routes/tickets.js
rename to backend/src/router/routes/tickets.js
index 5bf490f..201e3e8 100644
--- a/backend/src/routes/tickets.js
+++ b/backend/src/router/routes/tickets.js
@@ -1,7 +1,7 @@
const express = require("express");
-const isAuth = require("../middleware/is-auth");
+const isAuth = require("../../middleware/is-auth");
-const TicketController = require("../controllers/TicketController");
+const TicketController = require("../../controllers/TicketController");
const routes = express.Router();
diff --git a/backend/src/router/routes/users.js b/backend/src/router/routes/users.js
new file mode 100644
index 0000000..8a50e61
--- /dev/null
+++ b/backend/src/router/routes/users.js
@@ -0,0 +1,18 @@
+const express = require("express");
+
+const isAuth = require("../../middleware/is-auth");
+const UserController = require("../../controllers/UserController");
+
+const routes = express.Router();
+
+routes.get("/users", isAuth, UserController.index);
+
+routes.post("/users", isAuth, UserController.store);
+
+routes.put("/users/:userId", isAuth, UserController.update);
+
+routes.get("/users/:userId", isAuth, UserController.show);
+
+routes.delete("/users/:userId", isAuth, UserController.delete);
+
+module.exports = routes;
diff --git a/backend/src/router/routes/whatsapp.js b/backend/src/router/routes/whatsapp.js
new file mode 100644
index 0000000..66d0318
--- /dev/null
+++ b/backend/src/router/routes/whatsapp.js
@@ -0,0 +1,18 @@
+const express = require("express");
+const isAuth = require("../../middleware/is-auth");
+
+const WhatsAppController = require("../../controllers/WhatsAppController");
+
+const routes = express.Router();
+
+routes.get("/whatsapp/", isAuth, WhatsAppController.index);
+
+routes.post("/whatsapp/", isAuth, WhatsAppController.store);
+
+routes.get("/whatsapp/:whatsappId", isAuth, WhatsAppController.show);
+
+routes.put("/whatsapp/:whatsappId", isAuth, WhatsAppController.update);
+
+routes.delete("/whatsapp/:whatsappId", isAuth, WhatsAppController.delete);
+
+module.exports = routes;
diff --git a/backend/src/router/routes/whatsappsessions.js b/backend/src/router/routes/whatsappsessions.js
new file mode 100644
index 0000000..2eb87f9
--- /dev/null
+++ b/backend/src/router/routes/whatsappsessions.js
@@ -0,0 +1,20 @@
+const express = require("express");
+const isAuth = require("../../middleware/is-auth");
+
+const WhatsAppSessionController = require("../../controllers/WhatsAppSessionController");
+
+const routes = express.Router();
+
+routes.get(
+ "/whatsappsession/:whatsappId",
+ isAuth,
+ WhatsAppSessionController.show
+);
+
+routes.delete(
+ "/whatsappsession/:whatsappId",
+ isAuth,
+ WhatsAppSessionController.delete
+);
+
+module.exports = routes;
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
deleted file mode 100644
index 7d0e52d..0000000
--- a/backend/src/routes/auth.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const express = require("express");
-const SessionController = require("../controllers/SessionController");
-const isAuth = require("../middleware/is-auth");
-
-const routes = express.Router();
-
-routes.post("/login", SessionController.store);
-
-routes.get("/check", isAuth, (req, res) => {
- res.status(200).json({ authenticated: true });
-});
-
-module.exports = routes;
diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js
deleted file mode 100644
index 7e6d46d..0000000
--- a/backend/src/routes/users.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const express = require("express");
-const { body } = require("express-validator");
-const User = require("../models/User");
-
-const isAuth = require("../middleware/is-auth");
-const UserController = require("../controllers/UserController");
-
-const routes = express.Router();
-
-routes.get("/users", isAuth, UserController.index);
-
-routes.post(
- "/users",
- [
- body("email")
- .isEmail()
- .withMessage("Email inválido")
- .custom((value, { req }) => {
- return User.findOne({ where: { email: value } }).then(user => {
- if (user) {
- return Promise.reject("An user with this email already exists!");
- }
- });
- })
- .normalizeEmail(),
- body("password").trim().isLength({ min: 5 }),
- body("name").trim().not().isEmpty(),
- ],
- UserController.store
-);
-
-routes.put("/users/:userId", isAuth, UserController.update);
-
-routes.delete("/users/:userId", isAuth, UserController.delete);
-
-module.exports = routes;
diff --git a/backend/src/routes/whatsapp.js b/backend/src/routes/whatsapp.js
deleted file mode 100644
index 7d27526..0000000
--- a/backend/src/routes/whatsapp.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const express = require("express");
-const isAuth = require("../middleware/is-auth");
-
-const WhatsAppSessionController = require("../controllers/WhatsAppSessionController");
-
-const routes = express.Router();
-
-routes.get(
- "/whatsapp/session/:sessionId",
- isAuth,
- WhatsAppSessionController.show
-);
-
-routes.delete(
- "/whatsapp/session/:sessionId",
- isAuth,
- WhatsAppSessionController.delete
-);
-
-// fetch contacts in user cellphone, not in use
-// routes.get("/whatsapp/contacts", isAuth, WhatsappController.getContacts);
-
-module.exports = routes;
diff --git a/backend/src/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js
index 0162838..aed420d 100644
--- a/backend/src/services/wbotMessageListener.js
+++ b/backend/src/services/wbotMessageListener.js
@@ -7,9 +7,10 @@ const Sentry = require("@sentry/node");
const Contact = require("../models/Contact");
const Ticket = require("../models/Ticket");
const Message = require("../models/Message");
+const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
-const { getWbot, init } = require("../libs/wbot");
+const { getWbot, initWbot } = require("../libs/wbot");
const verifyContact = async (msgContact, profilePicUrl) => {
let contact = await Contact.findOne({
@@ -29,7 +30,7 @@ const verifyContact = async (msgContact, profilePicUrl) => {
return contact;
};
-const verifyTicket = async contact => {
+const verifyTicket = async (contact, whatsappId) => {
let ticket = await Ticket.findOne({
where: {
status: {
@@ -57,6 +58,7 @@ const verifyTicket = async contact => {
ticket = await Ticket.create({
contactId: contact.id,
status: "pending",
+ whatsappId,
});
}
@@ -132,12 +134,14 @@ const handleMessage = async (msg, ticket, contact) => {
});
};
-const wbotMessageListener = () => {
- const wbot = getWbot();
+const wbotMessageListener = whatsapp => {
+ const whatsappId = whatsapp.id;
+ const wbot = getWbot(whatsappId);
const io = getIO();
wbot.on("message_create", async msg => {
// console.log(msg);
+
if (
msg.from === "status@broadcast" ||
msg.type === "location" ||
@@ -158,7 +162,7 @@ const wbotMessageListener = () => {
const profilePicUrl = await msgContact.getProfilePicUrl();
const contact = await verifyContact(msgContact, profilePicUrl);
- const ticket = await verifyTicket(contact);
+ const ticket = await verifyTicket(contact, whatsappId);
//return if message was already created by messageController
if (msg.fromMe) {
diff --git a/backend/src/services/wbotMonitor.js b/backend/src/services/wbotMonitor.js
index b8e4145..696b44d 100644
--- a/backend/src/services/wbotMonitor.js
+++ b/backend/src/services/wbotMonitor.js
@@ -3,17 +3,18 @@ const Sentry = require("@sentry/node");
const wbotMessageListener = require("./wbotMessageListener");
const { getIO } = require("../libs/socket");
-const { getWbot, init } = require("../libs/wbot");
+const { getWbot, initWbot } = require("../libs/wbot");
-const wbotMonitor = dbSession => {
+const wbotMonitor = whatsapp => {
const io = getIO();
- const wbot = getWbot();
+ const sessionName = whatsapp.name;
+ const wbot = getWbot(whatsapp.id);
try {
wbot.on("change_state", async newState => {
- console.log("monitor", newState);
+ console.log("Monitor session:", sessionName, newState);
try {
- await dbSession.update({ status: newState });
+ await whatsapp.update({ status: newState });
} catch (err) {
Sentry.captureException(err);
console.log(err);
@@ -21,16 +22,18 @@ const wbotMonitor = dbSession => {
io.emit("session", {
action: "update",
- session: dbSession,
+ session: whatsapp,
});
});
wbot.on("change_battery", async batteryInfo => {
const { battery, plugged } = batteryInfo;
- console.log(`Battery: ${battery}% - Charging? ${plugged}`);
+ console.log(
+ `Battery session: ${sessionName} ${battery}% - Charging? ${plugged}`
+ );
try {
- await dbSession.update({ battery, plugged });
+ await whatsapp.update({ battery, plugged });
} catch (err) {
Sentry.captureException(err);
console.log(err);
@@ -38,30 +41,30 @@ const wbotMonitor = dbSession => {
io.emit("session", {
action: "update",
- session: dbSession,
+ session: whatsapp,
});
});
wbot.on("disconnected", async reason => {
- console.log("disconnected", reason);
+ console.log("Disconnected session:", sessionName, reason);
try {
- await dbSession.update({ status: "disconnected" });
+ await whatsapp.update({ status: "disconnected" });
} catch (err) {
Sentry.captureException(err);
console.log(err);
}
io.emit("session", {
- action: "logout",
- session: dbSession,
+ action: "update",
+ session: whatsapp,
});
setTimeout(
() =>
- init()
- .then(({ dbSession }) => {
- wbotMessageListener();
- wbotMonitor(dbSession);
+ initWbot(whatsapp)
+ .then(() => {
+ wbotMessageListener(whatsapp);
+ wbotMonitor(whatsapp);
})
.catch(err => {
Sentry.captureException(err);
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 0deb2e7..be20bb0 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -2,6 +2,13 @@
# yarn lockfile v1
+"@babel/runtime@^7.10.5":
+ version "7.11.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
+ integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@pedroslopez/moduleraid@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@pedroslopez/moduleraid/-/moduleraid-4.1.0.tgz#468f7195fddc9f367e672ace9269f0698cf4c404"
@@ -854,14 +861,6 @@ express-async-errors@^3.1.1:
resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
-express-validator@^6.5.0:
- version "6.6.1"
- resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.6.1.tgz#c53046f615d27fcb78be786e018dcd60bd9c6c5c"
- integrity sha512-+MrZKJ3eGYXkNF9p9Zf7MS7NkPJFg9MDYATU5c80Cf4F62JdLBIjWxy6481tRC0y1NnC9cgOw8FuN364bWaGhA==
- dependencies:
- lodash "^4.17.19"
- validator "^13.1.1"
-
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -950,6 +949,11 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
+fn-name@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
+ integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
+
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -1382,6 +1386,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
+lodash-es@^4.17.11:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+ integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
+
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -1417,7 +1426,7 @@ lodash.once@^4.0.0:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
-lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5:
+lodash@^4.17.15, lodash@^4.17.5:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -1801,6 +1810,11 @@ progress@^2.0.1:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+property-expr@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
+ integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -1941,6 +1955,11 @@ redeyed@~2.1.0:
dependencies:
esprima "~4.0.0"
+regenerator-runtime@^0.13.4:
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
registry-auth-token@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
@@ -2266,6 +2285,11 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
+synchronous-promise@^2.0.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
+ integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
+
tar-fs@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5"
@@ -2332,6 +2356,11 @@ toposort-class@^1.0.1:
resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -2464,11 +2493,6 @@ validator@^10.11.0:
resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
-validator@^13.1.1:
- version "13.1.1"
- resolved "https://registry.yarnpkg.com/validator/-/validator-13.1.1.tgz#f8811368473d2173a9d8611572b58c5783f223bf"
- integrity sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw==
-
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -2614,3 +2638,16 @@ youch@^2.0.10:
cookie "^0.3.1"
mustache "^3.0.0"
stack-trace "0.0.10"
+
+yup@^0.29.3:
+ version "0.29.3"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
+ integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
+ dependencies:
+ "@babel/runtime" "^7.10.5"
+ fn-name "~3.0.0"
+ lodash "^4.17.15"
+ lodash-es "^4.17.11"
+ property-expr "^2.0.2"
+ synchronous-promise "^2.0.13"
+ toposort "^2.0.2"
diff --git a/frontend/package.json b/frontend/package.json
index f49fa02..1102b93 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -25,7 +25,8 @@
"react-scripts": "3.4.1",
"react-toastify": "^6.0.8",
"recharts": "^1.8.5",
- "socket.io-client": "^2.3.0"
+ "socket.io-client": "^2.3.0",
+ "yup": "^0.29.3"
},
"scripts": {
"start": "react-scripts start",
diff --git a/frontend/src/App.js b/frontend/src/App.js
index cb42d18..f1dfbde 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -10,8 +10,18 @@ const App = () => {
const theme = createMuiTheme(
{
+ scrollbarStyles: {
+ "&::-webkit-scrollbar": {
+ width: "8px",
+ height: "8px",
+ },
+ "&::-webkit-scrollbar-thumb": {
+ boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
+ backgroundColor: "#e8e8e8",
+ },
+ },
palette: {
- primary: { main: "#1976d2" },
+ primary: { main: "#2576d2" },
},
},
locale
diff --git a/frontend/src/components/ContactDrawer/index.js b/frontend/src/components/ContactDrawer/index.js
index d0b0b3d..da512bf 100644
--- a/frontend/src/components/ContactDrawer/index.js
+++ b/frontend/src/components/ContactDrawer/index.js
@@ -48,14 +48,7 @@ const useStyles = makeStyles(theme => ({
padding: "8px 0px 8px 8px",
height: "100%",
overflowY: "scroll",
- "&::-webkit-scrollbar": {
- width: "8px",
- height: "8px",
- },
- "&::-webkit-scrollbar-thumb": {
- boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
- backgroundColor: "#e8e8e8",
- },
+ ...theme.scrollbarStyles,
},
contactAvatar: {
@@ -80,17 +73,6 @@ const useStyles = makeStyles(theme => ({
padding: 8,
display: "flex",
flexDirection: "column",
- // overflowX: "scroll",
- // flex: 1,
- // "&::-webkit-scrollbar": {
- // width: "8px",
- // height: "8px",
- // },
- // "&::-webkit-scrollbar-thumb": {
- // // borderRadius: "2px",
- // boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
- // backgroundColor: "#e8e8e8",
- // },
},
contactExtraInfo: {
marginTop: 4,
diff --git a/frontend/src/components/ContactModal/index.js b/frontend/src/components/ContactModal/index.js
index 1270e71..326837d 100644
--- a/frontend/src/components/ContactModal/index.js
+++ b/frontend/src/components/ContactModal/index.js
@@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
-import { Formik, FieldArray } from "formik";
+import * as Yup from "yup";
+import { Formik, FieldArray, Form, Field } from "formik";
+import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
@@ -52,6 +54,15 @@ const useStyles = makeStyles(theme => ({
},
}));
+const ContactSchema = Yup.object().shape({
+ name: Yup.string()
+ .min(2, "Too Short!")
+ .max(50, "Too Long!")
+ .required("Required"),
+ number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
+ email: Yup.string().email("Invalid email"),
+});
+
const ContactModal = ({ open, onClose, contactId }) => {
const classes = useStyles();
@@ -66,8 +77,15 @@ const ContactModal = ({ open, onClose, contactId }) => {
useEffect(() => {
const fetchContact = async () => {
if (!contactId) return;
- const res = await api.get(`/contacts/${contactId}`);
- setContact(res.data);
+ try {
+ const { data } = await api.get(`/contacts/${contactId}`);
+ setContact(data);
+ } catch (err) {
+ console.log(err);
+ if (err.response && err.response.data && err.response.data.error) {
+ toast.error(err.response.data.error);
+ }
+ }
};
fetchContact();
@@ -85,78 +103,70 @@ const ContactModal = ({ open, onClose, contactId }) => {
} else {
await api.post("/contacts", values);
}
+ toast.success("Contact saved sucessfully!");
} catch (err) {
- alert(err.response.data.error);
console.log(err);
+ if (err.response && err.response.data && err.response.data.error) {
+ toast.error(err.response.data.error);
+ }
}
handleClose();
};
return (