├── .env.example ├── .gitattributes ├── .github └── stale.yml ├── LICENSE ├── README.md ├── backend ├── .editorconfig ├── .env.example ├── .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 │ │ ├── DialogflowController.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 │ │ │ ├── 20220218095931-create-dialogflow.ts │ │ │ ├── 20220218095932-add-dialogflow-to-queues.ts │ │ │ ├── 20220218095933-add-use-dialogflow-to-contacts.ts │ │ │ ├── 20220218095934-add-use-queues-to-contacts.ts │ │ │ ├── 20220223095932-add-whatsapp-to-user.ts │ │ │ ├── 20221123155118-add-acceptAudioMessages-to-contact.ts │ │ │ ├── 20221204153008-add-feedback-message-to-Whatsapps.ts │ │ │ ├── 20230304020754-add_fromMe_to_PK.ts │ │ │ ├── 20230615190440-add-menuname-to-Queues.ts │ │ │ ├── 20240529231622-add-number-and-requestCode-to-whatsapps.ts │ │ │ └── 20240530002923-add-pairingCode-to-whatsapp.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 │ │ ├── Dialogflow.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 │ │ ├── dialogflowRoutes.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 │ │ │ ├── ToggleAcceptAudioContactService.ts │ │ │ ├── ToggleUseDialogflowContactService.ts │ │ │ ├── ToggleUseQueuesContactService.ts │ │ │ └── UpdateContactService.ts │ │ ├── DialogflowServices │ │ │ ├── CreateDialogflowService.ts │ │ │ ├── CreateSessionDialogflow.ts │ │ │ ├── DeleteDialogflowService.ts │ │ │ ├── ListDialogflowService.ts │ │ │ ├── QueryDialogflow.ts │ │ │ ├── ShowDialogflowService.ts │ │ │ ├── TestSessionDialogflowService.ts │ │ │ └── UpdateDialogflowService.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.example ├── .gitignore ├── package.json ├── public ├── android-chrome-192x192.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json └── mstile-150x150.png ├── server.js └── src ├── App.js ├── assets ├── login-logo.png ├── logo.png ├── sound.mp3 ├── sound.ogg └── wa-background.png ├── components ├── Audio │ └── index.jsx ├── BackdropLoading │ └── index.js ├── ButtonWithSpinner │ └── index.js ├── Can │ └── index.js ├── ColorPicker │ └── index.js ├── ConfirmationModal │ └── index.js ├── ContactDrawer │ └── index.js ├── ContactDrawerSkeleton │ └── index.js ├── ContactModal │ └── index.js ├── DialogflowModal │ └── 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 ├── config.js ├── context ├── Auth │ └── AuthContext.js ├── ReplyingMessage │ └── ReplyingMessageContext.js └── WhatsApp │ └── WhatsAppsContext.js ├── errors └── toastError.js ├── hooks ├── useAuth.js │ └── index.js ├── useDialogflows │ └── 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 ├── Dialogflow │ └── index.js ├── Login │ └── index.js ├── Queues │ ├── index.js │ ├── useLoadData.js │ └── useSocket.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 └── socket-io.js └── translate ├── i18n.js └── languages ├── en.js ├── es.js ├── index.js └── pt.js /.env.example: -------------------------------------------------------------------------------- 1 | # MYSQL 2 | MYSQL_ENGINE= 3 | MYSQL_VERSION= 4 | MYSQL_ROOT_PASSWORD= 5 | MYSQL_DATABASE=whaticket 6 | MYSQL_PORT= 7 | TZ= 8 | 9 | # BACKEND 10 | BACKEND_PORT= 11 | BACKEND_SERVER_NAME=api.mydomain.com 12 | BACKEND_URL=https://api.mydomain.com 13 | PROXY_PORT=443 14 | JWT_SECRET= 15 | JWT_REFRESH_SECRET= 16 | 17 | # FRONTEND 18 | FRONTEND_PORT=80 19 | FRONTEND_SSL_PORT=443 20 | FRONTEND_SERVER_NAME=myapp.mydomain.com 21 | FRONTEND_URL=https://myapp.mydomain.com 22 | 23 | # BROWSERLESS 24 | MAX_CONCURRENT_SESSIONS= 25 | 26 | # PHPMYADMIN 27 | PMA_PORT= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | Dockerfile eol=lf 4 | docker-compose.yml eol=lf 5 | docker-compose.*.yml eol=lf 6 | *.jpg binary 7 | *.png binary 8 | *.gif binary 9 | *.woff binary 10 | *.tff binary 11 | *.eot binary 12 | *.otf binary -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 10 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 10 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 dollyzn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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.example: -------------------------------------------------------------------------------- 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= 8 | DB_HOST= 9 | DB_USER= 10 | DB_PASS= 11 | DB_NAME= 12 | 13 | JWT_SECRET= 14 | JWT_REFRESH_SECRET= 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 | 18 | .wwebjs_auth/ 19 | .wwebjs_cache/ -------------------------------------------------------------------------------- /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/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 | port: process.env.DB_PORT, 12 | database: process.env.DB_NAME, 13 | username: process.env.DB_USER, 14 | password: process.env.DB_PASS, 15 | logging: false 16 | }; 17 | -------------------------------------------------------------------------------- /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 | var arquivo = file.originalname; 12 | const fileName = arquivo.substring(0, arquivo.lastIndexOf(".")) + path.extname(file.originalname); 13 | 14 | return cb(null, fileName); 15 | } 16 | }) 17 | }; 18 | -------------------------------------------------------------------------------- /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/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, menuname, color, greetingMessage } = req.body; 17 | 18 | const queue = await CreateQueueService({ 19 | name, 20 | menuname, 21 | color, 22 | greetingMessage 23 | }); 24 | 25 | const io = getIO(); 26 | io.emit("queue", { 27 | action: "update", 28 | queue 29 | }); 30 | 31 | return res.status(200).json(queue); 32 | }; 33 | 34 | export const show = async (req: Request, res: Response): Promise => { 35 | const { queueId } = req.params; 36 | 37 | const queue = await ShowQueueService(queueId); 38 | 39 | return res.status(200).json(queue); 40 | }; 41 | 42 | export const update = async ( 43 | req: Request, 44 | res: Response 45 | ): Promise => { 46 | const { queueId } = req.params; 47 | 48 | const queue = await UpdateQueueService(queueId, req.body); 49 | 50 | const io = getIO(); 51 | io.emit("queue", { 52 | action: "update", 53 | queue 54 | }); 55 | 56 | return res.status(201).json(queue); 57 | }; 58 | 59 | export const remove = async ( 60 | req: Request, 61 | res: Response 62 | ): Promise => { 63 | const { queueId } = req.params; 64 | 65 | await DeleteQueueService(queueId); 66 | 67 | const io = getIO(); 68 | io.emit("queue", { 69 | action: "delete", 70 | queueId: +queueId 71 | }); 72 | 73 | return res.status(200).send(); 74 | }; 75 | -------------------------------------------------------------------------------- /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 { removeWbot } 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 | import { getIO } from "../libs/socket"; 7 | 8 | const store = async (req: Request, res: Response): Promise => { 9 | const { whatsappId } = req.params; 10 | const whatsapp = await ShowWhatsAppService(whatsappId); 11 | 12 | StartWhatsAppSession(whatsapp); 13 | 14 | return res.status(200).json({ message: "Starting session." }); 15 | }; 16 | 17 | const update = async (req: Request, res: Response): Promise => { 18 | const { whatsappId } = req.params; 19 | 20 | const { whatsapp } = await UpdateWhatsAppService({ 21 | whatsappId, 22 | whatsappData: { session: "" } 23 | }); 24 | 25 | StartWhatsAppSession(whatsapp); 26 | 27 | return res.status(200).json({ message: "Starting session." }); 28 | }; 29 | 30 | const remove = async (req: Request, res: Response): Promise => { 31 | const { whatsappId } = req.params; 32 | const whatsapp = await ShowWhatsAppService(whatsappId); 33 | 34 | await whatsapp.update({ 35 | status: "OPENING", 36 | qrcode: "", 37 | retries: 0 38 | }); 39 | 40 | const io = getIO(); 41 | io.emit("whatsappSession", { 42 | action: "update", 43 | session: whatsapp 44 | }); 45 | 46 | await removeWbot(whatsapp.id); 47 | 48 | return res.status(200).json({ message: "Session disconnected." }); 49 | }; 50 | 51 | export default { store, remove, update }; 52 | -------------------------------------------------------------------------------- /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 | import Dialogflow from "../models/Dialogflow"; 14 | 15 | // eslint-disable-next-line 16 | const dbConfig = require("../config/database"); 17 | // import dbConfig from "../config/database"; 18 | 19 | const sequelize = new Sequelize(dbConfig); 20 | 21 | const models = [ 22 | User, 23 | Contact, 24 | Ticket, 25 | Message, 26 | Whatsapp, 27 | ContactCustomField, 28 | Setting, 29 | Dialogflow, 30 | Queue, 31 | WhatsappQueue, 32 | UserQueue, 33 | QuickAnswer 34 | ]; 35 | 36 | sequelize.addModels(models); 37 | 38 | export default sequelize; 39 | -------------------------------------------------------------------------------- /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 | openingHours: { 36 | type: DataTypes.TIME 37 | }, 38 | closingHours: { 39 | type: DataTypes.TIME 40 | }, 41 | outServiceMessage: { 42 | type: DataTypes.TEXT 43 | }, 44 | useoutServiceMessage: { 45 | type: DataTypes.BOOLEAN, 46 | allowNull: false, 47 | defaultValue: false 48 | }, 49 | }); 50 | }, 51 | 52 | down: (queryInterface: QueryInterface) => { 53 | return queryInterface.dropTable("Whatsapps"); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /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/20220218095931-create-dialogflow.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Dialogflows", { 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 | projectName: { 18 | type: DataTypes.STRING, 19 | allowNull: false, 20 | unique: true 21 | }, 22 | jsonContent: { 23 | type: DataTypes.TEXT, 24 | allowNull: false, 25 | }, 26 | language: { 27 | type: DataTypes.STRING, 28 | allowNull: false, 29 | }, 30 | createdAt: { 31 | type: DataTypes.DATE, 32 | allowNull: false 33 | }, 34 | updatedAt: { 35 | type: DataTypes.DATE, 36 | allowNull: false 37 | } 38 | }); 39 | }, 40 | 41 | down: (queryInterface: QueryInterface) => { 42 | return queryInterface.dropTable("Dialogflows"); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Queues", "dialogflowId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Dialogflows", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Queues", "dialogflowId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220218095933-add-use-dialogflow-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "useDialogflow", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: true 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "useDialogflow"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220218095934-add-use-queues-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "useQueues", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: true 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "useQueues"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /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/migrations/20221123155118-add-acceptAudioMessages-to-contact.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "acceptAudioMessage", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: true 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "acceptAudioMessage"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20221204153008-add-feedback-message-to-Whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "feedbackMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "feedbackMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230304020754-add_fromMe_to_PK.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.sequelize.query( 6 | "ALTER TABLE Messages DROP PRIMARY KEY, ADD CONSTRAINT Messages_PK PRIMARY KEY (id, fromMe)" 7 | ); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.sequelize.query( 12 | "ALTER TABLE Messages DROP PRIMARY KEY, ADD CONSTRAINT Messages_PK PRIMARY KEY (id)" 13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230615190440-add-menuname-to-Queues.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Queues", "menuname", { 6 | type: DataTypes.STRING, 7 | allowNull: false 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Queues", "menuname"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240529231622-add-number-and-requestCode-to-whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addColumn("Whatsapps", "requestCode", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | await queryInterface.addColumn("Whatsapps", "number", { 11 | type: DataTypes.STRING, 12 | defaultValue: "" 13 | }); 14 | }, 15 | 16 | down: async (queryInterface: QueryInterface) => { 17 | await queryInterface.removeColumn("Whatsapps", "requestCode"); 18 | await queryInterface.removeColumn("Whatsapps", "number"); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240530002923-add-pairingCode-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addColumn("Whatsapps", "pairingCode", { 6 | type: DataTypes.STRING 7 | }); 8 | }, 9 | 10 | down: async (queryInterface: QueryInterface) => { 11 | await queryInterface.removeColumn("Whatsapps", "pairingCode"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /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: "disabled", 17 | createdAt: new Date(), 18 | updatedAt: new Date() 19 | }, 20 | { 21 | key: "transferTicket", 22 | value: "disabled", 23 | createdAt: new Date(), 24 | updatedAt: new Date() 25 | }, 26 | ], 27 | {} 28 | ); 29 | }, 30 | 31 | down: (queryInterface: QueryInterface) => { 32 | return queryInterface.bulkDelete("Settings", {}); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /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 | }; 26 | -------------------------------------------------------------------------------- /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(`A client joined to ${status} tickets channel.`); 29 | socket.join(status); 30 | }); 31 | 32 | socket.on("disconnect", () => { 33 | logger.info("Client disconnected"); 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 | @Default(true) 45 | @Column 46 | useQueues: boolean; 47 | 48 | @Default(true) 49 | @Column 50 | acceptAudioMessage: boolean; 51 | 52 | @Default(true) 53 | @Column 54 | useDialogflow: boolean; 55 | 56 | @CreatedAt 57 | createdAt: Date; 58 | 59 | @UpdatedAt 60 | updatedAt: Date; 61 | 62 | @HasMany(() => Ticket) 63 | tickets: Ticket[]; 64 | 65 | @HasMany(() => ContactCustomField) 66 | extraInfo: ContactCustomField[]; 67 | } 68 | 69 | export default Contact; 70 | -------------------------------------------------------------------------------- /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/Dialogflow.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | DataType, 8 | PrimaryKey, 9 | HasMany, 10 | AutoIncrement 11 | } from "sequelize-typescript"; 12 | import Queue from "./Queue"; 13 | 14 | @Table 15 | class Dialogflow extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @Column(DataType.TEXT) 22 | name: string; 23 | 24 | @Column(DataType.TEXT) 25 | projectName: string; 26 | 27 | @Column(DataType.TEXT) 28 | jsonContent: string; 29 | 30 | @Column(DataType.TEXT) 31 | language: string; 32 | 33 | @CreatedAt 34 | @Column(DataType.DATE(6)) 35 | createdAt: Date; 36 | 37 | @UpdatedAt 38 | @Column(DataType.DATE(6)) 39 | updatedAt: Date; 40 | 41 | @HasMany(() => Queue) 42 | queues: Queue[] 43 | } 44 | 45 | export default Dialogflow; 46 | -------------------------------------------------------------------------------- /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 | @PrimaryKey 31 | @Default(false) 32 | @Column 33 | fromMe: boolean; 34 | 35 | @Column(DataType.TEXT) 36 | body: string; 37 | 38 | @Column(DataType.STRING) 39 | get mediaUrl(): string | null { 40 | if (this.getDataValue("mediaUrl")) { 41 | return `${process.env.BACKEND_URL}:${ 42 | process.env.PROXY_PORT 43 | }/public/${this.getDataValue("mediaUrl")}`; 44 | } 45 | return null; 46 | } 47 | 48 | @Column 49 | mediaType: string; 50 | 51 | @Default(false) 52 | @Column 53 | isDeleted: boolean; 54 | 55 | @CreatedAt 56 | @Column(DataType.DATE(6)) 57 | createdAt: Date; 58 | 59 | @UpdatedAt 60 | @Column(DataType.DATE(6)) 61 | updatedAt: Date; 62 | 63 | @ForeignKey(() => Message) 64 | @Column 65 | quotedMsgId: string; 66 | 67 | @BelongsTo(() => Message, "quotedMsgId") 68 | quotedMsg: Message; 69 | 70 | @ForeignKey(() => Ticket) 71 | @Column 72 | ticketId: number; 73 | 74 | @BelongsTo(() => Ticket) 75 | ticket: Ticket; 76 | 77 | @ForeignKey(() => Contact) 78 | @Column 79 | contactId: number; 80 | 81 | @BelongsTo(() => Contact, "contactId") 82 | contact: Contact; 83 | } 84 | 85 | export default Message; 86 | -------------------------------------------------------------------------------- /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 | BelongsTo, 13 | ForeignKey 14 | } from "sequelize-typescript"; 15 | import User from "./User"; 16 | import UserQueue from "./UserQueue"; 17 | 18 | import Whatsapp from "./Whatsapp"; 19 | import WhatsappQueue from "./WhatsappQueue"; 20 | import Dialogflow from "./Dialogflow"; 21 | 22 | @Table 23 | class Queue extends Model { 24 | @PrimaryKey 25 | @AutoIncrement 26 | @Column 27 | id: number; 28 | 29 | @AllowNull(false) 30 | @Unique 31 | @Column 32 | name: string; 33 | 34 | @AllowNull(false) 35 | @Column 36 | menuname: string; 37 | 38 | @AllowNull(false) 39 | @Unique 40 | @Column 41 | color: string; 42 | 43 | @Column 44 | greetingMessage: string; 45 | 46 | @ForeignKey(() => Dialogflow) 47 | @Column 48 | dialogflowId: number; 49 | 50 | @BelongsTo(() => Dialogflow) 51 | dialogflow: Dialogflow; 52 | 53 | @CreatedAt 54 | createdAt: Date; 55 | 56 | @UpdatedAt 57 | updatedAt: Date; 58 | 59 | @BelongsToMany(() => Whatsapp, () => WhatsappQueue) 60 | whatsapps: Array; 61 | 62 | @BelongsToMany(() => User, () => UserQueue) 63 | users: Array; 64 | } 65 | 66 | export default Queue; 67 | -------------------------------------------------------------------------------- /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 | number: string; 34 | 35 | @Column(DataType.TEXT) 36 | session: string; 37 | 38 | @Column(DataType.TEXT) 39 | qrcode: string; 40 | 41 | @Column(DataType.STRING) 42 | pairingCode: string; 43 | 44 | @Column 45 | status: string; 46 | 47 | @Column 48 | battery: string; 49 | 50 | @Column 51 | plugged: boolean; 52 | 53 | @Column 54 | retries: number; 55 | 56 | @Column(DataType.TEXT) 57 | greetingMessage: string; 58 | 59 | @Column(DataType.TEXT) 60 | farewellMessage: string; 61 | 62 | @Column(DataType.TEXT) 63 | outServiceMessage: string; 64 | 65 | @Column(DataType.TEXT) 66 | feedbackMessage: string; 67 | 68 | @Column(DataType.TIME) 69 | openingHours: string; 70 | 71 | @Column(DataType.TIME) 72 | closingHours: string; 73 | 74 | @Default(false) 75 | @AllowNull 76 | @Column 77 | isDefault: boolean; 78 | 79 | @Default(false) 80 | @Column(DataType.BOOLEAN) 81 | requestCode: boolean; 82 | 83 | @Default(false) 84 | @AllowNull 85 | @Column 86 | useoutServiceMessage: boolean; 87 | 88 | @CreatedAt 89 | createdAt: Date; 90 | 91 | @UpdatedAt 92 | updatedAt: Date; 93 | 94 | @HasMany(() => Ticket) 95 | tickets: Ticket[]; 96 | 97 | @BelongsToMany(() => Queue, () => WhatsappQueue) 98 | queues: Array; 99 | 100 | @HasMany(() => WhatsappQueue) 101 | whatsappQueues: WhatsappQueue[]; 102 | } 103 | 104 | export default Whatsapp; 105 | -------------------------------------------------------------------------------- /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.put("/contacts/toggleUseQueues/:contactId", isAuth, ContactController.toggleUseQueue); 26 | 27 | contactRoutes.put("/contacts/toggleAcceptAudio/:contactId", isAuth, ContactController.toggleAcceptAudio); 28 | 29 | contactRoutes.put("/contacts/toggleUseDialogflow/:contactId", isAuth, ContactController.toggleUseDialogflow); 30 | 31 | contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); 32 | 33 | export default contactRoutes; 34 | -------------------------------------------------------------------------------- /backend/src/routes/dialogflowRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as DialogflowController from "../controllers/DialogflowController"; 5 | 6 | const dialogflowRoutes = Router(); 7 | 8 | dialogflowRoutes.get("/dialogflow", isAuth, DialogflowController.index); 9 | 10 | dialogflowRoutes.post("/dialogflow", isAuth, DialogflowController.store); 11 | 12 | dialogflowRoutes.get("/dialogflow/:dialogflowId", isAuth, DialogflowController.show); 13 | 14 | dialogflowRoutes.put("/dialogflow/:dialogflowId", isAuth, DialogflowController.update); 15 | 16 | dialogflowRoutes.delete("/dialogflow/:dialogflowId", isAuth, DialogflowController.remove); 17 | 18 | dialogflowRoutes.post("/dialogflow/testsession", isAuth, DialogflowController.testSession); 19 | 20 | export default dialogflowRoutes; 21 | -------------------------------------------------------------------------------- /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 | import dialogflowRoutes from "./dialogflowRoutes"; 15 | 16 | const routes = Router(); 17 | 18 | routes.use(userRoutes); 19 | routes.use("/auth", authRoutes); 20 | routes.use(settingRoutes); 21 | routes.use(contactRoutes); 22 | routes.use(ticketRoutes); 23 | routes.use(whatsappRoutes); 24 | routes.use(messageRoutes); 25 | routes.use(whatsappSessionRoutes); 26 | routes.use(queueRoutes); 27 | routes.use(quickAnswerRoutes); 28 | routes.use(dialogflowRoutes); 29 | routes.use("/api/messages", apiRoutes); 30 | 31 | export default routes; 32 | -------------------------------------------------------------------------------- /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 | acceptAudioMessage?: boolean; 14 | useDialogflow?: boolean; 15 | profilePicUrl?: string; 16 | extraInfo?: ExtraInfo[]; 17 | } 18 | 19 | const CreateContactService = async ({ 20 | name, 21 | number, 22 | email = "", 23 | acceptAudioMessage, 24 | useDialogflow, 25 | extraInfo = [] 26 | }: Request): Promise => { 27 | const numberExists = await Contact.findOne({ 28 | where: { number } 29 | }); 30 | 31 | if (numberExists) { 32 | throw new AppError("ERR_DUPLICATED_CONTACT"); 33 | } 34 | 35 | const contact = await Contact.create( 36 | { 37 | name, 38 | number, 39 | email, 40 | acceptAudioMessage, 41 | useDialogflow, 42 | extraInfo 43 | }, 44 | { 45 | include: ["extraInfo"] 46 | } 47 | ); 48 | 49 | return contact; 50 | }; 51 | 52 | export default CreateContactService; 53 | -------------------------------------------------------------------------------- /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/ToggleAcceptAudioContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface Request { 5 | contactId: string; 6 | } 7 | 8 | const ToggleUseQueuesContactService = async ({ 9 | contactId 10 | }: Request): Promise => { 11 | const contact = await Contact.findOne({ 12 | where: { id: contactId }, 13 | attributes: ["id", "acceptAudioMessage"] 14 | }); 15 | 16 | if (!contact) { 17 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 18 | } 19 | 20 | const acceptAudioMessage = contact.acceptAudioMessage ? false : true; 21 | 22 | await contact.update({ 23 | acceptAudioMessage 24 | }); 25 | 26 | await contact.reload({ 27 | attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "acceptAudioMessage", "useDialogflow"], 28 | include: ["extraInfo"] 29 | }); 30 | 31 | return contact; 32 | }; 33 | 34 | export default ToggleUseQueuesContactService; 35 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface Dialogflow { 5 | useDialogflow?: boolean; 6 | useDialogflowToggle?: boolean; 7 | } 8 | 9 | interface Request { 10 | contactId: string; 11 | setUseDialogFlow: Dialogflow; 12 | } 13 | 14 | const ToggleUseDialogflowContactService = async ({ 15 | contactId, 16 | setUseDialogFlow 17 | }: Request): Promise => { 18 | let { useDialogflow, useDialogflowToggle } = setUseDialogFlow; 19 | const contact = await Contact.findOne({ 20 | where: { id: contactId }, 21 | attributes: ["id", "useDialogflow"] 22 | }); 23 | 24 | if (useDialogflowToggle) { 25 | useDialogflow = contact?.useDialogflow ? false : true; 26 | } 27 | 28 | if (!contact) { 29 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 30 | } 31 | 32 | await contact.update({ 33 | useDialogflow 34 | }); 35 | 36 | await contact.reload({ 37 | attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "acceptAudioMessage", "useDialogflow"], 38 | include: ["extraInfo"] 39 | }); 40 | 41 | return contact; 42 | }; 43 | 44 | export default ToggleUseDialogflowContactService; 45 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/ToggleUseQueuesContactService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Contact from "../../models/Contact"; 3 | 4 | interface Request { 5 | contactId: string; 6 | } 7 | 8 | const ToggleUseQueuesContactService = async ({ 9 | contactId 10 | }: Request): Promise => { 11 | const contact = await Contact.findOne({ 12 | where: { id: contactId }, 13 | attributes: ["id", "useQueues"] 14 | }); 15 | 16 | if (!contact) { 17 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 18 | } 19 | 20 | const useQueues = contact.useQueues ? false : true; 21 | 22 | await contact.update({ 23 | useQueues 24 | }); 25 | 26 | await contact.reload({ 27 | attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "acceptAudioMessage", "useDialogflow"], 28 | include: ["extraInfo"] 29 | }); 30 | 31 | return contact; 32 | }; 33 | 34 | export default ToggleUseQueuesContactService; 35 | -------------------------------------------------------------------------------- /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 | acceptAudioMessage?: boolean; 15 | useDialogflow?: boolean; 16 | extraInfo?: ExtraInfo[]; 17 | } 18 | 19 | interface Request { 20 | contactData: ContactData; 21 | contactId: string; 22 | } 23 | 24 | const UpdateContactService = async ({ 25 | contactData, 26 | contactId 27 | }: Request): Promise => { 28 | const { email, name, number, extraInfo, acceptAudioMessage, useDialogflow } = contactData; 29 | 30 | const contact = await Contact.findOne({ 31 | where: { id: contactId }, 32 | attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "acceptAudioMessage", "useDialogflow"], 33 | include: ["extraInfo"] 34 | }); 35 | 36 | if (!contact) { 37 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 38 | } 39 | 40 | if (extraInfo) { 41 | await Promise.all( 42 | extraInfo.map(async info => { 43 | await ContactCustomField.upsert({ ...info, contactId: contact.id }); 44 | }) 45 | ); 46 | 47 | await Promise.all( 48 | contact.extraInfo.map(async oldInfo => { 49 | const stillExists = extraInfo.findIndex(info => info.id === oldInfo.id); 50 | 51 | if (stillExists === -1) { 52 | await ContactCustomField.destroy({ where: { id: oldInfo.id } }); 53 | } 54 | }) 55 | ); 56 | } 57 | 58 | await contact.update({ 59 | name, 60 | number, 61 | email, 62 | acceptAudioMessage, 63 | useDialogflow 64 | }); 65 | 66 | await contact.reload({ 67 | attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "acceptAudioMessage", "useDialogflow"], 68 | include: ["extraInfo"] 69 | }); 70 | 71 | return contact; 72 | }; 73 | 74 | export default UpdateContactService; 75 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/CreateDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | import Dialogflow from "../../models/Dialogflow"; 5 | 6 | 7 | interface Request { 8 | name: string; 9 | projectName: string; 10 | jsonContent: string; 11 | language: string; 12 | } 13 | 14 | const CreateDialogflowService = async ({ 15 | name, 16 | projectName, 17 | jsonContent, 18 | language 19 | }: Request): Promise => { 20 | const schema = Yup.object().shape({ 21 | name: Yup.string() 22 | .required() 23 | .min(2) 24 | .test( 25 | "Check-name", 26 | "This DialogFlow name is already used.", 27 | async value => { 28 | if (!value) return false; 29 | const nameExists = await Dialogflow.findOne({ 30 | where: { name: value } 31 | }); 32 | return !nameExists; 33 | } 34 | ), 35 | projectName: Yup.string() 36 | .required() 37 | .min(2) 38 | .test( 39 | "Check-name", 40 | "This DialogFlow projectName is already used.", 41 | async value => { 42 | if (!value) return false; 43 | const nameExists = await Dialogflow.findOne({ 44 | where: { projectName: value } 45 | }); 46 | return !nameExists; 47 | } 48 | ), 49 | jsonContent: Yup.string() 50 | .required() 51 | , 52 | language: Yup.string() 53 | .required() 54 | .min(2) 55 | }); 56 | 57 | try { 58 | await schema.validate({ name, projectName, jsonContent, language }); 59 | } catch (err) { 60 | throw new AppError(err.message); 61 | } 62 | 63 | 64 | const dialogflow = await Dialogflow.create( 65 | { 66 | name, 67 | projectName, 68 | jsonContent, 69 | language 70 | } 71 | ); 72 | 73 | return dialogflow; 74 | }; 75 | 76 | export default CreateDialogflowService; 77 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/CreateSessionDialogflow.ts: -------------------------------------------------------------------------------- 1 | import { SessionsClient } from "@google-cloud/dialogflow"; 2 | import Dialogflow from "../../models/Dialogflow"; 3 | import dir from 'path'; 4 | import fs from 'fs'; 5 | import os from 'os'; 6 | import { logger } from "../../utils/logger"; 7 | 8 | const sessions : Map = new Map(); 9 | 10 | const createDialogflowSession = async (id:number, projectName:string, jsonContent:string) : Promise => { 11 | if(sessions.has(id)) { 12 | return sessions.get(id); 13 | } 14 | 15 | const keyFilename = dir.join(os.tmpdir(), `whaticket_${id}.json`); 16 | 17 | logger.info(`Openig new dialogflow session #${projectName} in '${keyFilename}'`) 18 | 19 | await fs.writeFileSync(keyFilename, jsonContent); 20 | const session = new SessionsClient({ keyFilename }); 21 | 22 | sessions.set(id, session); 23 | 24 | return session; 25 | } 26 | 27 | const createDialogflowSessionWithModel = async (model: Dialogflow) : Promise => { 28 | return createDialogflowSession(model.id, model.projectName, model.jsonContent); 29 | } 30 | 31 | export { createDialogflowSession, createDialogflowSessionWithModel }; -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/DeleteDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import Dialogflow from "../../models/Dialogflow"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteDialogflowService = async (id: string): Promise => { 5 | const dialogflow = await Dialogflow.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!dialogflow) { 10 | throw new AppError("ERR_NO_DIALOG_FOUND", 404); 11 | } 12 | 13 | await dialogflow.destroy(); 14 | }; 15 | 16 | export default DeleteDialogflowService; 17 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/ListDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import DialogFLow from "../../models/Dialogflow"; 2 | 3 | const ListDialogflowService = async (): Promise => { 4 | const dialogFLows = await DialogFLow.findAll(); 5 | 6 | return dialogFLows; 7 | }; 8 | 9 | export default ListDialogflowService; 10 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/ShowDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import Dialogflow from "../../models/Dialogflow"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | 5 | const ShowDialogflowService = async (id: string | number): Promise => { 6 | const dialogflow = await Dialogflow.findByPk(id); 7 | 8 | if (!dialogflow) { 9 | throw new AppError("ERR_NO_DIALOG_FOUND", 404); 10 | } 11 | 12 | return dialogflow; 13 | }; 14 | 15 | export default ShowDialogflowService; 16 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/TestSessionDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | 5 | import { queryDialogFlow } from "./QueryDialogflow"; 6 | import { createDialogflowSession } from "./CreateSessionDialogflow"; 7 | 8 | interface Request { 9 | projectName: string; 10 | jsonContent: string; 11 | language: string; 12 | } 13 | 14 | interface Response { 15 | messages: string[]; 16 | } 17 | 18 | const TestDialogflowSession = async ({ 19 | projectName, 20 | jsonContent, 21 | language 22 | }: Request): Promise => { 23 | const schema = Yup.object().shape({ 24 | projectName: Yup.string().required().min(2), 25 | jsonContent: Yup.string().required(), 26 | language: Yup.string().required().min(2) 27 | }); 28 | 29 | try { 30 | await schema.validate({ projectName, jsonContent, language }); 31 | } catch (err) { 32 | throw new AppError(err.message); 33 | } 34 | 35 | const session = await createDialogflowSession(999, projectName, jsonContent); 36 | 37 | if (!session) { 38 | throw new AppError("ERR_TEST_SESSION_DIALOG", 400); 39 | } 40 | 41 | let dialogFlowReply = await queryDialogFlow( 42 | session, 43 | projectName, 44 | "TestSession", 45 | "Ola", 46 | language, 47 | undefined 48 | ); 49 | 50 | await session.close(); 51 | 52 | if (!dialogFlowReply) { 53 | throw new AppError("ERR_TEST_REPLY_DIALOG", 400); 54 | } 55 | 56 | const messages = []; 57 | for (let message of dialogFlowReply.responses) { 58 | messages.push(message.text.text[0]); 59 | } 60 | 61 | return { messages }; 62 | }; 63 | 64 | export default TestDialogflowSession; 65 | -------------------------------------------------------------------------------- /backend/src/services/DialogflowServices/UpdateDialogflowService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | import AppError from "../../errors/AppError"; 4 | import Dialogflow from "../../models/Dialogflow"; 5 | import ShowDialogflowService from "./ShowDialogflowService"; 6 | 7 | interface DialogflowData { 8 | name?: string; 9 | projectName?: string; 10 | jsonContent?: string; 11 | language?: string; 12 | } 13 | 14 | interface Request { 15 | dialogflowData: DialogflowData; 16 | dialogflowId: string; 17 | } 18 | 19 | const UpdateDialogflowService = async ({ 20 | dialogflowData, 21 | dialogflowId 22 | }: Request): Promise => { 23 | const schema = Yup.object().shape({ 24 | name: Yup.string().min(2), 25 | projectName: Yup.string().min(2), 26 | jsonContent: Yup.string().min(2), 27 | language: Yup.string().min(2) 28 | }); 29 | 30 | const { 31 | name, 32 | projectName, 33 | jsonContent, 34 | language 35 | } = dialogflowData; 36 | 37 | try { 38 | await schema.validate({ name, projectName, jsonContent, language }); 39 | } catch (err) { 40 | throw new AppError(err.message); 41 | } 42 | 43 | const dialogflow = await ShowDialogflowService(dialogflowId); 44 | 45 | await dialogflow.update({ 46 | name, 47 | projectName, 48 | jsonContent, 49 | language 50 | }); 51 | 52 | return dialogflow; 53 | }; 54 | 55 | export default UpdateDialogflowService; 56 | -------------------------------------------------------------------------------- /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; 67 | -------------------------------------------------------------------------------- /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 | limit, 35 | include: [ 36 | "contact", 37 | { 38 | model: Message, 39 | as: "quotedMsg", 40 | include: ["contact"] 41 | } 42 | ], 43 | offset, 44 | order: [["createdAt", "DESC"]] 45 | }); 46 | 47 | const hasMore = count > offset + messages.length; 48 | 49 | return { 50 | messages: messages.reverse(), 51 | ticket, 52 | count, 53 | hasMore 54 | }; 55 | }; 56 | 57 | export default ListMessagesService; 58 | -------------------------------------------------------------------------------- /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 | menuname: string; 8 | color: string; 9 | greetingMessage?: string; 10 | } 11 | 12 | const CreateQueueService = async (queueData: QueueData): Promise => { 13 | const { color, name, menuname } = queueData; 14 | 15 | const queueSchema = Yup.object().shape({ 16 | name: Yup.string() 17 | .min(2, "ERR_QUEUE_INVALID_NAME") 18 | .required("ERR_QUEUE_INVALID_NAME") 19 | .test( 20 | "Check-unique-name", 21 | "ERR_QUEUE_NAME_ALREADY_EXISTS", 22 | async value => { 23 | if (value) { 24 | const queueWithSameName = await Queue.findOne({ 25 | where: { name: value } 26 | }); 27 | 28 | return !queueWithSameName; 29 | } 30 | return false; 31 | } 32 | ), 33 | menuname: Yup.string() 34 | .min(2, "ERR_QUEUE_INVALID_NAME") 35 | .required("ERR_QUEUE_INVALID_NAME"), 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 false; 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 } 52 | }); 53 | return !queueWithSameColor; 54 | } 55 | return false; 56 | } 57 | ) 58 | }); 59 | 60 | try { 61 | await queueSchema.validate({ color, name, menuname }); 62 | } catch (err) { 63 | throw new AppError(err.message); 64 | } 65 | 66 | const queue = await Queue.create(queueData); 67 | 68 | return queue; 69 | }; 70 | 71 | export default CreateQueueService; 72 | -------------------------------------------------------------------------------- /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/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("shortcut")), 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: [["shortcut", "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 | 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: [ 15 | "id", 16 | "name", 17 | "number", 18 | "profilePicUrl", 19 | "useDialogflow", 20 | "useQueues", 21 | "acceptAudioMessage" 22 | ], 23 | include: ["extraInfo"] 24 | }, 25 | { 26 | model: User, 27 | as: "user", 28 | attributes: ["id", "name"] 29 | }, 30 | { 31 | model: Queue, 32 | as: "queue", 33 | attributes: ["id", "name", "color"], 34 | include: ["dialogflow"] 35 | }, 36 | { 37 | model: Whatsapp, 38 | as: "whatsapp", 39 | attributes: ["name"] 40 | } 41 | ] 42 | }); 43 | 44 | if (!ticket) { 45 | throw new AppError("ERR_NO_TICKET_FOUND", 404); 46 | } 47 | 48 | return ticket; 49 | }; 50 | 51 | export default ShowTicketService; 52 | -------------------------------------------------------------------------------- /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/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/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", "menuname", "color", "greetingMessage"] 12 | } 13 | ], 14 | order: [["queues", "id", "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/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.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BACKEND_URL = http://localhost:8080/ 2 | REACT_APP_HOURS_CLOSE_TICKETS_AUTO = 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 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "@material-ui/lab": "^4.0.0-alpha.56", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.0.4", 11 | "@testing-library/user-event": "^12.1.7", 12 | "axios": "^1.6.8", 13 | "date-fns": "^2.16.1", 14 | "emoji-mart": "^3.0.1", 15 | "formik": "^2.2.0", 16 | "i18next": "^19.8.2", 17 | "i18next-browser-languagedetector": "^6.0.1", 18 | "markdown-to-jsx": "^7.1.0", 19 | "mic-recorder-to-mp3": "^2.2.2", 20 | "qrcode.react": "^1.0.0", 21 | "react": "^16.13.1", 22 | "react-color": "^2.19.3", 23 | "react-dom": "^16.13.1", 24 | "react-modal-image": "^2.5.0", 25 | "react-router-dom": "^5.2.0", 26 | "react-scripts": "^5.0.1", 27 | "react-toastify": "^6.0.9", 28 | "recharts": "^2.0.2", 29 | "socket.io-client": "^3.0.5", 30 | "use-sound": "^2.0.1", 31 | "yup": "^0.32.8" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test", 37 | "eject": "react-scripts eject" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cero Ticket 5 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CeroTicket", 3 | "name": "CeroTicket", 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/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/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: "#C90056" }, 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/login-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/src/assets/login-logo.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/src/assets/sound.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/src/assets/sound.ogg -------------------------------------------------------------------------------- /frontend/src/assets/wa-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollyzn/whaticket-cero/45f09a435f55ae458202020290bf2c24013d3cae/frontend/src/assets/wa-background.png -------------------------------------------------------------------------------- /frontend/src/components/Audio/index.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import React, { useRef } from "react"; 3 | import { useEffect } from "react"; 4 | import { useState } from "react"; 5 | 6 | const LS_NAME = 'audioMessageRate'; 7 | 8 | export default function({url}) { 9 | const audioRef = useRef(null); 10 | const [audioRate, setAudioRate] = useState( parseFloat(localStorage.getItem(LS_NAME) || "1") ); 11 | const [showButtonRate, setShowButtonRate] = useState(false); 12 | 13 | useEffect(() => { 14 | audioRef.current.playbackRate = audioRate; 15 | localStorage.setItem(LS_NAME, audioRate); 16 | }, [audioRate]); 17 | 18 | useEffect(() => { 19 | audioRef.current.onplaying = () => { 20 | setShowButtonRate(true); 21 | }; 22 | audioRef.current.onpause = () => { 23 | setShowButtonRate(false); 24 | }; 25 | audioRef.current.onended = () => { 26 | setShowButtonRate(false); 27 | }; 28 | }, []); 29 | 30 | const toogleRate = () => { 31 | let newRate = null; 32 | 33 | switch(audioRate) { 34 | case 0.5: 35 | newRate = 1; 36 | break; 37 | case 1: 38 | newRate = 1.5; 39 | break; 40 | case 1.5: 41 | newRate = 2; 42 | break; 43 | case 2: 44 | newRate = 0.5; 45 | break; 46 | default: 47 | newRate = 1; 48 | break; 49 | } 50 | 51 | setAudioRate(newRate); 52 | }; 53 | 54 | return ( 55 | <> 56 | 59 | {showButtonRate && } 60 | 61 | ); 62 | } -------------------------------------------------------------------------------- /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/ColorPicker/index.js: -------------------------------------------------------------------------------- 1 | import { Dialog } from "@material-ui/core"; 2 | import React, { useState } from "react"; 3 | 4 | import { GithubPicker } from "react-color"; 5 | 6 | const ColorPicker = ({ onChange, currentColor, handleClose, open }) => { 7 | const [selectedColor, setSelectedColor] = useState(currentColor); 8 | const colors = [ 9 | "#B80000", 10 | "#DB3E00", 11 | "#FCCB00", 12 | "#008B02", 13 | "#006B76", 14 | "#1273DE", 15 | "#004DCF", 16 | "#5300EB", 17 | "#EB9694", 18 | "#FAD0C3", 19 | "#FEF3BD", 20 | "#C1E1C5", 21 | "#BEDADC", 22 | "#C4DEF6", 23 | "#BED3F3", 24 | "#D4C4FB", 25 | "#4D4D4D", 26 | "#999999", 27 | "#FFFFFF", 28 | "#F44E3B", 29 | "#FE9200", 30 | "#FCDC00", 31 | "#DBDF00", 32 | "#A4DD00", 33 | "#68CCCA", 34 | "#73D8FF", 35 | "#AEA1FF", 36 | "#FDA1FF", 37 | "#333333", 38 | "#808080", 39 | "#cccccc", 40 | "#D33115", 41 | "#E27300", 42 | "#FCC400", 43 | "#B0BC00", 44 | "#68BC00", 45 | "#16A5A5", 46 | "#009CE0", 47 | "#7B64FF", 48 | "#FA28FF", 49 | "#666666", 50 | "#B3B3B3", 51 | "#9F0500", 52 | "#C45100", 53 | "#FB9E00", 54 | "#808900", 55 | "#194D33", 56 | "#0C797D", 57 | "#0062B1", 58 | "#653294", 59 | "#AB149E", 60 | ]; 61 | 62 | const handleChange = (color) => { 63 | setSelectedColor(color.hex); 64 | handleClose(); 65 | }; 66 | 67 | return ( 68 | 75 | onChange(color.hex)} 82 | /> 83 | 84 | ); 85 | }; 86 | 87 | export default ColorPicker; 88 | -------------------------------------------------------------------------------- /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 { Button, Divider } from "@material-ui/core"; 6 | 7 | const LocationPreview = ({ image, link, description }) => { 8 | useEffect(() => {}, [image, link, description]); 9 | 10 | const handleLocation = async () => { 11 | try { 12 | window.open(link); 13 | } catch (err) { 14 | toastError(err); 15 | } 16 | }; 17 | 18 | return ( 19 | <> 20 |
25 |
26 |
27 | location 33 |
34 | {description && ( 35 |
36 | 47 |
"), 50 | }} 51 | >
52 |
53 |
54 | )} 55 |
56 |
57 | 58 | 66 |
67 |
68 |
69 | 70 | ); 71 | }; 72 | 73 | export default LocationPreview; 74 | -------------------------------------------------------------------------------- /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/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/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, { useEffect } 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 | const handleKeyDown = (event) => { 29 | if (event.keyCode === 27) { 30 | handleBack(); 31 | } 32 | }; 33 | 34 | useEffect(() => { 35 | window.addEventListener("keydown", handleKeyDown); 36 | return () => { 37 | window.removeEventListener("keydown", handleKeyDown); 38 | }; 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, []); 41 | 42 | return ( 43 | <> 44 | {loading ? ( 45 | 46 | ) : ( 47 | 48 | 51 | {children} 52 | 53 | )} 54 | 55 | ); 56 | }; 57 | 58 | export default TicketHeader; 59 | -------------------------------------------------------------------------------- /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} #${ticket.id}`} 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/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/config.js: -------------------------------------------------------------------------------- 1 | function getConfig(name, defaultValue=null) { 2 | // If inside a docker container, use window.ENV 3 | if( window.ENV !== undefined ) { 4 | return window.ENV[name] || defaultValue; 5 | } 6 | 7 | return process.env[name] || defaultValue; 8 | } 9 | 10 | export function getBackendUrl() { 11 | return getConfig('REACT_APP_BACKEND_URL'); 12 | } 13 | 14 | export function getHoursCloseTicketsAuto() { 15 | return getConfig('REACT_APP_HOURS_CLOSE_TICKETS_AUTO'); 16 | } -------------------------------------------------------------------------------- /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("An error occurred!"); 18 | } 19 | }; 20 | 21 | export default toastError; 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useDialogflows/index.js: -------------------------------------------------------------------------------- 1 | import api from "../../services/api"; 2 | 3 | const useDialogflows = () => { 4 | const findAll = async () => { 5 | const { data } = await api.get("/dialogflow"); 6 | return data; 7 | } 8 | 9 | return { findAll }; 10 | }; 11 | 12 | export default useDialogflows; 13 | -------------------------------------------------------------------------------- /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/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/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/pages/Queues/useLoadData.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import toastError from "../../errors/toastError"; 3 | import api from "../../services/api"; 4 | 5 | const useLoadData = (setLoading, dispatch, route, dispatchType) => { 6 | useEffect(() => { 7 | (async () => { 8 | setLoading(true); 9 | try { 10 | const { data } = await api.get(route); 11 | dispatch({ type: dispatchType, payload: data }); 12 | 13 | setLoading(false); 14 | } catch (err) { 15 | toastError(err); 16 | setLoading(false); 17 | } 18 | })(); 19 | }, [setLoading, dispatch, route, dispatchType]); 20 | }; 21 | 22 | export default useLoadData; 23 | -------------------------------------------------------------------------------- /frontend/src/pages/Queues/useSocket.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import openSocket from "../../services/socket-io"; 3 | 4 | const useSocket = ( 5 | dispatch, 6 | socketEvent, 7 | typeUpdate, 8 | typeDelete, 9 | payloadUpdate, 10 | payloadDelete 11 | ) => { 12 | useEffect(() => { 13 | const socket = openSocket(); 14 | 15 | socket.on(socketEvent, (data) => { 16 | if (data.action === "update" || data.action === "create") { 17 | dispatch({ type: typeUpdate, payload: data[payloadUpdate] }); 18 | } 19 | 20 | if (data.action === "delete") { 21 | dispatch({ type: typeDelete, payload: data[payloadDelete] }); 22 | } 23 | }); 24 | 25 | return () => { 26 | socket.disconnect(); 27 | }; 28 | }, [ 29 | dispatch, 30 | socketEvent, 31 | typeUpdate, 32 | typeDelete, 33 | payloadUpdate, 34 | payloadDelete, 35 | ]); 36 | }; 37 | 38 | export default useSocket; 39 | -------------------------------------------------------------------------------- /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/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 | "sign-message:disable", 11 | "user-modal:editProfile", 12 | "user-modal:editQueues", 13 | "ticket-options:deleteTicket", 14 | "ticket-options:transferWhatsapp", 15 | "ticket-options:transferUser", 16 | "contacts-page:deleteContact", 17 | ], 18 | }, 19 | }; 20 | 21 | export default rules; 22 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getBackendUrl } from "../config"; 3 | 4 | const api = axios.create({ 5 | baseURL: getBackendUrl(), 6 | withCredentials: true, 7 | }); 8 | 9 | export default api; 10 | -------------------------------------------------------------------------------- /frontend/src/services/socket-io.js: -------------------------------------------------------------------------------- 1 | import openSocket from "socket.io-client"; 2 | import { getBackendUrl } from "../config"; 3 | 4 | function connectToSocket() { 5 | return openSocket(getBackendUrl()); 6 | } 7 | 8 | export default connectToSocket; -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------