├── README.md ├── backend ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .sequelizerc ├── jest.config.js ├── package.json ├── prettier.config.js ├── src │ ├── @types │ │ ├── express.d.ts │ │ └── qrcode-terminal.d.ts │ ├── __tests__ │ │ ├── unit │ │ │ └── User │ │ │ │ ├── AuthUserService.spec.ts │ │ │ │ ├── CreateUserService.spec.ts │ │ │ │ ├── DeleteUserService.spec.ts │ │ │ │ ├── ListUserService.spec.ts │ │ │ │ ├── ShowUserService.spec.ts │ │ │ │ └── UpdateUserService.spec.ts │ │ └── utils │ │ │ └── database.ts │ ├── app.ts │ ├── bootstrap.ts │ ├── config │ │ ├── auth.ts │ │ ├── database.ts │ │ └── upload.ts │ ├── controllers │ │ ├── ApiController.ts │ │ ├── ContactController.ts │ │ ├── ImportPhoneContactsController.ts │ │ ├── MessageController.ts │ │ ├── QueueController.ts │ │ ├── QuickAnswerController.ts │ │ ├── SessionController.ts │ │ ├── SettingController.ts │ │ ├── TicketController.ts │ │ ├── UserController.ts │ │ ├── WhatsAppController.ts │ │ └── WhatsAppSessionController.ts │ ├── database │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 20200717133438-create-users.ts │ │ │ ├── 20200717144403-create-contacts.ts │ │ │ ├── 20200717145643-create-tickets.ts │ │ │ ├── 20200717151645-create-messages.ts │ │ │ ├── 20200717170223-create-whatsapps.ts │ │ │ ├── 20200723200315-create-contacts-custom-fields.ts │ │ │ ├── 20200723202116-add-email-field-to-contacts.ts │ │ │ ├── 20200730153237-remove-user-association-from-messages.ts │ │ │ ├── 20200730153545-add-fromMe-to-messages.ts │ │ │ ├── 20200813114236-change-ticket-lastMessage-column-type.ts │ │ │ ├── 20200901235509-add-profile-column-to-users.ts │ │ │ ├── 20200903215941-create-settings.ts │ │ │ ├── 20200904220257-add-name-to-whatsapp.ts │ │ │ ├── 20200906122228-add-name-default-field-to-whatsapp.ts │ │ │ ├── 20200906155658-add-whatsapp-field-to-tickets.ts │ │ │ ├── 20200919124112-update-default-column-name-on-whatsappp.ts │ │ │ ├── 20200927220708-add-isDeleted-column-to-messages.ts │ │ │ ├── 20200929145451-add-user-tokenVersion-column.ts │ │ │ ├── 20200930162323-add-isGroup-column-to-tickets.ts │ │ │ ├── 20200930194808-add-isGroup-column-to-contacts.ts │ │ │ ├── 20201004150008-add-contactId-column-to-messages.ts │ │ │ ├── 20201004155719-add-vcardContactId-column-to-messages.ts │ │ │ ├── 20201004955719-remove-vcardContactId-column-to-messages.ts │ │ │ ├── 20201026215410-add-retries-to-whatsapps.ts │ │ │ ├── 20201028124427-add-quoted-msg-to-messages.ts │ │ │ ├── 20210108001431-add-unreadMessages-to-tickets.ts │ │ │ ├── 20210108164404-create-queues.ts │ │ │ ├── 20210108164504-add-queueId-to-tickets.ts │ │ │ ├── 20210108174594-associate-whatsapp-queue.ts │ │ │ ├── 20210108204708-associate-users-queue.ts │ │ │ ├── 20210109192513-add-greetingMessage-to-whatsapp.ts │ │ │ ├── 20210818102605-create-quickAnswers.ts │ │ │ ├── 20211016014719-add-farewellMessage-to-whatsapp.ts │ │ │ └── 20220223095932-add-whatsapp-to-user.ts │ │ └── seeds │ │ │ ├── 20200904070004-create-default-settings.ts │ │ │ ├── 20200904070004-create-default-users.ts │ │ │ └── 20200904070006-create-apiToken-settings.ts │ ├── errors │ │ └── AppError.ts │ ├── helpers │ │ ├── CheckContactOpenTickets.ts │ │ ├── CheckSettings.ts │ │ ├── CreateTokens.ts │ │ ├── Debounce.ts │ │ ├── GetDefaultWhatsApp.ts │ │ ├── GetDefaultWhatsAppByUser.ts │ │ ├── GetTicketWbot.ts │ │ ├── GetWbotMessage.ts │ │ ├── Mustache.ts │ │ ├── SendRefreshToken.ts │ │ ├── SerializeUser.ts │ │ ├── SerializeWbotMsgId.ts │ │ ├── SetTicketMessagesAsRead.ts │ │ └── UpdateDeletedUserOpenTicketsStatus.ts │ ├── libs │ │ ├── socket.ts │ │ └── wbot.ts │ ├── middleware │ │ ├── isAuth.ts │ │ └── isAuthApi.ts │ ├── models │ │ ├── Contact.ts │ │ ├── ContactCustomField.ts │ │ ├── Message.ts │ │ ├── Queue.ts │ │ ├── QuickAnswer.ts │ │ ├── Setting.ts │ │ ├── Ticket.ts │ │ ├── User.ts │ │ ├── UserQueue.ts │ │ ├── Whatsapp.ts │ │ └── WhatsappQueue.ts │ ├── routes │ │ ├── apiRoutes.ts │ │ ├── authRoutes.ts │ │ ├── contactRoutes.ts │ │ ├── index.ts │ │ ├── messageRoutes.ts │ │ ├── queueRoutes.ts │ │ ├── quickAnswerRoutes.ts │ │ ├── settingRoutes.ts │ │ ├── ticketRoutes.ts │ │ ├── userRoutes.ts │ │ ├── whatsappRoutes.ts │ │ └── whatsappSessionRoutes.ts │ ├── server.ts │ ├── services │ │ ├── AuthServices │ │ │ └── RefreshTokenService.ts │ │ ├── ContactServices │ │ │ ├── CreateContactService.ts │ │ │ ├── CreateOrUpdateContactService.ts │ │ │ ├── DeleteContactService.ts │ │ │ ├── GetContactService.ts │ │ │ ├── ListContactsService.ts │ │ │ ├── ShowContactService.ts │ │ │ └── UpdateContactService.ts │ │ ├── MessageServices │ │ │ ├── CreateMessageService.ts │ │ │ └── ListMessagesService.ts │ │ ├── QueueService │ │ │ ├── CreateQueueService.ts │ │ │ ├── DeleteQueueService.ts │ │ │ ├── ListQueuesService.ts │ │ │ ├── ShowQueueService.ts │ │ │ └── UpdateQueueService.ts │ │ ├── QuickAnswerService │ │ │ ├── CreateQuickAnswerService.ts │ │ │ ├── DeleteQuickAnswerService.ts │ │ │ ├── ListQuickAnswerService.ts │ │ │ ├── ShowQuickAnswerService.ts │ │ │ └── UpdateQuickAnswerService.ts │ │ ├── SettingServices │ │ │ ├── ListSettingByValueService.ts │ │ │ ├── ListSettingsService.ts │ │ │ ├── ListSettingsServiceOne.ts │ │ │ └── UpdateSettingService.ts │ │ ├── TicketServices │ │ │ ├── CreateTicketService.ts │ │ │ ├── DeleteTicketService.ts │ │ │ ├── FindOrCreateTicketService.ts │ │ │ ├── ListTicketsService.ts │ │ │ ├── ShowTicketService.ts │ │ │ └── UpdateTicketService.ts │ │ ├── UserServices │ │ │ ├── AuthUserService.ts │ │ │ ├── CreateUserService.ts │ │ │ ├── DeleteUserService.ts │ │ │ ├── ListUsersService.ts │ │ │ ├── ShowUserService.ts │ │ │ └── UpdateUserService.ts │ │ ├── WbotServices │ │ │ ├── CheckIsValidContact.ts │ │ │ ├── CheckNumber.ts │ │ │ ├── DeleteWhatsAppMessage.ts │ │ │ ├── GetProfilePicUrl.ts │ │ │ ├── ImportContactsService.ts │ │ │ ├── SendWhatsAppMedia.ts │ │ │ ├── SendWhatsAppMessage.ts │ │ │ ├── StartAllWhatsAppsSessions.ts │ │ │ ├── StartWhatsAppSession.ts │ │ │ ├── wbotMessageListener.ts │ │ │ └── wbotMonitor.ts │ │ └── WhatsappService │ │ │ ├── AssociateWhatsappQueue.ts │ │ │ ├── CreateWhatsAppService.ts │ │ │ ├── DeleteWhatsAppService.ts │ │ │ ├── ListWhatsAppsService.ts │ │ │ ├── ShowWhatsAppService.ts │ │ │ └── UpdateWhatsAppService.ts │ └── utils │ │ └── logger.ts └── tsconfig.json └── frontend ├── .env ├── .gitignore ├── package.json ├── public ├── android-chrome-192x192.png ├── apple-touch-icon.png ├── default-profile.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json └── mstile-150x150.png ├── server.js └── src ├── App.js ├── assets ├── default-profile.png ├── sound.mp3 ├── sound.ogg └── wa-background.png ├── components ├── BackdropLoading │ └── index.js ├── ButtonWithSpinner │ └── index.js ├── Can │ └── index.js ├── ColorPicker │ └── index.js ├── ConfirmationModal │ └── index.js ├── ContactDrawer │ └── index.js ├── ContactDrawerSkeleton │ └── index.js ├── ContactModal │ └── index.js ├── LocationPreview │ └── index.js ├── MainContainer │ └── index.js ├── MainHeader │ └── index.js ├── MainHeaderButtonsWrapper │ └── index.js ├── MarkdownWrapper │ └── index.js ├── MessageInput │ ├── RecordingTimer.js │ └── index.js ├── MessageOptionsMenu │ └── index.js ├── MessagesList │ └── index.js ├── ModalImageCors │ └── index.js ├── NewTicketModal │ └── index.js ├── NotificationsPopOver │ └── index.js ├── QrcodeModal │ └── index.js ├── QueueModal │ └── index.js ├── QueueSelect │ └── index.js ├── QuickAnswersModal │ └── index.js ├── TabPanel │ └── index.js ├── TableRowSkeleton │ └── index.js ├── Ticket │ └── index.js ├── TicketActionButtons │ └── index.js ├── TicketHeader │ └── index.js ├── TicketHeaderSkeleton │ └── index.js ├── TicketInfo │ └── index.js ├── TicketListItem │ └── index.js ├── TicketOptionsMenu │ └── index.js ├── TicketsList │ └── index.js ├── TicketsListSkeleton │ └── index.js ├── TicketsManager │ └── index.js ├── TicketsQueueSelect │ └── index.js ├── Title │ └── index.js ├── TransferTicketModal │ └── index.js ├── UserModal │ └── index.js ├── VcardPreview │ └── index.js └── WhatsAppModal │ └── index.js ├── context ├── Auth │ └── AuthContext.js ├── ReplyingMessage │ └── ReplyingMessageContext.js └── WhatsApp │ └── WhatsAppsContext.js ├── errors └── toastError.js ├── hooks ├── useAuth.js │ └── index.js ├── useLocalStorage │ └── index.js ├── useQueues │ └── index.js ├── useTickets │ └── index.js └── useWhatsApps │ └── index.js ├── index.js ├── layout ├── MainListItems.js └── index.js ├── pages ├── Connections │ └── index.js ├── Contacts │ └── index.js ├── Dashboard │ ├── Chart.js │ ├── Title.js │ └── index.js ├── Login │ └── index.js ├── Queues │ └── index.js ├── QuickAnswers │ └── index.js ├── Settings │ └── index.js ├── Signup │ └── index.js ├── Tickets │ └── index.js └── Users │ └── index.js ├── routes ├── Route.js └── index.js ├── rules.js ├── services └── api.js └── translate ├── i18n.js └── languages ├── en.js ├── es.js ├── index.js └── pt.js /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | BACKEND_URL=http://localhost 3 | FRONTEND_URL=http://localhost:3000 4 | PROXY_PORT=8080 5 | PORT=8080 6 | 7 | DB_DIALECT=mysql 8 | DB_HOST=localhost 9 | DB_USER=root 10 | DB_PASS= 11 | DB_NAME=wpp 12 | 13 | JWT_SECRET=3123123213123 14 | JWT_REFRESH_SECRET=75756756756 15 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "airbnb-base", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "rules": { 20 | "@typescript-eslint/no-non-null-assertion": "off", 21 | "@typescript-eslint/no-unused-vars": [ 22 | "error", 23 | { "argsIgnorePattern": "_" } 24 | ], 25 | "import/prefer-default-export": "off", 26 | "no-console": "off", 27 | "no-param-reassign": "off", 28 | "prettier/prettier": "error", 29 | "import/extensions": [ 30 | "error", 31 | "ignorePackages", 32 | { 33 | "ts": "never" 34 | } 35 | ], 36 | "quotes": [ 37 | 1, 38 | "double", 39 | { 40 | "avoidEscape": true 41 | } 42 | ] 43 | }, 44 | "settings": { 45 | "import/resolver": { 46 | "typescript": {} 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/* 3 | dist 4 | !public/.gitkeep 5 | .env 6 | .env.test 7 | 8 | package-lock.json 9 | yarn.lock 10 | yarn-error.log 11 | 12 | /src/config/sentry.js 13 | 14 | # Ignore test-related files 15 | /coverage.data 16 | /coverage/ 17 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | module.exports = { 4 | "config": resolve(__dirname, "dist", "config", "database.js"), 5 | "modules-path": resolve(__dirname, "dist", "models"), 6 | "migrations-path": resolve(__dirname, "dist", "database", "migrations"), 7 | "seeders-path": resolve(__dirname, "dist", "database", "seeds") 8 | }; 9 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "start": "nodemon dist/server.js", 10 | "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts", 11 | "pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all", 12 | "test": "NODE_ENV=test jest", 13 | "posttest": "NODE_ENV=test sequelize db:migrate:undo:all" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@sentry/node": "^5.29.2", 19 | "@types/pino": "^6.3.4", 20 | "bcryptjs": "^2.4.3", 21 | "cookie-parser": "^1.4.5", 22 | "cors": "^2.8.5", 23 | "date-fns": "^2.16.1", 24 | "dotenv": "^8.2.0", 25 | "express": "^4.17.1", 26 | "express-async-errors": "^3.1.1", 27 | "http-graceful-shutdown": "^2.3.2", 28 | "jsonwebtoken": "^8.5.1", 29 | "multer": "^1.4.2", 30 | "mustache": "^4.2.0", 31 | "mysql2": "^2.2.5", 32 | "pg": "^8.4.1", 33 | "pino": "^6.9.0", 34 | "pino-pretty": "^4.3.0", 35 | "puppeteer": "^13.5.1", 36 | "qrcode-terminal": "^0.12.0", 37 | "reflect-metadata": "^0.1.13", 38 | "sequelize": "^5.22.3", 39 | "sequelize-cli": "^5.5.1", 40 | "sequelize-typescript": "^1.1.0", 41 | "socket.io": "^3.0.5", 42 | "uuid": "^8.3.2", 43 | "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js", 44 | "yup": "^0.32.8" 45 | }, 46 | "devDependencies": { 47 | "@types/bcryptjs": "^2.4.2", 48 | "@types/bluebird": "^3.5.32", 49 | "@types/cookie-parser": "^1.4.2", 50 | "@types/cors": "^2.8.7", 51 | "@types/express": "^4.17.13", 52 | "@types/factory-girl": "^5.0.2", 53 | "@types/faker": "^5.1.3", 54 | "@types/jest": "^26.0.15", 55 | "@types/jsonwebtoken": "^8.5.0", 56 | "@types/multer": "^1.4.4", 57 | "@types/mustache": "^4.1.2", 58 | "@types/node": "^14.11.8", 59 | "@types/supertest": "^2.0.10", 60 | "@types/uuid": "^8.3.3", 61 | "@types/validator": "^13.1.0", 62 | "@types/yup": "^0.29.8", 63 | "@typescript-eslint/eslint-plugin": "^4.4.0", 64 | "@typescript-eslint/parser": "^4.4.0", 65 | "eslint": "^7.10.0", 66 | "eslint-config-airbnb-base": "^14.2.0", 67 | "eslint-config-prettier": "^6.12.0", 68 | "eslint-import-resolver-typescript": "^2.3.0", 69 | "eslint-plugin-import": "^2.22.1", 70 | "eslint-plugin-prettier": "^3.1.4", 71 | "factory-girl": "^5.0.4", 72 | "faker": "^5.1.0", 73 | "jest": "^26.6.0", 74 | "nodemon": "^2.0.4", 75 | "prettier": "^2.1.2", 76 | "supertest": "^5.0.0", 77 | "ts-jest": "^26.4.1", 78 | "ts-node-dev": "^1.0.0-pre.63", 79 | "typescript": "4.0.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /backend/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: false, 3 | trailingComma: "none", 4 | arrowParens: "avoid" 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: { id: string; profile: string }; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/@types/qrcode-terminal.d.ts: -------------------------------------------------------------------------------- 1 | declare module "qrcode-terminal"; 2 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/AuthUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import AppError from "../../../errors/AppError"; 3 | import AuthUserService from "../../../services/UserServices/AuthUserService"; 4 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 5 | import { disconnect, truncate } from "../../utils/database"; 6 | 7 | describe("Auth", () => { 8 | beforeEach(async () => { 9 | await truncate(); 10 | }); 11 | 12 | afterEach(async () => { 13 | await truncate(); 14 | }); 15 | 16 | afterAll(async () => { 17 | await disconnect(); 18 | }); 19 | 20 | it("should be able to login with an existing user", async () => { 21 | const password = faker.internet.password(); 22 | const email = faker.internet.email(); 23 | 24 | await CreateUserService({ 25 | name: faker.name.findName(), 26 | email, 27 | password 28 | }); 29 | 30 | const response = await AuthUserService({ 31 | email, 32 | password 33 | }); 34 | 35 | expect(response).toHaveProperty("token"); 36 | }); 37 | 38 | it("should not be able to login with not registered email", async () => { 39 | try { 40 | await AuthUserService({ 41 | email: faker.internet.email(), 42 | password: faker.internet.password() 43 | }); 44 | } catch (err) { 45 | expect(err).toBeInstanceOf(AppError); 46 | expect(err.statusCode).toBe(401); 47 | expect(err.message).toBe("ERR_INVALID_CREDENTIALS"); 48 | } 49 | }); 50 | 51 | it("should not be able to login with incorret password", async () => { 52 | await CreateUserService({ 53 | name: faker.name.findName(), 54 | email: "mail@test.com", 55 | password: faker.internet.password() 56 | }); 57 | 58 | try { 59 | await AuthUserService({ 60 | email: "mail@test.com", 61 | password: faker.internet.password() 62 | }); 63 | } catch (err) { 64 | expect(err).toBeInstanceOf(AppError); 65 | expect(err.statusCode).toBe(401); 66 | expect(err.message).toBe("ERR_INVALID_CREDENTIALS"); 67 | } 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/CreateUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import AppError from "../../../errors/AppError"; 3 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 4 | import { disconnect, truncate } from "../../utils/database"; 5 | 6 | describe("User", () => { 7 | beforeEach(async () => { 8 | await truncate(); 9 | }); 10 | 11 | afterEach(async () => { 12 | await truncate(); 13 | }); 14 | 15 | afterAll(async () => { 16 | await disconnect(); 17 | }); 18 | 19 | it("should be able to create a new user", async () => { 20 | const user = await CreateUserService({ 21 | name: faker.name.findName(), 22 | email: faker.internet.email(), 23 | password: faker.internet.password() 24 | }); 25 | 26 | expect(user).toHaveProperty("id"); 27 | }); 28 | 29 | it("should not be able to create a user with duplicated email", async () => { 30 | await CreateUserService({ 31 | name: faker.name.findName(), 32 | email: "teste@sameemail.com", 33 | password: faker.internet.password() 34 | }); 35 | 36 | try { 37 | await CreateUserService({ 38 | name: faker.name.findName(), 39 | email: "teste@sameemail.com", 40 | password: faker.internet.password() 41 | }); 42 | } catch (err) { 43 | expect(err).toBeInstanceOf(AppError); 44 | expect(err.statusCode).toBe(400); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/DeleteUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import AppError from "../../../errors/AppError"; 3 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 4 | import DeleteUserService from "../../../services/UserServices/DeleteUserService"; 5 | import { disconnect, truncate } from "../../utils/database"; 6 | 7 | describe("User", () => { 8 | beforeEach(async () => { 9 | await truncate(); 10 | }); 11 | 12 | afterEach(async () => { 13 | await truncate(); 14 | }); 15 | 16 | afterAll(async () => { 17 | await disconnect(); 18 | }); 19 | 20 | it("should be delete a existing user", async () => { 21 | const { id } = await CreateUserService({ 22 | name: faker.name.findName(), 23 | email: faker.internet.email(), 24 | password: faker.internet.password() 25 | }); 26 | 27 | expect(DeleteUserService(id)).resolves.not.toThrow(); 28 | }); 29 | 30 | it("to throw an error if tries to delete a non existing user", async () => { 31 | expect(DeleteUserService(faker.random.number())).rejects.toBeInstanceOf( 32 | AppError 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/ListUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import User from "../../../models/User"; 3 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 4 | import ListUsersService from "../../../services/UserServices/ListUsersService"; 5 | import { disconnect, truncate } from "../../utils/database"; 6 | 7 | describe("User", () => { 8 | beforeEach(async () => { 9 | await truncate(); 10 | }); 11 | 12 | afterEach(async () => { 13 | await truncate(); 14 | }); 15 | 16 | afterAll(async () => { 17 | await disconnect(); 18 | }); 19 | 20 | it("should be able to list users", async () => { 21 | await CreateUserService({ 22 | name: faker.name.findName(), 23 | email: faker.internet.email(), 24 | password: faker.internet.password() 25 | }); 26 | 27 | const response = await ListUsersService({ 28 | pageNumber: 1 29 | }); 30 | 31 | expect(response).toHaveProperty("users"); 32 | expect(response.users[0]).toBeInstanceOf(User); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/ShowUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import AppError from "../../../errors/AppError"; 3 | import User from "../../../models/User"; 4 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 5 | import ShowUserService from "../../../services/UserServices/ShowUserService"; 6 | import { disconnect, truncate } from "../../utils/database"; 7 | 8 | describe("User", () => { 9 | beforeEach(async () => { 10 | await truncate(); 11 | }); 12 | 13 | afterEach(async () => { 14 | await truncate(); 15 | }); 16 | 17 | afterAll(async () => { 18 | await disconnect(); 19 | }); 20 | 21 | it("should be able to find a user", async () => { 22 | const newUser = await CreateUserService({ 23 | name: faker.name.findName(), 24 | email: faker.internet.email(), 25 | password: faker.internet.password() 26 | }); 27 | 28 | const user = await ShowUserService(newUser.id); 29 | 30 | expect(user).toHaveProperty("id"); 31 | expect(user).toBeInstanceOf(User); 32 | }); 33 | 34 | it("should not be able to find a inexisting user", async () => { 35 | expect(ShowUserService(faker.random.number())).rejects.toBeInstanceOf( 36 | AppError 37 | ); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /backend/src/__tests__/unit/User/UpdateUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import AppError from "../../../errors/AppError"; 3 | import CreateUserService from "../../../services/UserServices/CreateUserService"; 4 | import UpdateUserService from "../../../services/UserServices/UpdateUserService"; 5 | import { disconnect, truncate } from "../../utils/database"; 6 | 7 | describe("User", () => { 8 | beforeEach(async () => { 9 | await truncate(); 10 | }); 11 | 12 | afterEach(async () => { 13 | await truncate(); 14 | }); 15 | 16 | afterAll(async () => { 17 | await disconnect(); 18 | }); 19 | 20 | it("should be able to find a user", async () => { 21 | const newUser = await CreateUserService({ 22 | name: faker.name.findName(), 23 | email: faker.internet.email(), 24 | password: faker.internet.password() 25 | }); 26 | 27 | const updatedUser = await UpdateUserService({ 28 | userId: newUser.id, 29 | userData: { 30 | name: "New name", 31 | email: "newmail@email.com" 32 | } 33 | }); 34 | 35 | expect(updatedUser).toHaveProperty("name", "New name"); 36 | expect(updatedUser).toHaveProperty("email", "newmail@email.com"); 37 | }); 38 | 39 | it("should not be able to updated a inexisting user", async () => { 40 | const userId = faker.random.number(); 41 | const userData = { 42 | name: faker.name.findName(), 43 | email: faker.internet.email() 44 | }; 45 | 46 | expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf( 47 | AppError 48 | ); 49 | }); 50 | 51 | it("should not be able to updated an user with invalid data", async () => { 52 | const newUser = await CreateUserService({ 53 | name: faker.name.findName(), 54 | email: faker.internet.email(), 55 | password: faker.internet.password() 56 | }); 57 | 58 | const userId = newUser.id; 59 | const userData = { 60 | name: faker.name.findName(), 61 | email: "test.worgn.email" 62 | }; 63 | 64 | expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf( 65 | AppError 66 | ); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /backend/src/__tests__/utils/database.ts: -------------------------------------------------------------------------------- 1 | import database from "../../database"; 2 | 3 | const truncate = async (): Promise => { 4 | await database.truncate({ force: true, cascade: true }); 5 | }; 6 | 7 | const disconnect = async (): Promise => { 8 | return database.connectionManager.close(); 9 | }; 10 | 11 | export { truncate, disconnect }; 12 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import "./bootstrap"; 2 | import "reflect-metadata"; 3 | import "express-async-errors"; 4 | import express, { Request, Response, NextFunction } from "express"; 5 | import cors from "cors"; 6 | import cookieParser from "cookie-parser"; 7 | import * as Sentry from "@sentry/node"; 8 | 9 | import "./database"; 10 | import uploadConfig from "./config/upload"; 11 | import AppError from "./errors/AppError"; 12 | import routes from "./routes"; 13 | import { logger } from "./utils/logger"; 14 | 15 | Sentry.init({ dsn: process.env.SENTRY_DSN }); 16 | 17 | const app = express(); 18 | 19 | app.use( 20 | cors({ 21 | credentials: true, 22 | origin: process.env.FRONTEND_URL 23 | }) 24 | ); 25 | app.use(cookieParser()); 26 | app.use(express.json()); 27 | app.use(Sentry.Handlers.requestHandler()); 28 | app.use("/public", express.static(uploadConfig.directory)); 29 | app.use(routes); 30 | 31 | app.use(Sentry.Handlers.errorHandler()); 32 | 33 | app.use(async (err: Error, req: Request, res: Response, _: NextFunction) => { 34 | if (err instanceof AppError) { 35 | logger.warn(err); 36 | return res.status(err.statusCode).json({ error: err.message }); 37 | } 38 | 39 | logger.error(err); 40 | return res.status(500).json({ error: "Internal server error" }); 41 | }); 42 | 43 | export default app; 44 | -------------------------------------------------------------------------------- /backend/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | dotenv.config({ 4 | path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" 5 | }); 6 | -------------------------------------------------------------------------------- /backend/src/config/auth.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | secret: process.env.JWT_SECRET || "mysecret", 3 | expiresIn: "15m", 4 | refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret", 5 | refreshExpiresIn: "7d" 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/config/database.ts: -------------------------------------------------------------------------------- 1 | require("../bootstrap"); 2 | 3 | module.exports = { 4 | define: { 5 | charset: "utf8mb4", 6 | collate: "utf8mb4_bin" 7 | }, 8 | dialect: process.env.DB_DIALECT || "mysql", 9 | timezone: "-03:00", 10 | host: process.env.DB_HOST, 11 | database: process.env.DB_NAME, 12 | username: process.env.DB_USER, 13 | password: process.env.DB_PASS, 14 | logging: false 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/config/upload.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import multer from "multer"; 3 | 4 | const publicFolder = path.resolve(__dirname, "..", "..", "public"); 5 | export default { 6 | directory: publicFolder, 7 | 8 | storage: multer.diskStorage({ 9 | destination: publicFolder, 10 | filename(req, file, cb) { 11 | // const fileName = new Date().getTime() + path.extname(file.originalname); 12 | 13 | var arquivo = file.originalname; 14 | const fileName = arquivo.substring(0, arquivo.lastIndexOf(".")) + ' -' + new Date().getTime() + path.extname(file.originalname); 15 | 16 | return cb(null, fileName); 17 | } 18 | }) 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/controllers/ImportPhoneContactsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import ImportContactsService from "../services/WbotServices/ImportContactsService"; 3 | 4 | export const store = async (req: Request, res: Response): Promise => { 5 | const userId:number = parseInt(req.user.id); 6 | await ImportContactsService(userId); 7 | 8 | return res.status(200).json({ message: "contacts imported" }); 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/controllers/MessageController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead"; 4 | import { getIO } from "../libs/socket"; 5 | import Message from "../models/Message"; 6 | 7 | import ListMessagesService from "../services/MessageServices/ListMessagesService"; 8 | import ShowTicketService from "../services/TicketServices/ShowTicketService"; 9 | import DeleteWhatsAppMessage from "../services/WbotServices/DeleteWhatsAppMessage"; 10 | import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia"; 11 | import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage"; 12 | 13 | type IndexQuery = { 14 | pageNumber: string; 15 | }; 16 | 17 | type MessageData = { 18 | body: string; 19 | fromMe: boolean; 20 | read: boolean; 21 | quotedMsg?: Message; 22 | }; 23 | 24 | export const index = async (req: Request, res: Response): Promise => { 25 | const { ticketId } = req.params; 26 | const { pageNumber } = req.query as IndexQuery; 27 | 28 | const { count, messages, ticket, hasMore } = await ListMessagesService({ 29 | pageNumber, 30 | ticketId 31 | }); 32 | 33 | SetTicketMessagesAsRead(ticket); 34 | 35 | return res.json({ count, messages, ticket, hasMore }); 36 | }; 37 | 38 | export const store = async (req: Request, res: Response): Promise => { 39 | const { ticketId } = req.params; 40 | const { body, quotedMsg }: MessageData = req.body; 41 | const medias = req.files as Express.Multer.File[]; 42 | 43 | const ticket = await ShowTicketService(ticketId); 44 | 45 | SetTicketMessagesAsRead(ticket); 46 | 47 | if (medias) { 48 | await Promise.all( 49 | medias.map(async (media: Express.Multer.File) => { 50 | await SendWhatsAppMedia({ media, ticket }); 51 | }) 52 | ); 53 | } else { 54 | await SendWhatsAppMessage({ body, ticket, quotedMsg }); 55 | } 56 | 57 | return res.send(); 58 | }; 59 | 60 | export const remove = async ( 61 | req: Request, 62 | res: Response 63 | ): Promise => { 64 | const { messageId } = req.params; 65 | 66 | const message = await DeleteWhatsAppMessage(messageId); 67 | 68 | const io = getIO(); 69 | io.to(message.ticketId.toString()).emit("appMessage", { 70 | action: "update", 71 | message 72 | }); 73 | 74 | return res.send(); 75 | }; 76 | -------------------------------------------------------------------------------- /backend/src/controllers/QueueController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getIO } from "../libs/socket"; 3 | import CreateQueueService from "../services/QueueService/CreateQueueService"; 4 | import DeleteQueueService from "../services/QueueService/DeleteQueueService"; 5 | import ListQueuesService from "../services/QueueService/ListQueuesService"; 6 | import ShowQueueService from "../services/QueueService/ShowQueueService"; 7 | import UpdateQueueService from "../services/QueueService/UpdateQueueService"; 8 | 9 | export const index = async (req: Request, res: Response): Promise => { 10 | const queues = await ListQueuesService(); 11 | 12 | return res.status(200).json(queues); 13 | }; 14 | 15 | export const store = async (req: Request, res: Response): Promise => { 16 | const { name, color, greetingMessage } = req.body; 17 | 18 | const queue = await CreateQueueService({ name, color, greetingMessage }); 19 | 20 | const io = getIO(); 21 | io.emit("queue", { 22 | action: "update", 23 | queue 24 | }); 25 | 26 | return res.status(200).json(queue); 27 | }; 28 | 29 | export const show = async (req: Request, res: Response): Promise => { 30 | const { queueId } = req.params; 31 | 32 | const queue = await ShowQueueService(queueId); 33 | 34 | return res.status(200).json(queue); 35 | }; 36 | 37 | export const update = async ( 38 | req: Request, 39 | res: Response 40 | ): Promise => { 41 | const { queueId } = req.params; 42 | 43 | const queue = await UpdateQueueService(queueId, req.body); 44 | 45 | const io = getIO(); 46 | io.emit("queue", { 47 | action: "update", 48 | queue 49 | }); 50 | 51 | return res.status(201).json(queue); 52 | }; 53 | 54 | export const remove = async ( 55 | req: Request, 56 | res: Response 57 | ): Promise => { 58 | const { queueId } = req.params; 59 | 60 | await DeleteQueueService(queueId); 61 | 62 | const io = getIO(); 63 | io.emit("queue", { 64 | action: "delete", 65 | queueId: +queueId 66 | }); 67 | 68 | return res.status(200).send(); 69 | }; 70 | -------------------------------------------------------------------------------- /backend/src/controllers/SessionController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import AppError from "../errors/AppError"; 3 | 4 | import AuthUserService from "../services/UserServices/AuthUserService"; 5 | import { SendRefreshToken } from "../helpers/SendRefreshToken"; 6 | import { RefreshTokenService } from "../services/AuthServices/RefreshTokenService"; 7 | 8 | export const store = async (req: Request, res: Response): Promise => { 9 | const { email, password } = req.body; 10 | 11 | const { token, serializedUser, refreshToken } = await AuthUserService({ 12 | email, 13 | password 14 | }); 15 | 16 | SendRefreshToken(res, refreshToken); 17 | 18 | return res.status(200).json({ 19 | token, 20 | user: serializedUser 21 | }); 22 | }; 23 | 24 | export const update = async ( 25 | req: Request, 26 | res: Response 27 | ): Promise => { 28 | const token: string = req.cookies.jrt; 29 | 30 | if (!token) { 31 | throw new AppError("ERR_SESSION_EXPIRED", 401); 32 | } 33 | 34 | const { user, newToken, refreshToken } = await RefreshTokenService( 35 | res, 36 | token 37 | ); 38 | 39 | SendRefreshToken(res, refreshToken); 40 | 41 | return res.json({ token: newToken, user }); 42 | }; 43 | 44 | export const remove = async ( 45 | req: Request, 46 | res: Response 47 | ): Promise => { 48 | res.clearCookie("jrt"); 49 | 50 | return res.send(); 51 | }; 52 | -------------------------------------------------------------------------------- /backend/src/controllers/SettingController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import { getIO } from "../libs/socket"; 4 | import AppError from "../errors/AppError"; 5 | 6 | import UpdateSettingService from "../services/SettingServices/UpdateSettingService"; 7 | import ListSettingsService from "../services/SettingServices/ListSettingsService"; 8 | 9 | export const index = async (req: Request, res: Response): Promise => { 10 | if (req.user.profile !== "admin") { 11 | throw new AppError("ERR_NO_PERMISSION", 403); 12 | } 13 | 14 | const settings = await ListSettingsService(); 15 | 16 | return res.status(200).json(settings); 17 | }; 18 | 19 | export const update = async ( 20 | req: Request, 21 | res: Response 22 | ): Promise => { 23 | if (req.user.profile !== "admin") { 24 | throw new AppError("ERR_NO_PERMISSION", 403); 25 | } 26 | const { settingKey: key } = req.params; 27 | const { value } = req.body; 28 | 29 | const setting = await UpdateSettingService({ 30 | key, 31 | value 32 | }); 33 | 34 | const io = getIO(); 35 | io.emit("settings", { 36 | action: "update", 37 | setting 38 | }); 39 | 40 | return res.status(200).json(setting); 41 | }; 42 | -------------------------------------------------------------------------------- /backend/src/controllers/WhatsAppSessionController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getWbot } from "../libs/wbot"; 3 | import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; 4 | import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession"; 5 | import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService"; 6 | 7 | const store = async (req: Request, res: Response): Promise => { 8 | const { whatsappId } = req.params; 9 | const whatsapp = await ShowWhatsAppService(whatsappId); 10 | 11 | StartWhatsAppSession(whatsapp); 12 | 13 | return res.status(200).json({ message: "Starting session." }); 14 | }; 15 | 16 | const update = async (req: Request, res: Response): Promise => { 17 | const { whatsappId } = req.params; 18 | 19 | const { whatsapp } = await UpdateWhatsAppService({ 20 | whatsappId, 21 | whatsappData: { session: "" } 22 | }); 23 | 24 | StartWhatsAppSession(whatsapp); 25 | 26 | return res.status(200).json({ message: "Starting session." }); 27 | }; 28 | 29 | const remove = async (req: Request, res: Response): Promise => { 30 | const { whatsappId } = req.params; 31 | const whatsapp = await ShowWhatsAppService(whatsappId); 32 | 33 | const wbot = getWbot(whatsapp.id); 34 | 35 | wbot.logout(); 36 | 37 | return res.status(200).json({ message: "Session disconnected." }); 38 | }; 39 | 40 | export default { store, remove, update }; 41 | -------------------------------------------------------------------------------- /backend/src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize-typescript"; 2 | import User from "../models/User"; 3 | import Setting from "../models/Setting"; 4 | import Contact from "../models/Contact"; 5 | import Ticket from "../models/Ticket"; 6 | import Whatsapp from "../models/Whatsapp"; 7 | import ContactCustomField from "../models/ContactCustomField"; 8 | import Message from "../models/Message"; 9 | import Queue from "../models/Queue"; 10 | import WhatsappQueue from "../models/WhatsappQueue"; 11 | import UserQueue from "../models/UserQueue"; 12 | import QuickAnswer from "../models/QuickAnswer"; 13 | 14 | // eslint-disable-next-line 15 | const dbConfig = require("../config/database"); 16 | // import dbConfig from "../config/database"; 17 | 18 | const sequelize = new Sequelize(dbConfig); 19 | 20 | const models = [ 21 | User, 22 | Contact, 23 | Ticket, 24 | Message, 25 | Whatsapp, 26 | ContactCustomField, 27 | Setting, 28 | Queue, 29 | WhatsappQueue, 30 | UserQueue, 31 | QuickAnswer 32 | ]; 33 | 34 | sequelize.addModels(models); 35 | 36 | export default sequelize; 37 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717133438-create-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Users", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | email: { 17 | type: DataTypes.STRING, 18 | allowNull: false, 19 | unique: true 20 | }, 21 | passwordHash: { 22 | type: DataTypes.STRING, 23 | allowNull: false 24 | }, 25 | createdAt: { 26 | type: DataTypes.DATE, 27 | allowNull: false 28 | }, 29 | updatedAt: { 30 | type: DataTypes.DATE, 31 | allowNull: false 32 | } 33 | }); 34 | }, 35 | 36 | down: (queryInterface: QueryInterface) => { 37 | return queryInterface.dropTable("Users"); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717144403-create-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Contacts", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | number: { 17 | type: DataTypes.STRING, 18 | allowNull: false, 19 | unique: true 20 | }, 21 | profilePicUrl: { 22 | type: DataTypes.STRING 23 | }, 24 | createdAt: { 25 | type: DataTypes.DATE, 26 | allowNull: false 27 | }, 28 | updatedAt: { 29 | type: DataTypes.DATE, 30 | allowNull: false 31 | } 32 | }); 33 | }, 34 | 35 | down: (queryInterface: QueryInterface) => { 36 | return queryInterface.dropTable("Contacts"); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717145643-create-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Tickets", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | status: { 13 | type: DataTypes.STRING, 14 | defaultValue: "pending", 15 | allowNull: false 16 | }, 17 | lastMessage: { 18 | type: DataTypes.STRING 19 | }, 20 | contactId: { 21 | type: DataTypes.INTEGER, 22 | references: { model: "Contacts", key: "id" }, 23 | onUpdate: "CASCADE", 24 | onDelete: "CASCADE" 25 | }, 26 | userId: { 27 | type: DataTypes.INTEGER, 28 | references: { model: "Users", key: "id" }, 29 | onUpdate: "CASCADE", 30 | onDelete: "SET NULL" 31 | }, 32 | createdAt: { 33 | type: DataTypes.DATE(6), 34 | allowNull: false 35 | }, 36 | updatedAt: { 37 | type: DataTypes.DATE(6), 38 | allowNull: false 39 | } 40 | }); 41 | }, 42 | 43 | down: (queryInterface: QueryInterface) => { 44 | return queryInterface.dropTable("Tickets"); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717151645-create-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Messages", { 6 | id: { 7 | type: DataTypes.STRING, 8 | primaryKey: true, 9 | allowNull: false 10 | }, 11 | body: { 12 | type: DataTypes.TEXT, 13 | allowNull: false 14 | }, 15 | ack: { 16 | type: DataTypes.INTEGER, 17 | allowNull: false, 18 | defaultValue: 0 19 | }, 20 | read: { 21 | type: DataTypes.BOOLEAN, 22 | allowNull: false, 23 | defaultValue: false 24 | }, 25 | mediaType: { 26 | type: DataTypes.STRING 27 | }, 28 | mediaUrl: { 29 | type: DataTypes.STRING 30 | }, 31 | userId: { 32 | type: DataTypes.INTEGER, 33 | references: { model: "Users", key: "id" }, 34 | onUpdate: "CASCADE", 35 | onDelete: "SET NULL" 36 | }, 37 | ticketId: { 38 | type: DataTypes.INTEGER, 39 | references: { model: "Tickets", key: "id" }, 40 | onUpdate: "CASCADE", 41 | onDelete: "CASCADE", 42 | allowNull: false 43 | }, 44 | createdAt: { 45 | type: DataTypes.DATE(6), 46 | allowNull: false 47 | }, 48 | updatedAt: { 49 | type: DataTypes.DATE(6), 50 | allowNull: false 51 | } 52 | }); 53 | }, 54 | 55 | down: (queryInterface: QueryInterface) => { 56 | return queryInterface.dropTable("Messages"); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717170223-create-whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Whatsapps", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | session: { 13 | type: DataTypes.TEXT 14 | }, 15 | qrcode: { 16 | type: DataTypes.TEXT 17 | }, 18 | status: { 19 | type: DataTypes.STRING 20 | }, 21 | battery: { 22 | type: DataTypes.STRING 23 | }, 24 | plugged: { 25 | type: DataTypes.BOOLEAN 26 | }, 27 | createdAt: { 28 | type: DataTypes.DATE, 29 | allowNull: false 30 | }, 31 | updatedAt: { 32 | type: DataTypes.DATE, 33 | allowNull: false 34 | } 35 | }); 36 | }, 37 | 38 | down: (queryInterface: QueryInterface) => { 39 | return queryInterface.dropTable("Whatsapps"); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200723200315-create-contacts-custom-fields.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("ContactCustomFields", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | value: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | }, 20 | contactId: { 21 | type: DataTypes.INTEGER, 22 | references: { model: "Contacts", key: "id" }, 23 | onUpdate: "CASCADE", 24 | onDelete: "CASCADE", 25 | allowNull: false 26 | }, 27 | createdAt: { 28 | type: DataTypes.DATE, 29 | allowNull: false 30 | }, 31 | updatedAt: { 32 | type: DataTypes.DATE, 33 | allowNull: false 34 | } 35 | }); 36 | }, 37 | 38 | down: (queryInterface: QueryInterface) => { 39 | return queryInterface.dropTable("ContactCustomFields"); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200723202116-add-email-field-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "email", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | defaultValue: "" 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "email"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200730153237-remove-user-association-from-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeColumn("Messages", "userId"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.addColumn("Messages", "userId", { 10 | type: DataTypes.INTEGER, 11 | references: { model: "Users", key: "id" }, 12 | onUpdate: "CASCADE", 13 | onDelete: "SET NULL" 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200730153545-add-fromMe-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "fromMe", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Messages", "fromMe"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200813114236-change-ticket-lastMessage-column-type.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.changeColumn("Tickets", "lastMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.changeColumn("Tickets", "lastMessage", { 12 | type: DataTypes.STRING 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200901235509-add-profile-column-to-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "profile", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | defaultValue: "admin" 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Users", "profile"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200903215941-create-settings.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Settings", { 6 | key: { 7 | type: DataTypes.STRING, 8 | primaryKey: true, 9 | allowNull: false 10 | }, 11 | value: { 12 | type: DataTypes.TEXT, 13 | allowNull: false 14 | }, 15 | createdAt: { 16 | type: DataTypes.DATE, 17 | allowNull: false 18 | }, 19 | updatedAt: { 20 | type: DataTypes.DATE, 21 | allowNull: false 22 | } 23 | }); 24 | }, 25 | 26 | down: (queryInterface: QueryInterface) => { 27 | return queryInterface.dropTable("Settings"); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200904220257-add-name-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "name", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | unique: true 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "name"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "default", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "default"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "whatsappId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Whatsapps", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Tickets", "whatsappId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200919124112-update-default-column-name-on-whatsappp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.renameColumn("Whatsapps", "default", "isDefault"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.renameColumn("Whatsapps", "isDefault", "default"); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200927220708-add-isDeleted-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "isDeleted", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Messages", "isDeleted"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200929145451-add-user-tokenVersion-column.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "tokenVersion", { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | defaultValue: 0 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Users", "tokenVersion"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200930162323-add-isGroup-column-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "isGroup", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Tickets", "isGroup"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200930194808-add-isGroup-column-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "isGroup", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "isGroup"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "contactId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Contacts", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "CASCADE" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "contactId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "vcardContactId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Contacts", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "CASCADE" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "vcardContactId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004955719-remove-vcardContactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeColumn("Messages", "vcardContactId"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.addColumn("Messages", "vcardContactId", { 10 | type: DataTypes.INTEGER, 11 | references: { model: "Contacts", key: "id" }, 12 | onUpdate: "CASCADE", 13 | onDelete: "CASCADE" 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201026215410-add-retries-to-whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "retries", { 6 | type: DataTypes.INTEGER, 7 | defaultValue: 0, 8 | allowNull: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "retries"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201028124427-add-quoted-msg-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "quotedMsgId", { 6 | type: DataTypes.STRING, 7 | references: { model: "Messages", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "quotedMsgId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "unreadMessages", { 6 | type: DataTypes.INTEGER 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Tickets", "unreadMessages"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108164404-create-queues.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Queues", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false, 15 | unique: true 16 | }, 17 | color: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | unique: true 21 | }, 22 | greetingMessage: { 23 | type: DataTypes.TEXT 24 | }, 25 | createdAt: { 26 | type: DataTypes.DATE, 27 | allowNull: false 28 | }, 29 | updatedAt: { 30 | type: DataTypes.DATE, 31 | allowNull: false 32 | } 33 | }); 34 | }, 35 | 36 | down: (queryInterface: QueryInterface) => { 37 | return queryInterface.dropTable("Queues"); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "queueId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Queues", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Tickets", "queueId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("WhatsappQueues", { 6 | whatsappId: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true 9 | }, 10 | queueId: { 11 | type: DataTypes.INTEGER, 12 | primaryKey: true 13 | }, 14 | createdAt: { 15 | type: DataTypes.DATE, 16 | allowNull: false 17 | }, 18 | updatedAt: { 19 | type: DataTypes.DATE, 20 | allowNull: false 21 | } 22 | }); 23 | }, 24 | 25 | down: (queryInterface: QueryInterface) => { 26 | return queryInterface.dropTable("WhatsappQueues"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108204708-associate-users-queue.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("UserQueues", { 6 | userId: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true 9 | }, 10 | queueId: { 11 | type: DataTypes.INTEGER, 12 | primaryKey: true 13 | }, 14 | createdAt: { 15 | type: DataTypes.DATE, 16 | allowNull: false 17 | }, 18 | updatedAt: { 19 | type: DataTypes.DATE, 20 | allowNull: false 21 | } 22 | }); 23 | }, 24 | 25 | down: (queryInterface: QueryInterface) => { 26 | return queryInterface.dropTable("UserQueues"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "greetingMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "greetingMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210818102605-create-quickAnswers.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("QuickAnswers", { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | allowNull: false 11 | }, 12 | shortcut: { 13 | type: DataTypes.TEXT, 14 | allowNull: false 15 | }, 16 | message: { 17 | type: DataTypes.TEXT, 18 | allowNull: false 19 | }, 20 | createdAt: { 21 | type: DataTypes.DATE, 22 | allowNull: false 23 | }, 24 | updatedAt: { 25 | type: DataTypes.DATE, 26 | allowNull: false 27 | } 28 | }); 29 | }, 30 | 31 | down: (queryInterface: QueryInterface) => { 32 | return queryInterface.dropTable("QuickAnswers"); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20211016014719-add-farewellMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "farewellMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "farewellMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220223095932-add-whatsapp-to-user.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "whatsappId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Whatsapps", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL", 10 | allowNull: true 11 | },); 12 | }, 13 | 14 | down: (queryInterface: QueryInterface) => { 15 | return queryInterface.removeColumn("Users", "whatsappId"); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/database/seeds/20200904070004-create-default-settings.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.bulkInsert( 6 | "Settings", 7 | [ 8 | { 9 | key: "userCreation", 10 | value: "enabled", 11 | createdAt: new Date(), 12 | updatedAt: new Date() 13 | }, 14 | { 15 | key: "call", 16 | value: "enabled", 17 | createdAt: new Date(), 18 | updatedAt: new Date() 19 | }, 20 | { 21 | key: "timeCreateNewTicket", 22 | value: "43200", 23 | createdAt: new Date(), 24 | updatedAt: new Date() 25 | }, 26 | { 27 | key: "CheckMsgIsGroup", 28 | value: "enabled", 29 | createdAt: new Date(), 30 | updatedAt: new Date() 31 | } 32 | ], 33 | {} 34 | ); 35 | }, 36 | 37 | down: (queryInterface: QueryInterface) => { 38 | return queryInterface.bulkDelete("Settings", {}); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /backend/src/database/seeds/20200904070004-create-default-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.bulkInsert( 6 | "Users", 7 | [ 8 | { 9 | name: "Administrador", 10 | email: "admin@whaticket.com", 11 | passwordHash: "$2a$08$WaEmpmFDD/XkDqorkpQ42eUZozOqRCPkPcTkmHHMyuTGUOkI8dHsq", 12 | profile: "admin", 13 | tokenVersion: 0, 14 | createdAt: new Date(), 15 | updatedAt: new Date() 16 | } 17 | ], 18 | {} 19 | ); 20 | }, 21 | 22 | down: (queryInterface: QueryInterface) => { 23 | return queryInterface.bulkDelete("Users", {}); 24 | } 25 | }; -------------------------------------------------------------------------------- /backend/src/database/seeds/20200904070006-create-apiToken-settings.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | module.exports = { 5 | up: (queryInterface: QueryInterface) => { 6 | return queryInterface.bulkInsert( 7 | "Settings", 8 | [ 9 | { 10 | key: "userApiToken", 11 | value: uuidv4(), 12 | createdAt: new Date(), 13 | updatedAt: new Date() 14 | } 15 | ], 16 | {} 17 | ); 18 | }, 19 | 20 | down: (queryInterface: QueryInterface) => { 21 | return queryInterface.bulkDelete("Settings", {}); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/errors/AppError.ts: -------------------------------------------------------------------------------- 1 | class AppError { 2 | public readonly message: string; 3 | 4 | public readonly statusCode: number; 5 | 6 | constructor(message: string, statusCode = 400) { 7 | this.message = message; 8 | this.statusCode = statusCode; 9 | } 10 | } 11 | 12 | export default AppError; 13 | -------------------------------------------------------------------------------- /backend/src/helpers/CheckContactOpenTickets.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | import AppError from "../errors/AppError"; 3 | import Ticket from "../models/Ticket"; 4 | 5 | const CheckContactOpenTickets = async ( 6 | contactId: number, 7 | whatsappId: number 8 | ): Promise => { 9 | const ticket = await Ticket.findOne({ 10 | where: { contactId, whatsappId, status: { [Op.or]: ["open", "pending"] } } 11 | }); 12 | 13 | if (ticket) { 14 | throw new AppError("ERR_OTHER_OPEN_TICKET"); 15 | } 16 | }; 17 | 18 | export default CheckContactOpenTickets; 19 | -------------------------------------------------------------------------------- /backend/src/helpers/CheckSettings.ts: -------------------------------------------------------------------------------- 1 | import Setting from "../models/Setting"; 2 | import AppError from "../errors/AppError"; 3 | 4 | const CheckSettings = async (key: string): Promise => { 5 | const setting = await Setting.findOne({ 6 | where: { key } 7 | }); 8 | 9 | if (!setting) { 10 | throw new AppError("ERR_NO_SETTING_FOUND", 404); 11 | } 12 | 13 | return setting.value; 14 | }; 15 | 16 | export default CheckSettings; 17 | -------------------------------------------------------------------------------- /backend/src/helpers/CreateTokens.ts: -------------------------------------------------------------------------------- 1 | import { sign } from "jsonwebtoken"; 2 | import authConfig from "../config/auth"; 3 | import User from "../models/User"; 4 | 5 | export const createAccessToken = (user: User): string => { 6 | const { secret, expiresIn } = authConfig; 7 | 8 | return sign( 9 | { usarname: user.name, profile: user.profile, id: user.id }, 10 | secret, 11 | { 12 | expiresIn 13 | } 14 | ); 15 | }; 16 | 17 | export const createRefreshToken = (user: User): string => { 18 | const { refreshSecret, refreshExpiresIn } = authConfig; 19 | 20 | return sign({ id: user.id, tokenVersion: user.tokenVersion }, refreshSecret, { 21 | expiresIn: refreshExpiresIn 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/helpers/Debounce.ts: -------------------------------------------------------------------------------- 1 | interface Timeout { 2 | id: number; 3 | timeout: NodeJS.Timeout; 4 | } 5 | 6 | const timeouts: Timeout[] = []; 7 | 8 | const findAndClearTimeout = (ticketId: number) => { 9 | if (timeouts.length > 0) { 10 | const timeoutIndex = timeouts.findIndex(timeout => timeout.id === ticketId); 11 | 12 | if (timeoutIndex !== -1) { 13 | clearTimeout(timeouts[timeoutIndex].timeout); 14 | timeouts.splice(timeoutIndex, 1); 15 | } 16 | } 17 | }; 18 | 19 | const debounce = ( 20 | func: { (): Promise; (...args: never[]): void }, 21 | wait: number, 22 | ticketId: number 23 | ) => { 24 | return function executedFunction(...args: never[]): void { 25 | const later = () => { 26 | findAndClearTimeout(ticketId); 27 | func(...args); 28 | }; 29 | 30 | findAndClearTimeout(ticketId); 31 | 32 | const newTimeout = { 33 | id: ticketId, 34 | timeout: setTimeout(later, wait) 35 | }; 36 | 37 | timeouts.push(newTimeout); 38 | }; 39 | }; 40 | 41 | export { debounce }; 42 | -------------------------------------------------------------------------------- /backend/src/helpers/GetDefaultWhatsApp.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../errors/AppError"; 2 | import Whatsapp from "../models/Whatsapp"; 3 | import GetDefaultWhatsAppByUser from "./GetDefaultWhatsAppByUser"; 4 | 5 | const GetDefaultWhatsApp = async ( 6 | userId?: number 7 | ): Promise => { 8 | if(userId) { 9 | const whatsappByUser = await GetDefaultWhatsAppByUser(userId); 10 | if(whatsappByUser !== null) { 11 | return whatsappByUser; 12 | } 13 | } 14 | 15 | const defaultWhatsapp = await Whatsapp.findOne({ 16 | where: { isDefault: true } 17 | }); 18 | 19 | if (!defaultWhatsapp) { 20 | throw new AppError("ERR_NO_DEF_WAPP_FOUND"); 21 | } 22 | 23 | return defaultWhatsapp; 24 | }; 25 | 26 | export default GetDefaultWhatsApp; 27 | -------------------------------------------------------------------------------- /backend/src/helpers/GetDefaultWhatsAppByUser.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/User"; 2 | import Whatsapp from "../models/Whatsapp"; 3 | import { logger } from "../utils/logger"; 4 | 5 | const GetDefaultWhatsAppByUser = async ( 6 | userId: number 7 | ): Promise => { 8 | const user = await User.findByPk(userId, {include: ["whatsapp"]}); 9 | if( user === null ) { 10 | return null; 11 | } 12 | 13 | if(user.whatsapp !== null) { 14 | logger.info(`Found whatsapp linked to user '${user.name}' is '${user.whatsapp.name}'.`); 15 | } 16 | 17 | return user.whatsapp; 18 | }; 19 | 20 | export default GetDefaultWhatsAppByUser; 21 | -------------------------------------------------------------------------------- /backend/src/helpers/GetTicketWbot.ts: -------------------------------------------------------------------------------- 1 | import { Client as Session } from "whatsapp-web.js"; 2 | import { getWbot } from "../libs/wbot"; 3 | import GetDefaultWhatsApp from "./GetDefaultWhatsApp"; 4 | import Ticket from "../models/Ticket"; 5 | 6 | const GetTicketWbot = async (ticket: Ticket): Promise => { 7 | if (!ticket.whatsappId) { 8 | const defaultWhatsapp = await GetDefaultWhatsApp(ticket.user.id); 9 | 10 | await ticket.$set("whatsapp", defaultWhatsapp); 11 | } 12 | 13 | const wbot = getWbot(ticket.whatsappId); 14 | 15 | return wbot; 16 | }; 17 | 18 | export default GetTicketWbot; 19 | -------------------------------------------------------------------------------- /backend/src/helpers/GetWbotMessage.ts: -------------------------------------------------------------------------------- 1 | import { Message as WbotMessage } from "whatsapp-web.js"; 2 | import Ticket from "../models/Ticket"; 3 | import GetTicketWbot from "./GetTicketWbot"; 4 | import AppError from "../errors/AppError"; 5 | 6 | export const GetWbotMessage = async ( 7 | ticket: Ticket, 8 | messageId: string 9 | ): Promise => { 10 | const wbot = await GetTicketWbot(ticket); 11 | 12 | const wbotChat = await wbot.getChatById( 13 | `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us` 14 | ); 15 | 16 | let limit = 20; 17 | 18 | const fetchWbotMessagesGradually = async (): Promise => { 19 | const chatMessages = await wbotChat.fetchMessages({ limit }); 20 | 21 | const msgFound = chatMessages.find(msg => msg.id.id === messageId); 22 | 23 | if (!msgFound && limit < 100) { 24 | limit += 20; 25 | return fetchWbotMessagesGradually(); 26 | } 27 | 28 | return msgFound; 29 | }; 30 | 31 | try { 32 | const msgFound = await fetchWbotMessagesGradually(); 33 | 34 | if (!msgFound) { 35 | throw new Error("Cannot found message within 100 last messages"); 36 | } 37 | 38 | return msgFound; 39 | } catch (err) { 40 | throw new AppError("ERR_FETCH_WAPP_MSG"); 41 | } 42 | }; 43 | 44 | export default GetWbotMessage; 45 | -------------------------------------------------------------------------------- /backend/src/helpers/Mustache.ts: -------------------------------------------------------------------------------- 1 | import Mustache from "mustache"; 2 | import Contact from "../models/Contact"; 3 | 4 | export default (body: string, contact: Contact): string => { 5 | const view = { 6 | name: contact ? contact.name : "" 7 | }; 8 | return Mustache.render(body, view); 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/helpers/SendRefreshToken.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | 3 | export const SendRefreshToken = (res: Response, token: string): void => { 4 | res.cookie("jrt", token, { httpOnly: true }); 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/helpers/SerializeUser.ts: -------------------------------------------------------------------------------- 1 | import Queue from "../models/Queue"; 2 | import User from "../models/User"; 3 | import Whatsapp from "../models/Whatsapp"; 4 | 5 | interface SerializedUser { 6 | id: number; 7 | name: string; 8 | email: string; 9 | profile: string; 10 | queues: Queue[]; 11 | whatsapp: Whatsapp; 12 | } 13 | 14 | export const SerializeUser = (user: User): SerializedUser => { 15 | return { 16 | id: user.id, 17 | name: user.name, 18 | email: user.email, 19 | profile: user.profile, 20 | queues: user.queues, 21 | whatsapp: user.whatsapp 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/helpers/SerializeWbotMsgId.ts: -------------------------------------------------------------------------------- 1 | import Message from "../models/Message"; 2 | import Ticket from "../models/Ticket"; 3 | 4 | const SerializeWbotMsgId = (ticket: Ticket, message: Message): string => { 5 | const serializedMsgId = `${message.fromMe}_${ticket.contact.number}@${ 6 | ticket.isGroup ? "g" : "c" 7 | }.us_${message.id}`; 8 | 9 | return serializedMsgId; 10 | }; 11 | 12 | export default SerializeWbotMsgId; 13 | -------------------------------------------------------------------------------- /backend/src/helpers/SetTicketMessagesAsRead.ts: -------------------------------------------------------------------------------- 1 | import { getIO } from "../libs/socket"; 2 | import Message from "../models/Message"; 3 | import Ticket from "../models/Ticket"; 4 | import { logger } from "../utils/logger"; 5 | import GetTicketWbot from "./GetTicketWbot"; 6 | 7 | const SetTicketMessagesAsRead = async (ticket: Ticket): Promise => { 8 | await Message.update( 9 | { read: true }, 10 | { 11 | where: { 12 | ticketId: ticket.id, 13 | read: false 14 | } 15 | } 16 | ); 17 | 18 | await ticket.update({ unreadMessages: 0 }); 19 | 20 | try { 21 | const wbot = await GetTicketWbot(ticket); 22 | await wbot.sendSeen( 23 | `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us` 24 | ); 25 | } catch (err) { 26 | logger.warn( 27 | `Could not mark messages as read. Maybe whatsapp session disconnected? Err: ${err}` 28 | ); 29 | } 30 | 31 | const io = getIO(); 32 | io.to(ticket.status).to("notification").emit("ticket", { 33 | action: "updateUnread", 34 | ticketId: ticket.id 35 | }); 36 | }; 37 | 38 | export default SetTicketMessagesAsRead; 39 | -------------------------------------------------------------------------------- /backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts: -------------------------------------------------------------------------------- 1 | import Ticket from "../models/Ticket"; 2 | import UpdateTicketService from "../services/TicketServices/UpdateTicketService"; 3 | 4 | const UpdateDeletedUserOpenTicketsStatus = async ( 5 | tickets: Ticket[] 6 | ): Promise => { 7 | tickets.forEach(async t => { 8 | const ticketId = t.id.toString(); 9 | 10 | await UpdateTicketService({ 11 | ticketData: { status: "pending" }, 12 | ticketId 13 | }); 14 | }); 15 | }; 16 | 17 | export default UpdateDeletedUserOpenTicketsStatus; 18 | -------------------------------------------------------------------------------- /backend/src/libs/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server as SocketIO } from "socket.io"; 2 | import { Server } from "http"; 3 | import AppError from "../errors/AppError"; 4 | import { logger } from "../utils/logger"; 5 | 6 | let io: SocketIO; 7 | 8 | export const initIO = (httpServer: Server): SocketIO => { 9 | io = new SocketIO(httpServer, { 10 | cors: { 11 | origin: process.env.FRONTEND_URL 12 | } 13 | }); 14 | 15 | io.on("connection", socket => { 16 | logger.info("Client Connected"); 17 | socket.on("joinChatBox", (ticketId: string) => { 18 | logger.info("A client joined a ticket channel"); 19 | socket.join(ticketId); 20 | }); 21 | 22 | socket.on("joinNotification", () => { 23 | logger.info("A client joined notification channel"); 24 | socket.join("notification"); 25 | }); 26 | 27 | socket.on("joinTickets", (status: string) => { 28 | logger.info(`Um cliente entrou nas conversas com status ${status}`); 29 | socket.join(status); 30 | }); 31 | 32 | socket.on("disconnect", () => { 33 | logger.info("Cliente Desconectado"); 34 | }); 35 | }); 36 | return io; 37 | }; 38 | 39 | export const getIO = (): SocketIO => { 40 | if (!io) { 41 | throw new AppError("Socket IO not initialized"); 42 | } 43 | return io; 44 | }; 45 | -------------------------------------------------------------------------------- /backend/src/middleware/isAuth.ts: -------------------------------------------------------------------------------- 1 | import { verify } from "jsonwebtoken"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | import AppError from "../errors/AppError"; 5 | import authConfig from "../config/auth"; 6 | 7 | interface TokenPayload { 8 | id: string; 9 | username: string; 10 | profile: string; 11 | iat: number; 12 | exp: number; 13 | } 14 | 15 | const isAuth = (req: Request, res: Response, next: NextFunction): void => { 16 | const authHeader = req.headers.authorization; 17 | 18 | if (!authHeader) { 19 | throw new AppError("ERR_SESSION_EXPIRED", 401); 20 | } 21 | 22 | const [, token] = authHeader.split(" "); 23 | 24 | try { 25 | const decoded = verify(token, authConfig.secret); 26 | const { id, profile } = decoded as TokenPayload; 27 | 28 | req.user = { 29 | id, 30 | profile 31 | }; 32 | } catch (err) { 33 | throw new AppError( 34 | "Invalid token. We'll try to assign a new one on next request", 35 | 403 36 | ); 37 | } 38 | 39 | return next(); 40 | }; 41 | 42 | export default isAuth; 43 | -------------------------------------------------------------------------------- /backend/src/middleware/isAuthApi.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | import AppError from "../errors/AppError"; 4 | import ListSettingByValueService from "../services/SettingServices/ListSettingByValueService"; 5 | 6 | const isAuthApi = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise => { 11 | const authHeader = req.headers.authorization; 12 | 13 | if (!authHeader) { 14 | throw new AppError("ERR_SESSION_EXPIRED", 401); 15 | } 16 | 17 | const [, token] = authHeader.split(" "); 18 | 19 | try { 20 | const getToken = await ListSettingByValueService(token); 21 | if (!getToken) { 22 | throw new AppError("ERR_SESSION_EXPIRED", 401); 23 | } 24 | 25 | if (getToken.value !== token) { 26 | throw new AppError("ERR_SESSION_EXPIRED", 401); 27 | } 28 | } catch (err) { 29 | console.log(err); 30 | throw new AppError( 31 | "Invalid token. We'll try to assign a new one on next request", 32 | 403 33 | ); 34 | } 35 | 36 | return next(); 37 | }; 38 | 39 | export default isAuthApi; 40 | -------------------------------------------------------------------------------- /backend/src/models/Contact.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | AllowNull, 10 | Unique, 11 | Default, 12 | HasMany 13 | } from "sequelize-typescript"; 14 | import ContactCustomField from "./ContactCustomField"; 15 | import Ticket from "./Ticket"; 16 | 17 | @Table 18 | class Contact extends Model { 19 | @PrimaryKey 20 | @AutoIncrement 21 | @Column 22 | id: number; 23 | 24 | @Column 25 | name: string; 26 | 27 | @AllowNull(false) 28 | @Unique 29 | @Column 30 | number: string; 31 | 32 | @AllowNull(false) 33 | @Default("") 34 | @Column 35 | email: string; 36 | 37 | @Column 38 | profilePicUrl: string; 39 | 40 | @Default(false) 41 | @Column 42 | isGroup: boolean; 43 | 44 | @CreatedAt 45 | createdAt: Date; 46 | 47 | @UpdatedAt 48 | updatedAt: Date; 49 | 50 | @HasMany(() => Ticket) 51 | tickets: Ticket[]; 52 | 53 | @HasMany(() => ContactCustomField) 54 | extraInfo: ContactCustomField[]; 55 | } 56 | 57 | export default Contact; 58 | -------------------------------------------------------------------------------- /backend/src/models/ContactCustomField.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | ForeignKey, 10 | BelongsTo 11 | } from "sequelize-typescript"; 12 | import Contact from "./Contact"; 13 | 14 | @Table 15 | class ContactCustomField extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @Column 22 | name: string; 23 | 24 | @Column 25 | value: string; 26 | 27 | @ForeignKey(() => Contact) 28 | @Column 29 | contactId: number; 30 | 31 | @BelongsTo(() => Contact) 32 | contact: Contact; 33 | 34 | @CreatedAt 35 | createdAt: Date; 36 | 37 | @UpdatedAt 38 | updatedAt: Date; 39 | } 40 | 41 | export default ContactCustomField; 42 | -------------------------------------------------------------------------------- /backend/src/models/Message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | DataType, 8 | PrimaryKey, 9 | Default, 10 | BelongsTo, 11 | ForeignKey 12 | } from "sequelize-typescript"; 13 | import Contact from "./Contact"; 14 | import Ticket from "./Ticket"; 15 | 16 | @Table 17 | class Message extends Model { 18 | @PrimaryKey 19 | @Column 20 | id: string; 21 | 22 | @Default(0) 23 | @Column 24 | ack: number; 25 | 26 | @Default(false) 27 | @Column 28 | read: boolean; 29 | 30 | @Default(false) 31 | @Column 32 | fromMe: boolean; 33 | 34 | @Column(DataType.TEXT) 35 | body: string; 36 | 37 | @Column(DataType.STRING) 38 | get mediaUrl(): string | null { 39 | if (this.getDataValue("mediaUrl")) { 40 | return `${process.env.BACKEND_URL}:${ 41 | process.env.PROXY_PORT 42 | }/public/${this.getDataValue("mediaUrl")}`; 43 | } 44 | return null; 45 | } 46 | 47 | @Column 48 | mediaType: string; 49 | 50 | @Default(false) 51 | @Column 52 | isDeleted: boolean; 53 | 54 | @CreatedAt 55 | @Column(DataType.DATE(6)) 56 | createdAt: Date; 57 | 58 | @UpdatedAt 59 | @Column(DataType.DATE(6)) 60 | updatedAt: Date; 61 | 62 | @ForeignKey(() => Message) 63 | @Column 64 | quotedMsgId: string; 65 | 66 | @BelongsTo(() => Message, "quotedMsgId") 67 | quotedMsg: Message; 68 | 69 | @ForeignKey(() => Ticket) 70 | @Column 71 | ticketId: number; 72 | 73 | @BelongsTo(() => Ticket) 74 | ticket: Ticket; 75 | 76 | @ForeignKey(() => Contact) 77 | @Column 78 | contactId: number; 79 | 80 | @BelongsTo(() => Contact, "contactId") 81 | contact: Contact; 82 | } 83 | 84 | export default Message; 85 | -------------------------------------------------------------------------------- /backend/src/models/Queue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | AllowNull, 10 | Unique, 11 | BelongsToMany 12 | } from "sequelize-typescript"; 13 | import User from "./User"; 14 | import UserQueue from "./UserQueue"; 15 | 16 | import Whatsapp from "./Whatsapp"; 17 | import WhatsappQueue from "./WhatsappQueue"; 18 | 19 | @Table 20 | class Queue extends Model { 21 | @PrimaryKey 22 | @AutoIncrement 23 | @Column 24 | id: number; 25 | 26 | @AllowNull(false) 27 | @Unique 28 | @Column 29 | name: string; 30 | 31 | @AllowNull(false) 32 | @Unique 33 | @Column 34 | color: string; 35 | 36 | @Column 37 | greetingMessage: string; 38 | 39 | @CreatedAt 40 | createdAt: Date; 41 | 42 | @UpdatedAt 43 | updatedAt: Date; 44 | 45 | @BelongsToMany(() => Whatsapp, () => WhatsappQueue) 46 | whatsapps: Array; 47 | 48 | @BelongsToMany(() => User, () => UserQueue) 49 | users: Array; 50 | } 51 | 52 | export default Queue; 53 | -------------------------------------------------------------------------------- /backend/src/models/QuickAnswer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | DataType, 5 | CreatedAt, 6 | UpdatedAt, 7 | Model, 8 | PrimaryKey, 9 | AutoIncrement 10 | } from "sequelize-typescript"; 11 | 12 | @Table 13 | class QuickAnswer extends Model { 14 | @PrimaryKey 15 | @AutoIncrement 16 | @Column 17 | id: number; 18 | 19 | @Column(DataType.TEXT) 20 | shortcut: string; 21 | 22 | @Column(DataType.TEXT) 23 | message: string; 24 | 25 | @CreatedAt 26 | createdAt: Date; 27 | 28 | @UpdatedAt 29 | updatedAt: Date; 30 | } 31 | 32 | export default QuickAnswer; 33 | -------------------------------------------------------------------------------- /backend/src/models/Setting.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey 8 | } from "sequelize-typescript"; 9 | 10 | @Table 11 | class Setting extends Model { 12 | @PrimaryKey 13 | @Column 14 | key: string; 15 | 16 | @Column 17 | value: string; 18 | 19 | @CreatedAt 20 | createdAt: Date; 21 | 22 | @UpdatedAt 23 | updatedAt: Date; 24 | } 25 | 26 | export default Setting; 27 | -------------------------------------------------------------------------------- /backend/src/models/Ticket.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | ForeignKey, 9 | BelongsTo, 10 | HasMany, 11 | AutoIncrement, 12 | Default 13 | } from "sequelize-typescript"; 14 | 15 | import Contact from "./Contact"; 16 | import Message from "./Message"; 17 | import Queue from "./Queue"; 18 | import User from "./User"; 19 | import Whatsapp from "./Whatsapp"; 20 | 21 | @Table 22 | class Ticket extends Model { 23 | @PrimaryKey 24 | @AutoIncrement 25 | @Column 26 | id: number; 27 | 28 | @Column({ defaultValue: "pending" }) 29 | status: string; 30 | 31 | @Column 32 | unreadMessages: number; 33 | 34 | @Column 35 | lastMessage: string; 36 | 37 | @Default(false) 38 | @Column 39 | isGroup: boolean; 40 | 41 | @CreatedAt 42 | createdAt: Date; 43 | 44 | @UpdatedAt 45 | updatedAt: Date; 46 | 47 | @ForeignKey(() => User) 48 | @Column 49 | userId: number; 50 | 51 | @BelongsTo(() => User) 52 | user: User; 53 | 54 | @ForeignKey(() => Contact) 55 | @Column 56 | contactId: number; 57 | 58 | @BelongsTo(() => Contact) 59 | contact: Contact; 60 | 61 | @ForeignKey(() => Whatsapp) 62 | @Column 63 | whatsappId: number; 64 | 65 | @BelongsTo(() => Whatsapp) 66 | whatsapp: Whatsapp; 67 | 68 | @ForeignKey(() => Queue) 69 | @Column 70 | queueId: number; 71 | 72 | @BelongsTo(() => Queue) 73 | queue: Queue; 74 | 75 | @HasMany(() => Message) 76 | messages: Message[]; 77 | } 78 | 79 | export default Ticket; 80 | -------------------------------------------------------------------------------- /backend/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | DataType, 8 | BeforeCreate, 9 | BeforeUpdate, 10 | PrimaryKey, 11 | AutoIncrement, 12 | Default, 13 | HasMany, 14 | BelongsToMany, 15 | ForeignKey, 16 | BelongsTo 17 | } from "sequelize-typescript"; 18 | import { hash, compare } from "bcryptjs"; 19 | import Ticket from "./Ticket"; 20 | import Queue from "./Queue"; 21 | import UserQueue from "./UserQueue"; 22 | import Whatsapp from "./Whatsapp"; 23 | 24 | @Table 25 | class User extends Model { 26 | @PrimaryKey 27 | @AutoIncrement 28 | @Column 29 | id: number; 30 | 31 | @Column 32 | name: string; 33 | 34 | @Column 35 | email: string; 36 | 37 | @Column(DataType.VIRTUAL) 38 | password: string; 39 | 40 | @Column 41 | passwordHash: string; 42 | 43 | @Default(0) 44 | @Column 45 | tokenVersion: number; 46 | 47 | @Default("admin") 48 | @Column 49 | profile: string; 50 | 51 | @ForeignKey(() => Whatsapp) 52 | @Column 53 | whatsappId: number; 54 | 55 | @BelongsTo(() => Whatsapp) 56 | whatsapp: Whatsapp; 57 | 58 | @CreatedAt 59 | createdAt: Date; 60 | 61 | @UpdatedAt 62 | updatedAt: Date; 63 | 64 | @HasMany(() => Ticket) 65 | tickets: Ticket[]; 66 | 67 | @BelongsToMany(() => Queue, () => UserQueue) 68 | queues: Queue[]; 69 | 70 | @BeforeUpdate 71 | @BeforeCreate 72 | static hashPassword = async (instance: User): Promise => { 73 | if (instance.password) { 74 | instance.passwordHash = await hash(instance.password, 8); 75 | } 76 | }; 77 | 78 | public checkPassword = async (password: string): Promise => { 79 | return compare(password, this.getDataValue("passwordHash")); 80 | }; 81 | } 82 | 83 | export default User; 84 | -------------------------------------------------------------------------------- /backend/src/models/UserQueue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey 8 | } from "sequelize-typescript"; 9 | import Queue from "./Queue"; 10 | import User from "./User"; 11 | 12 | @Table 13 | class UserQueue extends Model { 14 | @ForeignKey(() => User) 15 | @Column 16 | userId: number; 17 | 18 | @ForeignKey(() => Queue) 19 | @Column 20 | queueId: number; 21 | 22 | @CreatedAt 23 | createdAt: Date; 24 | 25 | @UpdatedAt 26 | updatedAt: Date; 27 | } 28 | 29 | export default UserQueue; 30 | -------------------------------------------------------------------------------- /backend/src/models/Whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | DataType, 8 | PrimaryKey, 9 | AutoIncrement, 10 | Default, 11 | AllowNull, 12 | HasMany, 13 | Unique, 14 | BelongsToMany 15 | } from "sequelize-typescript"; 16 | import Queue from "./Queue"; 17 | import Ticket from "./Ticket"; 18 | import WhatsappQueue from "./WhatsappQueue"; 19 | 20 | @Table 21 | class Whatsapp extends Model { 22 | @PrimaryKey 23 | @AutoIncrement 24 | @Column 25 | id: number; 26 | 27 | @AllowNull 28 | @Unique 29 | @Column(DataType.TEXT) 30 | name: string; 31 | 32 | @Column(DataType.TEXT) 33 | session: string; 34 | 35 | @Column(DataType.TEXT) 36 | qrcode: string; 37 | 38 | @Column 39 | status: string; 40 | 41 | @Column 42 | battery: string; 43 | 44 | @Column 45 | plugged: boolean; 46 | 47 | @Column 48 | retries: number; 49 | 50 | @Column(DataType.TEXT) 51 | greetingMessage: string; 52 | 53 | @Column(DataType.TEXT) 54 | farewellMessage: string; 55 | 56 | @Default(false) 57 | @AllowNull 58 | @Column 59 | isDefault: boolean; 60 | 61 | @CreatedAt 62 | createdAt: Date; 63 | 64 | @UpdatedAt 65 | updatedAt: Date; 66 | 67 | @HasMany(() => Ticket) 68 | tickets: Ticket[]; 69 | 70 | @BelongsToMany(() => Queue, () => WhatsappQueue) 71 | queues: Array; 72 | 73 | @HasMany(() => WhatsappQueue) 74 | whatsappQueues: WhatsappQueue[]; 75 | } 76 | 77 | export default Whatsapp; 78 | -------------------------------------------------------------------------------- /backend/src/models/WhatsappQueue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey, 8 | BelongsTo 9 | } from "sequelize-typescript"; 10 | import Queue from "./Queue"; 11 | import Whatsapp from "./Whatsapp"; 12 | 13 | @Table 14 | class WhatsappQueue extends Model { 15 | @ForeignKey(() => Whatsapp) 16 | @Column 17 | whatsappId: number; 18 | 19 | @ForeignKey(() => Queue) 20 | @Column 21 | queueId: number; 22 | 23 | @CreatedAt 24 | createdAt: Date; 25 | 26 | @UpdatedAt 27 | updatedAt: Date; 28 | 29 | @BelongsTo(() => Queue) 30 | queue: Queue; 31 | } 32 | 33 | export default WhatsappQueue; 34 | -------------------------------------------------------------------------------- /backend/src/routes/apiRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import multer from "multer"; 3 | import uploadConfig from "../config/upload"; 4 | 5 | import * as ApiController from "../controllers/ApiController"; 6 | import isAuthApi from "../middleware/isAuthApi"; 7 | 8 | const upload = multer(uploadConfig); 9 | 10 | const ApiRoutes = express.Router(); 11 | 12 | ApiRoutes.post("/send", isAuthApi, upload.array("medias"), ApiController.index); 13 | 14 | export default ApiRoutes; 15 | -------------------------------------------------------------------------------- /backend/src/routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as SessionController from "../controllers/SessionController"; 3 | import * as UserController from "../controllers/UserController"; 4 | import isAuth from "../middleware/isAuth"; 5 | 6 | const authRoutes = Router(); 7 | 8 | authRoutes.post("/signup", UserController.store); 9 | 10 | authRoutes.post("/login", SessionController.store); 11 | 12 | authRoutes.post("/refresh_token", SessionController.update); 13 | 14 | authRoutes.delete("/logout", isAuth, SessionController.remove); 15 | 16 | export default authRoutes; 17 | -------------------------------------------------------------------------------- /backend/src/routes/contactRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as ContactController from "../controllers/ContactController"; 5 | import * as ImportPhoneContactsController from "../controllers/ImportPhoneContactsController"; 6 | 7 | const contactRoutes = express.Router(); 8 | 9 | contactRoutes.post( 10 | "/contacts/import", 11 | isAuth, 12 | ImportPhoneContactsController.store 13 | ); 14 | 15 | contactRoutes.get("/contacts", isAuth, ContactController.index); 16 | 17 | contactRoutes.get("/contacts/:contactId", isAuth, ContactController.show); 18 | 19 | contactRoutes.post("/contacts", isAuth, ContactController.store); 20 | 21 | contactRoutes.post("/contact", isAuth, ContactController.getContact); 22 | 23 | contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); 24 | 25 | contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); 26 | 27 | export default contactRoutes; 28 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import userRoutes from "./userRoutes"; 4 | import authRoutes from "./authRoutes"; 5 | import settingRoutes from "./settingRoutes"; 6 | import contactRoutes from "./contactRoutes"; 7 | import ticketRoutes from "./ticketRoutes"; 8 | import whatsappRoutes from "./whatsappRoutes"; 9 | import messageRoutes from "./messageRoutes"; 10 | import whatsappSessionRoutes from "./whatsappSessionRoutes"; 11 | import queueRoutes from "./queueRoutes"; 12 | import quickAnswerRoutes from "./quickAnswerRoutes"; 13 | import apiRoutes from "./apiRoutes"; 14 | 15 | const routes = Router(); 16 | 17 | routes.use(userRoutes); 18 | routes.use("/auth", authRoutes); 19 | routes.use(settingRoutes); 20 | routes.use(contactRoutes); 21 | routes.use(ticketRoutes); 22 | routes.use(whatsappRoutes); 23 | routes.use(messageRoutes); 24 | routes.use(whatsappSessionRoutes); 25 | routes.use(queueRoutes); 26 | routes.use(quickAnswerRoutes); 27 | routes.use("/api/messages", apiRoutes); 28 | 29 | export default routes; 30 | -------------------------------------------------------------------------------- /backend/src/routes/messageRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import multer from "multer"; 3 | import isAuth from "../middleware/isAuth"; 4 | import uploadConfig from "../config/upload"; 5 | 6 | import * as MessageController from "../controllers/MessageController"; 7 | 8 | const messageRoutes = Router(); 9 | 10 | const upload = multer(uploadConfig); 11 | 12 | messageRoutes.get("/messages/:ticketId", isAuth, MessageController.index); 13 | 14 | messageRoutes.post( 15 | "/messages/:ticketId", 16 | isAuth, 17 | upload.array("medias"), 18 | MessageController.store 19 | ); 20 | 21 | messageRoutes.delete("/messages/:messageId", isAuth, MessageController.remove); 22 | 23 | export default messageRoutes; 24 | -------------------------------------------------------------------------------- /backend/src/routes/queueRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as QueueController from "../controllers/QueueController"; 5 | 6 | const queueRoutes = Router(); 7 | 8 | queueRoutes.get("/queue", isAuth, QueueController.index); 9 | 10 | queueRoutes.post("/queue", isAuth, QueueController.store); 11 | 12 | queueRoutes.get("/queue/:queueId", isAuth, QueueController.show); 13 | 14 | queueRoutes.put("/queue/:queueId", isAuth, QueueController.update); 15 | 16 | queueRoutes.delete("/queue/:queueId", isAuth, QueueController.remove); 17 | 18 | export default queueRoutes; 19 | -------------------------------------------------------------------------------- /backend/src/routes/quickAnswerRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as QuickAnswerController from "../controllers/QuickAnswerController"; 5 | 6 | const quickAnswerRoutes = express.Router(); 7 | 8 | quickAnswerRoutes.get("/quickAnswers", isAuth, QuickAnswerController.index); 9 | 10 | quickAnswerRoutes.get( 11 | "/quickAnswers/:quickAnswerId", 12 | isAuth, 13 | QuickAnswerController.show 14 | ); 15 | 16 | quickAnswerRoutes.post("/quickAnswers", isAuth, QuickAnswerController.store); 17 | 18 | quickAnswerRoutes.put( 19 | "/quickAnswers/:quickAnswerId", 20 | isAuth, 21 | QuickAnswerController.update 22 | ); 23 | 24 | quickAnswerRoutes.delete( 25 | "/quickAnswers/:quickAnswerId", 26 | isAuth, 27 | QuickAnswerController.remove 28 | ); 29 | 30 | export default quickAnswerRoutes; 31 | -------------------------------------------------------------------------------- /backend/src/routes/settingRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as SettingController from "../controllers/SettingController"; 5 | 6 | const settingRoutes = Router(); 7 | 8 | settingRoutes.get("/settings", isAuth, SettingController.index); 9 | 10 | // routes.get("/settings/:settingKey", isAuth, SettingsController.show); 11 | 12 | // change setting key to key in future 13 | settingRoutes.put("/settings/:settingKey", isAuth, SettingController.update); 14 | 15 | export default settingRoutes; 16 | -------------------------------------------------------------------------------- /backend/src/routes/ticketRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as TicketController from "../controllers/TicketController"; 5 | 6 | const ticketRoutes = express.Router(); 7 | 8 | ticketRoutes.get("/tickets", isAuth, TicketController.index); 9 | 10 | ticketRoutes.get("/tickets/:ticketId", isAuth, TicketController.show); 11 | 12 | ticketRoutes.post("/tickets", isAuth, TicketController.store); 13 | 14 | ticketRoutes.put("/tickets/:ticketId", isAuth, TicketController.update); 15 | 16 | ticketRoutes.delete("/tickets/:ticketId", isAuth, TicketController.remove); 17 | 18 | export default ticketRoutes; 19 | -------------------------------------------------------------------------------- /backend/src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import isAuth from "../middleware/isAuth"; 4 | import * as UserController from "../controllers/UserController"; 5 | 6 | const userRoutes = Router(); 7 | 8 | userRoutes.get("/users", isAuth, UserController.index); 9 | 10 | userRoutes.post("/users", isAuth, UserController.store); 11 | 12 | userRoutes.put("/users/:userId", isAuth, UserController.update); 13 | 14 | userRoutes.get("/users/:userId", isAuth, UserController.show); 15 | 16 | userRoutes.delete("/users/:userId", isAuth, UserController.remove); 17 | 18 | export default userRoutes; 19 | -------------------------------------------------------------------------------- /backend/src/routes/whatsappRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as WhatsAppController from "../controllers/WhatsAppController"; 5 | 6 | const whatsappRoutes = express.Router(); 7 | 8 | whatsappRoutes.get("/whatsapp/", isAuth, WhatsAppController.index); 9 | 10 | whatsappRoutes.post("/whatsapp/", isAuth, WhatsAppController.store); 11 | 12 | whatsappRoutes.get("/whatsapp/:whatsappId", isAuth, WhatsAppController.show); 13 | 14 | whatsappRoutes.put("/whatsapp/:whatsappId", isAuth, WhatsAppController.update); 15 | 16 | whatsappRoutes.delete( 17 | "/whatsapp/:whatsappId", 18 | isAuth, 19 | WhatsAppController.remove 20 | ); 21 | 22 | export default whatsappRoutes; 23 | -------------------------------------------------------------------------------- /backend/src/routes/whatsappSessionRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import WhatsAppSessionController from "../controllers/WhatsAppSessionController"; 5 | 6 | const whatsappSessionRoutes = Router(); 7 | 8 | whatsappSessionRoutes.post( 9 | "/whatsappsession/:whatsappId", 10 | isAuth, 11 | WhatsAppSessionController.store 12 | ); 13 | 14 | whatsappSessionRoutes.put( 15 | "/whatsappsession/:whatsappId", 16 | isAuth, 17 | WhatsAppSessionController.update 18 | ); 19 | 20 | whatsappSessionRoutes.delete( 21 | "/whatsappsession/:whatsappId", 22 | isAuth, 23 | WhatsAppSessionController.remove 24 | ); 25 | 26 | export default whatsappSessionRoutes; 27 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import gracefulShutdown from "http-graceful-shutdown"; 2 | import app from "./app"; 3 | import { initIO } from "./libs/socket"; 4 | import { logger } from "./utils/logger"; 5 | import { StartAllWhatsAppsSessions } from "./services/WbotServices/StartAllWhatsAppsSessions"; 6 | 7 | const server = app.listen(process.env.PORT, () => { 8 | logger.info(`Server started on port: ${process.env.PORT}`); 9 | }); 10 | 11 | initIO(server); 12 | StartAllWhatsAppsSessions(); 13 | gracefulShutdown(server); 14 | -------------------------------------------------------------------------------- /backend/src/services/AuthServices/RefreshTokenService.ts: -------------------------------------------------------------------------------- 1 | import { verify } from "jsonwebtoken"; 2 | import { Response as Res } from "express"; 3 | 4 | import User from "../../models/User"; 5 | import AppError from "../../errors/AppError"; 6 | import ShowUserService from "../UserServices/ShowUserService"; 7 | import authConfig from "../../config/auth"; 8 | import { 9 | createAccessToken, 10 | createRefreshToken 11 | } from "../../helpers/CreateTokens"; 12 | 13 | interface RefreshTokenPayload { 14 | id: string; 15 | tokenVersion: number; 16 | } 17 | 18 | interface Response { 19 | user: User; 20 | newToken: string; 21 | refreshToken: string; 22 | } 23 | 24 | export const RefreshTokenService = async ( 25 | res: Res, 26 | token: string 27 | ): Promise => { 28 | try { 29 | const decoded = verify(token, authConfig.refreshSecret); 30 | const { id, tokenVersion } = decoded as RefreshTokenPayload; 31 | 32 | const user = await ShowUserService(id); 33 | 34 | if (user.tokenVersion !== tokenVersion) { 35 | res.clearCookie("jrt"); 36 | throw new AppError("ERR_SESSION_EXPIRED", 401); 37 | } 38 | 39 | const newToken = createAccessToken(user); 40 | const refreshToken = createRefreshToken(user); 41 | 42 | return { user, newToken, refreshToken }; 43 | } catch (err) { 44 | res.clearCookie("jrt"); 45 | throw new AppError("ERR_SESSION_EXPIRED", 401); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/CreateContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface ExtraInfo { 5 | name: string; 6 | value: string; 7 | } 8 | 9 | interface Request { 10 | name: string; 11 | number: string; 12 | email?: string; 13 | profilePicUrl?: string; 14 | extraInfo?: ExtraInfo[]; 15 | } 16 | 17 | const CreateContactService = async ({ 18 | name, 19 | number, 20 | email = "", 21 | extraInfo = [] 22 | }: Request): Promise => { 23 | const numberExists = await Contact.findOne({ 24 | where: { number } 25 | }); 26 | 27 | if (numberExists) { 28 | throw new AppError("ERR_DUPLICATED_CONTACT"); 29 | } 30 | 31 | const contact = await Contact.create( 32 | { 33 | name, 34 | number, 35 | email, 36 | extraInfo 37 | }, 38 | { 39 | include: ["extraInfo"] 40 | } 41 | ); 42 | 43 | return contact; 44 | }; 45 | 46 | export default CreateContactService; 47 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/CreateOrUpdateContactService.ts: -------------------------------------------------------------------------------- 1 | import { getIO } from "../../libs/socket"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface ExtraInfo { 5 | name: string; 6 | value: string; 7 | } 8 | 9 | interface Request { 10 | name: string; 11 | number: string; 12 | isGroup: boolean; 13 | email?: string; 14 | profilePicUrl?: string; 15 | extraInfo?: ExtraInfo[]; 16 | } 17 | 18 | const CreateOrUpdateContactService = async ({ 19 | name, 20 | number: rawNumber, 21 | profilePicUrl, 22 | isGroup, 23 | email = "", 24 | extraInfo = [] 25 | }: Request): Promise => { 26 | const number = isGroup ? rawNumber : rawNumber.replace(/[^0-9]/g, ""); 27 | 28 | const io = getIO(); 29 | let contact: Contact | null; 30 | 31 | contact = await Contact.findOne({ where: { number } }); 32 | 33 | if (contact) { 34 | contact.update({ profilePicUrl }); 35 | 36 | io.emit("contact", { 37 | action: "update", 38 | contact 39 | }); 40 | } else { 41 | contact = await Contact.create({ 42 | name, 43 | number, 44 | profilePicUrl, 45 | email, 46 | isGroup, 47 | extraInfo 48 | }); 49 | 50 | io.emit("contact", { 51 | action: "create", 52 | contact 53 | }); 54 | } 55 | 56 | return contact; 57 | }; 58 | 59 | export default CreateOrUpdateContactService; 60 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/DeleteContactService.ts: -------------------------------------------------------------------------------- 1 | import Contact from "../../models/Contact"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteContactService = async (id: string): Promise => { 5 | const contact = await Contact.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!contact) { 10 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 11 | } 12 | 13 | await contact.destroy(); 14 | }; 15 | 16 | export default DeleteContactService; 17 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/GetContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | import CreateContactService from "./CreateContactService"; 4 | 5 | interface ExtraInfo { 6 | name: string; 7 | value: string; 8 | } 9 | 10 | interface Request { 11 | name: string; 12 | number: string; 13 | email?: string; 14 | profilePicUrl?: string; 15 | extraInfo?: ExtraInfo[]; 16 | } 17 | 18 | const GetContactService = async ({ name, number }: Request): Promise => { 19 | const numberExists = await Contact.findOne({ 20 | where: { number } 21 | }); 22 | 23 | if (!numberExists) { 24 | const contact = await CreateContactService({ 25 | name, 26 | number, 27 | }) 28 | 29 | if (contact == null) 30 | throw new AppError("CONTACT_NOT_FIND") 31 | else 32 | return contact 33 | } 34 | 35 | return numberExists 36 | }; 37 | 38 | export default GetContactService; -------------------------------------------------------------------------------- /backend/src/services/ContactServices/ListContactsService.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Op } from "sequelize"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface Request { 5 | searchParam?: string; 6 | pageNumber?: string; 7 | } 8 | 9 | interface Response { 10 | contacts: Contact[]; 11 | count: number; 12 | hasMore: boolean; 13 | } 14 | 15 | const ListContactsService = async ({ 16 | searchParam = "", 17 | pageNumber = "1" 18 | }: Request): Promise => { 19 | const whereCondition = { 20 | [Op.or]: [ 21 | { 22 | name: Sequelize.where( 23 | Sequelize.fn("LOWER", Sequelize.col("name")), 24 | "LIKE", 25 | `%${searchParam.toLowerCase().trim()}%` 26 | ) 27 | }, 28 | { number: { [Op.like]: `%${searchParam.toLowerCase().trim()}%` } } 29 | ] 30 | }; 31 | const limit = 20; 32 | const offset = limit * (+pageNumber - 1); 33 | 34 | const { count, rows: contacts } = await Contact.findAndCountAll({ 35 | where: whereCondition, 36 | limit, 37 | offset, 38 | order: [["name", "ASC"]] 39 | }); 40 | 41 | const hasMore = count > offset + contacts.length; 42 | 43 | return { 44 | contacts, 45 | count, 46 | hasMore 47 | }; 48 | }; 49 | 50 | export default ListContactsService; 51 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/ShowContactService.ts: -------------------------------------------------------------------------------- 1 | import Contact from "../../models/Contact"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowContactService = async (id: string | number): Promise => { 5 | const contact = await Contact.findByPk(id, { include: ["extraInfo"] }); 6 | 7 | if (!contact) { 8 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 9 | } 10 | 11 | return contact; 12 | }; 13 | 14 | export default ShowContactService; 15 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/UpdateContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | import ContactCustomField from "../../models/ContactCustomField"; 4 | 5 | interface ExtraInfo { 6 | id?: number; 7 | name: string; 8 | value: string; 9 | } 10 | interface ContactData { 11 | email?: string; 12 | number?: string; 13 | name?: string; 14 | extraInfo?: ExtraInfo[]; 15 | } 16 | 17 | interface Request { 18 | contactData: ContactData; 19 | contactId: string; 20 | } 21 | 22 | const UpdateContactService = async ({ 23 | contactData, 24 | contactId 25 | }: Request): Promise => { 26 | const { email, name, number, extraInfo } = contactData; 27 | 28 | const contact = await Contact.findOne({ 29 | where: { id: contactId }, 30 | attributes: ["id", "name", "number", "email", "profilePicUrl"], 31 | include: ["extraInfo"] 32 | }); 33 | 34 | if (!contact) { 35 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 36 | } 37 | 38 | if (extraInfo) { 39 | await Promise.all( 40 | extraInfo.map(async info => { 41 | await ContactCustomField.upsert({ ...info, contactId: contact.id }); 42 | }) 43 | ); 44 | 45 | await Promise.all( 46 | contact.extraInfo.map(async oldInfo => { 47 | const stillExists = extraInfo.findIndex(info => info.id === oldInfo.id); 48 | 49 | if (stillExists === -1) { 50 | await ContactCustomField.destroy({ where: { id: oldInfo.id } }); 51 | } 52 | }) 53 | ); 54 | } 55 | 56 | await contact.update({ 57 | name, 58 | number, 59 | email 60 | }); 61 | 62 | await contact.reload({ 63 | attributes: ["id", "name", "number", "email", "profilePicUrl"], 64 | include: ["extraInfo"] 65 | }); 66 | 67 | return contact; 68 | }; 69 | 70 | export default UpdateContactService; 71 | -------------------------------------------------------------------------------- /backend/src/services/MessageServices/CreateMessageService.ts: -------------------------------------------------------------------------------- 1 | import { getIO } from "../../libs/socket"; 2 | import Message from "../../models/Message"; 3 | import Ticket from "../../models/Ticket"; 4 | import Whatsapp from "../../models/Whatsapp"; 5 | 6 | interface MessageData { 7 | id: string; 8 | ticketId: number; 9 | body: string; 10 | contactId?: number; 11 | fromMe?: boolean; 12 | read?: boolean; 13 | mediaType?: string; 14 | mediaUrl?: string; 15 | } 16 | interface Request { 17 | messageData: MessageData; 18 | } 19 | 20 | const CreateMessageService = async ({ 21 | messageData 22 | }: Request): Promise => { 23 | await Message.upsert(messageData); 24 | 25 | const message = await Message.findByPk(messageData.id, { 26 | include: [ 27 | "contact", 28 | { 29 | model: Ticket, 30 | as: "ticket", 31 | include: [ 32 | "contact", "queue", 33 | { 34 | model: Whatsapp, 35 | as: "whatsapp", 36 | attributes: ["name"] 37 | } 38 | ] 39 | }, 40 | { 41 | model: Message, 42 | as: "quotedMsg", 43 | include: ["contact"] 44 | } 45 | ] 46 | }); 47 | 48 | if (!message) { 49 | throw new Error("ERR_CREATING_MESSAGE"); 50 | } 51 | 52 | const io = getIO(); 53 | io.to(message.ticketId.toString()) 54 | .to(message.ticket.status) 55 | .to("notification") 56 | .emit("appMessage", { 57 | action: "create", 58 | message, 59 | ticket: message.ticket, 60 | contact: message.ticket.contact 61 | }); 62 | 63 | return message; 64 | }; 65 | 66 | export default CreateMessageService; -------------------------------------------------------------------------------- /backend/src/services/MessageServices/ListMessagesService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Message from "../../models/Message"; 3 | import Ticket from "../../models/Ticket"; 4 | import ShowTicketService from "../TicketServices/ShowTicketService"; 5 | 6 | interface Request { 7 | ticketId: string; 8 | pageNumber?: string; 9 | } 10 | 11 | interface Response { 12 | messages: Message[]; 13 | ticket: Ticket; 14 | count: number; 15 | hasMore: boolean; 16 | } 17 | 18 | const ListMessagesService = async ({ 19 | pageNumber = "1", 20 | ticketId 21 | }: Request): Promise => { 22 | const ticket = await ShowTicketService(ticketId); 23 | 24 | if (!ticket) { 25 | throw new AppError("ERR_NO_TICKET_FOUND", 404); 26 | } 27 | 28 | // await setMessagesAsRead(ticket); 29 | const limit = 20; 30 | const offset = limit * (+pageNumber - 1); 31 | 32 | const { count, rows: messages } = await Message.findAndCountAll({ 33 | //where: { ticketId }, 34 | //where: {contactid : ticket.contactId}, 35 | limit, 36 | include: [ 37 | "contact", 38 | { 39 | model: Message, 40 | as: "quotedMsg", 41 | include: ["contact"] 42 | }, 43 | { 44 | model: Ticket, 45 | where: {contactId: ticket.contactId }, 46 | required: true 47 | } 48 | ], 49 | offset, 50 | order: [["createdAt", "DESC"]] 51 | }); 52 | 53 | const hasMore = count > offset + messages.length; 54 | 55 | return { 56 | messages: messages.reverse(), 57 | ticket, 58 | count, 59 | hasMore 60 | }; 61 | }; 62 | 63 | export default ListMessagesService; 64 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/CreateQueueService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | import AppError from "../../errors/AppError"; 3 | import Queue from "../../models/Queue"; 4 | 5 | interface QueueData { 6 | name: string; 7 | color: string; 8 | greetingMessage?: string; 9 | } 10 | 11 | const CreateQueueService = async (queueData: QueueData): Promise => { 12 | const { color, name } = queueData; 13 | 14 | const queueSchema = Yup.object().shape({ 15 | name: Yup.string() 16 | .min(2, "ERR_QUEUE_INVALID_NAME") 17 | .required("ERR_QUEUE_INVALID_NAME") 18 | .test( 19 | "Check-unique-name", 20 | "ERR_QUEUE_NAME_ALREADY_EXISTS", 21 | async value => { 22 | if (value) { 23 | const queueWithSameName = await Queue.findOne({ 24 | where: { name: value } 25 | }); 26 | 27 | return !queueWithSameName; 28 | } 29 | return false; 30 | } 31 | ), 32 | color: Yup.string() 33 | .required("ERR_QUEUE_INVALID_COLOR") 34 | .test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => { 35 | if (value) { 36 | const colorTestRegex = /^#[0-9a-f]{3,6}$/i; 37 | return colorTestRegex.test(value); 38 | } 39 | return false; 40 | }) 41 | .test( 42 | "Check-color-exists", 43 | "ERR_QUEUE_COLOR_ALREADY_EXISTS", 44 | async value => { 45 | if (value) { 46 | const queueWithSameColor = await Queue.findOne({ 47 | where: { color: value } 48 | }); 49 | return !queueWithSameColor; 50 | } 51 | return false; 52 | } 53 | ) 54 | }); 55 | 56 | try { 57 | await queueSchema.validate({ color, name }); 58 | } catch (err) { 59 | throw new AppError(err.message); 60 | } 61 | 62 | const queue = await Queue.create(queueData); 63 | 64 | return queue; 65 | }; 66 | 67 | export default CreateQueueService; 68 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/DeleteQueueService.ts: -------------------------------------------------------------------------------- 1 | import ShowQueueService from "./ShowQueueService"; 2 | 3 | const DeleteQueueService = async (queueId: number | string): Promise => { 4 | const queue = await ShowQueueService(queueId); 5 | 6 | await queue.destroy(); 7 | }; 8 | 9 | export default DeleteQueueService; 10 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/ListQueuesService.ts: -------------------------------------------------------------------------------- 1 | import Queue from "../../models/Queue"; 2 | 3 | const ListQueuesService = async (): Promise => { 4 | const queues = await Queue.findAll({ order: [["name", "ASC"]] }); 5 | 6 | return queues; 7 | }; 8 | 9 | export default ListQueuesService; 10 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/ShowQueueService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Queue from "../../models/Queue"; 3 | 4 | const ShowQueueService = async (queueId: number | string): Promise => { 5 | const queue = await Queue.findByPk(queueId); 6 | 7 | if (!queue) { 8 | throw new AppError("ERR_QUEUE_NOT_FOUND"); 9 | } 10 | 11 | return queue; 12 | }; 13 | 14 | export default ShowQueueService; 15 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/UpdateQueueService.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | import * as Yup from "yup"; 3 | import AppError from "../../errors/AppError"; 4 | import Queue from "../../models/Queue"; 5 | import ShowQueueService from "./ShowQueueService"; 6 | 7 | interface QueueData { 8 | name?: string; 9 | color?: string; 10 | greetingMessage?: string; 11 | } 12 | 13 | const UpdateQueueService = async ( 14 | queueId: number | string, 15 | queueData: QueueData 16 | ): Promise => { 17 | const { color, name } = queueData; 18 | 19 | const queueSchema = Yup.object().shape({ 20 | name: Yup.string() 21 | .min(2, "ERR_QUEUE_INVALID_NAME") 22 | .test( 23 | "Check-unique-name", 24 | "ERR_QUEUE_NAME_ALREADY_EXISTS", 25 | async value => { 26 | if (value) { 27 | const queueWithSameName = await Queue.findOne({ 28 | where: { name: value, id: { [Op.not]: queueId } } 29 | }); 30 | 31 | return !queueWithSameName; 32 | } 33 | return true; 34 | } 35 | ), 36 | color: Yup.string() 37 | .required("ERR_QUEUE_INVALID_COLOR") 38 | .test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => { 39 | if (value) { 40 | const colorTestRegex = /^#[0-9a-f]{3,6}$/i; 41 | return colorTestRegex.test(value); 42 | } 43 | return true; 44 | }) 45 | .test( 46 | "Check-color-exists", 47 | "ERR_QUEUE_COLOR_ALREADY_EXISTS", 48 | async value => { 49 | if (value) { 50 | const queueWithSameColor = await Queue.findOne({ 51 | where: { color: value, id: { [Op.not]: queueId } } 52 | }); 53 | return !queueWithSameColor; 54 | } 55 | return true; 56 | } 57 | ) 58 | }); 59 | 60 | try { 61 | await queueSchema.validate({ color, name }); 62 | } catch (err) { 63 | throw new AppError(err.message); 64 | } 65 | 66 | const queue = await ShowQueueService(queueId); 67 | 68 | await queue.update(queueData); 69 | 70 | return queue; 71 | }; 72 | 73 | export default UpdateQueueService; 74 | -------------------------------------------------------------------------------- /backend/src/services/QuickAnswerService/CreateQuickAnswerService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import QuickAnswer from "../../models/QuickAnswer"; 3 | 4 | interface Request { 5 | shortcut: string; 6 | message: string; 7 | } 8 | 9 | const CreateQuickAnswerService = async ({ 10 | shortcut, 11 | message 12 | }: Request): Promise => { 13 | const nameExists = await QuickAnswer.findOne({ 14 | where: { shortcut } 15 | }); 16 | 17 | if (nameExists) { 18 | throw new AppError("ERR__SHORTCUT_DUPLICATED"); 19 | } 20 | 21 | const quickAnswer = await QuickAnswer.create({ shortcut, message }); 22 | 23 | return quickAnswer; 24 | }; 25 | 26 | export default CreateQuickAnswerService; 27 | -------------------------------------------------------------------------------- /backend/src/services/QuickAnswerService/DeleteQuickAnswerService.ts: -------------------------------------------------------------------------------- 1 | import QuickAnswer from "../../models/QuickAnswer"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteQuickAnswerService = async (id: string): Promise => { 5 | const quickAnswer = await QuickAnswer.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!quickAnswer) { 10 | throw new AppError("ERR_NO_QUICK_ANSWER_FOUND", 404); 11 | } 12 | 13 | await quickAnswer.destroy(); 14 | }; 15 | 16 | export default DeleteQuickAnswerService; 17 | -------------------------------------------------------------------------------- /backend/src/services/QuickAnswerService/ListQuickAnswerService.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize"; 2 | import QuickAnswer from "../../models/QuickAnswer"; 3 | 4 | interface Request { 5 | searchParam?: string; 6 | pageNumber?: string; 7 | } 8 | 9 | interface Response { 10 | quickAnswers: QuickAnswer[]; 11 | count: number; 12 | hasMore: boolean; 13 | } 14 | 15 | const ListQuickAnswerService = async ({ 16 | searchParam = "", 17 | pageNumber = "1" 18 | }: Request): Promise => { 19 | const whereCondition = { 20 | message: Sequelize.where( 21 | Sequelize.fn("LOWER", Sequelize.col("message")), 22 | "LIKE", 23 | `%${searchParam.toLowerCase().trim()}%` 24 | ) 25 | }; 26 | const limit = 20; 27 | const offset = limit * (+pageNumber - 1); 28 | 29 | const { count, rows: quickAnswers } = await QuickAnswer.findAndCountAll({ 30 | where: whereCondition, 31 | limit, 32 | offset, 33 | order: [["message", "ASC"]] 34 | }); 35 | 36 | const hasMore = count > offset + quickAnswers.length; 37 | 38 | return { 39 | quickAnswers, 40 | count, 41 | hasMore 42 | }; 43 | }; 44 | 45 | export default ListQuickAnswerService; 46 | -------------------------------------------------------------------------------- /backend/src/services/QuickAnswerService/ShowQuickAnswerService.ts: -------------------------------------------------------------------------------- 1 | import QuickAnswer from "../../models/QuickAnswer"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowQuickAnswerService = async (id: string): Promise => { 5 | const quickAnswer = await QuickAnswer.findByPk(id); 6 | 7 | if (!quickAnswer) { 8 | throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404); 9 | } 10 | 11 | return quickAnswer; 12 | }; 13 | 14 | export default ShowQuickAnswerService; 15 | -------------------------------------------------------------------------------- /backend/src/services/QuickAnswerService/UpdateQuickAnswerService.ts: -------------------------------------------------------------------------------- 1 | import QuickAnswer from "../../models/QuickAnswer"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | interface QuickAnswerData { 5 | shortcut?: string; 6 | message?: string; 7 | } 8 | 9 | interface Request { 10 | quickAnswerData: QuickAnswerData; 11 | quickAnswerId: string; 12 | } 13 | 14 | const UpdateQuickAnswerService = async ({ 15 | quickAnswerData, 16 | quickAnswerId 17 | }: Request): Promise => { 18 | const { shortcut, message } = quickAnswerData; 19 | 20 | const quickAnswer = await QuickAnswer.findOne({ 21 | where: { id: quickAnswerId }, 22 | attributes: ["id", "shortcut", "message"] 23 | }); 24 | 25 | if (!quickAnswer) { 26 | throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404); 27 | } 28 | await quickAnswer.update({ 29 | shortcut, 30 | message 31 | }); 32 | 33 | await quickAnswer.reload({ 34 | attributes: ["id", "shortcut", "message"] 35 | }); 36 | 37 | return quickAnswer; 38 | }; 39 | 40 | export default UpdateQuickAnswerService; 41 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/ListSettingByValueService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Setting from "../../models/Setting"; 3 | 4 | interface Response { 5 | key: string; 6 | value: string; 7 | } 8 | const ListSettingByKeyService = async ( 9 | value: string 10 | ): Promise => { 11 | const settings = await Setting.findOne({ 12 | where: { value } 13 | }); 14 | 15 | if (!settings) { 16 | throw new AppError("ERR_NO_API_TOKEN_FOUND", 404); 17 | } 18 | 19 | return { key: settings.key, value: settings.value }; 20 | }; 21 | 22 | export default ListSettingByKeyService; 23 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/ListSettingsService.ts: -------------------------------------------------------------------------------- 1 | import Setting from "../../models/Setting"; 2 | 3 | const ListSettingsService = async (): Promise => { 4 | const settings = await Setting.findAll(); 5 | 6 | return settings; 7 | }; 8 | 9 | export default ListSettingsService; 10 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/ListSettingsServiceOne.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Setting from "../../models/Setting"; 3 | 4 | interface Request { 5 | key: string; 6 | } 7 | 8 | const ListSettingsServiceOne = async ({ 9 | key 10 | }: Request): Promise => { 11 | const setting = await Setting.findOne({ 12 | where: { key } 13 | }); 14 | 15 | if (!setting) { 16 | throw new AppError("ERR_NO_SETTING_FOUND", 404); 17 | } 18 | 19 | return setting; 20 | }; 21 | 22 | export default ListSettingsServiceOne; -------------------------------------------------------------------------------- /backend/src/services/SettingServices/UpdateSettingService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Setting from "../../models/Setting"; 3 | 4 | interface Request { 5 | key: string; 6 | value: string; 7 | } 8 | 9 | const UpdateSettingService = async ({ 10 | key, 11 | value 12 | }: Request): Promise => { 13 | const setting = await Setting.findOne({ 14 | where: { key } 15 | }); 16 | 17 | if (!setting) { 18 | throw new AppError("ERR_NO_SETTING_FOUND", 404); 19 | } 20 | 21 | await setting.update({ value }); 22 | 23 | return setting; 24 | }; 25 | 26 | export default UpdateSettingService; 27 | -------------------------------------------------------------------------------- /backend/src/services/TicketServices/CreateTicketService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; 3 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 4 | import Ticket from "../../models/Ticket"; 5 | import User from "../../models/User"; 6 | import ShowContactService from "../ContactServices/ShowContactService"; 7 | 8 | interface Request { 9 | contactId: number; 10 | status: string; 11 | userId: number; 12 | queueId ?: number; 13 | } 14 | 15 | const CreateTicketService = async ({ 16 | contactId, 17 | status, 18 | userId, 19 | queueId 20 | }: Request): Promise => { 21 | const defaultWhatsapp = await GetDefaultWhatsApp(userId); 22 | 23 | await CheckContactOpenTickets(contactId, defaultWhatsapp.id); 24 | 25 | const { isGroup } = await ShowContactService(contactId); 26 | 27 | if(queueId === undefined) { 28 | const user = await User.findByPk(userId, { include: ["queues"]}); 29 | queueId = user?.queues.length === 1 ? user.queues[0].id : undefined; 30 | } 31 | 32 | const { id }: Ticket = await defaultWhatsapp.$create("ticket", { 33 | contactId, 34 | status, 35 | isGroup, 36 | userId, 37 | queueId 38 | }); 39 | 40 | const ticket = await Ticket.findByPk(id, { include: ["contact"] }); 41 | 42 | if (!ticket) { 43 | throw new AppError("ERR_CREATING_TICKET"); 44 | } 45 | 46 | return ticket; 47 | }; 48 | 49 | export default CreateTicketService; 50 | -------------------------------------------------------------------------------- /backend/src/services/TicketServices/DeleteTicketService.ts: -------------------------------------------------------------------------------- 1 | import Ticket from "../../models/Ticket"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteTicketService = async (id: string): Promise => { 5 | const ticket = await Ticket.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!ticket) { 10 | throw new AppError("ERR_NO_TICKET_FOUND", 404); 11 | } 12 | 13 | await ticket.destroy(); 14 | 15 | return ticket; 16 | }; 17 | 18 | export default DeleteTicketService; 19 | -------------------------------------------------------------------------------- /backend/src/services/TicketServices/FindOrCreateTicketService.ts: -------------------------------------------------------------------------------- 1 | import { subHours } from "date-fns"; 2 | import { Op } from "sequelize"; 3 | import Contact from "../../models/Contact"; 4 | import Ticket from "../../models/Ticket"; 5 | import ShowTicketService from "./ShowTicketService"; 6 | 7 | const FindOrCreateTicketService = async ( 8 | contact: Contact, 9 | whatsappId: number, 10 | unreadMessages: number, 11 | groupContact?: Contact 12 | ): Promise => { 13 | let ticket = await Ticket.findOne({ 14 | where: { 15 | status: { 16 | [Op.or]: ["open", "pending"] 17 | }, 18 | contactId: groupContact ? groupContact.id : contact.id, 19 | whatsappId: whatsappId 20 | } 21 | }); 22 | 23 | if (ticket) { 24 | await ticket.update({ unreadMessages }); 25 | } 26 | 27 | if (!ticket && groupContact) { 28 | ticket = await Ticket.findOne({ 29 | where: { 30 | contactId: groupContact.id, 31 | whatsappId: whatsappId 32 | }, 33 | order: [["updatedAt", "DESC"]] 34 | }); 35 | 36 | if (ticket) { 37 | await ticket.update({ 38 | status: "pending", 39 | userId: null, 40 | unreadMessages 41 | }); 42 | } 43 | } 44 | 45 | if (!ticket && !groupContact) { 46 | ticket = await Ticket.findOne({ 47 | where: { 48 | updatedAt: { 49 | [Op.between]: [+subHours(new Date(), 2), +new Date()] 50 | }, 51 | contactId: contact.id, 52 | whatsappId: whatsappId 53 | }, 54 | order: [["updatedAt", "DESC"]] 55 | }); 56 | 57 | if (ticket) { 58 | await ticket.update({ 59 | status: "pending", 60 | userId: null, 61 | unreadMessages 62 | }); 63 | } 64 | } 65 | 66 | if (!ticket) { 67 | ticket = await Ticket.create({ 68 | contactId: groupContact ? groupContact.id : contact.id, 69 | status: "pending", 70 | isGroup: !!groupContact, 71 | unreadMessages, 72 | whatsappId 73 | }); 74 | } 75 | 76 | ticket = await ShowTicketService(ticket.id); 77 | 78 | return ticket; 79 | }; 80 | 81 | export default FindOrCreateTicketService; 82 | -------------------------------------------------------------------------------- /backend/src/services/TicketServices/ShowTicketService.ts: -------------------------------------------------------------------------------- 1 | import Ticket from "../../models/Ticket"; 2 | import AppError from "../../errors/AppError"; 3 | import Contact from "../../models/Contact"; 4 | import User from "../../models/User"; 5 | import Queue from "../../models/Queue"; 6 | import Whatsapp from "../../models/Whatsapp"; 7 | 8 | const ShowTicketService = async (id: string | number): Promise => { 9 | const ticket = await Ticket.findByPk(id, { 10 | include: [ 11 | { 12 | model: Contact, 13 | as: "contact", 14 | attributes: ["id", "name", "number", "profilePicUrl"], 15 | include: ["extraInfo"] 16 | }, 17 | { 18 | model: User, 19 | as: "user", 20 | attributes: ["id", "name"] 21 | }, 22 | { 23 | model: Queue, 24 | as: "queue", 25 | attributes: ["id", "name", "color"] 26 | }, 27 | { 28 | model: Whatsapp, 29 | as: "whatsapp", 30 | attributes: ["name"] 31 | } 32 | ] 33 | }); 34 | 35 | if (!ticket) { 36 | throw new AppError("ERR_NO_TICKET_FOUND", 404); 37 | } 38 | 39 | return ticket; 40 | }; 41 | 42 | export default ShowTicketService; -------------------------------------------------------------------------------- /backend/src/services/TicketServices/UpdateTicketService.ts: -------------------------------------------------------------------------------- 1 | import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; 2 | import SetTicketMessagesAsRead from "../../helpers/SetTicketMessagesAsRead"; 3 | import { getIO } from "../../libs/socket"; 4 | import Ticket from "../../models/Ticket"; 5 | import SendWhatsAppMessage from "../WbotServices/SendWhatsAppMessage"; 6 | import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; 7 | import ShowTicketService from "./ShowTicketService"; 8 | 9 | interface TicketData { 10 | status?: string; 11 | userId?: number; 12 | queueId?: number; 13 | whatsappId?: number; 14 | } 15 | 16 | interface Request { 17 | ticketData: TicketData; 18 | ticketId: string | number; 19 | } 20 | 21 | interface Response { 22 | ticket: Ticket; 23 | oldStatus: string; 24 | oldUserId: number | undefined; 25 | } 26 | 27 | const UpdateTicketService = async ({ 28 | ticketData, 29 | ticketId 30 | }: Request): Promise => { 31 | const { status, userId, queueId, whatsappId } = ticketData; 32 | 33 | const ticket = await ShowTicketService(ticketId); 34 | await SetTicketMessagesAsRead(ticket); 35 | 36 | if(whatsappId && ticket.whatsappId !== whatsappId) { 37 | await CheckContactOpenTickets(ticket.contactId, whatsappId); 38 | } 39 | 40 | const oldStatus = ticket.status; 41 | const oldUserId = ticket.user?.id; 42 | 43 | if (oldStatus === "closed") { 44 | await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); 45 | } 46 | 47 | await ticket.update({ 48 | status, 49 | queueId, 50 | userId 51 | }); 52 | 53 | 54 | if(whatsappId) { 55 | await ticket.update({ 56 | whatsappId 57 | }); 58 | } 59 | 60 | await ticket.reload(); 61 | 62 | const io = getIO(); 63 | 64 | if (ticket.status !== oldStatus || ticket.user?.id !== oldUserId) { 65 | io.to(oldStatus).emit("ticket", { 66 | action: "delete", 67 | ticketId: ticket.id 68 | }); 69 | } 70 | 71 | 72 | 73 | io.to(ticket.status) 74 | .to("notification") 75 | .to(ticketId.toString()) 76 | .emit("ticket", { 77 | action: "update", 78 | ticket 79 | }); 80 | 81 | return { ticket, oldStatus, oldUserId }; 82 | }; 83 | 84 | export default UpdateTicketService; 85 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/AuthUserService.ts: -------------------------------------------------------------------------------- 1 | import User from "../../models/User"; 2 | import AppError from "../../errors/AppError"; 3 | import { 4 | createAccessToken, 5 | createRefreshToken 6 | } from "../../helpers/CreateTokens"; 7 | import { SerializeUser } from "../../helpers/SerializeUser"; 8 | import Queue from "../../models/Queue"; 9 | 10 | interface SerializedUser { 11 | id: number; 12 | name: string; 13 | email: string; 14 | profile: string; 15 | queues: Queue[]; 16 | } 17 | 18 | interface Request { 19 | email: string; 20 | password: string; 21 | } 22 | 23 | interface Response { 24 | serializedUser: SerializedUser; 25 | token: string; 26 | refreshToken: string; 27 | } 28 | 29 | const AuthUserService = async ({ 30 | email, 31 | password 32 | }: Request): Promise => { 33 | const user = await User.findOne({ 34 | where: { email }, 35 | include: ["queues"] 36 | }); 37 | 38 | if (!user) { 39 | throw new AppError("ERR_INVALID_CREDENTIALS", 401); 40 | } 41 | 42 | if (!(await user.checkPassword(password))) { 43 | throw new AppError("ERR_INVALID_CREDENTIALS", 401); 44 | } 45 | 46 | const token = createAccessToken(user); 47 | const refreshToken = createRefreshToken(user); 48 | 49 | const serializedUser = SerializeUser(user); 50 | 51 | return { 52 | serializedUser, 53 | token, 54 | refreshToken 55 | }; 56 | }; 57 | 58 | export default AuthUserService; 59 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/CreateUserService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | import { SerializeUser } from "../../helpers/SerializeUser"; 5 | import User from "../../models/User"; 6 | 7 | interface Request { 8 | email: string; 9 | password: string; 10 | name: string; 11 | queueIds?: number[]; 12 | profile?: string; 13 | whatsappId?: number; 14 | } 15 | 16 | interface Response { 17 | email: string; 18 | name: string; 19 | id: number; 20 | profile: string; 21 | } 22 | 23 | const CreateUserService = async ({ 24 | email, 25 | password, 26 | name, 27 | queueIds = [], 28 | profile = "admin", 29 | whatsappId 30 | }: Request): Promise => { 31 | const schema = Yup.object().shape({ 32 | name: Yup.string().required().min(2), 33 | email: Yup.string() 34 | .email() 35 | .required() 36 | .test( 37 | "Check-email", 38 | "An user with this email already exists.", 39 | async value => { 40 | if (!value) return false; 41 | const emailExists = await User.findOne({ 42 | where: { email: value } 43 | }); 44 | return !emailExists; 45 | } 46 | ), 47 | password: Yup.string().required().min(5) 48 | }); 49 | 50 | try { 51 | await schema.validate({ email, password, name }); 52 | } catch (err) { 53 | throw new AppError(err.message); 54 | } 55 | 56 | const user = await User.create( 57 | { 58 | email, 59 | password, 60 | name, 61 | profile, 62 | whatsappId: whatsappId ? whatsappId : null 63 | }, 64 | { include: ["queues", "whatsapp"] } 65 | ); 66 | 67 | await user.$set("queues", queueIds); 68 | 69 | await user.reload(); 70 | 71 | return SerializeUser(user); 72 | }; 73 | 74 | export default CreateUserService; 75 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/DeleteUserService.ts: -------------------------------------------------------------------------------- 1 | import User from "../../models/User"; 2 | import AppError from "../../errors/AppError"; 3 | import Ticket from "../../models/Ticket"; 4 | import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus"; 5 | 6 | const DeleteUserService = async (id: string | number): Promise => { 7 | const user = await User.findOne({ 8 | where: { id } 9 | }); 10 | 11 | if (!user) { 12 | throw new AppError("ERR_NO_USER_FOUND", 404); 13 | } 14 | 15 | const userOpenTickets: Ticket[] = await user.$get("tickets", { 16 | where: { status: "open" } 17 | }); 18 | 19 | if (userOpenTickets.length > 0) { 20 | UpdateDeletedUserOpenTicketsStatus(userOpenTickets); 21 | } 22 | 23 | await user.destroy(); 24 | }; 25 | 26 | export default DeleteUserService; 27 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/ListUsersService.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Op } from "sequelize"; 2 | import Queue from "../../models/Queue"; 3 | import User from "../../models/User"; 4 | import Whatsapp from "../../models/Whatsapp"; 5 | 6 | interface Request { 7 | searchParam?: string; 8 | pageNumber?: string | number; 9 | } 10 | 11 | interface Response { 12 | users: User[]; 13 | count: number; 14 | hasMore: boolean; 15 | } 16 | 17 | const ListUsersService = async ({ 18 | searchParam = "", 19 | pageNumber = "1" 20 | }: Request): Promise => { 21 | const whereCondition = { 22 | [Op.or]: [ 23 | { 24 | "$User.name$": Sequelize.where( 25 | Sequelize.fn("LOWER", Sequelize.col("User.name")), 26 | "LIKE", 27 | `%${searchParam.toLowerCase()}%` 28 | ) 29 | }, 30 | { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } } 31 | ] 32 | }; 33 | const limit = 20; 34 | const offset = limit * (+pageNumber - 1); 35 | 36 | const { count, rows: users } = await User.findAndCountAll({ 37 | where: whereCondition, 38 | attributes: ["name", "id", "email", "profile", "createdAt"], 39 | limit, 40 | offset, 41 | order: [["createdAt", "DESC"]], 42 | include: [ 43 | { model: Queue, as: "queues", attributes: ["id", "name", "color"] }, 44 | { model: Whatsapp, as: "whatsapp", attributes: ["id", "name"] }, 45 | ] 46 | }); 47 | 48 | const hasMore = count > offset + users.length; 49 | 50 | return { 51 | users, 52 | count, 53 | hasMore 54 | }; 55 | }; 56 | 57 | export default ListUsersService; 58 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/ShowUserService.ts: -------------------------------------------------------------------------------- 1 | import User from "../../models/User"; 2 | import AppError from "../../errors/AppError"; 3 | import Queue from "../../models/Queue"; 4 | import Whatsapp from "../../models/Whatsapp"; 5 | 6 | const ShowUserService = async (id: string | number): Promise => { 7 | const user = await User.findByPk(id, { 8 | attributes: ["name", "id", "email", "profile", "tokenVersion", "whatsappId"], 9 | include: [ 10 | { model: Queue, as: "queues", attributes: ["id", "name", "color"] }, 11 | { model: Whatsapp, as: "whatsapp", attributes: ["id", "name"] }, 12 | ], 13 | order: [ [ { model: Queue, as: "queues"}, 'name', 'asc' ] ] 14 | }); 15 | if (!user) { 16 | throw new AppError("ERR_NO_USER_FOUND", 404); 17 | } 18 | 19 | return user; 20 | }; 21 | 22 | export default ShowUserService; 23 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/UpdateUserService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | import { SerializeUser } from "../../helpers/SerializeUser"; 5 | import ShowUserService from "./ShowUserService"; 6 | 7 | interface UserData { 8 | email?: string; 9 | password?: string; 10 | name?: string; 11 | profile?: string; 12 | queueIds?: number[]; 13 | whatsappId?: number; 14 | } 15 | 16 | interface Request { 17 | userData: UserData; 18 | userId: string | number; 19 | } 20 | 21 | interface Response { 22 | id: number; 23 | name: string; 24 | email: string; 25 | profile: string; 26 | } 27 | 28 | const UpdateUserService = async ({ 29 | userData, 30 | userId 31 | }: Request): Promise => { 32 | const user = await ShowUserService(userId); 33 | 34 | const schema = Yup.object().shape({ 35 | name: Yup.string().min(2), 36 | email: Yup.string().email(), 37 | profile: Yup.string(), 38 | password: Yup.string() 39 | }); 40 | 41 | const { email, password, profile, name, queueIds = [], whatsappId } = userData; 42 | 43 | try { 44 | await schema.validate({ email, password, profile, name }); 45 | } catch (err) { 46 | throw new AppError(err.message); 47 | } 48 | 49 | await user.update({ 50 | email, 51 | password, 52 | profile, 53 | name, 54 | whatsappId: whatsappId ? whatsappId : null 55 | }); 56 | 57 | await user.$set("queues", queueIds); 58 | 59 | await user.reload(); 60 | 61 | return SerializeUser(user); 62 | }; 63 | 64 | export default UpdateUserService; 65 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/CheckIsValidContact.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 3 | import { getWbot } from "../../libs/wbot"; 4 | 5 | const CheckIsValidContact = async (number: string): Promise => { 6 | const defaultWhatsapp = await GetDefaultWhatsApp(); 7 | 8 | const wbot = getWbot(defaultWhatsapp.id); 9 | 10 | try { 11 | const isValidNumber = await wbot.isRegisteredUser(`${number}@c.us`); 12 | if (!isValidNumber) { 13 | throw new AppError("invalidNumber"); 14 | } 15 | } catch (err) { 16 | if (err.message === "invalidNumber") { 17 | throw new AppError("ERR_WAPP_INVALID_CONTACT"); 18 | } 19 | throw new AppError("ERR_WAPP_CHECK_CONTACT"); 20 | } 21 | }; 22 | 23 | export default CheckIsValidContact; 24 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/CheckNumber.ts: -------------------------------------------------------------------------------- 1 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 2 | import { getWbot } from "../../libs/wbot"; 3 | 4 | const CheckContactNumber = async (number: string): Promise => { 5 | const defaultWhatsapp = await GetDefaultWhatsApp(); 6 | 7 | const wbot = getWbot(defaultWhatsapp.id); 8 | 9 | const validNumber : any = await wbot.getNumberId(`${number}@c.us`); 10 | return validNumber.user 11 | }; 12 | 13 | export default CheckContactNumber; 14 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/DeleteWhatsAppMessage.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import GetWbotMessage from "../../helpers/GetWbotMessage"; 3 | import Message from "../../models/Message"; 4 | import Ticket from "../../models/Ticket"; 5 | 6 | const DeleteWhatsAppMessage = async (messageId: string): Promise => { 7 | const message = await Message.findByPk(messageId, { 8 | include: [ 9 | { 10 | model: Ticket, 11 | as: "ticket", 12 | include: ["contact"] 13 | } 14 | ] 15 | }); 16 | 17 | if (!message) { 18 | throw new AppError("No message found with this ID."); 19 | } 20 | 21 | const { ticket } = message; 22 | 23 | const messageToDelete = await GetWbotMessage(ticket, messageId); 24 | 25 | try { 26 | await messageToDelete.delete(true); 27 | } catch (err) { 28 | throw new AppError("ERR_DELETE_WAPP_MSG"); 29 | } 30 | 31 | await message.update({ isDeleted: true }); 32 | 33 | return message; 34 | }; 35 | 36 | export default DeleteWhatsAppMessage; 37 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/GetProfilePicUrl.ts: -------------------------------------------------------------------------------- 1 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 2 | import { getWbot } from "../../libs/wbot"; 3 | 4 | const GetProfilePicUrl = async (number: string): Promise => { 5 | const defaultWhatsapp = await GetDefaultWhatsApp(); 6 | 7 | const wbot = getWbot(defaultWhatsapp.id); 8 | 9 | const profilePicUrl = await wbot.getProfilePicUrl(`${number}@c.us`); 10 | 11 | return profilePicUrl; 12 | }; 13 | 14 | export default GetProfilePicUrl; 15 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/ImportContactsService.ts: -------------------------------------------------------------------------------- 1 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 2 | import { getWbot } from "../../libs/wbot"; 3 | import Contact from "../../models/Contact"; 4 | import { logger } from "../../utils/logger"; 5 | 6 | const ImportContactsService = async (userId:number): Promise => { 7 | const defaultWhatsapp = await GetDefaultWhatsApp(userId); 8 | 9 | const wbot = getWbot(defaultWhatsapp.id); 10 | 11 | let phoneContacts; 12 | 13 | try { 14 | phoneContacts = await wbot.getContacts(); 15 | } catch (err) { 16 | logger.error(`Could not get whatsapp contacts from phone. Err: ${err}`); 17 | } 18 | 19 | if (phoneContacts) { 20 | await Promise.all( 21 | phoneContacts.map(async ({ number, name }) => { 22 | if (!number) { 23 | return null; 24 | } 25 | if (!name) { 26 | name = number; 27 | } 28 | 29 | const numberExists = await Contact.findOne({ 30 | where: { number } 31 | }); 32 | 33 | if (numberExists) return null; 34 | 35 | return Contact.create({ number, name }); 36 | }) 37 | ); 38 | } 39 | }; 40 | 41 | export default ImportContactsService; 42 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/SendWhatsAppMedia.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { MessageMedia, Message as WbotMessage } from "whatsapp-web.js"; 3 | import AppError from "../../errors/AppError"; 4 | import GetTicketWbot from "../../helpers/GetTicketWbot"; 5 | import Ticket from "../../models/Ticket"; 6 | 7 | import formatBody from "../../helpers/Mustache"; 8 | 9 | interface Request { 10 | media: Express.Multer.File; 11 | ticket: Ticket; 12 | body?: string; 13 | } 14 | 15 | const SendWhatsAppMedia = async ({ 16 | media, 17 | ticket, 18 | body 19 | }: Request): Promise => { 20 | try { 21 | const wbot = await GetTicketWbot(ticket); 22 | const hasBody = body 23 | ? formatBody(body as string, ticket.contact) 24 | : undefined; 25 | 26 | const newMedia = MessageMedia.fromFilePath(media.path); 27 | const sentMessage = await wbot.sendMessage( 28 | `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, 29 | newMedia, 30 | { 31 | caption: hasBody, 32 | sendAudioAsVoice: true 33 | } 34 | ); 35 | 36 | await ticket.update({ lastMessage: body || media.filename }); 37 | 38 | fs.unlinkSync(media.path); 39 | 40 | return sentMessage; 41 | } catch (err) { 42 | console.log(err); 43 | throw new AppError("ERR_SENDING_WAPP_MSG"); 44 | } 45 | }; 46 | 47 | export default SendWhatsAppMedia; 48 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/SendWhatsAppMessage.ts: -------------------------------------------------------------------------------- 1 | import { Message as WbotMessage } from "whatsapp-web.js"; 2 | import AppError from "../../errors/AppError"; 3 | import GetTicketWbot from "../../helpers/GetTicketWbot"; 4 | import GetWbotMessage from "../../helpers/GetWbotMessage"; 5 | import SerializeWbotMsgId from "../../helpers/SerializeWbotMsgId"; 6 | import Message from "../../models/Message"; 7 | import Ticket from "../../models/Ticket"; 8 | 9 | import formatBody from "../../helpers/Mustache"; 10 | 11 | interface Request { 12 | body: string; 13 | ticket: Ticket; 14 | quotedMsg?: Message; 15 | } 16 | 17 | const SendWhatsAppMessage = async ({ 18 | body, 19 | ticket, 20 | quotedMsg 21 | }: Request): Promise => { 22 | let quotedMsgSerializedId: string | undefined; 23 | if (quotedMsg) { 24 | await GetWbotMessage(ticket, quotedMsg.id); 25 | quotedMsgSerializedId = SerializeWbotMsgId(ticket, quotedMsg); 26 | } 27 | 28 | const wbot = await GetTicketWbot(ticket); 29 | 30 | try { 31 | const sentMessage = await wbot.sendMessage( 32 | `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, 33 | formatBody(body, ticket.contact), 34 | { 35 | quotedMessageId: quotedMsgSerializedId, 36 | linkPreview: false 37 | } 38 | ); 39 | 40 | await ticket.update({ lastMessage: body }); 41 | return sentMessage; 42 | } catch (err) { 43 | throw new AppError("ERR_SENDING_WAPP_MSG"); 44 | } 45 | }; 46 | 47 | export default SendWhatsAppMessage; 48 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts: -------------------------------------------------------------------------------- 1 | import ListWhatsAppsService from "../WhatsappService/ListWhatsAppsService"; 2 | import { StartWhatsAppSession } from "./StartWhatsAppSession"; 3 | 4 | export const StartAllWhatsAppsSessions = async (): Promise => { 5 | const whatsapps = await ListWhatsAppsService(); 6 | if (whatsapps.length > 0) { 7 | whatsapps.forEach(whatsapp => { 8 | StartWhatsAppSession(whatsapp); 9 | }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/StartWhatsAppSession.ts: -------------------------------------------------------------------------------- 1 | import { initWbot } from "../../libs/wbot"; 2 | import Whatsapp from "../../models/Whatsapp"; 3 | import { wbotMessageListener } from "./wbotMessageListener"; 4 | import { getIO } from "../../libs/socket"; 5 | import wbotMonitor from "./wbotMonitor"; 6 | import { logger } from "../../utils/logger"; 7 | 8 | export const StartWhatsAppSession = async ( 9 | whatsapp: Whatsapp 10 | ): Promise => { 11 | await whatsapp.update({ status: "OPENING" }); 12 | 13 | const io = getIO(); 14 | io.emit("whatsappSession", { 15 | action: "update", 16 | session: whatsapp 17 | }); 18 | 19 | try { 20 | const wbot = await initWbot(whatsapp); 21 | wbotMessageListener(wbot); 22 | wbotMonitor(wbot, whatsapp); 23 | } catch (err) { 24 | logger.error(err); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/wbotMonitor.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node"; 2 | import { Client } from "whatsapp-web.js"; 3 | 4 | import { getIO } from "../../libs/socket"; 5 | import Whatsapp from "../../models/Whatsapp"; 6 | import { logger } from "../../utils/logger"; 7 | import { StartWhatsAppSession } from "./StartWhatsAppSession"; 8 | 9 | interface Session extends Client { 10 | id?: number; 11 | } 12 | 13 | const wbotMonitor = async ( 14 | wbot: Session, 15 | whatsapp: Whatsapp 16 | ): Promise => { 17 | const io = getIO(); 18 | const sessionName = whatsapp.name; 19 | 20 | try { 21 | wbot.on("change_state", async newState => { 22 | logger.info(`Monitor session: ${sessionName}, ${newState}`); 23 | try { 24 | await whatsapp.update({ status: newState }); 25 | } catch (err) { 26 | Sentry.captureException(err); 27 | logger.error(err); 28 | } 29 | 30 | io.emit("whatsappSession", { 31 | action: "update", 32 | session: whatsapp 33 | }); 34 | }); 35 | 36 | wbot.on("change_battery", async batteryInfo => { 37 | const { battery, plugged } = batteryInfo; 38 | logger.info( 39 | `Battery session: ${sessionName} ${battery}% - Charging? ${plugged}` 40 | ); 41 | 42 | try { 43 | await whatsapp.update({ battery, plugged }); 44 | } catch (err) { 45 | Sentry.captureException(err); 46 | logger.error(err); 47 | } 48 | 49 | io.emit("whatsappSession", { 50 | action: "update", 51 | session: whatsapp 52 | }); 53 | }); 54 | 55 | wbot.on("disconnected", async reason => { 56 | logger.info(`Disconnected session: ${sessionName}, reason: ${reason}`); 57 | try { 58 | await whatsapp.update({ status: "OPENING", session: "" }); 59 | } catch (err) { 60 | Sentry.captureException(err); 61 | logger.error(err); 62 | } 63 | 64 | io.emit("whatsappSession", { 65 | action: "update", 66 | session: whatsapp 67 | }); 68 | 69 | setTimeout(() => StartWhatsAppSession(whatsapp), 2000); 70 | }); 71 | } catch (err) { 72 | Sentry.captureException(err); 73 | logger.error(err); 74 | } 75 | }; 76 | 77 | export default wbotMonitor; 78 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/AssociateWhatsappQueue.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | 3 | const AssociateWhatsappQueue = async ( 4 | whatsapp: Whatsapp, 5 | queueIds: number[] 6 | ): Promise => { 7 | await whatsapp.$set("queues", queueIds); 8 | 9 | await whatsapp.reload(); 10 | }; 11 | 12 | export default AssociateWhatsappQueue; 13 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/CreateWhatsAppService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | import Whatsapp from "../../models/Whatsapp"; 5 | import AssociateWhatsappQueue from "./AssociateWhatsappQueue"; 6 | 7 | interface Request { 8 | name: string; 9 | queueIds?: number[]; 10 | greetingMessage?: string; 11 | farewellMessage?: string; 12 | status?: string; 13 | isDefault?: boolean; 14 | } 15 | 16 | interface Response { 17 | whatsapp: Whatsapp; 18 | oldDefaultWhatsapp: Whatsapp | null; 19 | } 20 | 21 | const CreateWhatsAppService = async ({ 22 | name, 23 | status = "OPENING", 24 | queueIds = [], 25 | greetingMessage, 26 | farewellMessage, 27 | isDefault = false 28 | }: Request): Promise => { 29 | const schema = Yup.object().shape({ 30 | name: Yup.string() 31 | .required() 32 | .min(2) 33 | .test( 34 | "Check-name", 35 | "This whatsapp name is already used.", 36 | async value => { 37 | if (!value) return false; 38 | const nameExists = await Whatsapp.findOne({ 39 | where: { name: value } 40 | }); 41 | return !nameExists; 42 | } 43 | ), 44 | isDefault: Yup.boolean().required() 45 | }); 46 | 47 | try { 48 | await schema.validate({ name, status, isDefault }); 49 | } catch (err) { 50 | throw new AppError(err.message); 51 | } 52 | 53 | const whatsappFound = await Whatsapp.findOne(); 54 | 55 | isDefault = !whatsappFound; 56 | 57 | let oldDefaultWhatsapp: Whatsapp | null = null; 58 | 59 | if (isDefault) { 60 | oldDefaultWhatsapp = await Whatsapp.findOne({ 61 | where: { isDefault: true } 62 | }); 63 | if (oldDefaultWhatsapp) { 64 | await oldDefaultWhatsapp.update({ isDefault: false }); 65 | } 66 | } 67 | 68 | if (queueIds.length > 1 && !greetingMessage) { 69 | throw new AppError("ERR_WAPP_GREETING_REQUIRED"); 70 | } 71 | 72 | const whatsapp = await Whatsapp.create( 73 | { 74 | name, 75 | status, 76 | greetingMessage, 77 | farewellMessage, 78 | isDefault 79 | }, 80 | { include: ["queues"] } 81 | ); 82 | 83 | await AssociateWhatsappQueue(whatsapp, queueIds); 84 | 85 | return { whatsapp, oldDefaultWhatsapp }; 86 | }; 87 | 88 | export default CreateWhatsAppService; 89 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/DeleteWhatsAppService.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteWhatsAppService = async (id: string): Promise => { 5 | const whatsapp = await Whatsapp.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!whatsapp) { 10 | throw new AppError("ERR_NO_WAPP_FOUND", 404); 11 | } 12 | 13 | await whatsapp.destroy(); 14 | }; 15 | 16 | export default DeleteWhatsAppService; 17 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/ListWhatsAppsService.ts: -------------------------------------------------------------------------------- 1 | import Queue from "../../models/Queue"; 2 | import Whatsapp from "../../models/Whatsapp"; 3 | 4 | const ListWhatsAppsService = async (): Promise => { 5 | const whatsapps = await Whatsapp.findAll({ 6 | include: [ 7 | { 8 | model: Queue, 9 | as: "queues", 10 | attributes: ["id", "name", "color", "greetingMessage"] 11 | } 12 | ] 13 | }); 14 | 15 | return whatsapps; 16 | }; 17 | 18 | export default ListWhatsAppsService; 19 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/ShowWhatsAppService.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | import AppError from "../../errors/AppError"; 3 | import Queue from "../../models/Queue"; 4 | 5 | const ShowWhatsAppService = async (id: string | number): Promise => { 6 | const whatsapp = await Whatsapp.findByPk(id, { 7 | include: [ 8 | { 9 | model: Queue, 10 | as: "queues", 11 | attributes: ["id", "name", "color", "greetingMessage"] 12 | } 13 | ], 14 | order: [["queues", "name", "ASC"]] 15 | }); 16 | 17 | if (!whatsapp) { 18 | throw new AppError("ERR_NO_WAPP_FOUND", 404); 19 | } 20 | 21 | return whatsapp; 22 | }; 23 | 24 | export default ShowWhatsAppService; 25 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/UpdateWhatsAppService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | import { Op } from "sequelize"; 3 | 4 | import AppError from "../../errors/AppError"; 5 | import Whatsapp from "../../models/Whatsapp"; 6 | import ShowWhatsAppService from "./ShowWhatsAppService"; 7 | import AssociateWhatsappQueue from "./AssociateWhatsappQueue"; 8 | 9 | interface WhatsappData { 10 | name?: string; 11 | status?: string; 12 | session?: string; 13 | isDefault?: boolean; 14 | greetingMessage?: string; 15 | farewellMessage?: string; 16 | queueIds?: number[]; 17 | } 18 | 19 | interface Request { 20 | whatsappData: WhatsappData; 21 | whatsappId: string; 22 | } 23 | 24 | interface Response { 25 | whatsapp: Whatsapp; 26 | oldDefaultWhatsapp: Whatsapp | null; 27 | } 28 | 29 | const UpdateWhatsAppService = async ({ 30 | whatsappData, 31 | whatsappId 32 | }: Request): Promise => { 33 | const schema = Yup.object().shape({ 34 | name: Yup.string().min(2), 35 | status: Yup.string(), 36 | isDefault: Yup.boolean() 37 | }); 38 | 39 | const { 40 | name, 41 | status, 42 | isDefault, 43 | session, 44 | greetingMessage, 45 | farewellMessage, 46 | queueIds = [] 47 | } = whatsappData; 48 | 49 | try { 50 | await schema.validate({ name, status, isDefault }); 51 | } catch (err) { 52 | throw new AppError(err.message); 53 | } 54 | 55 | if (queueIds.length > 1 && !greetingMessage) { 56 | throw new AppError("ERR_WAPP_GREETING_REQUIRED"); 57 | } 58 | 59 | let oldDefaultWhatsapp: Whatsapp | null = null; 60 | 61 | if (isDefault) { 62 | oldDefaultWhatsapp = await Whatsapp.findOne({ 63 | where: { isDefault: true, id: { [Op.not]: whatsappId } } 64 | }); 65 | if (oldDefaultWhatsapp) { 66 | await oldDefaultWhatsapp.update({ isDefault: false }); 67 | } 68 | } 69 | 70 | const whatsapp = await ShowWhatsAppService(whatsappId); 71 | 72 | await whatsapp.update({ 73 | name, 74 | status, 75 | session, 76 | greetingMessage, 77 | farewellMessage, 78 | isDefault 79 | }); 80 | 81 | await AssociateWhatsappQueue(whatsapp, queueIds); 82 | 83 | return { whatsapp, oldDefaultWhatsapp }; 84 | }; 85 | 86 | export default UpdateWhatsAppService; 87 | -------------------------------------------------------------------------------- /backend/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | 3 | const logger = pino({ 4 | prettyPrint: { 5 | ignore: "pid,hostname" 6 | } 7 | }); 8 | 9 | export { logger }; 10 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "strictPropertyInitialization": false, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BACKEND_URL = http://localhost:8080/ 2 | REACT_APP_HOURS_CLOSE_TICKETS_AUTO = 24 3 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | package-lock.json 27 | yarn.lock 28 | 29 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.9.0", 7 | "@emotion/styled": "^11.8.1", 8 | "@material-ui/core": "^4.11.0", 9 | "@material-ui/icons": "^4.9.1", 10 | "@material-ui/lab": "^4.0.0-alpha.56", 11 | "@mui/icons-material": "^5.5.1", 12 | "@mui/material": "^5.5.2", 13 | "@mui/styles": "^5.5.1", 14 | "@testing-library/jest-dom": "^5.11.4", 15 | "@testing-library/react": "^11.0.4", 16 | "@testing-library/user-event": "^12.1.7", 17 | "axios": "^0.21.1", 18 | "date-fns": "^2.16.1", 19 | "emoji-mart": "^3.0.1", 20 | "formik": "^2.2.0", 21 | "i18next": "^19.8.2", 22 | "i18next-browser-languagedetector": "^6.0.1", 23 | "markdown-to-jsx": "^7.1.0", 24 | "mic-recorder-to-mp3": "^2.2.2", 25 | "qrcode.react": "^1.0.0", 26 | "react": "^16.13.1", 27 | "react-color": "^2.19.3", 28 | "react-dom": "^16.13.1", 29 | "react-modal-image": "^2.5.0", 30 | "react-router-dom": "^5.2.0", 31 | "react-scripts": "^5.0.1", 32 | "react-toastify": "^6.0.9", 33 | "recharts": "^2.0.2", 34 | "socket.io-client": "^3.0.5", 35 | "use-sound": "^2.0.1", 36 | "yup": "^0.32.8" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": {} 60 | } 61 | -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/default-profile.png -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Whatsapp 5 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Whatsapp", 3 | "name": "Whatsapp", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /frontend/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/public/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/server.js: -------------------------------------------------------------------------------- 1 | //simple express server to run frontend production build; 2 | const express = require("express"); 3 | const path = require("path"); 4 | const app = express(); 5 | app.use(express.static(path.join(__dirname, "build"))); 6 | app.get("/*", function (req, res) { 7 | res.sendFile(path.join(__dirname, "build", "index.html")); 8 | }); 9 | app.listen(3333); 10 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Routes from "./routes"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | 5 | import { createTheme, ThemeProvider } from "@material-ui/core/styles"; 6 | import { ptBR } from "@material-ui/core/locale"; 7 | 8 | const App = () => { 9 | const [locale, setLocale] = useState(); 10 | 11 | const theme = createTheme( 12 | { 13 | scrollbarStyles: { 14 | "&::-webkit-scrollbar": { 15 | width: "8px", 16 | height: "8px", 17 | }, 18 | "&::-webkit-scrollbar-thumb": { 19 | boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", 20 | backgroundColor: "#e8e8e8", 21 | }, 22 | }, 23 | palette: { 24 | primary: { main: "#00A884" }, 25 | }, 26 | }, 27 | locale 28 | ); 29 | 30 | useEffect(() => { 31 | const i18nlocale = localStorage.getItem("i18nextLng"); 32 | const browserLocale = 33 | i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); 34 | 35 | if (browserLocale === "ptBR") { 36 | setLocale(ptBR); 37 | } 38 | }, []); 39 | 40 | return ( 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /frontend/src/assets/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/src/assets/default-profile.png -------------------------------------------------------------------------------- /frontend/src/assets/sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/src/assets/sound.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/src/assets/sound.ogg -------------------------------------------------------------------------------- /frontend/src/assets/wa-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsninja/whatsninja-community/7731138d8f878c8e2b4f807d743115041cbeb6bf/frontend/src/assets/wa-background.png -------------------------------------------------------------------------------- /frontend/src/components/BackdropLoading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Backdrop from "@material-ui/core/Backdrop"; 4 | import CircularProgress from "@material-ui/core/CircularProgress"; 5 | import { makeStyles } from "@material-ui/core/styles"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | backdrop: { 9 | zIndex: theme.zIndex.drawer + 1, 10 | color: "#fff", 11 | }, 12 | })); 13 | 14 | const BackdropLoading = () => { 15 | const classes = useStyles(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BackdropLoading; 24 | -------------------------------------------------------------------------------- /frontend/src/components/ButtonWithSpinner/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import { green } from "@material-ui/core/colors"; 5 | import { CircularProgress, Button } from "@material-ui/core"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | button: { 9 | position: "relative", 10 | }, 11 | 12 | buttonProgress: { 13 | color: green[500], 14 | position: "absolute", 15 | top: "50%", 16 | left: "50%", 17 | marginTop: -12, 18 | marginLeft: -12, 19 | }, 20 | })); 21 | 22 | const ButtonWithSpinner = ({ loading, children, ...rest }) => { 23 | const classes = useStyles(); 24 | 25 | return ( 26 | 32 | ); 33 | }; 34 | 35 | export default ButtonWithSpinner; 36 | -------------------------------------------------------------------------------- /frontend/src/components/Can/index.js: -------------------------------------------------------------------------------- 1 | import rules from "../../rules"; 2 | 3 | const check = (role, action, data) => { 4 | const permissions = rules[role]; 5 | if (!permissions) { 6 | // role is not present in the rules 7 | return false; 8 | } 9 | 10 | const staticPermissions = permissions.static; 11 | 12 | if (staticPermissions && staticPermissions.includes(action)) { 13 | // static rule not provided for action 14 | return true; 15 | } 16 | 17 | const dynamicPermissions = permissions.dynamic; 18 | 19 | if (dynamicPermissions) { 20 | const permissionCondition = dynamicPermissions[action]; 21 | if (!permissionCondition) { 22 | // dynamic rule not provided for action 23 | return false; 24 | } 25 | 26 | return permissionCondition(data); 27 | } 28 | return false; 29 | }; 30 | 31 | const Can = ({ role, perform, data, yes, no }) => 32 | check(role, perform, data) ? yes() : no(); 33 | 34 | Can.defaultProps = { 35 | yes: () => null, 36 | no: () => null, 37 | }; 38 | 39 | export { Can }; 40 | -------------------------------------------------------------------------------- /frontend/src/components/ConfirmationModal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@material-ui/core/Button"; 3 | import Dialog from "@material-ui/core/Dialog"; 4 | import DialogActions from "@material-ui/core/DialogActions"; 5 | import DialogContent from "@material-ui/core/DialogContent"; 6 | import DialogTitle from "@material-ui/core/DialogTitle"; 7 | import Typography from "@material-ui/core/Typography"; 8 | 9 | import { i18n } from "../../translate/i18n"; 10 | 11 | const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => { 12 | return ( 13 | onClose(false)} 16 | aria-labelledby="confirm-dialog" 17 | > 18 | {title} 19 | 20 | {children} 21 | 22 | 23 | 30 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default ConfirmationModal; 46 | -------------------------------------------------------------------------------- /frontend/src/components/ContactDrawerSkeleton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import Paper from "@material-ui/core/Paper"; 5 | import { i18n } from "../../translate/i18n"; 6 | 7 | const ContactDrawerSkeleton = ({ classes }) => { 8 | return ( 9 |
10 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {i18n.t("contactDrawer.extraInfo")} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default ContactDrawerSkeleton; 44 | -------------------------------------------------------------------------------- /frontend/src/components/LocationPreview/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import toastError from "../../errors/toastError"; 3 | 4 | import Typography from "@material-ui/core/Typography"; 5 | import Grid from "@material-ui/core/Grid"; 6 | 7 | import { Button, Divider, } from "@material-ui/core"; 8 | 9 | const LocationPreview = ({ image, link, description }) => { 10 | useEffect(() => {}, [image, link, description]); 11 | 12 | const handleLocation = async() => { 13 | try { 14 | window.open(link); 15 | } catch (err) { 16 | toastError(err); 17 | } 18 | } 19 | 20 | return ( 21 | <> 22 |
25 |
26 |
27 | 28 |
29 | { description && ( 30 |
31 | 32 |
') }}>
33 |
34 |
35 | )} 36 |
37 |
38 | 39 | 45 |
46 |
47 |
48 | 49 | ); 50 | 51 | }; 52 | 53 | export default LocationPreview; -------------------------------------------------------------------------------- /frontend/src/components/MainContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Container from "@material-ui/core/Container"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | mainContainer: { 8 | flex: 1, 9 | // padding: theme.spacing(2), 10 | // height: `calc(100% - 48px)`, 11 | padding: 0, 12 | height: "100%", 13 | }, 14 | 15 | contentWrapper: { 16 | height: "100%", 17 | overflowY: "hidden", 18 | display: "flex", 19 | flexDirection: "column", 20 | }, 21 | })); 22 | 23 | const MainContainer = ({ children }) => { 24 | const classes = useStyles(); 25 | 26 | return ( 27 | 28 |
{children}
29 |
30 | ); 31 | }; 32 | 33 | export default MainContainer; 34 | -------------------------------------------------------------------------------- /frontend/src/components/MainHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | contactsHeader: { 7 | display: "flex", 8 | alignItems: "center", 9 | padding: "0px 6px 6px 6px", 10 | }, 11 | })); 12 | 13 | const MainHeader = ({ children }) => { 14 | const classes = useStyles(); 15 | 16 | return
{children}
; 17 | }; 18 | 19 | export default MainHeader; 20 | -------------------------------------------------------------------------------- /frontend/src/components/MainHeaderButtonsWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | MainHeaderButtonsWrapper: { 7 | flex: "none", 8 | marginLeft: "auto", 9 | "& > *": { 10 | margin: theme.spacing(1), 11 | }, 12 | }, 13 | })); 14 | 15 | const MainHeaderButtonsWrapper = ({ children }) => { 16 | const classes = useStyles(); 17 | 18 | return
{children}
; 19 | }; 20 | 21 | export default MainHeaderButtonsWrapper; 22 | -------------------------------------------------------------------------------- /frontend/src/components/MessageInput/RecordingTimer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | timerBox: { 6 | display: "flex", 7 | marginLeft: 10, 8 | marginRight: 10, 9 | alignItems: "center", 10 | }, 11 | })); 12 | 13 | const RecordingTimer = () => { 14 | const classes = useStyles(); 15 | const initialState = { 16 | minutes: 0, 17 | seconds: 0, 18 | }; 19 | const [timer, setTimer] = useState(initialState); 20 | 21 | useEffect(() => { 22 | const interval = setInterval( 23 | () => 24 | setTimer(prevState => { 25 | if (prevState.seconds === 59) { 26 | return { ...prevState, minutes: prevState.minutes + 1, seconds: 0 }; 27 | } 28 | return { ...prevState, seconds: prevState.seconds + 1 }; 29 | }), 30 | 1000 31 | ); 32 | return () => { 33 | clearInterval(interval); 34 | }; 35 | }, []); 36 | 37 | const addZero = n => { 38 | return n < 10 ? "0" + n : n; 39 | }; 40 | 41 | return ( 42 |
43 | {`${addZero(timer.minutes)}:${addZero(timer.seconds)}`} 44 |
45 | ); 46 | }; 47 | 48 | export default RecordingTimer; 49 | -------------------------------------------------------------------------------- /frontend/src/components/MessageOptionsMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | 3 | import MenuItem from "@material-ui/core/MenuItem"; 4 | 5 | import { i18n } from "../../translate/i18n"; 6 | import api from "../../services/api"; 7 | import ConfirmationModal from "../ConfirmationModal"; 8 | import { Menu } from "@material-ui/core"; 9 | import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"; 10 | import toastError from "../../errors/toastError"; 11 | 12 | const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { 13 | const { setReplyingMessage } = useContext(ReplyMessageContext); 14 | const [confirmationOpen, setConfirmationOpen] = useState(false); 15 | 16 | const handleDeleteMessage = async () => { 17 | try { 18 | await api.delete(`/messages/${message.id}`); 19 | } catch (err) { 20 | toastError(err); 21 | } 22 | }; 23 | 24 | const hanldeReplyMessage = () => { 25 | setReplyingMessage(message); 26 | handleClose(); 27 | }; 28 | 29 | const handleOpenConfirmationModal = (e) => { 30 | setConfirmationOpen(true); 31 | handleClose(); 32 | }; 33 | 34 | return ( 35 | <> 36 | 42 | {i18n.t("messageOptionsMenu.confirmationModal.message")} 43 | 44 | 58 | {message.fromMe && ( 59 | 60 | {i18n.t("messageOptionsMenu.delete")} 61 | 62 | )} 63 | 64 | {i18n.t("messageOptionsMenu.reply")} 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default MessageOptionsMenu; 72 | -------------------------------------------------------------------------------- /frontend/src/components/ModalImageCors/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | import ModalImage from "react-modal-image"; 5 | import api from "../../services/api"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | messageMedia: { 9 | objectFit: "cover", 10 | width: 250, 11 | height: 200, 12 | borderTopLeftRadius: 8, 13 | borderTopRightRadius: 8, 14 | borderBottomLeftRadius: 8, 15 | borderBottomRightRadius: 8, 16 | }, 17 | })); 18 | 19 | const ModalImageCors = ({ imageUrl }) => { 20 | const classes = useStyles(); 21 | const [fetching, setFetching] = useState(true); 22 | const [blobUrl, setBlobUrl] = useState(""); 23 | 24 | useEffect(() => { 25 | if (!imageUrl) return; 26 | const fetchImage = async () => { 27 | const { data, headers } = await api.get(imageUrl, { 28 | responseType: "blob", 29 | }); 30 | const url = window.URL.createObjectURL( 31 | new Blob([data], { type: headers["content-type"] }) 32 | ); 33 | setBlobUrl(url); 34 | setFetching(false); 35 | }; 36 | fetchImage(); 37 | }, [imageUrl]); 38 | 39 | return ( 40 | 48 | ); 49 | }; 50 | 51 | export default ModalImageCors; 52 | -------------------------------------------------------------------------------- /frontend/src/components/QrcodeModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import QRCode from "qrcode.react"; 3 | import openSocket from "socket.io-client"; 4 | import toastError from "../../errors/toastError"; 5 | 6 | import { Dialog, DialogContent, Paper, Typography } from "@material-ui/core"; 7 | import { i18n } from "../../translate/i18n"; 8 | import api from "../../services/api"; 9 | 10 | const QrcodeModal = ({ open, onClose, whatsAppId }) => { 11 | const [qrCode, setQrCode] = useState(""); 12 | 13 | useEffect(() => { 14 | const fetchSession = async () => { 15 | if (!whatsAppId) return; 16 | 17 | try { 18 | const { data } = await api.get(`/whatsapp/${whatsAppId}`); 19 | setQrCode(data.qrcode); 20 | } catch (err) { 21 | toastError(err); 22 | } 23 | }; 24 | fetchSession(); 25 | }, [whatsAppId]); 26 | 27 | useEffect(() => { 28 | if (!whatsAppId) return; 29 | const socket = openSocket(process.env.REACT_APP_BACKEND_URL); 30 | 31 | socket.on("whatsappSession", data => { 32 | if (data.action === "update" && data.session.id === whatsAppId) { 33 | setQrCode(data.session.qrcode); 34 | } 35 | 36 | if (data.action === "update" && data.session.qrcode === "") { 37 | onClose(); 38 | } 39 | }); 40 | 41 | return () => { 42 | socket.disconnect(); 43 | }; 44 | }, [whatsAppId, onClose]); 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | {i18n.t("qrCode.message")} 52 | 53 | {qrCode ? ( 54 | 55 | ) : ( 56 | Waiting for QR Code 57 | )} 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default React.memo(QrcodeModal); 65 | -------------------------------------------------------------------------------- /frontend/src/components/QueueSelect/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import InputLabel from "@material-ui/core/InputLabel"; 4 | import MenuItem from "@material-ui/core/MenuItem"; 5 | import FormControl from "@material-ui/core/FormControl"; 6 | import Select from "@material-ui/core/Select"; 7 | import Chip from "@material-ui/core/Chip"; 8 | import toastError from "../../errors/toastError"; 9 | import api from "../../services/api"; 10 | import { i18n } from "../../translate/i18n"; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | chips: { 14 | display: "flex", 15 | flexWrap: "wrap", 16 | }, 17 | chip: { 18 | margin: 2, 19 | }, 20 | })); 21 | 22 | const QueueSelect = ({ selectedQueueIds, onChange }) => { 23 | const classes = useStyles(); 24 | const [queues, setQueues] = useState([]); 25 | 26 | useEffect(() => { 27 | (async () => { 28 | try { 29 | const { data } = await api.get("/queue"); 30 | setQueues(data); 31 | } catch (err) { 32 | toastError(err); 33 | } 34 | })(); 35 | }, []); 36 | 37 | const handleChange = e => { 38 | onChange(e.target.value); 39 | }; 40 | 41 | return ( 42 |
43 | 44 | {i18n.t("queueSelect.inputLabel")} 45 | 85 | 86 |
87 | ); 88 | }; 89 | 90 | export default QueueSelect; 91 | -------------------------------------------------------------------------------- /frontend/src/components/TabPanel/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TabPanel = ({ children, value, name, ...rest }) => { 4 | if (value === name) { 5 | return ( 6 |
12 | <>{children} 13 |
14 | ); 15 | } else return null; 16 | }; 17 | 18 | export default TabPanel; 19 | -------------------------------------------------------------------------------- /frontend/src/components/TableRowSkeleton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TableCell from "@material-ui/core/TableCell"; 3 | import TableRow from "@material-ui/core/TableRow"; 4 | import Skeleton from "@material-ui/lab/Skeleton"; 5 | import { makeStyles } from "@material-ui/core"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | customTableCell: { 9 | display: "flex", 10 | alignItems: "center", 11 | justifyContent: "center", 12 | }, 13 | })); 14 | 15 | const TableRowSkeleton = ({ avatar, columns }) => { 16 | const classes = useStyles(); 17 | return ( 18 | <> 19 | 20 | {avatar && ( 21 | <> 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | )} 35 | {Array.from({ length: columns }, (_, index) => ( 36 | 37 |
38 | 44 |
45 |
46 | ))} 47 |
48 | 49 | ); 50 | }; 51 | 52 | export default TableRowSkeleton; 53 | -------------------------------------------------------------------------------- /frontend/src/components/TicketHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Card, Button } from "@material-ui/core"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import TicketHeaderSkeleton from "../TicketHeaderSkeleton"; 6 | import ArrowBackIos from "@material-ui/icons/ArrowBackIos"; 7 | import { useHistory } from "react-router-dom"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | ticketHeader: { 11 | display: "flex", 12 | backgroundColor: "#eee", 13 | flex: "none", 14 | borderBottom: "1px solid rgba(0, 0, 0, 0.12)", 15 | [theme.breakpoints.down("sm")]: { 16 | flexWrap: "wrap", 17 | }, 18 | }, 19 | })); 20 | 21 | const TicketHeader = ({ loading, children }) => { 22 | const classes = useStyles(); 23 | const history = useHistory(); 24 | const handleBack = () => { 25 | history.push("/tickets"); 26 | }; 27 | 28 | return ( 29 | <> 30 | {loading ? ( 31 | 32 | ) : ( 33 | 34 | 37 | {children} 38 | 39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default TicketHeader; 45 | -------------------------------------------------------------------------------- /frontend/src/components/TicketHeaderSkeleton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import { Avatar, Card, CardHeader } from "@material-ui/core"; 5 | import Skeleton from "@material-ui/lab/Skeleton"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | ticketHeader: { 9 | display: "flex", 10 | backgroundColor: "#eee", 11 | flex: "none", 12 | borderBottom: "1px solid rgba(0, 0, 0, 0.12)", 13 | }, 14 | })); 15 | 16 | const TicketHeaderSkeleton = () => { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 21 | 26 | 27 | 28 | } 29 | title={} 30 | subheader={} 31 | /> 32 | 33 | ); 34 | }; 35 | 36 | export default TicketHeaderSkeleton; 37 | -------------------------------------------------------------------------------- /frontend/src/components/TicketInfo/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Avatar, CardHeader } from "@material-ui/core"; 4 | 5 | import { i18n } from "../../translate/i18n"; 6 | 7 | const TicketInfo = ({ contact, ticket, onClick }) => { 8 | return ( 9 | } 15 | title={`${contact.name}`} 16 | subheader={ 17 | ticket.user && 18 | `${i18n.t("messagesList.header.assignedTo")} ${ticket.user.name}` 19 | } 20 | /> 21 | ); 22 | }; 23 | 24 | export default TicketInfo; 25 | -------------------------------------------------------------------------------- /frontend/src/components/TicketOptionsMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef, useState } from "react"; 2 | 3 | import MenuItem from "@material-ui/core/MenuItem"; 4 | import Menu from "@material-ui/core/Menu"; 5 | 6 | import { i18n } from "../../translate/i18n"; 7 | import api from "../../services/api"; 8 | import ConfirmationModal from "../ConfirmationModal"; 9 | import TransferTicketModal from "../TransferTicketModal"; 10 | import toastError from "../../errors/toastError"; 11 | import { Can } from "../Can"; 12 | import { AuthContext } from "../../context/Auth/AuthContext"; 13 | 14 | const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { 15 | const [confirmationOpen, setConfirmationOpen] = useState(false); 16 | const [transferTicketModalOpen, setTransferTicketModalOpen] = useState(false); 17 | const isMounted = useRef(true); 18 | const { user } = useContext(AuthContext); 19 | 20 | useEffect(() => { 21 | return () => { 22 | isMounted.current = false; 23 | }; 24 | }, []); 25 | 26 | const handleDeleteTicket = async () => { 27 | try { 28 | await api.delete(`/tickets/${ticket.id}`); 29 | } catch (err) { 30 | toastError(err); 31 | } 32 | }; 33 | 34 | const handleOpenConfirmationModal = e => { 35 | setConfirmationOpen(true); 36 | handleClose(); 37 | }; 38 | 39 | const handleOpenTransferModal = e => { 40 | setTransferTicketModalOpen(true); 41 | handleClose(); 42 | }; 43 | 44 | const handleCloseTransferTicketModal = () => { 45 | if (isMounted.current) { 46 | setTransferTicketModalOpen(false); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | 68 | 69 | {i18n.t("ticketOptionsMenu.transfer")} 70 | 71 | ( 75 | 76 | {i18n.t("ticketOptionsMenu.delete")} 77 | 78 | )} 79 | /> 80 | 81 | 91 | {i18n.t("ticketOptionsMenu.confirmationModal.message")} 92 | 93 | 98 | 99 | ); 100 | }; 101 | 102 | export default TicketOptionsMenu; 103 | -------------------------------------------------------------------------------- /frontend/src/components/TicketsListSkeleton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ListItem from "@material-ui/core/ListItem"; 4 | import ListItemText from "@material-ui/core/ListItemText"; 5 | import ListItemAvatar from "@material-ui/core/ListItemAvatar"; 6 | import Divider from "@material-ui/core/Divider"; 7 | import Skeleton from "@material-ui/lab/Skeleton"; 8 | 9 | const TicketsSkeleton = () => { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | } 18 | secondary={} 19 | /> 20 | 21 | 22 | 23 | 24 | 25 | 26 | } 28 | secondary={} 29 | /> 30 | 31 | 32 | 33 | 34 | 35 | 36 | } 38 | secondary={} 39 | /> 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default TicketsSkeleton; 47 | -------------------------------------------------------------------------------- /frontend/src/components/TicketsQueueSelect/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import MenuItem from "@material-ui/core/MenuItem"; 4 | import FormControl from "@material-ui/core/FormControl"; 5 | import Select from "@material-ui/core/Select"; 6 | import { Checkbox, ListItemText } from "@material-ui/core"; 7 | import { i18n } from "../../translate/i18n"; 8 | 9 | const TicketsQueueSelect = ({ 10 | userQueues, 11 | selectedQueueIds = [], 12 | onChange, 13 | }) => { 14 | const handleChange = e => { 15 | onChange(e.target.value); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default TicketsQueueSelect; 61 | -------------------------------------------------------------------------------- /frontend/src/components/Title/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | 4 | export default function Title(props) { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/VcardPreview/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { useHistory } from "react-router-dom"; 3 | import toastError from "../../errors/toastError"; 4 | import api from "../../services/api"; 5 | 6 | import Avatar from "@material-ui/core/Avatar"; 7 | import Typography from "@material-ui/core/Typography"; 8 | import Grid from "@material-ui/core/Grid"; 9 | 10 | import { AuthContext } from "../../context/Auth/AuthContext"; 11 | 12 | import { Button, Divider, } from "@material-ui/core"; 13 | 14 | const VcardPreview = ({ contact, numbers }) => { 15 | const history = useHistory(); 16 | const { user } = useContext(AuthContext); 17 | 18 | const [selectedContact, setContact] = useState({ 19 | name: "", 20 | number: 0, 21 | profilePicUrl: "" 22 | }); 23 | 24 | useEffect(() => { 25 | const delayDebounceFn = setTimeout(() => { 26 | const fetchContacts = async() => { 27 | try { 28 | let contactObj = { 29 | name: contact, 30 | number: numbers.replace(/\D/g, ""), 31 | email: "" 32 | } 33 | const { data } = await api.post("/contact", contactObj); 34 | setContact(data) 35 | 36 | } catch (err) { 37 | console.log(err) 38 | toastError(err); 39 | } 40 | }; 41 | fetchContacts(); 42 | }, 500); 43 | return () => clearTimeout(delayDebounceFn); 44 | }, [contact, numbers]); 45 | 46 | const handleNewChat = async() => { 47 | try { 48 | const { data: ticket } = await api.post("/tickets", { 49 | contactId: selectedContact.id, 50 | userId: user.id, 51 | status: "open", 52 | }); 53 | history.push(`/tickets/${ticket.id}`); 54 | } catch (err) { 55 | toastError(err); 56 | } 57 | } 58 | 59 | return ( 60 | <> 61 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | {selectedContact.name} 71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 |
84 | 85 | ); 86 | 87 | }; 88 | 89 | export default VcardPreview; -------------------------------------------------------------------------------- /frontend/src/context/Auth/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | import useAuth from "../../hooks/useAuth.js"; 4 | 5 | const AuthContext = createContext(); 6 | 7 | const AuthProvider = ({ children }) => { 8 | const { loading, user, isAuth, handleLogin, handleLogout } = useAuth(); 9 | 10 | return ( 11 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export { AuthContext, AuthProvider }; 20 | -------------------------------------------------------------------------------- /frontend/src/context/ReplyingMessage/ReplyingMessageContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from "react"; 2 | 3 | const ReplyMessageContext = createContext(); 4 | 5 | const ReplyMessageProvider = ({ children }) => { 6 | const [replyingMessage, setReplyingMessage] = useState(null); 7 | 8 | return ( 9 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { ReplyMessageContext, ReplyMessageProvider }; 18 | -------------------------------------------------------------------------------- /frontend/src/context/WhatsApp/WhatsAppsContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | import useWhatsApps from "../../hooks/useWhatsApps"; 4 | 5 | const WhatsAppsContext = createContext(); 6 | 7 | const WhatsAppsProvider = ({ children }) => { 8 | const { loading, whatsApps } = useWhatsApps(); 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { WhatsAppsContext, WhatsAppsProvider }; 18 | -------------------------------------------------------------------------------- /frontend/src/errors/toastError.js: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | import { i18n } from "../translate/i18n"; 3 | 4 | const toastError = err => { 5 | const errorMsg = err.response?.data?.message || err.response.data.error; 6 | if (errorMsg) { 7 | if (i18n.exists(`backendErrors.${errorMsg}`)) { 8 | toast.error(i18n.t(`backendErrors.${errorMsg}`), { 9 | toastId: errorMsg, 10 | }); 11 | } else { 12 | toast.error(errorMsg, { 13 | toastId: errorMsg, 14 | }); 15 | } 16 | } else { 17 | toast.error("500 Erro Interno no Servidor"); 18 | } 19 | }; 20 | 21 | export default toastError; 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLocalStorage/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import toastError from "../../errors/toastError"; 3 | 4 | export function useLocalStorage(key, initialValue) { 5 | const [storedValue, setStoredValue] = useState(() => { 6 | try { 7 | const item = localStorage.getItem(key); 8 | return item ? JSON.parse(item) : initialValue; 9 | } catch (error) { 10 | toastError(error); 11 | return initialValue; 12 | } 13 | }); 14 | 15 | const setValue = value => { 16 | try { 17 | const valueToStore = 18 | value instanceof Function ? value(storedValue) : value; 19 | 20 | setStoredValue(valueToStore); 21 | 22 | localStorage.setItem(key, JSON.stringify(valueToStore)); 23 | } catch (error) { 24 | toastError(error); 25 | } 26 | }; 27 | 28 | return [storedValue, setValue]; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/hooks/useQueues/index.js: -------------------------------------------------------------------------------- 1 | import api from "../../services/api"; 2 | 3 | const useQueues = () => { 4 | const findAll = async () => { 5 | const { data } = await api.get("/queue"); 6 | return data; 7 | } 8 | 9 | return { findAll }; 10 | }; 11 | 12 | export default useQueues; 13 | -------------------------------------------------------------------------------- /frontend/src/hooks/useWhatsApps/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useReducer } from "react"; 2 | import openSocket from "socket.io-client"; 3 | import toastError from "../../errors/toastError"; 4 | 5 | import api from "../../services/api"; 6 | 7 | const reducer = (state, action) => { 8 | if (action.type === "LOAD_WHATSAPPS") { 9 | const whatsApps = action.payload; 10 | 11 | return [...whatsApps]; 12 | } 13 | 14 | if (action.type === "UPDATE_WHATSAPPS") { 15 | const whatsApp = action.payload; 16 | const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id); 17 | 18 | if (whatsAppIndex !== -1) { 19 | state[whatsAppIndex] = whatsApp; 20 | return [...state]; 21 | } else { 22 | return [whatsApp, ...state]; 23 | } 24 | } 25 | 26 | if (action.type === "UPDATE_SESSION") { 27 | const whatsApp = action.payload; 28 | const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id); 29 | 30 | if (whatsAppIndex !== -1) { 31 | state[whatsAppIndex].status = whatsApp.status; 32 | state[whatsAppIndex].updatedAt = whatsApp.updatedAt; 33 | state[whatsAppIndex].qrcode = whatsApp.qrcode; 34 | state[whatsAppIndex].retries = whatsApp.retries; 35 | return [...state]; 36 | } else { 37 | return [...state]; 38 | } 39 | } 40 | 41 | if (action.type === "DELETE_WHATSAPPS") { 42 | const whatsAppId = action.payload; 43 | 44 | const whatsAppIndex = state.findIndex(s => s.id === whatsAppId); 45 | if (whatsAppIndex !== -1) { 46 | state.splice(whatsAppIndex, 1); 47 | } 48 | return [...state]; 49 | } 50 | 51 | if (action.type === "RESET") { 52 | return []; 53 | } 54 | }; 55 | 56 | const useWhatsApps = () => { 57 | const [whatsApps, dispatch] = useReducer(reducer, []); 58 | const [loading, setLoading] = useState(true); 59 | 60 | useEffect(() => { 61 | setLoading(true); 62 | const fetchSession = async () => { 63 | try { 64 | const { data } = await api.get("/whatsapp/"); 65 | dispatch({ type: "LOAD_WHATSAPPS", payload: data }); 66 | setLoading(false); 67 | } catch (err) { 68 | setLoading(false); 69 | toastError(err); 70 | } 71 | }; 72 | fetchSession(); 73 | }, []); 74 | 75 | useEffect(() => { 76 | const socket = openSocket(process.env.REACT_APP_BACKEND_URL); 77 | 78 | socket.on("whatsapp", data => { 79 | if (data.action === "update") { 80 | dispatch({ type: "UPDATE_WHATSAPPS", payload: data.whatsapp }); 81 | } 82 | }); 83 | 84 | socket.on("whatsapp", data => { 85 | if (data.action === "delete") { 86 | dispatch({ type: "DELETE_WHATSAPPS", payload: data.whatsappId }); 87 | } 88 | }); 89 | 90 | socket.on("whatsappSession", data => { 91 | if (data.action === "update") { 92 | dispatch({ type: "UPDATE_SESSION", payload: data.session }); 93 | } 94 | }); 95 | 96 | return () => { 97 | socket.disconnect(); 98 | }; 99 | }, []); 100 | 101 | return { whatsApps, loading }; 102 | }; 103 | 104 | export default useWhatsApps; 105 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import CssBaseline from "@material-ui/core/CssBaseline"; 4 | 5 | import App from "./App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | 14 | // ReactDOM.render( 15 | // 16 | // 17 | // 18 | // , 19 | // 20 | 21 | // document.getElementById("root") 22 | // ); 23 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/Chart.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { useTheme } from "@material-ui/core/styles"; 3 | import { 4 | BarChart, 5 | CartesianGrid, 6 | Bar, 7 | XAxis, 8 | YAxis, 9 | Label, 10 | ResponsiveContainer, 11 | } from "recharts"; 12 | import { startOfHour, parseISO, format } from "date-fns"; 13 | 14 | import { i18n } from "../../translate/i18n"; 15 | 16 | import Title from "./Title"; 17 | import useTickets from "../../hooks/useTickets"; 18 | 19 | const Chart = () => { 20 | const theme = useTheme(); 21 | 22 | const date = useRef(new Date().toISOString()); 23 | const { tickets } = useTickets({ date: date.current }); 24 | 25 | const [chartData, setChartData] = useState([ 26 | { time: "08:00", amount: 0 }, 27 | { time: "09:00", amount: 0 }, 28 | { time: "10:00", amount: 0 }, 29 | { time: "11:00", amount: 0 }, 30 | { time: "12:00", amount: 0 }, 31 | { time: "13:00", amount: 0 }, 32 | { time: "14:00", amount: 0 }, 33 | { time: "15:00", amount: 0 }, 34 | { time: "16:00", amount: 0 }, 35 | { time: "17:00", amount: 0 }, 36 | { time: "18:00", amount: 0 }, 37 | { time: "19:00", amount: 0 }, 38 | ]); 39 | 40 | useEffect(() => { 41 | setChartData(prevState => { 42 | let aux = [...prevState]; 43 | 44 | aux.forEach(a => { 45 | tickets.forEach(ticket => { 46 | format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === a.time && 47 | a.amount++; 48 | }); 49 | }); 50 | 51 | return aux; 52 | }); 53 | }, [tickets]); 54 | 55 | return ( 56 | 57 | {`${i18n.t("dashboard.charts.perDay.title")}${ 58 | tickets.length 59 | }`} 60 | 61 | 73 | 74 | 75 | 80 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default Chart; 96 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/Title.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | 4 | const Title = props => { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ); 10 | }; 11 | 12 | export default Title; 13 | -------------------------------------------------------------------------------- /frontend/src/routes/Route.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Route as RouterRoute, Redirect } from "react-router-dom"; 3 | 4 | import { AuthContext } from "../context/Auth/AuthContext"; 5 | import BackdropLoading from "../components/BackdropLoading"; 6 | 7 | const Route = ({ component: Component, isPrivate = false, ...rest }) => { 8 | const { isAuth, loading } = useContext(AuthContext); 9 | 10 | if (!isAuth && isPrivate) { 11 | return ( 12 | <> 13 | {loading && } 14 | 15 | 16 | ); 17 | } 18 | 19 | if (isAuth && !isPrivate) { 20 | return ( 21 | <> 22 | {loading && } 23 | ; 24 | 25 | ); 26 | } 27 | 28 | return ( 29 | <> 30 | {loading && } 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Route; 37 | -------------------------------------------------------------------------------- /frontend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Switch } from "react-router-dom"; 3 | import { ToastContainer } from "react-toastify"; 4 | 5 | import LoggedInLayout from "../layout"; 6 | import Dashboard from "../pages/Dashboard/"; 7 | import Tickets from "../pages/Tickets/"; 8 | import Signup from "../pages/Signup/"; 9 | import Login from "../pages/Login/"; 10 | import Connections from "../pages/Connections/"; 11 | import Settings from "../pages/Settings/"; 12 | import Users from "../pages/Users"; 13 | import Contacts from "../pages/Contacts/"; 14 | import QuickAnswers from "../pages/QuickAnswers/"; 15 | import Queues from "../pages/Queues/"; 16 | import { AuthProvider } from "../context/Auth/AuthContext"; 17 | import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; 18 | import Route from "./Route"; 19 | 20 | const Routes = () => { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default Routes; 62 | -------------------------------------------------------------------------------- /frontend/src/rules.js: -------------------------------------------------------------------------------- 1 | const rules = { 2 | user: { 3 | static: [], 4 | }, 5 | 6 | admin: { 7 | static: [ 8 | "drawer-admin-items:view", 9 | "tickets-manager:showall", 10 | "user-modal:editProfile", 11 | "user-modal:editQueues", 12 | "ticket-options:deleteTicket", 13 | "contacts-page:deleteContact", 14 | ], 15 | }, 16 | }; 17 | 18 | export default rules; 19 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api = axios.create({ 4 | baseURL: process.env.REACT_APP_BACKEND_URL, 5 | withCredentials: true, 6 | }); 7 | 8 | export default api; 9 | -------------------------------------------------------------------------------- /frontend/src/translate/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import LanguageDetector from "i18next-browser-languagedetector"; 3 | 4 | import { messages } from "./languages"; 5 | 6 | i18n.use(LanguageDetector).init({ 7 | debug: false, 8 | defaultNS: ["translations"], 9 | fallbackLng: "en", 10 | ns: ["translations"], 11 | resources: messages, 12 | }); 13 | 14 | export { i18n }; 15 | -------------------------------------------------------------------------------- /frontend/src/translate/languages/index.js: -------------------------------------------------------------------------------- 1 | import { messages as portugueseMessages } from "./pt"; 2 | import { messages as englishMessages } from "./en"; 3 | import { messages as spanishMessages } from "./es"; 4 | 5 | const messages = { 6 | ...portugueseMessages, 7 | ...englishMessages, 8 | ...spanishMessages, 9 | }; 10 | 11 | export { messages }; 12 | --------------------------------------------------------------------------------