From dfde175c07b70cc1d3f6e1c856376ac13125a9e8 Mon Sep 17 00:00:00 2001 From: canove Date: Wed, 2 Sep 2020 21:01:02 -0300 Subject: [PATCH 01/25] feat: added users page --- backend/package.json | 4 +- backend/src/app.js | 1 - backend/src/controllers/ContactController.js | 8 +- backend/src/controllers/TicketController.js | 2 +- backend/src/controllers/UserController.js | 108 +++++- ...00901235509-add-profile-column-to-users.js | 15 + backend/src/middleware/is-auth.js | 28 +- backend/src/models/User.js | 1 + backend/src/routes/auth.js | 3 + backend/src/routes/users.js | 23 +- backend/src/services/wbotMessageListener.js | 2 +- backend/yarn.lock | 65 +++- frontend/package.json | 3 +- frontend/src/App.js | 12 +- .../src/components/ContactDrawer/index.js | 20 +- frontend/src/components/ContactModal/index.js | 88 ++--- .../src/components/ContactsSekeleton/index.js | 163 -------- .../src/components/MainContainer/index.js | 31 ++ frontend/src/components/MainHeader/index.js | 19 + .../MainHeaderButtonsWrapper/index.js | 21 ++ frontend/src/components/MessagesList/index.js | 10 +- .../components/NotificationsPopOver/index.js | 9 +- .../src/components/PaginationActions/index.js | 71 ---- .../src/components/TableRowSkeleton/index.js | 78 ++++ frontend/src/components/TicketsList/index.js | 9 +- frontend/src/components/Title/index.js | 10 + frontend/src/components/UserModal/index.js | 212 +++++++++++ .../src/components/_layout/MainListItems.js | 4 +- frontend/src/pages/Contacts/index.js | 356 ++++++++---------- frontend/src/pages/Dashboard/Title.js | 6 +- frontend/src/pages/Signup/index.js | 162 ++++---- frontend/src/pages/Users/index.js | 234 ++++++++++++ frontend/src/routes/index.js | 2 + 33 files changed, 1121 insertions(+), 659 deletions(-) create mode 100644 backend/src/database/migrations/20200901235509-add-profile-column-to-users.js delete mode 100644 frontend/src/components/ContactsSekeleton/index.js create mode 100644 frontend/src/components/MainContainer/index.js create mode 100644 frontend/src/components/MainHeader/index.js create mode 100644 frontend/src/components/MainHeaderButtonsWrapper/index.js delete mode 100644 frontend/src/components/PaginationActions/index.js create mode 100644 frontend/src/components/TableRowSkeleton/index.js create mode 100644 frontend/src/components/Title/index.js create mode 100644 frontend/src/components/UserModal/index.js create mode 100644 frontend/src/pages/Users/index.js 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..f743ec9 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -82,6 +82,5 @@ 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/controllers/ContactController.js b/backend/src/controllers/ContactController.js index 2fc503a..3ee343b 100644 --- a/backend/src/controllers/ContactController.js +++ b/backend/src/controllers/ContactController.js @@ -8,7 +8,7 @@ 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 +23,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,7 +33,9 @@ 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) => { diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js index 5ae04ab..fa40be8 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -16,7 +16,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); diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index fbbb94b..771d2b2 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -1,34 +1,101 @@ -const { validationResult } = require("express-validator"); +const Sequelize = require("sequelize"); +const Yup = require("yup"); +const { Op } = require("sequelize"); const User = require("../models/User"); +const { getIO } = require("../libs/socket"); + exports.index = async (req, res) => { - // const { searchParam = "", pageNumber = 1 } = req.query; + const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query; - const users = await User.findAll({ attributes: ["name", "id", "email"] }); + const whereCondition = { + [Op.or]: [ + { + name: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("name")), + "LIKE", + "%" + searchParam.toLowerCase() + "%" + ), + }, + { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } }, + ], + }; - return res.status(200).json(users); + let limit = +rowsPerPage; + let offset = limit * (pageNumber - 1); + + const { count, rows: users } = await User.findAndCountAll({ + attributes: ["name", "id", "email", "profile"], + where: whereCondition, + limit, + offset, + order: [["createdAt", "DESC"]], + }); + + return res.status(200).json({ users, count }); }; exports.store = async (req, res, next) => { - const errors = validationResult(req); + 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()) { - return res - .status(400) - .json({ error: "Validation failed", data: errors.array() }); - } + await schema.validate(req.body); - const { name, id, email } = await User.create(req.body); + const io = getIO(); - res.status(201).json({ message: "User created!", userId: id }); + 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.show = async (req, res) => { + const { userId } = req.params; + + const { id, name, email, profile } = await User.findByPk(userId); + + return res.status(200).json({ + id, + name, + email, + profile, + }); }; exports.update = async (req, res) => { + const schema = Yup.object().shape({ + name: Yup.string().min(2), + email: Yup.string().email(), + password: Yup.string(), + }); + + console.log("cai aqui"); + + await schema.validate(req.body); + + const io = getIO(); const { userId } = req.params; const user = await User.findByPk(userId, { - attributes: ["name", "id", "email"], + attributes: ["name", "id", "email", "profile"], }); if (!user) { @@ -37,12 +104,16 @@ exports.update = async (req, res) => { 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); @@ -53,5 +124,10 @@ exports.delete = async (req, res) => { 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/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/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/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/routes/auth.js b/backend/src/routes/auth.js index 7d0e52d..28a944e 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,9 +1,12 @@ 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) => { diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 7e6d46d..bbdb0de 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -1,5 +1,4 @@ const express = require("express"); -const { body } = require("express-validator"); const User = require("../models/User"); const isAuth = require("../middleware/is-auth"); @@ -9,28 +8,12 @@ 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.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/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index 0162838..329640b 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -137,7 +137,7 @@ const wbotMessageListener = () => { const io = getIO(); wbot.on("message_create", async msg => { - // console.log(msg); + console.log(msg); if ( msg.from === "status@broadcast" || msg.type === "location" || 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..1d5a0d3 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; -import { Formik, FieldArray } from "formik"; +import * as Yup from "yup"; +import { Formik, FieldArray, Form, Field } from "formik"; import { makeStyles } from "@material-ui/core/styles"; import { green } from "@material-ui/core/colors"; @@ -52,6 +53,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(); @@ -86,7 +96,7 @@ const ContactModal = ({ open, onClose, contactId }) => { await api.post("/contacts", values); } } catch (err) { - alert(err.response.data.error); + alert(JSON.stringify(err.response.data, null, 2)); console.log(err); } handleClose(); @@ -94,69 +104,57 @@ const ContactModal = ({ open, onClose, contactId }) => { return (
- + + + {contactId + ? `${i18n.t("contactModal.title.edit")}` + : `${i18n.t("contactModal.title.add")}`} + { + validationSchema={ContactSchema} + onSubmit={(values, actions) => { setTimeout(() => { handleSaveContact(values); - setSubmitting(false); + actions.setSubmitting(false); }, 400); }} > - {({ - values, - errors, - touched, - handleChange, - handleBlur, - handleSubmit, - isSubmitting, - }) => ( -
- - {contactId - ? `${i18n.t("contactModal.title.edit")}` - : `${i18n.t("contactModal.title.add")}`} - + {({ values, errors, touched, isSubmitting }) => ( + {i18n.t("contactModal.form.mainInfo")} - -
- { className={classes.extraAttr} key={`${index}-info`} > - - { )} - + )}
diff --git a/frontend/src/components/ContactsSekeleton/index.js b/frontend/src/components/ContactsSekeleton/index.js deleted file mode 100644 index fc5ffe6..0000000 --- a/frontend/src/components/ContactsSekeleton/index.js +++ /dev/null @@ -1,163 +0,0 @@ -import React from "react"; -import TableCell from "@material-ui/core/TableCell"; -import TableRow from "@material-ui/core/TableRow"; -import Skeleton from "@material-ui/lab/Skeleton"; - -const ContactsSekeleton = () => { - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default ContactsSekeleton; diff --git a/frontend/src/components/MainContainer/index.js b/frontend/src/components/MainContainer/index.js new file mode 100644 index 0000000..24b7b9a --- /dev/null +++ b/frontend/src/components/MainContainer/index.js @@ -0,0 +1,31 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; +import Container from "@material-ui/core/Container"; + +const useStyles = makeStyles(theme => ({ + mainContainer: { + flex: 1, + padding: theme.spacing(2), + height: `calc(100% - 48px)`, + }, + + contentWrapper: { + height: "100%", + overflowY: "hidden", + display: "flex", + flexDirection: "column", + }, +})); + +const MainContainer = ({ children }) => { + const classes = useStyles(); + + return ( + +
{children}
+
+ ); +}; + +export default MainContainer; diff --git a/frontend/src/components/MainHeader/index.js b/frontend/src/components/MainHeader/index.js new file mode 100644 index 0000000..46fa8ab --- /dev/null +++ b/frontend/src/components/MainHeader/index.js @@ -0,0 +1,19 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + contactsHeader: { + display: "flex", + alignItems: "center", + padding: "0px 6px 6px 6px", + }, +})); + +const MainHeader = ({ children }) => { + const classes = useStyles(); + + return
{children}
; +}; + +export default MainHeader; diff --git a/frontend/src/components/MainHeaderButtonsWrapper/index.js b/frontend/src/components/MainHeaderButtonsWrapper/index.js new file mode 100644 index 0000000..ed5887c --- /dev/null +++ b/frontend/src/components/MainHeaderButtonsWrapper/index.js @@ -0,0 +1,21 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + MainHeaderButtonsWrapper: { + flex: "none", + marginLeft: "auto", + "& > *": { + margin: theme.spacing(1), + }, + }, +})); + +const MainHeaderButtonsWrapper = ({ children }) => { + const classes = useStyles(); + + return
{children}
; +}; + +export default MainHeaderButtonsWrapper; diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index cd2b2ed..6e04f27 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -101,15 +101,7 @@ const useStyles = makeStyles(theme => ({ padding: "20px 20px 20px 20px", // scrollBehavior: "smooth", overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - // borderRadius: "2px", - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, circleLoading: { diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index 5fadd84..a1234f3 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -24,14 +24,7 @@ const useStyles = makeStyles(theme => ({ tabContainer: { overflowY: "auto", maxHeight: 350, - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, popoverPaper: { width: "100%", diff --git a/frontend/src/components/PaginationActions/index.js b/frontend/src/components/PaginationActions/index.js deleted file mode 100644 index 854eae7..0000000 --- a/frontend/src/components/PaginationActions/index.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; - -import { makeStyles } from "@material-ui/core/styles"; - -import FirstPageIcon from "@material-ui/icons/FirstPage"; -import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; -import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; -import LastPageIcon from "@material-ui/icons/LastPage"; -import IconButton from "@material-ui/core/IconButton"; - -const useStyles = makeStyles(theme => ({ - root: { - flexShrink: 0, - marginLeft: theme.spacing(2.5), - }, -})); - -const PaginationActions = ({ count, page, rowsPerPage, onChangePage }) => { - const classes = useStyles(); - - const handleFirstPageButtonClick = event => { - onChangePage(event, 0); - }; - - const handleBackButtonClick = event => { - onChangePage(event, page - 1); - }; - - const handleNextButtonClick = event => { - onChangePage(event, page + 1); - }; - - const handleLastPageButtonClick = event => { - onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( -
- - {} - - - {} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="next page" - > - {} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="last page" - > - {} - -
- ); -}; - -export default PaginationActions; diff --git a/frontend/src/components/TableRowSkeleton/index.js b/frontend/src/components/TableRowSkeleton/index.js new file mode 100644 index 0000000..e8fc618 --- /dev/null +++ b/frontend/src/components/TableRowSkeleton/index.js @@ -0,0 +1,78 @@ +import React from "react"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import Skeleton from "@material-ui/lab/Skeleton"; + +const TableRowSkeleton = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TableRowSkeleton; diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index dabd112..d34b9bd 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -23,14 +23,7 @@ const useStyles = makeStyles(theme => ({ ticketsList: { flex: 1, 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, borderTop: "2px solid rgba(0, 0, 0, 0.12)", }, diff --git a/frontend/src/components/Title/index.js b/frontend/src/components/Title/index.js new file mode 100644 index 0000000..b3fc24c --- /dev/null +++ b/frontend/src/components/Title/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import Typography from "@material-ui/core/Typography"; + +export default function Title(props) { + return ( + + {props.children} + + ); +} diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js new file mode 100644 index 0000000..f5402bb --- /dev/null +++ b/frontend/src/components/UserModal/index.js @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from "react"; + +import * as Yup from "yup"; +import { Formik, Form, Field } from "formik"; + +import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Select from "@material-ui/core/Select"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; + +// import { i18n } from "../../translate/i18n"; + +import api from "../../services/api"; + +const useStyles = makeStyles(theme => ({ + root: { + display: "flex", + flexWrap: "wrap", + }, + textField: { + // marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + // width: "25ch", + flex: 1, + }, + + btnWrapper: { + // margin: theme.spacing(1), + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + formControl: { + margin: theme.spacing(1), + minWidth: 120, + }, +})); + +const UserSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), + email: Yup.string().email("Invalid email").required("Required"), +}); + +const UserModal = ({ open, onClose, userId }) => { + const classes = useStyles(); + + const initialState = { + name: "", + email: "", + password: "", + profile: "user", + }; + + const [user, setUser] = useState(initialState); + + useEffect(() => { + const fetchUser = async () => { + if (!userId) return; + const { data } = await api.get(`/users/${userId}`); + setUser(prevState => { + return { ...prevState, ...data }; + }); + }; + + fetchUser(); + }, [userId, open]); + + const handleClose = () => { + onClose(); + setUser(initialState); + }; + + const handleSaveUser = async values => { + try { + if (userId) { + await api.put(`/users/${userId}`, values); + } else { + await api.post("/users", values); + } + } catch (err) { + alert(JSON.stringify(err.response.data, null, 2)); + console.log(err); + } + handleClose(); + }; + + return ( +
+ + + {userId ? `Edit User` : `New User`} + + { + setTimeout(() => { + handleSaveUser(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting }) => ( +
+ + + +
+ + + + Profile + + + Admin + User + + +
+
+ + + + +
+ )} +
+
+
+ ); +}; + +export default UserModal; diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index abe5727..79ba541 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -60,12 +60,12 @@ const MainListItems = () => { Administration } /> } /> diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index 74b882b..e8a7b08 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useReducer } from "react"; import openSocket from "socket.io-client"; import { makeStyles } from "@material-ui/core/styles"; @@ -10,93 +10,100 @@ import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import Button from "@material-ui/core/Button"; import Avatar from "@material-ui/core/Avatar"; -import TableFooter from "@material-ui/core/TableFooter"; -import TablePagination from "@material-ui/core/TablePagination"; + import SearchIcon from "@material-ui/icons/Search"; import TextField from "@material-ui/core/TextField"; -import Container from "@material-ui/core/Container"; import InputAdornment from "@material-ui/core/InputAdornment"; -import Typography from "@material-ui/core/Typography"; import IconButton from "@material-ui/core/IconButton"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import EditIcon from "@material-ui/icons/Edit"; -import PaginationActions from "../../components/PaginationActions"; import api from "../../services/api"; -import ContactsSekeleton from "../../components/ContactsSekeleton"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; import ContactModal from "../../components/ContactModal"; import ConfirmationModal from "../../components/ConfirmationModal/"; import { i18n } from "../../translate/i18n"; +import MainHeader from "../../components/MainHeader"; +import Title from "../../components/Title"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import MainContainer from "../../components/MainContainer"; + +const reducer = (state, action) => { + if (action.type === "LOAD_CONTACTS") { + const contacts = action.payload; + const newContacts = []; + + contacts.forEach(contact => { + const contactIndex = state.findIndex(c => c.id === contact.id); + if (contactIndex !== -1) { + state[contactIndex] = contact; + } else { + newContacts.push(contact); + } + }); + + return [...state, ...newContacts]; + } + + if (action.type === "UPDATE_CONTACT") { + const updatedContact = action.payload; + const contactIndex = state.findIndex(c => c.id === updatedContact.id); + + if (contactIndex !== -1) { + state[contactIndex] = updatedContact; + } + + return [...state]; + } + + if (action.type === "DELETE_CONTACT") { + const contactId = action.payload; + console.log("cai aqui", contactId); + + const contactIndex = state.findIndex(c => c.id === contactId); + if (contactIndex !== -1) { + console.log("cai no if"); + + state.splice(contactIndex, 1); + } + return [...state]; + } +}; const useStyles = makeStyles(theme => ({ - mainContainer: { - flex: 1, - padding: theme.spacing(2), - height: `calc(100% - 48px)`, - }, - - contentWrapper: { - height: "100%", - overflowY: "hidden", - display: "flex", - flexDirection: "column", - }, - - contactsHeader: { - display: "flex", - alignItems: "center", - padding: "0px 6px 6px 6px", - }, - - actionButtons: { - flex: "none", - marginLeft: "auto", - "& > *": { - margin: theme.spacing(1), - }, - }, - mainPaper: { flex: 1, - padding: theme.spacing(2), + padding: theme.spacing(1), 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, }, })); const Contacts = () => { const classes = useStyles(); - const [loading, setLoading] = useState(true); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [count, setCount] = useState(0); + const [loading, setLoading] = useState(false); + const [pageNumber, setPageNumber] = useState(1); const [searchParam, setSearchParam] = useState(""); - const [contacts, setContacts] = useState([]); + const [contacts, dispatch] = useReducer(reducer, []); const [selectedContactId, setSelectedContactId] = useState(null); const [contactModalOpen, setContactModalOpen] = useState(false); const [deletingContact, setDeletingContact] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); + const [hasMore, setHasMore] = useState(false); useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { const fetchContacts = async () => { try { - const res = await api.get("/contacts/", { - params: { searchParam, pageNumber: page + 1, rowsPerPage }, + const { data } = await api.get("/contacts/", { + params: { searchParam, pageNumber }, }); - setContacts(res.data.contacts); - setCount(res.data.count); + dispatch({ type: "LOAD_CONTACTS", payload: data.contacts }); + setHasMore(data.hasMore); setLoading(false); } catch (err) { console.log(err); @@ -106,17 +113,17 @@ const Contacts = () => { fetchContacts(); }, 500); return () => clearTimeout(delayDebounceFn); - }, [searchParam, page, rowsPerPage]); + }, [searchParam, pageNumber]); useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); socket.on("contact", data => { if (data.action === "update" || data.action === "create") { - updateContacts(data.contact); + dispatch({ type: "UPDATE_CONTACT", payload: data.contact }); } if (data.action === "delete") { - deleteContact(data.contactId); + dispatch({ type: "DELETE_CONTACT", payload: +data.contactId }); } }); @@ -125,40 +132,6 @@ const Contacts = () => { }; }, []); - const updateContacts = contact => { - setContacts(prevState => { - const contactIndex = prevState.findIndex(c => c.id === contact.id); - - if (contactIndex === -1) { - return [contact, ...prevState]; - } - const aux = [...prevState]; - aux[contactIndex] = contact; - return aux; - }); - }; - - const deleteContact = contactId => { - setContacts(prevState => { - const contactIndex = prevState.findIndex(c => c.id === +contactId); - - if (contactIndex === -1) return prevState; - - const aux = [...prevState]; - aux.splice(contactIndex, 1); - return aux; - }); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = event => { - setRowsPerPage(+event.target.value); - setPage(0); - }; - const handleSearch = event => { setSearchParam(event.target.value.toLowerCase()); }; @@ -186,7 +159,7 @@ const Contacts = () => { } setDeletingContact(null); setSearchParam(""); - setPage(0); + setPageNumber(1); }; const handleimportContact = async () => { @@ -197,8 +170,20 @@ const Contacts = () => { } }; + const loadMore = () => { + setPageNumber(prevState => prevState + 1); + }; + + const handleScroll = e => { + if (!hasMore || loading) return; + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore(); + } + }; + return ( - + { ? `${i18n.t("contacts.confirmationModal.deleteMessage")}` : `${i18n.t("contacts.confirmationModal.importMessage")}`} -
-
- - {i18n.t("contacts.title")} - + + {i18n.t("contacts.title")} + + + + + ), + }} + /> + + + + + + + + + + {i18n.t("contacts.table.name")} + {i18n.t("contacts.table.whatsapp")} + {i18n.t("contacts.table.email")} + + {i18n.t("contacts.table.actions")} + + + + + <> + {contacts.map(contact => ( + + + {} + + {contact.name} + {contact.number} + {contact.email} + + hadleEditContact(contact.id)} + > + + -
- - - - ), - }} - /> - - -
- - -
- - - - {i18n.t("contacts.table.name")} - {i18n.t("contacts.table.whatsapp")} - {i18n.t("contacts.table.email")} - - {i18n.t("contacts.table.actions")} - - - - - {loading ? ( - - ) : ( - <> - {contacts.map(contact => ( - - - {} - - {contact.name} - {contact.number} - {contact.email} - - hadleEditContact(contact.id)} - > - - - - { - setConfirmOpen(true); - setDeletingContact(contact); - }} - > - - - - - ))} - - )} - - - - - - -
-
-
- + { + setConfirmOpen(true); + setDeletingContact(contact); + }} + > + + + + + ))} + {loading && } + + + + + ); }; diff --git a/frontend/src/pages/Dashboard/Title.js b/frontend/src/pages/Dashboard/Title.js index cd59203..8fa5dad 100644 --- a/frontend/src/pages/Dashboard/Title.js +++ b/frontend/src/pages/Dashboard/Title.js @@ -1,10 +1,12 @@ import React from "react"; import Typography from "@material-ui/core/Typography"; -export default function Title(props) { +const Title = props => { return ( {props.children} ); -} +}; + +export default Title; diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index c02e995..9823670 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -1,8 +1,10 @@ import React, { useState } from "react"; +import * as Yup from "yup"; import { useHistory } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom"; import { toast } from "react-toastify"; +import { Formik, Form, Field } from "formik"; import Avatar from "@material-ui/core/Avatar"; import Button from "@material-ui/core/Button"; @@ -53,20 +55,26 @@ const useStyles = makeStyles(theme => ({ }, })); +const UserSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), + email: Yup.string().email("Invalid email").required("Required"), +}); + const SignUp = () => { const classes = useStyles(); const history = useHistory(); - const [user, setUser] = useState({ name: "", email: "", password: "" }); + const initialState = { name: "", email: "", password: "" }; - const handleChangeInput = e => { - setUser({ ...user, [e.target.name]: e.target.value }); - }; + const [user] = useState(initialState); - const handleSignUp = async e => { - e.preventDefault(); + const handleSignUp = async values => { try { - await api.post("/users", user); + await api.post("/auth/signup", values); toast.success(i18n.t("signup.toasts.success")); history.push("/login"); } catch (err) { @@ -84,68 +92,88 @@ const SignUp = () => { {i18n.t("signup.title")} -
- - - - + {/* */} + { + setTimeout(() => { + handleSignUp(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting }) => ( + + + + + - - + + + + + + + - - - - {i18n.t("signup.buttons.login")} - - - - + variant="contained" + color="primary" + className={classes.submit} + > + {i18n.t("signup.buttons.submit")} + + + + + {i18n.t("signup.buttons.login")} + + + + + )} +
diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js new file mode 100644 index 0000000..faa76c9 --- /dev/null +++ b/frontend/src/pages/Users/index.js @@ -0,0 +1,234 @@ +import React, { useState, useEffect } from "react"; + +import openSocket from "socket.io-client"; + +import { makeStyles } from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import Button from "@material-ui/core/Button"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import IconButton from "@material-ui/core/IconButton"; +import SearchIcon from "@material-ui/icons/Search"; +import TextField from "@material-ui/core/TextField"; +import InputAdornment from "@material-ui/core/InputAdornment"; + +import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; +import EditIcon from "@material-ui/icons/Edit"; + +import MainContainer from "../../components/MainContainer"; +import MainHeader from "../../components/MainHeader"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import Title from "../../components/Title"; + +import api from "../../services/api"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; +import UserModal from "../../components/UserModal"; +import ConfirmationModal from "../../components/ConfirmationModal"; + +const useStyles = makeStyles(theme => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, +})); + +const Users = () => { + const classes = useStyles(); + + const [loading, setLoading] = useState(false); + const [pageNumber, setPageNumber] = useState(1); + const [selectedUserId, setSelectedUserId] = useState(null); + const [userModalOpen, setUserModalOpen] = useState(false); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const [deletingUser, setDeletingUser] = useState(null); + const [searchParam, setSearchParam] = useState(""); + const [users, setUsers] = useState([]); + + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchUsers = async () => { + try { + const res = await api.get("/users/", { + params: { searchParam, pageNumber }, + }); + setUsers(res.data.users); + setLoading(false); + } catch (err) { + console.log(err); + alert(err); + } + }; + fetchUsers(); + }, 500); + return () => clearTimeout(delayDebounceFn); + }, [searchParam, pageNumber]); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("user", data => { + if (data.action === "update" || data.action === "create") { + updateUsers(data.user); + } + + if (data.action === "delete") { + deleteUser(data.userId); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + + const updateUsers = user => { + setUsers(prevState => { + const userIndex = prevState.findIndex(c => c.id === user.id); + + if (userIndex === -1) { + return [user, ...prevState]; + } + const aux = [...prevState]; + aux[userIndex] = user; + return aux; + }); + }; + + const deleteUser = userId => { + setUsers(prevState => { + const userIndex = prevState.findIndex(c => c.id === +userId); + + if (userIndex === -1) return prevState; + + const aux = [...prevState]; + aux.splice(userIndex, 1); + return aux; + }); + }; + + const handleOpenUserModal = () => { + setSelectedUserId(null); + setUserModalOpen(true); + }; + + const handleCloseUserModal = () => { + setSelectedUserId(null); + setUserModalOpen(false); + }; + + const handleSearch = event => { + setSearchParam(event.target.value.toLowerCase()); + }; + + const handleEditUser = userId => { + setSelectedUserId(userId); + setUserModalOpen(true); + }; + + const handleDeleteUser = async userId => { + try { + await api.delete(`/users/${userId}`); + } catch (err) { + alert(err); + } + setDeletingUser(null); + setSearchParam(""); + setPageNumber(1); + }; + + return ( + + handleDeleteUser(deletingUser.id)} + > + Are you sure? It canoot be reverted. + + + + Usuários + + + + + ), + }} + /> + + + + + + + + Name + Email + Profile + Actions + + + + {loading ? ( + + ) : ( + <> + {users.map(user => ( + + {user.name} + {user.email} + {user.profile} + + handleEditUser(user.id)} + > + + + + { + setConfirmModalOpen(true); + setDeletingUser(user); + }} + > + + + + + ))} + + )} + +
+
+
+ ); +}; + +export default Users; diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index bd55707..4042e26 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -8,6 +8,7 @@ import Chat from "../pages/Chat/"; import Signup from "../pages/Signup/"; import Login from "../pages/Login/"; import WhatsAuth from "../pages/WhatsAuth/WhatsAuth"; +import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; import { AuthProvider } from "../context/Auth/AuthContext"; import Route from "./Route"; @@ -24,6 +25,7 @@ const Routes = () => { + From 514f1c3b753789e542d590b5cedd6f192d2eb045 Mon Sep 17 00:00:00 2001 From: canove Date: Thu, 3 Sep 2020 17:43:04 -0300 Subject: [PATCH 02/25] improvement: adopted infinity scroll in users and contacts --- backend/src/controllers/UserController.js | 8 +- frontend/src/pages/Contacts/index.js | 15 ++-- frontend/src/pages/Users/index.js | 96 +++++++++++++++-------- 3 files changed, 75 insertions(+), 44 deletions(-) diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index 771d2b2..0a88394 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -7,7 +7,7 @@ const User = require("../models/User"); const { getIO } = require("../libs/socket"); exports.index = async (req, res) => { - const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query; + const { searchParam = "", pageNumber = 1 } = req.query; const whereCondition = { [Op.or]: [ @@ -22,7 +22,7 @@ exports.index = async (req, res) => { ], }; - let limit = +rowsPerPage; + let limit = 20; let offset = limit * (pageNumber - 1); const { count, rows: users } = await User.findAndCountAll({ @@ -33,7 +33,9 @@ exports.index = async (req, res) => { order: [["createdAt", "DESC"]], }); - return res.status(200).json({ users, count }); + const hasMore = count > offset + users.length; + + return res.status(200).json({ users, count, hasMore }); }; exports.store = async (req, res, next) => { diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index e8a7b08..ab6bb4b 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -47,25 +47,22 @@ const reducer = (state, action) => { return [...state, ...newContacts]; } - if (action.type === "UPDATE_CONTACT") { - const updatedContact = action.payload; - const contactIndex = state.findIndex(c => c.id === updatedContact.id); + if (action.type === "UPDATE_CONTACTS") { + const contact = action.payload; + const contactIndex = state.findIndex(c => c.id === contact.id); if (contactIndex !== -1) { - state[contactIndex] = updatedContact; + state[contactIndex] = contact; } - return [...state]; + return [contact, ...state]; } if (action.type === "DELETE_CONTACT") { const contactId = action.payload; - console.log("cai aqui", contactId); const contactIndex = state.findIndex(c => c.id === contactId); if (contactIndex !== -1) { - console.log("cai no if"); - state.splice(contactIndex, 1); } return [...state]; @@ -119,7 +116,7 @@ const Contacts = () => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); socket.on("contact", data => { if (data.action === "update" || data.action === "create") { - dispatch({ type: "UPDATE_CONTACT", payload: data.contact }); + dispatch({ type: "UPDATE_CONTACTS", payload: data.contact }); } if (data.action === "delete") { diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index faa76c9..45c3476 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useReducer } from "react"; import openSocket from "socket.io-client"; @@ -28,6 +28,45 @@ import TableRowSkeleton from "../../components/TableRowSkeleton"; import UserModal from "../../components/UserModal"; import ConfirmationModal from "../../components/ConfirmationModal"; +const reducer = (state, action) => { + if (action.type === "LOAD_USERS") { + const users = action.payload; + const newUsers = []; + + users.forEach(user => { + const userIndex = state.findIndex(u => u.id === user.id); + if (userIndex !== -1) { + state[userIndex] = user; + } else { + newUsers.push(user); + } + }); + + return [...state, ...newUsers]; + } + + if (action.type === "UPDATE_USERS") { + const user = action.payload; + const userIndex = state.findIndex(u => u.id === user.id); + + if (userIndex !== -1) { + state[userIndex] = user; + } + + return [user, ...state]; + } + + if (action.type === "DELETE_USER") { + const userId = action.payload; + + const userIndex = state.findIndex(u => u.id === userId); + if (userIndex !== -1) { + state.splice(userIndex, 1); + } + return [...state]; + } +}; + const useStyles = makeStyles(theme => ({ mainPaper: { flex: 1, @@ -42,22 +81,24 @@ const Users = () => { const [loading, setLoading] = useState(false); const [pageNumber, setPageNumber] = useState(1); + const [hasMore, setHasMore] = useState(false); const [selectedUserId, setSelectedUserId] = useState(null); const [userModalOpen, setUserModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [deletingUser, setDeletingUser] = useState(null); const [searchParam, setSearchParam] = useState(""); - const [users, setUsers] = useState([]); + const [users, dispatch] = useReducer(reducer, []); useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { const fetchUsers = async () => { try { - const res = await api.get("/users/", { + const { data } = await api.get("/users/", { params: { searchParam, pageNumber }, }); - setUsers(res.data.users); + dispatch({ type: "LOAD_USERS", payload: data.users }); + setHasMore(data.hasMore); setLoading(false); } catch (err) { console.log(err); @@ -73,11 +114,11 @@ const Users = () => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); socket.on("user", data => { if (data.action === "update" || data.action === "create") { - updateUsers(data.user); + dispatch({ type: "UPDATE_USERS", payload: data.user }); } if (data.action === "delete") { - deleteUser(data.userId); + dispatch({ type: "DELETE_USER", payload: +data.userId }); } }); @@ -86,31 +127,6 @@ const Users = () => { }; }, []); - const updateUsers = user => { - setUsers(prevState => { - const userIndex = prevState.findIndex(c => c.id === user.id); - - if (userIndex === -1) { - return [user, ...prevState]; - } - const aux = [...prevState]; - aux[userIndex] = user; - return aux; - }); - }; - - const deleteUser = userId => { - setUsers(prevState => { - const userIndex = prevState.findIndex(c => c.id === +userId); - - if (userIndex === -1) return prevState; - - const aux = [...prevState]; - aux.splice(userIndex, 1); - return aux; - }); - }; - const handleOpenUserModal = () => { setSelectedUserId(null); setUserModalOpen(true); @@ -141,6 +157,18 @@ const Users = () => { setPageNumber(1); }; + const loadMore = () => { + setPageNumber(prevState => prevState + 1); + }; + + const handleScroll = e => { + if (!hasMore || loading) return; + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore(); + } + }; + return ( { - + From c032ec309a52090c1bdd82ee6b565cbbced3676d Mon Sep 17 00:00:00 2001 From: canove Date: Thu, 3 Sep 2020 18:31:12 -0300 Subject: [PATCH 03/25] improvement: better names in pages, folders and routes --- README.md | 2 +- frontend/src/components/MessagesList/index.js | 4 ++-- frontend/src/components/NewTicketModal/index.js | 2 +- .../src/components/NotificationsPopOver/index.js | 4 ++-- frontend/src/components/TicketListItem/index.js | 4 ++-- frontend/src/components/TicketOptionsMenu/index.js | 2 +- frontend/src/components/_layout/MainListItems.js | 4 ++-- frontend/src/context/Auth/useAuth.js | 2 +- .../{WhatsAuth/WhatsAuth.js => Connection/index.js} | 2 +- frontend/src/pages/Settings/index.js | 0 frontend/src/pages/{Chat => Tickets}/index.js | 0 frontend/src/routes/index.js | 13 +++++++++---- 12 files changed, 22 insertions(+), 17 deletions(-) rename frontend/src/pages/{WhatsAuth/WhatsAuth.js => Connection/index.js} (94%) create mode 100644 frontend/src/pages/Settings/index.js rename frontend/src/pages/{Chat => Tickets}/index.js (100%) diff --git a/README.md b/README.md index bcf2b33..d0487b6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ If a contact sent a new message in less than 2 hours interval, and there is no t ## Screenshots - + ## Demo diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 6e04f27..16f16a0 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -304,7 +304,7 @@ const MessagesList = () => { } catch (err) { console.log(err); toast.error("Ticket não encontrado"); - history.push("/chat"); + history.push("/tickets"); } }; fetchMessages(); @@ -418,7 +418,7 @@ const MessagesList = () => { } catch (err) { console.log(err); } - history.push("/chat"); + history.push("/tickets"); }; const handleDrawerOpen = () => { diff --git a/frontend/src/components/NewTicketModal/index.js b/frontend/src/components/NewTicketModal/index.js index 0f273b9..83769e2 100644 --- a/frontend/src/components/NewTicketModal/index.js +++ b/frontend/src/components/NewTicketModal/index.js @@ -85,7 +85,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => { userId: userId, status: "open", }); - history.push(`/chat/${ticket.id}`); + history.push(`/tickets/${ticket.id}`); } catch (err) { alert(err); } diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index a1234f3..34917c1 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -116,7 +116,7 @@ const NotificationsPopOver = () => { notification.onclick = function (event) { event.preventDefault(); // - window.open(`/chat/${ticket.id}`, "_self"); + window.open(`/tickets/${ticket.id}`, "_self"); }; document.addEventListener("visibilitychange", () => { @@ -137,7 +137,7 @@ const NotificationsPopOver = () => { }, [setIsOpen]); const handleSelectTicket = (e, ticket) => { - history.push(`/chat/${ticket.id}`); + history.push(`/tickets/${ticket.id}`); handleClickAway(); }; diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 04f3720..8494f4d 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -100,11 +100,11 @@ const TicketListItem = ({ ticket }) => { } catch (err) { alert(err); } - history.push(`/chat/${ticketId}`); + history.push(`/tickets/${ticketId}`); }; const handleSelectTicket = (e, ticket) => { - history.push(`/chat/${ticket.id}`); + history.push(`/tickets/${ticket.id}`); }; return ( diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index a3f7a5c..fc92752 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -18,7 +18,7 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { try { await api.delete(`/tickets/${ticket.id}`); toast.success("Ticket deletado com sucesso."); - history.push("/chat"); + history.push("/tickets"); } catch (err) { toast.error("Erro ao deletar o ticket"); } diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index 79ba541..ebfa195 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -42,12 +42,12 @@ const MainListItems = () => {
} /> } /> } /> diff --git a/frontend/src/context/Auth/useAuth.js b/frontend/src/context/Auth/useAuth.js index e7611ea..995c10b 100644 --- a/frontend/src/context/Auth/useAuth.js +++ b/frontend/src/context/Auth/useAuth.js @@ -51,7 +51,7 @@ const useAuth = () => { api.defaults.headers.Authorization = `Bearer ${res.data.token}`; setIsAuth(true); toast.success(i18n.t("auth.toasts.success")); - history.push("/chat"); + history.push("/tickets"); } catch (err) { toast.error(i18n.t("auth.toasts.fail")); } diff --git a/frontend/src/pages/WhatsAuth/WhatsAuth.js b/frontend/src/pages/Connection/index.js similarity index 94% rename from frontend/src/pages/WhatsAuth/WhatsAuth.js rename to frontend/src/pages/Connection/index.js index 3773159..9fa4a15 100644 --- a/frontend/src/pages/WhatsAuth/WhatsAuth.js +++ b/frontend/src/pages/Connection/index.js @@ -71,7 +71,7 @@ const WhatsAuth = () => { if (data.action === "authentication") { setQrCode(""); setSession(data.session); - history.push("/chat"); + history.push("/tickets"); } }); diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/Chat/index.js b/frontend/src/pages/Tickets/index.js similarity index 100% rename from frontend/src/pages/Chat/index.js rename to frontend/src/pages/Tickets/index.js diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index 4042e26..3932021 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -4,10 +4,10 @@ import { ToastContainer } from "react-toastify"; import MainDrawer from "../components/_layout"; import Dashboard from "../pages/Dashboard/"; -import Chat from "../pages/Chat/"; +import Tickets from "../pages/Tickets/"; import Signup from "../pages/Signup/"; import Login from "../pages/Login/"; -import WhatsAuth from "../pages/WhatsAuth/WhatsAuth"; +import Connection from "../pages/Connection/"; import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; import { AuthProvider } from "../context/Auth/AuthContext"; @@ -22,8 +22,13 @@ const Routes = () => { - - + + From f52e7a667c6ad383b58a958469c4f30f604b80b4 Mon Sep 17 00:00:00 2001 From: canove Date: Thu, 3 Sep 2020 18:59:10 -0300 Subject: [PATCH 04/25] feat: started settings style --- frontend/src/pages/Settings/index.js | 73 ++++++++++++++++++++++++++++ frontend/src/routes/index.js | 2 + 2 files changed, 75 insertions(+) diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index e69de29..c4b1d32 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -0,0 +1,73 @@ +import React, { useState, useEffect } from "react"; +import { useHistory } from "react-router-dom"; +import api from "../../services/api"; +import openSocket from "socket.io-client"; + +import { makeStyles } from "@material-ui/core/styles"; + +import Paper from "@material-ui/core/Paper"; + +import Switch from "@material-ui/core/Switch"; +import Typography from "@material-ui/core/Typography"; +import Container from "@material-ui/core/Container"; + +const useStyles = makeStyles(theme => ({ + root: { + display: "flex", + alignItems: "center", + padding: theme.spacing(4), + }, + + paper: { + padding: theme.spacing(2), + display: "flex", + alignItems: "center", + }, + + switch: { + marginLeft: "auto", + }, +})); + +const WhatsAuth = () => { + const classes = useStyles(); + // const history = useHistory(); + + const [settings, setSettings] = useState([]); + + // useEffect(() => { + // const fetchSession = async () => { + // try { + // const { data } = await api.get("/whatsapp/session/1"); + // setQrCode(data.qrcode); + // setSession(data); + // } catch (err) { + // console.log(err); + // } + // }; + // fetchSession(); + // }, []); + + return ( +
+ + + Settings + + + Enable user creation + setShowAllTickets(prevState => !prevState)} + name="showAllTickets" + color="primary" + /> + + +
+ ); +}; + +export default WhatsAuth; diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index 3932021..b48a030 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -8,6 +8,7 @@ import Tickets from "../pages/Tickets/"; import Signup from "../pages/Signup/"; import Login from "../pages/Login/"; import Connection from "../pages/Connection/"; +import Settings from "../pages/Settings/"; import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; import { AuthProvider } from "../context/Auth/AuthContext"; @@ -31,6 +32,7 @@ const Routes = () => { + From bc376e2b1cfa5ef7a6f0208ac9a51390d24a23dc Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 05:08:30 -0300 Subject: [PATCH 05/25] feat: added settings api on beackend --- backend/src/app.js | 15 ++-------- backend/src/config/database.js | 1 + backend/src/controllers/SettingController.js | 23 +++++++++++++++ backend/src/database/index.js | 11 ++++++- .../20200903215941-create-settings.js | 29 +++++++++++++++++++ .../20200904070004-create-default-settings.js | 22 ++++++++++++++ backend/src/models/Setting.js | 24 +++++++++++++++ backend/src/router/index.js | 21 ++++++++++++++ backend/src/{ => router}/routes/auth.js | 6 ++-- backend/src/{ => router}/routes/contacts.js | 6 ++-- backend/src/{ => router}/routes/messages.js | 4 +-- backend/src/router/routes/settings.js | 14 +++++++++ backend/src/{ => router}/routes/tickets.js | 4 +-- backend/src/{ => router}/routes/users.js | 5 ++-- backend/src/{ => router}/routes/whatsapp.js | 4 +-- 15 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 backend/src/controllers/SettingController.js create mode 100644 backend/src/database/migrations/20200903215941-create-settings.js create mode 100644 backend/src/database/seeds/20200904070004-create-default-settings.js create mode 100644 backend/src/models/Setting.js create mode 100644 backend/src/router/index.js rename backend/src/{ => router}/routes/auth.js (58%) rename backend/src/{ => router}/routes/contacts.js (67%) rename backend/src/{ => router}/routes/messages.js (63%) create mode 100644 backend/src/router/routes/settings.js rename backend/src/{ => router}/routes/tickets.js (73%) rename backend/src/{ => router}/routes/users.js (71%) rename backend/src/{ => router}/routes/whatsapp.js (71%) diff --git a/backend/src/app.js b/backend/src/app.js index f743ec9..98f1607 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -11,12 +11,7 @@ const wBot = require("./libs/wbot"); const wbotMessageListener = require("./services/wbotMessageListener"); const wbotMonitor = require("./services/wbotMonitor"); -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 +35,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 => { 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/SettingController.js b/backend/src/controllers/SettingController.js new file mode 100644 index 0000000..0c1362d --- /dev/null +++ b/backend/src/controllers/SettingController.js @@ -0,0 +1,23 @@ +const Sequelize = require("sequelize"); + +const Setting = require("../models/Setting"); + +exports.index = async (req, res) => { + const settings = await Setting.findAll(); + + return res.status(200).json(settings); +}; + +exports.update = async (req, res) => { + const { settingKey } = req.params; + + const setting = await Setting.findByPk(settingKey); + + if (!setting) { + return res.status(400).json({ error: "No setting found with this ID" }); + } + + await setting.update(req.body); + + return res.status(200).json(setting); +}; 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/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/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/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/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/routes/auth.js b/backend/src/router/routes/auth.js similarity index 58% rename from backend/src/routes/auth.js rename to backend/src/router/routes/auth.js index 28a944e..930d1e6 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/router/routes/auth.js @@ -1,7 +1,7 @@ const express = require("express"); -const SessionController = require("../controllers/SessionController"); -const UserController = require("../controllers/UserController"); -const isAuth = require("../middleware/is-auth"); +const SessionController = require("../../controllers/SessionController"); +const UserController = require("../../controllers/UserController"); +const isAuth = require("../../middleware/is-auth"); const routes = express.Router(); 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/routes/users.js b/backend/src/router/routes/users.js similarity index 71% rename from backend/src/routes/users.js rename to backend/src/router/routes/users.js index bbdb0de..8a50e61 100644 --- a/backend/src/routes/users.js +++ b/backend/src/router/routes/users.js @@ -1,8 +1,7 @@ const express = require("express"); -const User = require("../models/User"); -const isAuth = require("../middleware/is-auth"); -const UserController = require("../controllers/UserController"); +const isAuth = require("../../middleware/is-auth"); +const UserController = require("../../controllers/UserController"); const routes = express.Router(); diff --git a/backend/src/routes/whatsapp.js b/backend/src/router/routes/whatsapp.js similarity index 71% rename from backend/src/routes/whatsapp.js rename to backend/src/router/routes/whatsapp.js index 7d27526..4eb8758 100644 --- a/backend/src/routes/whatsapp.js +++ b/backend/src/router/routes/whatsapp.js @@ -1,7 +1,7 @@ const express = require("express"); -const isAuth = require("../middleware/is-auth"); +const isAuth = require("../../middleware/is-auth"); -const WhatsAppSessionController = require("../controllers/WhatsAppSessionController"); +const WhatsAppSessionController = require("../../controllers/WhatsAppSessionController"); const routes = express.Router(); From e4e918ab90105369d9e7aae0a287e5ee06906d8c Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 10:35:34 -0300 Subject: [PATCH 06/25] feat: finished settings page --- backend/src/controllers/SettingController.js | 10 +- backend/src/controllers/TicketController.js | 6 +- frontend/src/pages/Contacts/index.js | 14 ++- frontend/src/pages/Settings/index.js | 108 +++++++++++++------ frontend/src/pages/Users/index.js | 14 ++- 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/backend/src/controllers/SettingController.js b/backend/src/controllers/SettingController.js index 0c1362d..322e8c9 100644 --- a/backend/src/controllers/SettingController.js +++ b/backend/src/controllers/SettingController.js @@ -1,6 +1,5 @@ -const Sequelize = require("sequelize"); - const Setting = require("../models/Setting"); +const { getIO } = require("../libs/socket"); exports.index = async (req, res) => { const settings = await Setting.findAll(); @@ -9,8 +8,8 @@ exports.index = async (req, res) => { }; exports.update = async (req, res) => { + const io = getIO(); const { settingKey } = req.params; - const setting = await Setting.findByPk(settingKey); if (!setting) { @@ -19,5 +18,10 @@ exports.update = async (req, res) => { 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 fa40be8..3e42513 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -121,7 +121,7 @@ exports.store = async (req, res) => { ticket: serializaedTicket, }); - res.status(200).json(ticket); + return res.status(200).json(ticket); }; exports.update = async (req, res) => { @@ -149,7 +149,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 +169,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/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index ab6bb4b..e46365f 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -53,9 +53,10 @@ const reducer = (state, action) => { if (contactIndex !== -1) { state[contactIndex] = contact; + return [...state]; + } else { + return [contact, ...state]; } - - return [contact, ...state]; } if (action.type === "DELETE_CONTACT") { @@ -67,6 +68,10 @@ const reducer = (state, action) => { } return [...state]; } + + if (action.type === "RESET") { + return []; + } }; const useStyles = makeStyles(theme => ({ @@ -91,6 +96,11 @@ const Contacts = () => { const [confirmOpen, setConfirmOpen] = useState(false); const [hasMore, setHasMore] = useState(false); + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); + }, [searchParam]); + useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index c4b1d32..418eebf 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -1,15 +1,14 @@ import React, { useState, useEffect } from "react"; -import { useHistory } from "react-router-dom"; -import api from "../../services/api"; import openSocket from "socket.io-client"; import { makeStyles } from "@material-ui/core/styles"; - import Paper from "@material-ui/core/Paper"; - -import Switch from "@material-ui/core/Switch"; import Typography from "@material-ui/core/Typography"; import Container from "@material-ui/core/Container"; +import Select from "@material-ui/core/Select"; +import { toast } from "react-toastify"; + +import api from "../../services/api"; const useStyles = makeStyles(theme => ({ root: { @@ -24,50 +23,97 @@ const useStyles = makeStyles(theme => ({ alignItems: "center", }, - switch: { + settingOption: { marginLeft: "auto", }, + margin: { + margin: theme.spacing(1), + }, })); -const WhatsAuth = () => { +const Settings = () => { const classes = useStyles(); - // const history = useHistory(); const [settings, setSettings] = useState([]); - // useEffect(() => { - // const fetchSession = async () => { - // try { - // const { data } = await api.get("/whatsapp/session/1"); - // setQrCode(data.qrcode); - // setSession(data); - // } catch (err) { - // console.log(err); - // } - // }; - // fetchSession(); - // }, []); + useEffect(() => { + const fetchSession = async () => { + try { + const { data } = await api.get("/settings"); + setSettings(data); + } catch (err) { + console.log(err); + } + }; + fetchSession(); + }, []); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("settings", data => { + if (data.action === "update") { + // dispatch({ type: "UPDATE_USERS", payload: data.user }); + setSettings(prevState => { + const aux = [...prevState]; + const settingIndex = aux.findIndex(s => s.key === data.setting.key); + aux[settingIndex].value = data.setting.value; + return aux; + }); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + + const handleChangeSetting = async e => { + const selectedValue = e.target.value; + const settingKey = e.target.name; + + try { + await api.put(`/settings/${settingKey}`, { + value: selectedValue, + }); + toast.success("Setting updated"); + } catch (err) { + alert(err); + console.log(err); + } + }; + + const getSettingValue = key => { + const setting = settings.find(s => s.key === key); + return setting.value; + }; return (
- + Settings - Enable user creation - setShowAllTickets(prevState => !prevState)} - name="showAllTickets" - color="primary" - /> + User creation +
); }; -export default WhatsAuth; +export default Settings; diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index 45c3476..de554f8 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -51,9 +51,10 @@ const reducer = (state, action) => { if (userIndex !== -1) { state[userIndex] = user; + return [...state]; + } else { + return [user, ...state]; } - - return [user, ...state]; } if (action.type === "DELETE_USER") { @@ -65,6 +66,10 @@ const reducer = (state, action) => { } return [...state]; } + + if (action.type === "RESET") { + return []; + } }; const useStyles = makeStyles(theme => ({ @@ -89,6 +94,11 @@ const Users = () => { const [searchParam, setSearchParam] = useState(""); const [users, dispatch] = useReducer(reducer, []); + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); + }, [searchParam]); + useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { From 364b94c8abd1d5cc43ae759ddd19eb8d2e741994 Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 10:56:47 -0300 Subject: [PATCH 07/25] feat: block user creation if userCreation setting is disabled --- backend/src/controllers/UserController.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index 0a88394..359cdf3 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -3,6 +3,7 @@ const Yup = require("yup"); const { Op } = require("sequelize"); const User = require("../models/User"); +const Setting = require("../models/Setting"); const { getIO } = require("../libs/socket"); @@ -55,6 +56,14 @@ exports.store = async (req, res, next) => { password: Yup.string().required().min(5), }); + const { value: userCreation } = await Setting.findByPk("userCreation"); + + if (userCreation === "disabled") { + return res + .status(403) + .json({ error: "User creation is disabled by administrator" }); + } + await schema.validate(req.body); const io = getIO(); From 3cb3fc1a20bbe070859968bd624761ac4660105a Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 11:22:53 -0300 Subject: [PATCH 08/25] feat: allowing user editing only to admins --- backend/src/controllers/UserController.js | 35 ++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index 359cdf3..710d433 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -40,6 +40,7 @@ exports.index = async (req, res) => { }; exports.store = async (req, res, next) => { + console.log(req.url); const schema = Yup.object().shape({ name: Yup.string().required().min(2), email: Yup.string() @@ -56,12 +57,18 @@ exports.store = async (req, res, next) => { password: Yup.string().required().min(5), }); - const { value: userCreation } = await Setting.findByPk("userCreation"); + if (req.url === "/signup") { + const { value: userCreation } = await Setting.findByPk("userCreation"); - if (userCreation === "disabled") { + if (userCreation === "disabled") { + return res + .status(403) + .json({ error: "User creation is disabled by administrator." }); + } + } else if (req.user.profile !== "admin") { return res .status(403) - .json({ error: "User creation is disabled by administrator" }); + .json({ error: "Only administrators can create users." }); } await schema.validate(req.body); @@ -98,7 +105,11 @@ exports.update = async (req, res) => { password: Yup.string(), }); - console.log("cai aqui"); + if (req.user.profile !== "admin") { + return res + .status(403) + .json({ error: "Only administrators can edit users." }); + } await schema.validate(req.body); @@ -113,6 +124,16 @@ exports.update = async (req, res) => { res.status(400).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." }); + } + console.log("found", adminUsers); + } + await user.update(req.body); io.emit("user", { @@ -133,6 +154,12 @@ 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(); io.emit("user", { From f7fe3286b84919dd0493b9fc4b1733180fdbd538 Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 17:09:39 -0300 Subject: [PATCH 09/25] improvement: better error handling --- backend/src/controllers/ContactController.js | 51 ++-- .../ImportPhoneContactsController.js | 11 +- backend/src/controllers/MessageController.js | 7 +- backend/src/controllers/SessionController.js | 2 +- backend/src/controllers/SettingController.js | 2 +- backend/src/controllers/TicketController.js | 4 +- backend/src/controllers/UserController.js | 23 +- .../controllers/WhatsAppSessionController.js | 2 +- .../seeds/20200824172424-create-contacts.js | 48 ---- .../seeds/20200824173654-create-tickets.js | 261 ------------------ .../seeds/20200824174824-create-messages.js | 148 ---------- frontend/src/components/ContactModal/index.js | 18 +- frontend/src/components/MessageInput/index.js | 13 +- frontend/src/components/MessagesList/index.js | 11 +- .../src/components/NewTicketModal/index.js | 24 +- frontend/src/components/SessionInfo/index.js | 4 + frontend/src/components/UserModal/index.js | 22 +- frontend/src/context/Auth/useAuth.js | 10 +- frontend/src/hooks/useTickets/index.js | 4 + frontend/src/pages/Connection/index.js | 4 + frontend/src/pages/Contacts/index.js | 13 +- frontend/src/pages/Settings/index.js | 11 +- frontend/src/pages/Signup/index.js | 5 +- 23 files changed, 166 insertions(+), 532 deletions(-) delete mode 100644 backend/src/database/seeds/20200824172424-create-contacts.js delete mode 100644 backend/src/database/seeds/20200824173654-create-tickets.js delete mode 100644 backend/src/database/seeds/20200824174824-create-messages.js diff --git a/backend/src/controllers/ContactController.js b/backend/src/controllers/ContactController.js index 3ee343b..2b0072f 100644 --- a/backend/src/controllers/ContactController.js +++ b/backend/src/controllers/ContactController.js @@ -43,23 +43,22 @@ exports.store = async (req, res) => { 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` ); @@ -76,26 +75,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) => { @@ -110,7 +105,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) { @@ -140,7 +135,7 @@ exports.update = async (req, res) => { contact: contact, }); - res.status(200).json(contact); + return res.status(200).json(contact); }; exports.delete = async (req, res) => { @@ -150,7 +145,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(); @@ -160,5 +155,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..19b5537 100644 --- a/backend/src/controllers/ImportPhoneContactsController.js +++ b/backend/src/controllers/ImportPhoneContactsController.js @@ -6,7 +6,16 @@ exports.store = async (req, res, next) => { const io = getIO(); const wbot = getWbot(); - const phoneContacts = await wbot.getContacts(); + 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..2639b55 100644 --- a/backend/src/controllers/MessageController.js +++ b/backend/src/controllers/MessageController.js @@ -38,9 +38,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; @@ -125,6 +122,10 @@ exports.store = async (req, res, next) => { ], }); + if (!ticket) { + return res.status(404).json({ error: "No ticket found with this ID" }); + } + try { if (media) { const newMedia = MessageMedia.fromFilePath(req.file.path); diff --git a/backend/src/controllers/SessionController.js b/backend/src/controllers/SessionController.js index f8d22ff..2f19dca 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))) { diff --git a/backend/src/controllers/SettingController.js b/backend/src/controllers/SettingController.js index 322e8c9..15e4203 100644 --- a/backend/src/controllers/SettingController.js +++ b/backend/src/controllers/SettingController.js @@ -13,7 +13,7 @@ exports.update = async (req, res) => { const setting = await Setting.findByPk(settingKey); if (!setting) { - return res.status(400).json({ error: "No setting found with this ID" }); + return res.status(404).json({ error: "No setting found with this ID" }); } await setting.update(req.body); diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js index 3e42513..825203e 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -105,7 +105,7 @@ 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) => { @@ -139,7 +139,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); diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index 710d433..a77da49 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -71,7 +71,11 @@ exports.store = async (req, res, next) => { .json({ error: "Only administrators can create users." }); } - await schema.validate(req.body); + try { + await schema.validate(req.body); + } catch (err) { + return res.status(400).json({ error: err.message }); + } const io = getIO(); @@ -88,14 +92,15 @@ exports.store = async (req, res, next) => { exports.show = async (req, res) => { const { userId } = req.params; - const { id, name, email, profile } = await User.findByPk(userId); - - return res.status(200).json({ - id, - name, - email, - profile, + const user = await User.findByPk(userId, { + 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) => { @@ -121,7 +126,7 @@ exports.update = async (req, res) => { }); if (!user) { - res.status(400).json({ error: "No user found with this id." }); + res.status(404).json({ error: "No user found with this id." }); } if (user.profile === "admin" && req.body.profile === "user") { diff --git a/backend/src/controllers/WhatsAppSessionController.js b/backend/src/controllers/WhatsAppSessionController.js index 643d330..882f2b1 100644 --- a/backend/src/controllers/WhatsAppSessionController.js +++ b/backend/src/controllers/WhatsAppSessionController.js @@ -21,7 +21,7 @@ exports.delete = async (req, res) => { const dbSession = await Whatsapp.findByPk(sessionId); if (!dbSession) { - return res.status(200).json({ message: "Session not found" }); + return res.status(404).json({ message: "Session not found" }); } await dbSession.update({ session: "", status: "pending" }); 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/frontend/src/components/ContactModal/index.js b/frontend/src/components/ContactModal/index.js index 1d5a0d3..326837d 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; 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"; @@ -76,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(); @@ -95,9 +103,12 @@ const ContactModal = ({ open, onClose, contactId }) => { } else { await api.post("/contacts", values); } + toast.success("Contact saved sucessfully!"); } catch (err) { - alert(JSON.stringify(err.response.data, null, 2)); console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } handleClose(); }; @@ -131,6 +142,7 @@ const ContactModal = ({ open, onClose, contactId }) => { as={TextField} label={i18n.t("contactModal.form.name")} name="name" + autoFocus error={touched.name && Boolean(errors.name)} helperText={touched.name && errors.name} variant="outlined" diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index 14d3373..20dc358 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import "emoji-mart/css/emoji-mart.css"; import { useParams } from "react-router-dom"; import { Picker } from "emoji-mart"; +import { toast } from "react-toastify"; import MicRecorder from "mic-recorder-to-mp3"; import { makeStyles } from "@material-ui/core/styles"; @@ -163,8 +164,10 @@ const MessageInput = () => { try { await api.post(`/messages/${ticketId}`, formData); } 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); + } } setLoading(false); setMedia(mediaInitialState); @@ -182,8 +185,10 @@ const MessageInput = () => { try { await api.post(`/messages/${ticketId}`, message); } 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); + } } setInputMessage(""); setShowEmoji(false); @@ -224,8 +229,10 @@ const MessageInput = () => { try { await api.post(`/messages/${ticketId}`, formData); } 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); + } } setRecording(false); setLoading(false); diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 16f16a0..00b4e55 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -303,8 +303,12 @@ const MessagesList = () => { } } catch (err) { console.log(err); - toast.error("Ticket não encontrado"); - history.push("/tickets"); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + if (err.response.status === 404) { + history.push("/tickets"); + } + } } }; fetchMessages(); @@ -417,6 +421,9 @@ const MessagesList = () => { }); } catch (err) { console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } history.push("/tickets"); }; diff --git a/frontend/src/components/NewTicketModal/index.js b/frontend/src/components/NewTicketModal/index.js index 83769e2..1a6a4a8 100644 --- a/frontend/src/components/NewTicketModal/index.js +++ b/frontend/src/components/NewTicketModal/index.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import { useHistory } from "react-router-dom"; +import { toast } from "react-toastify"; import Button from "@material-ui/core/Button"; import TextField from "@material-ui/core/TextField"; @@ -38,7 +39,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const NewTicketModal = ({ modalOpen, onClose, contactId }) => { +const NewTicketModal = ({ modalOpen, onClose }) => { const history = useHistory(); const classes = useStyles(); const userId = +localStorage.getItem("userId"); @@ -54,18 +55,21 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => { const delayDebounceFn = setTimeout(() => { const fetchContacts = async () => { try { - const res = await api.get("contacts", { - params: { searchParam, rowsPerPage: 20 }, + const { data } = await api.get("contacts", { + params: { searchParam }, }); - setOptions(res.data.contacts); + setOptions(data.contacts); setLoading(false); } catch (err) { - alert(err); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchContacts(); - }, 1000); + }, 500); return () => clearTimeout(delayDebounceFn); }, [searchParam, modalOpen]); @@ -87,7 +91,10 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => { }); history.push(`/tickets/${ticket.id}`); } catch (err) { - alert(err); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } setLoading(false); handleClose(); @@ -107,13 +114,14 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => { `${option.name} - ${option.number}`} onChange={(e, newValue) => { setSelectedContact(newValue); }} options={options} + noOptionsText="No contacts found. Try another term." loading={loading} renderInput={params => ( { await api.delete("/whatsapp/session/1"); } catch (err) { console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index f5402bb..52a33e7 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import * as Yup from "yup"; import { Formik, Form, Field } from "formik"; +import { toast } from "react-toastify"; import { makeStyles } from "@material-ui/core/styles"; import { green } from "@material-ui/core/colors"; @@ -76,10 +77,17 @@ const UserModal = ({ open, onClose, userId }) => { useEffect(() => { const fetchUser = async () => { if (!userId) return; - const { data } = await api.get(`/users/${userId}`); - setUser(prevState => { - return { ...prevState, ...data }; - }); + try { + const { data } = await api.get(`/users/${userId}`); + setUser(prevState => { + return { ...prevState, ...data }; + }); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } }; fetchUser(); @@ -97,9 +105,12 @@ const UserModal = ({ open, onClose, userId }) => { } else { await api.post("/users", values); } + toast.success("Success!"); } catch (err) { - alert(JSON.stringify(err.response.data, null, 2)); console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } handleClose(); }; @@ -127,6 +138,7 @@ const UserModal = ({ open, onClose, userId }) => { { } catch (err) { setLoading(false); setIsAuth(false); - toast.error(i18n.t("auth.toasts.fail")); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; checkAuth(); @@ -53,7 +56,10 @@ const useAuth = () => { toast.success(i18n.t("auth.toasts.success")); history.push("/tickets"); } catch (err) { - toast.error(i18n.t("auth.toasts.fail")); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; diff --git a/frontend/src/hooks/useTickets/index.js b/frontend/src/hooks/useTickets/index.js index 2bd2b36..535f4ca 100644 --- a/frontend/src/hooks/useTickets/index.js +++ b/frontend/src/hooks/useTickets/index.js @@ -1,5 +1,6 @@ import { useState, useEffect, useReducer } from "react"; import openSocket from "socket.io-client"; +import { toast } from "react-toastify"; import api from "../../services/api"; @@ -97,6 +98,9 @@ const useTickets = ({ searchParam, pageNumber, status, date, showAll }) => { setLoading(false); } catch (err) { console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchTickets(); diff --git a/frontend/src/pages/Connection/index.js b/frontend/src/pages/Connection/index.js index 9fa4a15..84ca425 100644 --- a/frontend/src/pages/Connection/index.js +++ b/frontend/src/pages/Connection/index.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { useHistory } from "react-router-dom"; import api from "../../services/api"; import openSocket from "socket.io-client"; +import { toast } from "react-toastify"; import { makeStyles } from "@material-ui/core/styles"; @@ -46,6 +47,9 @@ const WhatsAuth = () => { setSession(data); } catch (err) { console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchSession(); diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index e46365f..5f916a2 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useReducer } from "react"; import openSocket from "socket.io-client"; +import { toast } from "react-toastify"; import { makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -114,7 +115,9 @@ const Contacts = () => { setLoading(false); } catch (err) { console.log(err); - alert(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchContacts(); @@ -161,8 +164,12 @@ const Contacts = () => { const handleDeleteContact = async contactId => { try { await api.delete(`/contacts/${contactId}`); + toast.success("Contact deleted sucessfully!"); } catch (err) { - alert(err); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } setDeletingContact(null); setSearchParam(""); @@ -172,8 +179,10 @@ const Contacts = () => { const handleimportContact = async () => { try { await api.post("/contacts/import"); + window.location.reload(false); } catch (err) { console.log(err); + window.location.reload(false); } }; diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 418eebf..a1dc40d 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -43,6 +43,9 @@ const Settings = () => { setSettings(data); } catch (err) { console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchSession(); @@ -77,14 +80,16 @@ const Settings = () => { }); toast.success("Setting updated"); } catch (err) { - alert(err); console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; const getSettingValue = key => { - const setting = settings.find(s => s.key === key); - return setting.value; + const { value } = settings.find(s => s.key === key); + return value; }; return ( diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index 9823670..65680c4 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -78,7 +78,10 @@ const SignUp = () => { toast.success(i18n.t("signup.toasts.success")); history.push("/login"); } catch (err) { - toast.error(i18n.t("signup.toasts.fail")); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; From 8786c7ca5ef11d221cab18e2a890e5b5e2a3b262 Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 4 Sep 2020 20:18:11 -0300 Subject: [PATCH 10/25] feat: started multiple whatsapps support --- backend/src/app.js | 32 +++++++++--- .../20200904220257-add-name-to-whatsapp.js | 15 ++++++ backend/src/libs/wbot.js | 49 ++++++++++--------- backend/src/models/Whatsapp.js | 1 + backend/src/services/wbotMessageListener.js | 5 +- backend/src/services/wbotMonitor.js | 6 +-- 6 files changed, 72 insertions(+), 36 deletions(-) create mode 100644 backend/src/database/migrations/20200904220257-add-name-to-whatsapp.js diff --git a/backend/src/app.js b/backend/src/app.js index 98f1607..ba664b9 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -10,6 +10,7 @@ const Sentry = require("@sentry/node"); const wBot = require("./libs/wbot"); const wbotMessageListener = require("./services/wbotMessageListener"); const wbotMonitor = require("./services/wbotMonitor"); +const Whatsapp = require("./models/Whatsapp"); const Router = require("./router"); @@ -55,13 +56,30 @@ 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(dbSession => { + wBot + .init(dbSession) + .then(() => { + wbotMessageListener(dbSession); + wbotMonitor(dbSession); + }) + .catch(err => console.log(err)); + }); + } +}; +startWhatsAppSessions(); + +// wBot +// .init() +// .then(({ dbSession }) => { +// wbotMessageListener(); +// wbotMonitor(dbSession); +// }) +// .catch(err => console.log(err)); app.use(Sentry.Handlers.errorHandler()); 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/libs/wbot.js b/backend/src/libs/wbot.js index ff62ade..ee98d2f 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -3,28 +3,24 @@ const { Client } = require("whatsapp-web.js"); const Whatsapp = require("../models/Whatsapp"); const { getIO } = require("../libs/socket"); -let wbot; +let sessions = []; module.exports = { - init: async () => { + init: async dbSession => { + const sessionName = dbSession.name; let sessionCfg; - const [dbSession] = await Whatsapp.findOrCreate({ - where: { id: 1 }, - defaults: { - id: 1, - }, - }); if (dbSession && dbSession.session) { sessionCfg = JSON.parse(dbSession.session); } - wbot = new Client({ + 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 dbSession.update({ id: 1, qrcode: qr, status: "disconnected" }); getIO().emit("session", { @@ -34,9 +30,8 @@ module.exports = { }); }); wbot.on("authenticated", async session => { - console.log("AUTHENTICATED"); + console.log("Session:", sessionName, "AUTHENTICATED"); await dbSession.update({ - id: 1, session: JSON.stringify(session), status: "authenticated", }); @@ -46,15 +41,16 @@ module.exports = { }); }); wbot.on("auth_failure", async msg => { - console.error("AUTHENTICATION FAILURE", msg); - await Whatsapp.update({ session: "" }, { where: { id: 1 } }); + console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); + await dbSession.update({ session: "" }); }); wbot.on("ready", async () => { - console.log("READY"); - await dbSession.update( - { status: "CONNECTED", qrcode: "" }, - { where: { id: 1 } } - ); + console.log("Session:", sessionName, "READY"); + await dbSession.update({ + status: "CONNECTED", + qrcode: "", + }); + // 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) { @@ -66,15 +62,20 @@ module.exports = { // } // console.log(unreadMessages); - wbot.sendPresenceAvailable(); + // wbot.sendPresenceAvailable(); }); - return { wbot, dbSession }; + + wbot.name = sessionName; + sessions.push(wbot); + return null; }, - getWbot: () => { - if (!wbot) { - throw new Error("Wbot not initialized"); + getWbot: sessionName => { + const sessionIndex = sessions.findIndex(s => s.name === sessionName); + + if (sessionIndex === -1) { + throw new Error("This Wbot session is not initialized"); } - return wbot; + return sessions[sessionIndex]; }, }; diff --git a/backend/src/models/Whatsapp.js b/backend/src/models/Whatsapp.js index d60604a..05b57a3 100644 --- a/backend/src/models/Whatsapp.js +++ b/backend/src/models/Whatsapp.js @@ -6,6 +6,7 @@ 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 }, diff --git a/backend/src/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index 329640b..b6dcb32 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -132,12 +132,13 @@ const handleMessage = async (msg, ticket, contact) => { }); }; -const wbotMessageListener = () => { - const wbot = getWbot(); +const wbotMessageListener = dbSession => { + const wbot = getWbot(dbSession.name); const io = getIO(); wbot.on("message_create", async msg => { console.log(msg); + if ( msg.from === "status@broadcast" || msg.type === "location" || diff --git a/backend/src/services/wbotMonitor.js b/backend/src/services/wbotMonitor.js index b8e4145..b49b8a1 100644 --- a/backend/src/services/wbotMonitor.js +++ b/backend/src/services/wbotMonitor.js @@ -7,7 +7,7 @@ const { getWbot, init } = require("../libs/wbot"); const wbotMonitor = dbSession => { const io = getIO(); - const wbot = getWbot(); + const wbot = getWbot(dbSession.name); try { wbot.on("change_state", async newState => { @@ -58,8 +58,8 @@ const wbotMonitor = dbSession => { setTimeout( () => - init() - .then(({ dbSession }) => { + init(dbSession) + .then(() => { wbotMessageListener(); wbotMonitor(dbSession); }) From b8ff993e6f21dc18b09a3897cc8a4455f778b619 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 5 Sep 2020 14:41:55 -0300 Subject: [PATCH 11/25] feat: new connections page to handle multiple whats --- .../controllers/WhatsAppSessionController.js | 28 +- backend/src/libs/wbot.js | 43 ++- backend/src/router/routes/whatsapp.js | 9 +- backend/src/services/wbotMessageListener.js | 2 +- backend/src/services/wbotMonitor.js | 15 +- frontend/src/components/Qrcode/index.js | 18 -- frontend/src/components/QrcodeModal/index.js | 37 +++ frontend/src/components/SessionInfo/index.js | 4 +- frontend/src/pages/Connection/index.js | 298 ++++++++++++++---- 9 files changed, 335 insertions(+), 119 deletions(-) delete mode 100644 frontend/src/components/Qrcode/index.js create mode 100644 frontend/src/components/QrcodeModal/index.js diff --git a/backend/src/controllers/WhatsAppSessionController.js b/backend/src/controllers/WhatsAppSessionController.js index 882f2b1..67c5f16 100644 --- a/backend/src/controllers/WhatsAppSessionController.js +++ b/backend/src/controllers/WhatsAppSessionController.js @@ -2,6 +2,12 @@ const Whatsapp = require("../models/Whatsapp"); const { getIO } = require("../libs/socket"); const { getWbot } = require("../libs/wbot"); +exports.index = async (req, res) => { + const dbSession = await Whatsapp.findAll(); + + return res.status(200).json(dbSession); +}; + exports.show = async (req, res) => { const { sessionId } = req.params; const dbSession = await Whatsapp.findByPk(sessionId); @@ -13,33 +19,25 @@ exports.show = async (req, res) => { return res.status(200).json(dbSession); }; -exports.delete = async (req, res) => { - const wbot = getWbot(); - const io = getIO(); - +exports.update = async (req, res) => { const { sessionId } = req.params; + const dbSession = await Whatsapp.findByPk(sessionId); if (!dbSession) { return res.status(404).json({ message: "Session not found" }); } - await dbSession.update({ session: "", status: "pending" }); + const wbot = getWbot(dbSession.id); + const io = getIO(); + + await dbSession.update(req.body); wbot.logout(); io.emit("session", { - action: "logout", + action: "update", 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); -// }; diff --git a/backend/src/libs/wbot.js b/backend/src/libs/wbot.js index ee98d2f..2651db5 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -7,6 +7,7 @@ let sessions = []; module.exports = { init: async dbSession => { + const io = getIO(); const sessionName = dbSession.name; let sessionCfg; @@ -14,43 +15,64 @@ module.exports = { sessionCfg = JSON.parse(dbSession.session); } + const sessionIndex = sessions.findIndex(s => s.id === dbSession.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 dbSession.update({ id: 1, qrcode: qr, status: "disconnected" }); - getIO().emit("session", { + + await dbSession.update({ id: 1, qrcode: qr, status: "qrcode" }); + + io.emit("session", { action: "update", - qr: qr, session: dbSession, }); }); + wbot.on("authenticated", async session => { console.log("Session:", sessionName, "AUTHENTICATED"); + await dbSession.update({ session: JSON.stringify(session), status: "authenticated", }); - getIO().emit("session", { - action: "authentication", + + io.emit("session", { + action: "update", session: dbSession, }); }); + wbot.on("auth_failure", async msg => { console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); + await dbSession.update({ session: "" }); }); + wbot.on("ready", async () => { console.log("Session:", sessionName, "READY"); + await dbSession.update({ status: "CONNECTED", qrcode: "", }); + io.emit("session", { + action: "update", + session: dbSession, + }); + // 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) { @@ -62,16 +84,19 @@ module.exports = { // } // console.log(unreadMessages); - // wbot.sendPresenceAvailable(); + wbot.sendPresenceAvailable(); }); - wbot.name = sessionName; + wbot.id = dbSession.id; sessions.push(wbot); + return null; }, - getWbot: sessionName => { - const sessionIndex = sessions.findIndex(s => s.name === sessionName); + getWbot: sessionId => { + console.log(sessionId); + console.log(sessions.map(session => session.id)); + const sessionIndex = sessions.findIndex(s => s.id === sessionId); if (sessionIndex === -1) { throw new Error("This Wbot session is not initialized"); diff --git a/backend/src/router/routes/whatsapp.js b/backend/src/router/routes/whatsapp.js index 4eb8758..97f792c 100644 --- a/backend/src/router/routes/whatsapp.js +++ b/backend/src/router/routes/whatsapp.js @@ -5,19 +5,18 @@ const WhatsAppSessionController = require("../../controllers/WhatsAppSessionCont const routes = express.Router(); +routes.get("/whatsapp/session/", isAuth, WhatsAppSessionController.index); + routes.get( "/whatsapp/session/:sessionId", isAuth, WhatsAppSessionController.show ); -routes.delete( +routes.put( "/whatsapp/session/:sessionId", isAuth, - WhatsAppSessionController.delete + WhatsAppSessionController.update ); -// 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 b6dcb32..cc5f013 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -133,7 +133,7 @@ const handleMessage = async (msg, ticket, contact) => { }; const wbotMessageListener = dbSession => { - const wbot = getWbot(dbSession.name); + const wbot = getWbot(dbSession.id); const io = getIO(); wbot.on("message_create", async msg => { diff --git a/backend/src/services/wbotMonitor.js b/backend/src/services/wbotMonitor.js index b49b8a1..e6715d8 100644 --- a/backend/src/services/wbotMonitor.js +++ b/backend/src/services/wbotMonitor.js @@ -7,11 +7,12 @@ const { getWbot, init } = require("../libs/wbot"); const wbotMonitor = dbSession => { const io = getIO(); - const wbot = getWbot(dbSession.name); + const sessionName = dbSession.name; + const wbot = getWbot(dbSession.id); try { wbot.on("change_state", async newState => { - console.log("monitor", newState); + console.log("Monitor session:", sessionName, newState); try { await dbSession.update({ status: newState }); } catch (err) { @@ -27,7 +28,9 @@ const wbotMonitor = dbSession => { 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 }); @@ -43,7 +46,7 @@ const wbotMonitor = dbSession => { }); wbot.on("disconnected", async reason => { - console.log("disconnected", reason); + console.log("Disconnected session:", sessionName, reason); try { await dbSession.update({ status: "disconnected" }); } catch (err) { @@ -52,7 +55,7 @@ const wbotMonitor = dbSession => { } io.emit("session", { - action: "logout", + action: "update", session: dbSession, }); @@ -60,7 +63,7 @@ const wbotMonitor = dbSession => { () => init(dbSession) .then(() => { - wbotMessageListener(); + wbotMessageListener(dbSession); wbotMonitor(dbSession); }) .catch(err => { diff --git a/frontend/src/components/Qrcode/index.js b/frontend/src/components/Qrcode/index.js deleted file mode 100644 index c73daf1..0000000 --- a/frontend/src/components/Qrcode/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import QRCode from "qrcode.react"; -import Typography from "@material-ui/core/Typography"; - -import { i18n } from "../../translate/i18n"; - -const Qrcode = ({ qrCode }) => { - return ( -
- - {i18n.t("qrCode.message")} - - {qrCode ? : loading} -
- ); -}; - -export default Qrcode; diff --git a/frontend/src/components/QrcodeModal/index.js b/frontend/src/components/QrcodeModal/index.js new file mode 100644 index 0000000..de80025 --- /dev/null +++ b/frontend/src/components/QrcodeModal/index.js @@ -0,0 +1,37 @@ +import React from "react"; +import QRCode from "qrcode.react"; + +import { + Dialog, + DialogContent, + Paper, + Typography, + DialogTitle, +} from "@material-ui/core"; +import { i18n } from "../../translate/i18n"; + +const QrcodeModal = ({ open, onClose, session }) => { + if (session) { + return ( + + {session.name} + + + + {i18n.t("qrCode.message")} + + {session.qrcode ? ( + + ) : ( + loading + )} + + + + ); + } else { + return null; + } +}; + +export default QrcodeModal; diff --git a/frontend/src/components/SessionInfo/index.js b/frontend/src/components/SessionInfo/index.js index b43af32..bb048cc 100644 --- a/frontend/src/components/SessionInfo/index.js +++ b/frontend/src/components/SessionInfo/index.js @@ -10,7 +10,7 @@ import api from "../../services/api"; const SessionInfo = ({ session }) => { const handleDisconectSession = async () => { try { - await api.delete("/whatsapp/session/1"); + await api.put(`/whatsapp/session/${session.id}`); } catch (err) { console.log(err); if (err.response && err.response.data && err.response.data.error) { @@ -27,7 +27,7 @@ const SessionInfo = ({ session }) => { {`${i18n.t("sessionInfo.updatedAt")}`}{" "} {session.updatedAt && - format(parseISO(session.updatedAt), "dd/mm/yy HH:mm")} + format(parseISO(session.updatedAt), "dd/MM/yy HH:mm")} + + + +
+ + + Name + Status + Last update + Actions + + + + {false ? ( + + ) : ( + <> + {sessions && + sessions.length > 0 && + sessions.map(session => ( + + {session.name} + + {session.status === "qrcode" ? ( + + ) : ( + session.status + )} + + + {format(parseISO(session.updatedAt), "dd/MM/yy HH:mm")} + + + handleDisconnectSession(session.id)} + > + + + + { + // setConfirmModalOpen(true); + // setDeletingUser(user); + }} + > + + + + + ))} + + )} + +
+
+ + {/*
+ {sessions && + sessions.length > 0 && + sessions.map(session => { + if (session.status === "disconnected") + return ( + + + + ); + else { + return ( + + + + ); + } + })} +
*/} +
); }; From 40e2e5e8a625f9dd9510d83df53dd0110d830650 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 5 Sep 2020 15:43:52 -0300 Subject: [PATCH 12/25] improvement: better names on wbot methods --- backend/src/app.js | 5 +- .../ImportPhoneContactsController.js | 2 +- .../controllers/WhatsAppSessionController.js | 56 +++++- backend/src/libs/wbot.js | 167 +++++++++--------- backend/src/router/routes/whatsapp.js | 8 + backend/src/services/wbotMessageListener.js | 2 +- backend/src/services/wbotMonitor.js | 4 +- frontend/src/pages/Connection/index.js | 8 + 8 files changed, 160 insertions(+), 92 deletions(-) diff --git a/backend/src/app.js b/backend/src/app.js index ba664b9..73e8dcd 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -7,7 +7,7 @@ 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"); @@ -61,8 +61,7 @@ const startWhatsAppSessions = async () => { if (whatsapps.length > 0) { whatsapps.forEach(dbSession => { - wBot - .init(dbSession) + initWbot(dbSession) .then(() => { wbotMessageListener(dbSession); wbotMonitor(dbSession); diff --git a/backend/src/controllers/ImportPhoneContactsController.js b/backend/src/controllers/ImportPhoneContactsController.js index 19b5537..c9b79b0 100644 --- a/backend/src/controllers/ImportPhoneContactsController.js +++ b/backend/src/controllers/ImportPhoneContactsController.js @@ -1,6 +1,6 @@ const Contact = require("../models/Contact"); 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(); diff --git a/backend/src/controllers/WhatsAppSessionController.js b/backend/src/controllers/WhatsAppSessionController.js index 67c5f16..94937ca 100644 --- a/backend/src/controllers/WhatsAppSessionController.js +++ b/backend/src/controllers/WhatsAppSessionController.js @@ -1,6 +1,8 @@ const Whatsapp = require("../models/Whatsapp"); const { getIO } = require("../libs/socket"); -const { getWbot } = require("../libs/wbot"); +const { getWbot, initWbot, removeWbot } = require("../libs/wbot"); +const wbotMessageListener = require("../services/wbotMessageListener"); +const wbotMonitor = require("../services/wbotMonitor"); exports.index = async (req, res) => { const dbSession = await Whatsapp.findAll(); @@ -8,6 +10,29 @@ exports.index = async (req, res) => { return res.status(200).json(dbSession); }; +exports.store = async (req, res) => { + const io = getIO(); + const dbSession = await Whatsapp.create(req.body); + + if (!dbSession) { + return res.status(400).json({ error: "Cannot create whatsapp session." }); + } + + initWbot(dbSession) + .then(() => { + wbotMessageListener(dbSession); + wbotMonitor(dbSession); + }) + .catch(err => console.log(err)); + + io.emit("session", { + action: "update", + session: dbSession, + }); + + return res.status(200).json({ message: "Session created sucessfully." }); +}; + exports.show = async (req, res) => { const { sessionId } = req.params; const dbSession = await Whatsapp.findByPk(sessionId); @@ -20,6 +45,7 @@ exports.show = async (req, res) => { }; exports.update = async (req, res) => { + const io = getIO(); const { sessionId } = req.params; const dbSession = await Whatsapp.findByPk(sessionId); @@ -29,15 +55,37 @@ exports.update = async (req, res) => { } const wbot = getWbot(dbSession.id); - const io = getIO(); + wbot.logout(); await dbSession.update(req.body); - wbot.logout(); io.emit("session", { action: "update", session: dbSession, }); - return res.status(200).json({ message: "session disconnected" }); + return res.status(200).json({ message: "Session updated" }); +}; + +exports.delete = async (req, res) => { + const io = getIO(); + const { sessionId } = req.params; + + const dbSession = await Whatsapp.findByPk(sessionId); + + if (!dbSession) { + return res.status(404).json({ message: "Session not found" }); + } + + const wbot = getWbot(dbSession.id); + await dbSession.destroy(); + + removeWbot(dbSession.id); + + io.emit("session", { + action: "delete", + sessionId: dbSession.id, + }); + + return res.status(200).json({ message: "Session deleted." }); }; diff --git a/backend/src/libs/wbot.js b/backend/src/libs/wbot.js index 2651db5..39c76ba 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -6,90 +6,83 @@ const { getIO } = require("../libs/socket"); let sessions = []; module.exports = { - init: async dbSession => { - const io = getIO(); - const sessionName = dbSession.name; - let sessionCfg; + initWbot: async dbSession => { + try { + const io = getIO(); + const sessionName = dbSession.name; + let sessionCfg; - if (dbSession && dbSession.session) { - sessionCfg = JSON.parse(dbSession.session); + if (dbSession && dbSession.session) { + sessionCfg = JSON.parse(dbSession.session); + } + + const sessionIndex = sessions.findIndex(s => s.id === dbSession.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 dbSession.update({ id: 1, qrcode: qr, status: "qrcode" }); + + io.emit("session", { + action: "update", + session: dbSession, + }); + }); + + wbot.on("authenticated", async session => { + console.log("Session:", sessionName, "AUTHENTICATED"); + + await dbSession.update({ + session: JSON.stringify(session), + status: "authenticated", + }); + + io.emit("session", { + action: "update", + session: dbSession, + }); + }); + + wbot.on("auth_failure", async msg => { + console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); + + await dbSession.update({ session: "" }); + }); + + wbot.on("ready", async () => { + console.log("Session:", sessionName, "READY"); + + await dbSession.update({ + status: "CONNECTED", + qrcode: "", + }); + + io.emit("session", { + action: "update", + session: dbSession, + }); + + wbot.sendPresenceAvailable(); + }); + + wbot.id = dbSession.id; + sessions.push(wbot); + } catch (err) { + console.log(err); } - const sessionIndex = sessions.findIndex(s => s.id === dbSession.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 dbSession.update({ id: 1, qrcode: qr, status: "qrcode" }); - - io.emit("session", { - action: "update", - session: dbSession, - }); - }); - - wbot.on("authenticated", async session => { - console.log("Session:", sessionName, "AUTHENTICATED"); - - await dbSession.update({ - session: JSON.stringify(session), - status: "authenticated", - }); - - io.emit("session", { - action: "update", - session: dbSession, - }); - }); - - wbot.on("auth_failure", async msg => { - console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); - - await dbSession.update({ session: "" }); - }); - - wbot.on("ready", async () => { - console.log("Session:", sessionName, "READY"); - - await dbSession.update({ - status: "CONNECTED", - qrcode: "", - }); - - io.emit("session", { - action: "update", - session: dbSession, - }); - - // 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(); - }); - - wbot.id = dbSession.id; - sessions.push(wbot); - return null; }, @@ -103,4 +96,16 @@ module.exports = { } return sessions[sessionIndex]; }, + + removeWbot: sessionId => { + try { + const sessionIndex = sessions.findIndex(s => s.id === sessionId); + if (sessionIndex !== -1) { + sessions[sessionIndex].destroy(); + sessions.splice(sessionIndex, 1); + } + } catch (err) { + console.log(err); + } + }, }; diff --git a/backend/src/router/routes/whatsapp.js b/backend/src/router/routes/whatsapp.js index 97f792c..5c6125f 100644 --- a/backend/src/router/routes/whatsapp.js +++ b/backend/src/router/routes/whatsapp.js @@ -7,6 +7,8 @@ const routes = express.Router(); routes.get("/whatsapp/session/", isAuth, WhatsAppSessionController.index); +routes.post("/whatsapp/session", isAuth, WhatsAppSessionController.store); + routes.get( "/whatsapp/session/:sessionId", isAuth, @@ -19,4 +21,10 @@ routes.put( WhatsAppSessionController.update ); +routes.delete( + "/whatsapp/session/:sessionId", + isAuth, + WhatsAppSessionController.delete +); + module.exports = routes; diff --git a/backend/src/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index cc5f013..9056caa 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -9,7 +9,7 @@ const Ticket = require("../models/Ticket"); const Message = require("../models/Message"); 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({ diff --git a/backend/src/services/wbotMonitor.js b/backend/src/services/wbotMonitor.js index e6715d8..3bd286d 100644 --- a/backend/src/services/wbotMonitor.js +++ b/backend/src/services/wbotMonitor.js @@ -3,7 +3,7 @@ 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 io = getIO(); @@ -61,7 +61,7 @@ const wbotMonitor = dbSession => { setTimeout( () => - init(dbSession) + initWbot(dbSession) .then(() => { wbotMessageListener(dbSession); wbotMonitor(dbSession); diff --git a/frontend/src/pages/Connection/index.js b/frontend/src/pages/Connection/index.js index c8946a9..e3eb62d 100644 --- a/frontend/src/pages/Connection/index.js +++ b/frontend/src/pages/Connection/index.js @@ -47,6 +47,8 @@ const reducer = (state, action) => { if (action.type === "DELETE_SESSION") { const sessionId = action.payload; + console.log("cai aqui", sessionId); + const sessionIndex = state.findIndex(s => s.id === sessionId); if (sessionIndex !== -1) { state.splice(sessionIndex, 1); @@ -123,6 +125,12 @@ const WhatsAuth = () => { } }); + socket.on("session", data => { + if (data.action === "delete") { + dispatch({ type: "DELETE_SESSION", payload: data.sessionId }); + } + }); + return () => { socket.disconnect(); }; From f423dd60bc73d61c0d44fe70657244f1e2b61f85 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 5 Sep 2020 17:43:09 -0300 Subject: [PATCH 13/25] feat: continue session handle in frontend --- .../controllers/WhatsAppSessionController.js | 2 +- frontend/src/components/QrcodeModal/index.js | 74 ++++---- frontend/src/components/SessionModal/index.js | 168 ++++++++++++++++++ frontend/src/pages/Connection/index.js | 79 ++++---- frontend/src/pages/Users/index.js | 35 ++-- 5 files changed, 271 insertions(+), 87 deletions(-) create mode 100644 frontend/src/components/SessionModal/index.js diff --git a/backend/src/controllers/WhatsAppSessionController.js b/backend/src/controllers/WhatsAppSessionController.js index 94937ca..2eca860 100644 --- a/backend/src/controllers/WhatsAppSessionController.js +++ b/backend/src/controllers/WhatsAppSessionController.js @@ -30,7 +30,7 @@ exports.store = async (req, res) => { session: dbSession, }); - return res.status(200).json({ message: "Session created sucessfully." }); + return res.status(200).json(dbSession); }; exports.show = async (req, res) => { diff --git a/frontend/src/components/QrcodeModal/index.js b/frontend/src/components/QrcodeModal/index.js index de80025..09a4d39 100644 --- a/frontend/src/components/QrcodeModal/index.js +++ b/frontend/src/components/QrcodeModal/index.js @@ -1,37 +1,37 @@ -import React from "react"; -import QRCode from "qrcode.react"; - -import { - Dialog, - DialogContent, - Paper, - Typography, - DialogTitle, -} from "@material-ui/core"; -import { i18n } from "../../translate/i18n"; - -const QrcodeModal = ({ open, onClose, session }) => { - if (session) { - return ( - - {session.name} - - - - {i18n.t("qrCode.message")} - - {session.qrcode ? ( - - ) : ( - loading - )} - - - - ); - } else { - return null; - } -}; - -export default QrcodeModal; +import React from "react"; +import QRCode from "qrcode.react"; + +import { + Dialog, + DialogContent, + Paper, + Typography, + DialogTitle, +} from "@material-ui/core"; +import { i18n } from "../../translate/i18n"; + +const QrcodeModal = ({ open, onClose, session }) => { + if (session) { + return ( + + {session.name} + + + + {i18n.t("qrCode.message")} + + {session.qrcode ? ( + + ) : ( + Waiting for QR Code + )} + + + + ); + } else { + return null; + } +}; + +export default QrcodeModal; diff --git a/frontend/src/components/SessionModal/index.js b/frontend/src/components/SessionModal/index.js new file mode 100644 index 0000000..fc7c0a8 --- /dev/null +++ b/frontend/src/components/SessionModal/index.js @@ -0,0 +1,168 @@ +import React, { useState, useEffect } from "react"; +import QRCode from "qrcode.react"; +import * as Yup from "yup"; +import { Formik, Form, Field } from "formik"; +import { toast } from "react-toastify"; + +import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; + +import { + Dialog, + DialogContent, + Paper, + Typography, + DialogTitle, + Button, + DialogActions, + CircularProgress, + TextField, +} from "@material-ui/core"; + +import { i18n } from "../../translate/i18n"; +import api from "../../services/api"; + +const useStyles = makeStyles(theme => ({ + textField: { + marginRight: theme.spacing(1), + flex: 1, + }, + + btnWrapper: { + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, +})); + +const SessionSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), +}); + +const SessionModal = ({ open, onClose, sessionId }) => { + const classes = useStyles(); + const initialState = { + name: "", + status: "", + }; + const [session, setSession] = useState(initialState); + + useEffect(() => { + const fetchSession = async () => { + if (!sessionId) return; + + try { + const { data } = await api.get(`whatsapp/session/${sessionId}`); + setSession(data); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + }; + fetchSession(); + }, [sessionId]); + + const handleSaveSession = async values => { + try { + if (sessionId) { + await api.put(`/whatsapp/session/${sessionId}`, values); + } else { + await api.post("/whatsapp/session", values); + } + toast.success("Session created!"); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + handleClose(); + }; + + const handleClose = () => { + onClose(); + setSession(initialState); + }; + + return ( + + Edit Session + { + setTimeout(() => { + handleSaveSession(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting }) => ( +
+ + + + + + + + +
+ )} +
+
+ ); +}; + +export default SessionModal; diff --git a/frontend/src/pages/Connection/index.js b/frontend/src/pages/Connection/index.js index e3eb62d..c8e573c 100644 --- a/frontend/src/pages/Connection/index.js +++ b/frontend/src/pages/Connection/index.js @@ -23,7 +23,8 @@ import Title from "../../components/Title"; import TableRowSkeleton from "../../components/TableRowSkeleton"; import api from "../../services/api"; -import QrcodeModal from "../../components/QrcodeModal"; +import SessionModal from "../../components/SessionModal"; +import ConfirmationModal from "../../components/ConfirmationModal"; const reducer = (state, action) => { if (action.type === "LOAD_SESSIONS") { @@ -47,8 +48,6 @@ const reducer = (state, action) => { if (action.type === "DELETE_SESSION") { const sessionId = action.payload; - console.log("cai aqui", sessionId); - const sessionIndex = state.findIndex(s => s.id === sessionId); if (sessionIndex !== -1) { state.splice(sessionIndex, 1); @@ -98,8 +97,11 @@ const WhatsAuth = () => { const [sessions, dispatch] = useReducer(reducer, []); - const [qrModalOpen, setQrModalOpen] = useState(false); + const [sessionModalOpen, setSessionModalOpen] = useState(false); + // const [sessionModalOpen, setSessionModalOpen] = useState(false); const [selectedSession, setSelectedSession] = useState(null); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + // const [deletingSession, setDeletingSession] = useState(null); useEffect(() => { const fetchSession = async () => { @@ -150,46 +152,55 @@ const WhatsAuth = () => { } }; - console.log(sessions); - - const handleOpenQrMoral = session => { - setQrModalOpen(true); - setSelectedSession(session); + const handleOpenSessionModal = session => { + setSelectedSession(null); + setSessionModalOpen(true); }; - const handleCloseQrModal = () => { - setQrModalOpen(false); + const handleCloseSessionModal = () => { + setSessionModalOpen(false); setSelectedSession(null); }; + const handleEditUser = session => { + setSelectedSession(session); + setSessionModalOpen(true); + }; + + const handleDeleteSession = async sessionId => { + try { + await api.delete(`/whatsapp/session/${sessionId}`); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + }; + return ( - - {/* handleDeleteSession(selectedSession.id)} > - Qr Code - - - - - - */} + Are you sure? It cannot be reverted. + + Connections @@ -216,7 +227,7 @@ const WhatsAuth = () => { <> {sessions && sessions.length > 0 && - sessions.map(session => ( + sessions.map((session, index) => ( {session.name} @@ -225,7 +236,7 @@ const WhatsAuth = () => { size="small" variant="contained" color="primary" - onClick={() => handleOpenQrMoral(session)} + onClick={() => handleEditUser(session)} > QR CODE @@ -247,8 +258,8 @@ const WhatsAuth = () => { { - // setConfirmModalOpen(true); - // setDeletingUser(user); + setConfirmModalOpen(true); + setSelectedSession(session); }} > diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index de554f8..0b5a615 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useReducer } from "react"; - +import { toast } from "react-toastify"; import openSocket from "socket.io-client"; import { makeStyles } from "@material-ui/core/styles"; @@ -87,10 +87,9 @@ const Users = () => { const [loading, setLoading] = useState(false); const [pageNumber, setPageNumber] = useState(1); const [hasMore, setHasMore] = useState(false); - const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); const [userModalOpen, setUserModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); - const [deletingUser, setDeletingUser] = useState(null); const [searchParam, setSearchParam] = useState(""); const [users, dispatch] = useReducer(reducer, []); @@ -112,7 +111,9 @@ const Users = () => { setLoading(false); } catch (err) { console.log(err); - alert(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } }; fetchUsers(); @@ -138,12 +139,12 @@ const Users = () => { }, []); const handleOpenUserModal = () => { - setSelectedUserId(null); + setSelectedUser(null); setUserModalOpen(true); }; const handleCloseUserModal = () => { - setSelectedUserId(null); + setSelectedUser(null); setUserModalOpen(false); }; @@ -151,18 +152,22 @@ const Users = () => { setSearchParam(event.target.value.toLowerCase()); }; - const handleEditUser = userId => { - setSelectedUserId(userId); + const handleEditUser = user => { + setSelectedUser(user); setUserModalOpen(true); }; const handleDeleteUser = async userId => { try { await api.delete(`/users/${userId}`); + toast.success("User deleted!"); } catch (err) { - alert(err); + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } } - setDeletingUser(null); + setSelectedUser(null); setSearchParam(""); setPageNumber(1); }; @@ -182,10 +187,10 @@ const Users = () => { return ( handleDeleteUser(deletingUser.id)} + onConfirm={e => handleDeleteUser(selectedUser.id)} > Are you sure? It canoot be reverted. @@ -193,7 +198,7 @@ const Users = () => { open={userModalOpen} onClose={handleCloseUserModal} aria-labelledby="form-dialog-title" - userId={selectedUserId} + userId={selectedUser && selectedUser.id} /> Usuários @@ -247,7 +252,7 @@ const Users = () => { handleEditUser(user.id)} + onClick={() => handleEditUser(user)} > @@ -256,7 +261,7 @@ const Users = () => { size="small" onClick={e => { setConfirmModalOpen(true); - setDeletingUser(user); + setSelectedUser(user); }} > From b0f28c16ad4c08eddba52d6dd1cedc87d82662b8 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 12:09:12 -0300 Subject: [PATCH 14/25] feat: finished multiple whatsapps handle in frontend --- backend/src/app.js | 17 +- backend/src/controllers/WhatsAppController.js | 86 +++++ .../controllers/WhatsAppSessionController.js | 105 ++---- ...2228-add-name-default-field-to-whatsapp.js | 15 + backend/src/libs/wbot.js | 32 +- backend/src/models/Whatsapp.js | 5 + backend/src/router/routes/whatsapp.js | 24 +- backend/src/router/routes/whatsappsessions.js | 20 ++ backend/src/services/wbotMessageListener.js | 4 +- backend/src/services/wbotMonitor.js | 24 +- frontend/src/components/QrcodeModal/index.js | 92 ++++-- .../{SessionModal => WhatsAppModal}/index.js | 76 +++-- .../src/components/_layout/MainListItems.js | 4 +- frontend/src/pages/Connection/index.js | 299 ------------------ frontend/src/pages/Users/index.js | 9 +- frontend/src/pages/WhatsApps/index.js | 290 +++++++++++++++++ frontend/src/routes/index.js | 4 +- 17 files changed, 595 insertions(+), 511 deletions(-) create mode 100644 backend/src/controllers/WhatsAppController.js create mode 100644 backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.js create mode 100644 backend/src/router/routes/whatsappsessions.js rename frontend/src/components/{SessionModal => WhatsAppModal}/index.js (64%) delete mode 100644 frontend/src/pages/Connection/index.js create mode 100644 frontend/src/pages/WhatsApps/index.js diff --git a/backend/src/app.js b/backend/src/app.js index 73e8dcd..e1b5df1 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -60,11 +60,11 @@ const startWhatsAppSessions = async () => { const whatsapps = await Whatsapp.findAll(); if (whatsapps.length > 0) { - whatsapps.forEach(dbSession => { - initWbot(dbSession) + whatsapps.forEach(whatsapp => { + initWbot(whatsapp) .then(() => { - wbotMessageListener(dbSession); - wbotMonitor(dbSession); + wbotMessageListener(whatsapp); + wbotMonitor(whatsapp); }) .catch(err => console.log(err)); }); @@ -72,14 +72,6 @@ const startWhatsAppSessions = async () => { }; startWhatsAppSessions(); -// wBot -// .init() -// .then(({ dbSession }) => { -// wbotMessageListener(); -// wbotMonitor(dbSession); -// }) -// .catch(err => console.log(err)); - app.use(Sentry.Handlers.errorHandler()); app.use(async (err, req, res, next) => { @@ -88,5 +80,6 @@ app.use(async (err, req, res, next) => { console.log(err); return res.status(500).json(errors); } + return res.status(500).json({ error: "Internal server error" }); }); diff --git a/backend/src/controllers/WhatsAppController.js b/backend/src/controllers/WhatsAppController.js new file mode 100644 index 0000000..75d1d68 --- /dev/null +++ b/backend/src/controllers/WhatsAppController.js @@ -0,0 +1,86 @@ +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 whatsapp = await Whatsapp.findAll(); + + return res.status(200).json(whatsapp); +}; + +exports.store = async (req, res) => { + 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 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.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 2eca860..0bee528 100644 --- a/backend/src/controllers/WhatsAppSessionController.js +++ b/backend/src/controllers/WhatsAppSessionController.js @@ -1,91 +1,32 @@ -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"); +// 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 dbSession = await Whatsapp.findAll(); +// exports.show = async (req, res) => { +// const { whatsappId } = req.params; +// const dbSession = await Whatsapp.findByPk(whatsappId); - return res.status(200).json(dbSession); -}; +// if (!dbSession) { +// return res.status(200).json({ message: "Session not found" }); +// } -exports.store = async (req, res) => { - const io = getIO(); - const dbSession = await Whatsapp.create(req.body); +// return res.status(200).json(dbSession); +// }; - if (!dbSession) { - return res.status(400).json({ error: "Cannot create whatsapp session." }); - } +// exports.delete = async (req, res) => { +// const { whatsappId } = req.params; - initWbot(dbSession) - .then(() => { - wbotMessageListener(dbSession); - wbotMonitor(dbSession); - }) - .catch(err => console.log(err)); +// const dbSession = await Whatsapp.findByPk(whatsappId); - io.emit("session", { - action: "update", - session: dbSession, - }); +// if (!dbSession) { +// return res.status(404).json({ message: "Session not found" }); +// } - return res.status(200).json(dbSession); -}; +// const wbot = getWbot(dbSession.id); -exports.show = async (req, res) => { - const { sessionId } = req.params; - const dbSession = await Whatsapp.findByPk(sessionId); +// wbot.logout(); - if (!dbSession) { - return res.status(200).json({ message: "Session not found" }); - } - - return res.status(200).json(dbSession); -}; - -exports.update = async (req, res) => { - const io = getIO(); - const { sessionId } = req.params; - - const dbSession = await Whatsapp.findByPk(sessionId); - - if (!dbSession) { - return res.status(404).json({ message: "Session not found" }); - } - - const wbot = getWbot(dbSession.id); - wbot.logout(); - - await dbSession.update(req.body); - - io.emit("session", { - action: "update", - session: dbSession, - }); - - return res.status(200).json({ message: "Session updated" }); -}; - -exports.delete = async (req, res) => { - const io = getIO(); - const { sessionId } = req.params; - - const dbSession = await Whatsapp.findByPk(sessionId); - - if (!dbSession) { - return res.status(404).json({ message: "Session not found" }); - } - - const wbot = getWbot(dbSession.id); - await dbSession.destroy(); - - removeWbot(dbSession.id); - - io.emit("session", { - action: "delete", - sessionId: dbSession.id, - }); - - return res.status(200).json({ message: "Session deleted." }); -}; +// return res.status(200).json({ message: "Session disconnected." }); +// }; 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/libs/wbot.js b/backend/src/libs/wbot.js index 39c76ba..4118221 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -6,17 +6,17 @@ const { getIO } = require("../libs/socket"); let sessions = []; module.exports = { - initWbot: async dbSession => { + initWbot: async whatsapp => { try { const io = getIO(); - const sessionName = dbSession.name; + const sessionName = whatsapp.name; let sessionCfg; - 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 === dbSession.id); + const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id); if (sessionIndex !== -1) { sessions[sessionIndex].destroy(); sessions.splice(sessionIndex, 1); @@ -33,51 +33,51 @@ module.exports = { qrCode.generate(qr, { small: true }); - await dbSession.update({ id: 1, qrcode: qr, status: "qrcode" }); + await whatsapp.update({ qrcode: qr, status: "qrcode" }); - io.emit("session", { + io.emit("whatsappSession", { action: "update", - session: dbSession, + session: whatsapp, }); }); wbot.on("authenticated", async session => { console.log("Session:", sessionName, "AUTHENTICATED"); - await dbSession.update({ + await whatsapp.update({ session: JSON.stringify(session), status: "authenticated", }); - io.emit("session", { + io.emit("whatsappSession", { action: "update", - session: dbSession, + session: whatsapp, }); }); wbot.on("auth_failure", async msg => { console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); - await dbSession.update({ session: "" }); + await whatsapp.update({ session: "" }); }); wbot.on("ready", async () => { console.log("Session:", sessionName, "READY"); - await dbSession.update({ + await whatsapp.update({ status: "CONNECTED", qrcode: "", }); - io.emit("session", { + io.emit("whatsappSession", { action: "update", - session: dbSession, + session: whatsapp, }); wbot.sendPresenceAvailable(); }); - wbot.id = dbSession.id; + wbot.id = whatsapp.id; sessions.push(wbot); } catch (err) { console.log(err); diff --git a/backend/src/models/Whatsapp.js b/backend/src/models/Whatsapp.js index 05b57a3..754de3b 100644 --- a/backend/src/models/Whatsapp.js +++ b/backend/src/models/Whatsapp.js @@ -10,6 +10,11 @@ class Whatsapp extends Sequelize.Model { status: { type: Sequelize.STRING }, battery: { type: Sequelize.STRING }, plugged: { type: Sequelize.BOOLEAN }, + default: { + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false, + }, }, { sequelize, diff --git a/backend/src/router/routes/whatsapp.js b/backend/src/router/routes/whatsapp.js index 5c6125f..66d0318 100644 --- a/backend/src/router/routes/whatsapp.js +++ b/backend/src/router/routes/whatsapp.js @@ -1,30 +1,18 @@ const express = require("express"); const isAuth = require("../../middleware/is-auth"); -const WhatsAppSessionController = require("../../controllers/WhatsAppSessionController"); +const WhatsAppController = require("../../controllers/WhatsAppController"); const routes = express.Router(); -routes.get("/whatsapp/session/", isAuth, WhatsAppSessionController.index); +routes.get("/whatsapp/", isAuth, WhatsAppController.index); -routes.post("/whatsapp/session", isAuth, WhatsAppSessionController.store); +routes.post("/whatsapp/", isAuth, WhatsAppController.store); -routes.get( - "/whatsapp/session/:sessionId", - isAuth, - WhatsAppSessionController.show -); +routes.get("/whatsapp/:whatsappId", isAuth, WhatsAppController.show); -routes.put( - "/whatsapp/session/:sessionId", - isAuth, - WhatsAppSessionController.update -); +routes.put("/whatsapp/:whatsappId", isAuth, WhatsAppController.update); -routes.delete( - "/whatsapp/session/:sessionId", - isAuth, - WhatsAppSessionController.delete -); +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/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index 9056caa..38e16d0 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -132,8 +132,8 @@ const handleMessage = async (msg, ticket, contact) => { }); }; -const wbotMessageListener = dbSession => { - const wbot = getWbot(dbSession.id); +const wbotMessageListener = whatsapp => { + const wbot = getWbot(whatsapp.id); const io = getIO(); wbot.on("message_create", async msg => { diff --git a/backend/src/services/wbotMonitor.js b/backend/src/services/wbotMonitor.js index 3bd286d..696b44d 100644 --- a/backend/src/services/wbotMonitor.js +++ b/backend/src/services/wbotMonitor.js @@ -5,16 +5,16 @@ const wbotMessageListener = require("./wbotMessageListener"); const { getIO } = require("../libs/socket"); const { getWbot, initWbot } = require("../libs/wbot"); -const wbotMonitor = dbSession => { +const wbotMonitor = whatsapp => { const io = getIO(); - const sessionName = dbSession.name; - const wbot = getWbot(dbSession.id); + const sessionName = whatsapp.name; + const wbot = getWbot(whatsapp.id); try { wbot.on("change_state", async 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); @@ -22,7 +22,7 @@ const wbotMonitor = dbSession => { io.emit("session", { action: "update", - session: dbSession, + session: whatsapp, }); }); @@ -33,7 +33,7 @@ const wbotMonitor = dbSession => { ); try { - await dbSession.update({ battery, plugged }); + await whatsapp.update({ battery, plugged }); } catch (err) { Sentry.captureException(err); console.log(err); @@ -41,14 +41,14 @@ const wbotMonitor = dbSession => { io.emit("session", { action: "update", - session: dbSession, + session: whatsapp, }); }); wbot.on("disconnected", async 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); @@ -56,15 +56,15 @@ const wbotMonitor = dbSession => { io.emit("session", { action: "update", - session: dbSession, + session: whatsapp, }); setTimeout( () => - initWbot(dbSession) + initWbot(whatsapp) .then(() => { - wbotMessageListener(dbSession); - wbotMonitor(dbSession); + wbotMessageListener(whatsapp); + wbotMonitor(whatsapp); }) .catch(err => { Sentry.captureException(err); diff --git a/frontend/src/components/QrcodeModal/index.js b/frontend/src/components/QrcodeModal/index.js index 09a4d39..9616c66 100644 --- a/frontend/src/components/QrcodeModal/index.js +++ b/frontend/src/components/QrcodeModal/index.js @@ -1,37 +1,67 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import QRCode from "qrcode.react"; +import openSocket from "socket.io-client"; +import { toast } from "react-toastify"; -import { - Dialog, - DialogContent, - Paper, - Typography, - DialogTitle, -} from "@material-ui/core"; +import { Dialog, DialogContent, Paper, Typography } from "@material-ui/core"; import { i18n } from "../../translate/i18n"; +import api from "../../services/api"; -const QrcodeModal = ({ open, onClose, session }) => { - if (session) { - return ( - - {session.name} - - - - {i18n.t("qrCode.message")} - - {session.qrcode ? ( - - ) : ( - Waiting for QR Code - )} - - - - ); - } else { - return null; - } +const QrcodeModal = ({ open, onClose, whatsAppId }) => { + const [qrCode, setQrCode] = useState(""); + + useEffect(() => { + const fetchSession = async () => { + try { + const { data } = await api.get(`/whatsapp/${whatsAppId}`); + setQrCode(data.qrcode); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + }; + fetchSession(); + }, [whatsAppId]); + + useEffect(() => { + if (!whatsAppId) return; + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + console.log("connectiing"); + + socket.on("whatsappSession", data => { + if (data.action === "update" && data.session.id === whatsAppId) { + setQrCode(data.session.qrcode); + } + + if (data.action === "update" && data.session.qrcode === "") { + onClose(); + } + }); + + return () => { + socket.disconnect(); + console.log("disconnectiing"); + }; + }, [whatsAppId, onClose]); + + return ( + + + + + {i18n.t("qrCode.message")} + + {qrCode ? ( + + ) : ( + Waiting for QR Code + )} + + + + ); }; -export default QrcodeModal; +export default React.memo(QrcodeModal); diff --git a/frontend/src/components/SessionModal/index.js b/frontend/src/components/WhatsAppModal/index.js similarity index 64% rename from frontend/src/components/SessionModal/index.js rename to frontend/src/components/WhatsAppModal/index.js index fc7c0a8..538f867 100644 --- a/frontend/src/components/SessionModal/index.js +++ b/frontend/src/components/WhatsAppModal/index.js @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import QRCode from "qrcode.react"; import * as Yup from "yup"; import { Formik, Form, Field } from "formik"; import { toast } from "react-toastify"; @@ -10,21 +9,29 @@ import { green } from "@material-ui/core/colors"; import { Dialog, DialogContent, - Paper, - Typography, DialogTitle, Button, DialogActions, CircularProgress, TextField, + Switch, + FormControlLabel, } from "@material-ui/core"; -import { i18n } from "../../translate/i18n"; +// import { i18n } from "../../translate/i18n"; import api from "../../services/api"; const useStyles = makeStyles(theme => ({ + form: { + display: "flex", + alignItems: "center", + justifySelf: "center", + "& > *": { + margin: theme.spacing(1), + }, + }, + textField: { - marginRight: theme.spacing(1), flex: 1, }, @@ -49,21 +56,21 @@ const SessionSchema = Yup.object().shape({ .required("Required"), }); -const SessionModal = ({ open, onClose, sessionId }) => { +const WhatsAppModal = ({ open, onClose, whatsAppId }) => { const classes = useStyles(); const initialState = { name: "", - status: "", + default: false, }; - const [session, setSession] = useState(initialState); + const [whatsApp, setWhatsApp] = useState(initialState); useEffect(() => { const fetchSession = async () => { - if (!sessionId) return; + if (!whatsAppId) return; try { - const { data } = await api.get(`whatsapp/session/${sessionId}`); - setSession(data); + const { data } = await api.get(`whatsapp/${whatsAppId}`); + setWhatsApp(data); } catch (err) { console.log(err); if (err.response && err.response.data && err.response.data.error) { @@ -72,16 +79,19 @@ const SessionModal = ({ open, onClose, sessionId }) => { } }; fetchSession(); - }, [sessionId]); + }, [whatsAppId]); - const handleSaveSession = async values => { + const handleSaveWhatsApp = async values => { try { - if (sessionId) { - await api.put(`/whatsapp/session/${sessionId}`, values); + if (whatsAppId) { + await api.put(`/whatsapp/${whatsAppId}`, { + name: values.name, + default: values.default, + }); } else { - await api.post("/whatsapp/session", values); + await api.post("/whatsapp", values); } - toast.success("Session created!"); + toast.success("Success!"); } catch (err) { console.log(err); if (err.response && err.response.data && err.response.data.error) { @@ -93,26 +103,27 @@ const SessionModal = ({ open, onClose, sessionId }) => { const handleClose = () => { onClose(); - setSession(initialState); + setWhatsApp(initialState); }; return ( - Edit Session + WhatsApp { setTimeout(() => { - handleSaveSession(values); + handleSaveWhatsApp(values); + // alert(JSON.stringify(values, null, 2)); actions.setSubmitting(false); }, 400); }} > - {({ touched, errors, isSubmitting }) => ( + {({ values, touched, errors, isSubmitting }) => (
- + { margin="dense" className={classes.textField} /> - + } + label="Default" /> @@ -165,4 +179,4 @@ const SessionModal = ({ open, onClose, sessionId }) => { ); }; -export default SessionModal; +export default React.memo(WhatsAppModal); diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index ebfa195..df2a93a 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -42,8 +42,8 @@ const MainListItems = () => {
} /> } /> { - if (action.type === "LOAD_SESSIONS") { - const sessions = action.payload; - - return [...sessions]; - } - - if (action.type === "UPDATE_SESSIONS") { - const session = action.payload; - const sessionIndex = state.findIndex(s => s.id === session.id); - - if (sessionIndex !== -1) { - state[sessionIndex] = session; - return [...state]; - } else { - return [session, ...state]; - } - } - - if (action.type === "DELETE_SESSION") { - const sessionId = action.payload; - - const sessionIndex = state.findIndex(s => s.id === sessionId); - if (sessionIndex !== -1) { - state.splice(sessionIndex, 1); - } - return [...state]; - } - - if (action.type === "RESET") { - return []; - } -}; - -const useStyles = makeStyles(theme => ({ - // root: { - // display: "flex", - // alignItems: "center", - // justifyContent: "center", - // padding: theme.spacing(4), - // }, - - mainPaper: { - flex: 1, - padding: theme.spacing(1), - overflowY: "scroll", - ...theme.scrollbarStyles, - }, - - // paper: { - // padding: theme.spacing(2), - // margin: theme.spacing(1), - // display: "flex", - // width: 400, - // height: 270, - // overflow: "auto", - // flexDirection: "column", - // alignItems: "center", - // justifyContent: "center", - // }, - - // fixedHeight: { - // height: 640, - // }, -})); - -const WhatsAuth = () => { - const classes = useStyles(); - - const [sessions, dispatch] = useReducer(reducer, []); - - const [sessionModalOpen, setSessionModalOpen] = useState(false); - // const [sessionModalOpen, setSessionModalOpen] = useState(false); - const [selectedSession, setSelectedSession] = useState(null); - const [confirmModalOpen, setConfirmModalOpen] = useState(false); - // const [deletingSession, setDeletingSession] = useState(null); - - useEffect(() => { - const fetchSession = async () => { - try { - const { data } = await api.get("/whatsapp/session/"); - dispatch({ type: "LOAD_SESSIONS", payload: data }); - } catch (err) { - console.log(err); - if (err.response && err.response.data && err.response.data.error) { - toast.error(err.response.data.error); - } - } - }; - fetchSession(); - }, []); - - useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - - socket.on("session", data => { - if (data.action === "update") { - dispatch({ type: "UPDATE_SESSIONS", payload: data.session }); - } - }); - - socket.on("session", data => { - if (data.action === "delete") { - dispatch({ type: "DELETE_SESSION", payload: data.sessionId }); - } - }); - - return () => { - socket.disconnect(); - }; - }, []); - - const handleDisconnectSession = async sessionId => { - try { - await api.put(`/whatsapp/session/${sessionId}`, { - session: "", - status: "pending", - }); - } catch (err) { - console.log(err); - if (err.response && err.response.data && err.response.data.error) { - toast.error(err.response.data.error); - } - } - }; - - const handleOpenSessionModal = session => { - setSelectedSession(null); - setSessionModalOpen(true); - }; - - const handleCloseSessionModal = () => { - setSessionModalOpen(false); - setSelectedSession(null); - }; - - const handleEditUser = session => { - setSelectedSession(session); - setSessionModalOpen(true); - }; - - const handleDeleteSession = async sessionId => { - try { - await api.delete(`/whatsapp/session/${sessionId}`); - } catch (err) { - console.log(err); - if (err.response && err.response.data && err.response.data.error) { - toast.error(err.response.data.error); - } - } - }; - - return ( - - handleDeleteSession(selectedSession.id)} - > - Are you sure? It cannot be reverted. - - - - Connections - - - - - - - - - Name - Status - Last update - Actions - - - - {false ? ( - - ) : ( - <> - {sessions && - sessions.length > 0 && - sessions.map((session, index) => ( - - {session.name} - - {session.status === "qrcode" ? ( - - ) : ( - session.status - )} - - - {format(parseISO(session.updatedAt), "dd/MM/yy HH:mm")} - - - handleDisconnectSession(session.id)} - > - - - - { - setConfirmModalOpen(true); - setSelectedSession(session); - }} - > - - - - - ))} - - )} - -
-
- - {/*
- {sessions && - sessions.length > 0 && - sessions.map(session => { - if (session.status === "disconnected") - return ( - - - - ); - else { - return ( - - - - ); - } - })} -
*/} -
- ); -}; - -export default WhatsAuth; diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index 0b5a615..1165b34 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -88,6 +88,7 @@ const Users = () => { const [pageNumber, setPageNumber] = useState(1); const [hasMore, setHasMore] = useState(false); const [selectedUser, setSelectedUser] = useState(null); + const [deletingUser, setDeletingUser] = useState(null); const [userModalOpen, setUserModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [searchParam, setSearchParam] = useState(""); @@ -167,7 +168,7 @@ const Users = () => { toast.error(err.response.data.error); } } - setSelectedUser(null); + setDeletingUser(null); setSearchParam(""); setPageNumber(1); }; @@ -187,10 +188,10 @@ const Users = () => { return ( handleDeleteUser(selectedUser.id)} + onConfirm={e => handleDeleteUser(deletingUser.id)} > Are you sure? It canoot be reverted. @@ -261,7 +262,7 @@ const Users = () => { size="small" onClick={e => { setConfirmModalOpen(true); - setSelectedUser(user); + setDeletingUser(user); }} > diff --git a/frontend/src/pages/WhatsApps/index.js b/frontend/src/pages/WhatsApps/index.js new file mode 100644 index 0000000..36fdc67 --- /dev/null +++ b/frontend/src/pages/WhatsApps/index.js @@ -0,0 +1,290 @@ +import React, { useState, useEffect, useReducer, useCallback } from "react"; +import openSocket from "socket.io-client"; +import { toast } from "react-toastify"; +import { format, parseISO } from "date-fns"; + +import { makeStyles } from "@material-ui/core/styles"; +import { + Button, + TableBody, + TableRow, + TableCell, + IconButton, + Table, + TableHead, + Paper, +} from "@material-ui/core"; +import { Edit, DeleteOutline } from "@material-ui/icons"; + +import MainContainer from "../../components/MainContainer"; +import MainHeader from "../../components/MainHeader"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import Title from "../../components/Title"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; + +import api from "../../services/api"; +import WhatsAppModal from "../../components/WhatsAppModal"; +import ConfirmationModal from "../../components/ConfirmationModal"; +import QrcodeModal from "../../components/QrcodeModal"; + +const reducer = (state, action) => { + if (action.type === "LOAD_WHATSAPPS") { + const whatsApps = action.payload; + + return [...whatsApps]; + } + + if (action.type === "UPDATE_WHATSAPPS") { + const whatsApp = action.payload; + const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id); + + if (whatsAppIndex !== -1) { + state[whatsAppIndex] = whatsApp; + return [...state]; + } else { + return [whatsApp, ...state]; + } + } + + if (action.type === "UPDATE_SESSION") { + const whatsApp = action.payload; + const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id); + + if (whatsAppIndex !== -1) { + state[whatsAppIndex].status = whatsApp.status; + state[whatsAppIndex].qrcode = whatsApp.qrcode; + return [...state]; + } else { + return [...state]; + } + } + + if (action.type === "DELETE_WHATSAPPS") { + const whatsAppId = action.payload; + + const whatsAppIndex = state.findIndex(s => s.id === whatsAppId); + if (whatsAppIndex !== -1) { + state.splice(whatsAppIndex, 1); + } + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + +const useStyles = makeStyles(theme => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, +})); + +const WhatsApps = () => { + const classes = useStyles(); + + const [whatsApps, dispatch] = useReducer(reducer, []); + + const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false); + const [qrModalOpen, setQrModalOpen] = useState(false); + const [selectedWhatsApp, setSelectedWhatsApp] = useState(null); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const [deletingWhatsApp, setDeletingWhatsApp] = useState(null); + + useEffect(() => { + const fetchSession = async () => { + try { + const { data } = await api.get("/whatsapp/"); + dispatch({ type: "LOAD_WHATSAPPS", payload: data }); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + }; + fetchSession(); + }, []); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + + socket.on("whatsapp", data => { + if (data.action === "update") { + dispatch({ type: "UPDATE_WHATSAPPS", payload: data.whatsapp }); + } + }); + + socket.on("whatsapp", data => { + if (data.action === "delete") { + dispatch({ type: "DELETE_WHATSAPPS", payload: data.whatsappId }); + } + }); + + socket.on("whatsappSession", data => { + if (data.action === "update") { + dispatch({ type: "UPDATE_SESSION", payload: data.session }); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + + // const handleDisconnectSession = async whatsAppId => { + // try { + // await api.put(`/whatsapp/whatsApp/${whatsAppId}`, { + // whatsApp: "", + // status: "pending", + // }); + // } catch (err) { + // console.log(err); + // if (err.response && err.response.data && err.response.data.error) { + // toast.error(err.response.data.error); + // } + // } + // }; + + const handleOpenWhatsAppModal = () => { + setSelectedWhatsApp(null); + setWhatsAppModalOpen(true); + }; + + const handleCloseWhatsAppModal = useCallback(() => { + setWhatsAppModalOpen(false); + setSelectedWhatsApp(null); + }, [setSelectedWhatsApp, setWhatsAppModalOpen]); + + const handleOpenQrModal = whatsApp => { + setSelectedWhatsApp(whatsApp); + setQrModalOpen(true); + }; + + const handleCloseQrModal = useCallback(() => { + setQrModalOpen(false); + setSelectedWhatsApp(null); + }, [setQrModalOpen, setSelectedWhatsApp]); + + const handleEditWhatsApp = whatsApp => { + setSelectedWhatsApp(whatsApp); + setWhatsAppModalOpen(true); + }; + + const handleDeleteWhatsApp = async whatsAppId => { + try { + await api.delete(`/whatsapp/${whatsAppId}`); + toast.success("Deleted!"); + } catch (err) { + console.log(err); + if (err.response && err.response.data && err.response.data.error) { + toast.error(err.response.data.error); + } + } + setDeletingWhatsApp(null); + }; + + return ( + + handleDeleteWhatsApp(deletingWhatsApp.id)} + > + Are you sure? It cannot be reverted. + + + + + WhatsApps + + + + + + + + + Name + Status + Last update + Actions + + + + {false ? ( + + ) : ( + <> + {whatsApps && + whatsApps.length > 0 && + whatsApps.map((whatsApp, index) => ( + + {whatsApp.name} + + {whatsApp.status === "qrcode" ? ( + + ) : ( + whatsApp.status + )} + + + {format(parseISO(whatsApp.updatedAt), "dd/MM/yy HH:mm")} + + + handleEditWhatsApp(whatsApp)} + > + + + + { + setConfirmModalOpen(true); + setDeletingWhatsApp(whatsApp); + }} + > + + + + + ))} + + )} + +
+
+
+ ); +}; + +export default WhatsApps; diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index b48a030..5a9d14f 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -7,7 +7,7 @@ import Dashboard from "../pages/Dashboard/"; import Tickets from "../pages/Tickets/"; import Signup from "../pages/Signup/"; import Login from "../pages/Login/"; -import Connection from "../pages/Connection/"; +import WhatsApps from "../pages/WhatsApps/"; import Settings from "../pages/Settings/"; import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; @@ -29,7 +29,7 @@ const Routes = () => { component={Tickets} isPrivate /> - + From 72c7c83110bb4c3a277129fd3ae525c471f76722 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 12:13:26 -0300 Subject: [PATCH 15/25] code cleanup --- backend/src/libs/wbot.js | 10 ++++------ frontend/src/components/QrcodeModal/index.js | 2 -- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/src/libs/wbot.js b/backend/src/libs/wbot.js index 4118221..eee60d0 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -86,10 +86,8 @@ module.exports = { return null; }, - getWbot: sessionId => { - console.log(sessionId); - console.log(sessions.map(session => session.id)); - const sessionIndex = sessions.findIndex(s => s.id === sessionId); + getWbot: whatsappId => { + const sessionIndex = sessions.findIndex(s => s.id === whatsappId); if (sessionIndex === -1) { throw new Error("This Wbot session is not initialized"); @@ -97,9 +95,9 @@ module.exports = { return sessions[sessionIndex]; }, - removeWbot: sessionId => { + removeWbot: whatsappId => { try { - const sessionIndex = sessions.findIndex(s => s.id === sessionId); + const sessionIndex = sessions.findIndex(s => s.id === whatsappId); if (sessionIndex !== -1) { sessions[sessionIndex].destroy(); sessions.splice(sessionIndex, 1); diff --git a/frontend/src/components/QrcodeModal/index.js b/frontend/src/components/QrcodeModal/index.js index 9616c66..d6821ca 100644 --- a/frontend/src/components/QrcodeModal/index.js +++ b/frontend/src/components/QrcodeModal/index.js @@ -28,7 +28,6 @@ const QrcodeModal = ({ open, onClose, whatsAppId }) => { useEffect(() => { if (!whatsAppId) return; const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - console.log("connectiing"); socket.on("whatsappSession", data => { if (data.action === "update" && data.session.id === whatsAppId) { @@ -42,7 +41,6 @@ const QrcodeModal = ({ open, onClose, whatsAppId }) => { return () => { socket.disconnect(); - console.log("disconnectiing"); }; }, [whatsAppId, onClose]); From 60e9181ece837bf631802cd4edd4b18d22b75b98 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 12:43:27 -0300 Subject: [PATCH 16/25] feat: set default whatsapp account in frontend --- backend/src/controllers/WhatsAppController.js | 56 ++++++++++++++++++- frontend/src/pages/WhatsApps/index.js | 9 ++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/WhatsAppController.js b/backend/src/controllers/WhatsAppController.js index 75d1d68..22dccdc 100644 --- a/backend/src/controllers/WhatsAppController.js +++ b/backend/src/controllers/WhatsAppController.js @@ -1,3 +1,4 @@ +const Yup = require("yup"); const Whatsapp = require("../models/Whatsapp"); const { getIO } = require("../libs/socket"); const { getWbot, initWbot, removeWbot } = require("../libs/wbot"); @@ -5,13 +6,39 @@ const wbotMessageListener = require("../services/wbotMessageListener"); const wbotMonitor = require("../services/wbotMonitor"); exports.index = async (req, res) => { - const whatsapp = await Whatsapp.findAll(); + const whatsapps = await Whatsapp.findAll(); - return res.status(200).json(whatsapp); + 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 => { + // console.log("cai no if", value); + if (value === true) { + const defaultFound = await Whatsapp.findOne({ + where: { default: true }, + }); + return !Boolean(defaultFound); + } 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) { @@ -45,6 +72,31 @@ exports.show = async (req, res) => { }; exports.update = 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 => { + // console.log("cai no if", value); + if (value === true) { + const defaultFound = await Whatsapp.findOne({ + where: { default: true }, + }); + return !Boolean(defaultFound); + } else return true; + } + ), + }); + + try { + await schema.validate(req.body); + } catch (err) { + return res.status(400).json({ error: err.message }); + } + const io = getIO(); const { whatsappId } = req.params; diff --git a/frontend/src/pages/WhatsApps/index.js b/frontend/src/pages/WhatsApps/index.js index 36fdc67..e4dcbcb 100644 --- a/frontend/src/pages/WhatsApps/index.js +++ b/frontend/src/pages/WhatsApps/index.js @@ -4,6 +4,7 @@ import { toast } from "react-toastify"; import { format, parseISO } from "date-fns"; import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; import { Button, TableBody, @@ -14,7 +15,7 @@ import { TableHead, Paper, } from "@material-ui/core"; -import { Edit, DeleteOutline } from "@material-ui/icons"; +import { Edit, DeleteOutline, CheckCircle } from "@material-ui/icons"; import MainContainer from "../../components/MainContainer"; import MainHeader from "../../components/MainHeader"; @@ -228,6 +229,7 @@ const WhatsApps = () => { Name Status Last update + Default Actions @@ -258,6 +260,11 @@ const WhatsApps = () => { {format(parseISO(whatsApp.updatedAt), "dd/MM/yy HH:mm")} + + {whatsApp.default && ( + + )} + Date: Sun, 6 Sep 2020 12:51:16 -0300 Subject: [PATCH 17/25] fix: check whatsapp default when editing --- backend/src/controllers/WhatsAppController.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/WhatsAppController.js b/backend/src/controllers/WhatsAppController.js index 22dccdc..b50dbcc 100644 --- a/backend/src/controllers/WhatsAppController.js +++ b/backend/src/controllers/WhatsAppController.js @@ -22,10 +22,10 @@ exports.store = async (req, res) => { async value => { // console.log("cai no if", value); if (value === true) { - const defaultFound = await Whatsapp.findOne({ + const whatsappFound = await Whatsapp.findOne({ where: { default: true }, }); - return !Boolean(defaultFound); + return !Boolean(whatsappFound); } else return true; } ), @@ -72,6 +72,8 @@ exports.show = async (req, res) => { }; exports.update = async (req, res) => { + const { whatsappId } = req.params; + const schema = Yup.object().shape({ name: Yup.string().required().min(2), default: Yup.boolean() @@ -80,12 +82,13 @@ exports.update = async (req, res) => { "Check-default", "Only one default whatsapp is permited", async value => { - // console.log("cai no if", value); if (value === true) { - const defaultFound = await Whatsapp.findOne({ + const whatsappFound = await Whatsapp.findOne({ where: { default: true }, }); - return !Boolean(defaultFound); + if (whatsappFound) { + return !(whatsappFound.id !== +whatsappId); + } } else return true; } ), @@ -98,7 +101,6 @@ exports.update = async (req, res) => { } const io = getIO(); - const { whatsappId } = req.params; const whatsapp = await Whatsapp.findByPk(whatsappId); From b793d28e601719073d4650ae892a9cc1b1745b12 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 14:22:05 -0300 Subject: [PATCH 18/25] feat: checking for whatsapp session Id on ticket and message creation --- backend/src/controllers/MessageController.js | 22 ++++++++++++++++--- backend/src/controllers/TicketController.js | 14 +++++++++++- backend/src/controllers/WhatsAppController.js | 3 ++- ...906155658-add-whatsapp-field-to-tickets.js | 16 ++++++++++++++ backend/src/libs/wbot.js | 3 ++- backend/src/models/Ticket.js | 4 ++++ backend/src/models/Whatsapp.js | 4 ++++ 7 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.js diff --git a/backend/src/controllers/MessageController.js b/backend/src/controllers/MessageController.js index 2639b55..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 }, @@ -104,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; @@ -126,6 +126,22 @@ exports.store = async (req, res, next) => { 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); @@ -178,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/TicketController.js b/backend/src/controllers/TicketController.js index 825203e..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"); @@ -110,7 +111,18 @@ exports.index = async (req, res) => { 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(); diff --git a/backend/src/controllers/WhatsAppController.js b/backend/src/controllers/WhatsAppController.js index b50dbcc..30fc5b3 100644 --- a/backend/src/controllers/WhatsAppController.js +++ b/backend/src/controllers/WhatsAppController.js @@ -20,7 +20,6 @@ exports.store = async (req, res) => { "Check-default", "Only one default whatsapp is permited", async value => { - // console.log("cai no if", value); if (value === true) { const whatsappFound = await Whatsapp.findOne({ where: { default: true }, @@ -88,6 +87,8 @@ exports.update = async (req, res) => { }); if (whatsappFound) { return !(whatsappFound.id !== +whatsappId); + } else { + return true; } } else return true; } 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/libs/wbot.js b/backend/src/libs/wbot.js index eee60d0..86ee3a0 100644 --- a/backend/src/libs/wbot.js +++ b/backend/src/libs/wbot.js @@ -90,7 +90,8 @@ module.exports = { const sessionIndex = sessions.findIndex(s => s.id === whatsappId); if (sessionIndex === -1) { - throw new Error("This Wbot session is not initialized"); + console.log("This Wbot session is not initialized"); + return null; } return sessions[sessionIndex]; }, 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/Whatsapp.js b/backend/src/models/Whatsapp.js index 754de3b..7d77f17 100644 --- a/backend/src/models/Whatsapp.js +++ b/backend/src/models/Whatsapp.js @@ -23,6 +23,10 @@ class Whatsapp extends Sequelize.Model { return this; } + + static associate(models) { + this.hasMany(models.Ticket, { foreignKey: "whatsappId", as: "tickets" }); + } } module.exports = Whatsapp; From fae02cf8e3ad317c97f61f47712ffc67535a18d6 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 14:31:53 -0300 Subject: [PATCH 19/25] feat: checking default whatsapp on import and create contacts --- backend/src/controllers/ContactController.js | 13 ++++++++++++- .../controllers/ImportPhoneContactsController.js | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/ContactController.js b/backend/src/controllers/ContactController.js index 2b0072f..268d5a3 100644 --- a/backend/src/controllers/ContactController.js +++ b/backend/src/controllers/ContactController.js @@ -2,6 +2,7 @@ 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"); @@ -39,7 +40,17 @@ exports.index = async (req, res) => { }; 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; diff --git a/backend/src/controllers/ImportPhoneContactsController.js b/backend/src/controllers/ImportPhoneContactsController.js index c9b79b0..b24ee0d 100644 --- a/backend/src/controllers/ImportPhoneContactsController.js +++ b/backend/src/controllers/ImportPhoneContactsController.js @@ -1,10 +1,21 @@ const Contact = require("../models/Contact"); +const Whatsapp = require("../models/Whatsapp"); const { getIO } = require("../libs/socket"); const { getWbot, initWbot } = require("../libs/wbot"); exports.store = async (req, res, next) => { + const defaultWhatsapp = await Whatsapp.findOne({ + where: { default: true }, + }); + + if (!defaultWhatsapp) { + return res + .status(404) + .json({ error: "No default WhatsApp found. Check Connection page." }); + } + const io = getIO(); - const wbot = getWbot(); + const wbot = getWbot(defaultWhatsapp); let phoneContacts; From 8e8658425f4fb46e5825fbf24a0d28f45aec2631 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 6 Sep 2020 15:12:25 -0300 Subject: [PATCH 20/25] feat: finished multiple whatsapp support --- backend/src/services/wbotMessageListener.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index 38e16d0..aed420d 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -7,6 +7,7 @@ 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, initWbot } = require("../libs/wbot"); @@ -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, }); } @@ -133,11 +135,12 @@ const handleMessage = async (msg, ticket, contact) => { }; const wbotMessageListener = whatsapp => { - const wbot = getWbot(whatsapp.id); + const whatsappId = whatsapp.id; + const wbot = getWbot(whatsappId); const io = getIO(); wbot.on("message_create", async msg => { - console.log(msg); + // console.log(msg); if ( msg.from === "status@broadcast" || @@ -159,7 +162,7 @@ const wbotMessageListener = whatsapp => { 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) { From 10bc003d02dfe4632de579c11ba3da0bc532cf35 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 7 Sep 2020 09:38:52 -0300 Subject: [PATCH 21/25] feat: block no admin users to access some routes --- backend/src/controllers/SessionController.js | 9 ++++-- backend/src/controllers/SettingController.js | 12 ++++++++ backend/src/controllers/UserController.js | 6 ++++ .../src/components/_layout/MainListItems.js | 29 +++++++++++-------- frontend/src/context/Auth/useAuth.js | 12 ++++---- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/backend/src/controllers/SessionController.js b/backend/src/controllers/SessionController.js index 2f19dca..e9afcb0 100644 --- a/backend/src/controllers/SessionController.js +++ b/backend/src/controllers/SessionController.js @@ -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 index 15e4203..045a0ee 100644 --- a/backend/src/controllers/SettingController.js +++ b/backend/src/controllers/SettingController.js @@ -2,12 +2,24 @@ 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); diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index a77da49..0f00962 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -8,6 +8,12 @@ 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 { searchParam = "", pageNumber = 1 } = req.query; const whereCondition = { diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index df2a93a..0df631f 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -38,6 +38,7 @@ function ListItemLink(props) { } const MainListItems = () => { + const userProfile = localStorage.getItem("profile"); return (
} /> @@ -57,18 +58,22 @@ const MainListItems = () => { primary={i18n.t("mainDrawer.listItems.contacts")} icon={} /> - - Administration - } - /> - } - /> + {userProfile === "admin" && ( + <> + + Administration + } + /> + } + /> + + )}
); }; diff --git a/frontend/src/context/Auth/useAuth.js b/frontend/src/context/Auth/useAuth.js index 4396d6f..327727c 100644 --- a/frontend/src/context/Auth/useAuth.js +++ b/frontend/src/context/Auth/useAuth.js @@ -47,11 +47,12 @@ const useAuth = () => { const handleLogin = async (e, user) => { e.preventDefault(); try { - const res = await api.post("/auth/login", user); - localStorage.setItem("token", JSON.stringify(res.data.token)); - localStorage.setItem("username", res.data.username); - localStorage.setItem("userId", res.data.userId); - api.defaults.headers.Authorization = `Bearer ${res.data.token}`; + const { data } = await api.post("/auth/login", user); + localStorage.setItem("token", JSON.stringify(data.token)); + localStorage.setItem("username", data.username); + localStorage.setItem("profile", data.profile); + localStorage.setItem("userId", data.userId); + api.defaults.headers.Authorization = `Bearer ${data.token}`; setIsAuth(true); toast.success(i18n.t("auth.toasts.success")); history.push("/tickets"); @@ -68,6 +69,7 @@ const useAuth = () => { setIsAuth(false); localStorage.removeItem("token"); localStorage.removeItem("username"); + localStorage.removeItem("profile"); localStorage.removeItem("userId"); api.defaults.headers.Authorization = undefined; history.push("/login"); From 324a1996b0965c3e4edffbfb8c89ef8e42b005c8 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 7 Sep 2020 09:42:23 -0300 Subject: [PATCH 22/25] improvement: change page names --- frontend/src/components/_layout/MainListItems.js | 4 ++-- frontend/src/pages/{WhatsApps => Connections}/index.js | 6 +++--- frontend/src/routes/index.js | 9 +++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) rename frontend/src/pages/{WhatsApps => Connections}/index.js (95%) diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index 0df631f..ec10505 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -43,8 +43,8 @@ const MainListItems = () => {
} /> } /> ({ }, })); -const WhatsApps = () => { +const Connections = () => { const classes = useStyles(); const [whatsApps, dispatch] = useReducer(reducer, []); @@ -211,7 +211,7 @@ const WhatsApps = () => { whatsAppId={selectedWhatsApp && !qrModalOpen && selectedWhatsApp.id} /> - WhatsApps + Connections