mirror of
https://github.com/cheveguerra/Whaticket.git
synced 2026-04-17 19:26:18 +00:00
Initial commit
This commit is contained in:
27
.env.example
Normal file
27
.env.example
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# MYSQL
|
||||||
|
MYSQL_ENGINE=
|
||||||
|
MYSQL_VERSION=
|
||||||
|
MYSQL_ROOT_PASSWORD=
|
||||||
|
MYSQL_DATABASE=whaticket
|
||||||
|
MYSQL_PORT=
|
||||||
|
TZ=
|
||||||
|
|
||||||
|
# BACKEND
|
||||||
|
BACKEND_PORT=
|
||||||
|
BACKEND_SERVER_NAME=api.mydomain.com
|
||||||
|
BACKEND_URL=https://api.mydomain.com
|
||||||
|
PROXY_PORT=443
|
||||||
|
JWT_SECRET=
|
||||||
|
JWT_REFRESH_SECRET=
|
||||||
|
|
||||||
|
# FRONTEND
|
||||||
|
FRONTEND_PORT=80
|
||||||
|
FRONTEND_SSL_PORT=443
|
||||||
|
FRONTEND_SERVER_NAME=myapp.mydomain.com
|
||||||
|
FRONTEND_URL=https://myapp.mydomain.com
|
||||||
|
|
||||||
|
# BROWSERLESS
|
||||||
|
MAX_CONCURRENT_SESSIONS=
|
||||||
|
|
||||||
|
# PHPMYADMIN
|
||||||
|
PMA_PORT=
|
||||||
26
.fossa.yml
Normal file
26
.fossa.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
|
||||||
|
# Visit https://fossa.com to learn more
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
cli:
|
||||||
|
server: https://app.fossa.com
|
||||||
|
fetcher: custom
|
||||||
|
project: https://github.com/canove/whaticket.git
|
||||||
|
analyze:
|
||||||
|
modules:
|
||||||
|
- name: backend
|
||||||
|
type: npm
|
||||||
|
target: backend
|
||||||
|
path: backend
|
||||||
|
- name: backend
|
||||||
|
type: npm
|
||||||
|
target: backend
|
||||||
|
path: backend
|
||||||
|
- name: frontend
|
||||||
|
type: npm
|
||||||
|
target: frontend
|
||||||
|
path: frontend
|
||||||
|
- name: frontend
|
||||||
|
type: npm
|
||||||
|
target: frontend
|
||||||
|
path: frontend
|
||||||
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
* text=auto
|
||||||
|
*.sh text eol=lf
|
||||||
|
Dockerfile eol=lf
|
||||||
|
docker-compose.yml eol=lf
|
||||||
|
docker-compose.*.yml eol=lf
|
||||||
|
*.jpg binary
|
||||||
|
*.png binary
|
||||||
|
*.gif binary
|
||||||
|
*.woff binary
|
||||||
|
*.tff binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 10
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 10
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: stale
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
||||||
36
.github/workflows/build-backend.yaml
vendored
Normal file
36
.github/workflows/build-backend.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Test build backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: backend
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- run: npm run build
|
||||||
|
working-directory: backend
|
||||||
36
.github/workflows/build-frontend.yaml
vendored
Normal file
36
.github/workflows/build-frontend.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Test build frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- run: CI=false npm run build
|
||||||
|
working-directory: frontend
|
||||||
44
.github/workflows/push-image-backend.yaml
vendored
Normal file
44
.github/workflows/push-image-backend.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Create and publish a Backend Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||||
|
with:
|
||||||
|
context: ./backend/
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
44
.github/workflows/push-image-frontend.yaml
vendored
Normal file
44
.github/workflows/push-image-frontend.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Create and publish a Frontend Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-frontend
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||||
|
with:
|
||||||
|
context: ./frontend/
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.docker/data/
|
||||||
|
ssl/
|
||||||
|
.env
|
||||||
|
.wwebjs_auth
|
||||||
1
.sonarcloud.properties
Normal file
1
.sonarcloud.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sonar.exclusions=frontend/src/translate/languages/*,**/__tests__/**/*
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 canove
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
6
backend/.dockerignore
Normal file
6
backend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
*Dockerfile*
|
||||||
|
*docker-compose*
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.wwebjs_auth
|
||||||
9
backend/.editorconfig
Normal file
9
backend/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
3
backend/.eslintignore
Normal file
3
backend/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/*.js
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
49
backend/.eslintrc.json
Normal file
49
backend/.eslintrc.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2021": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"airbnb-base",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier/@typescript-eslint",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "argsIgnorePattern": "_" }
|
||||||
|
],
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"ts": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
1,
|
||||||
|
"double",
|
||||||
|
{
|
||||||
|
"avoidEscape": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/.gitignore
vendored
Normal file
18
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules
|
||||||
|
public/*
|
||||||
|
dist
|
||||||
|
!public/.gitkeep
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
/src/config/sentry.js
|
||||||
|
|
||||||
|
# Ignore test-related files
|
||||||
|
/coverage.data
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
.wwebjs_auth/
|
||||||
8
backend/.sequelizerc
Normal file
8
backend/.sequelizerc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const { resolve } = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"config": resolve(__dirname, "dist", "config", "database.js"),
|
||||||
|
"modules-path": resolve(__dirname, "dist", "models"),
|
||||||
|
"migrations-path": resolve(__dirname, "dist", "database", "migrations"),
|
||||||
|
"seeders-path": resolve(__dirname, "dist", "database", "seeds")
|
||||||
|
};
|
||||||
40
backend/Dockerfile
Normal file
40
backend/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
FROM node:14 as build-deps
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget
|
||||||
|
|
||||||
|
ENV DOCKERIZE_VERSION v0.6.1
|
||||||
|
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||||
|
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||||
|
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y wget gnupg \
|
||||||
|
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||||
|
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64 /usr/local/bin/dumb-init
|
||||||
|
RUN chmod +x /usr/local/bin/dumb-init
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV CHROME_BIN=google-chrome-stable
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
CMD dockerize -wait tcp://${DB_HOST}:3306 \
|
||||||
|
&& npx sequelize db:migrate \
|
||||||
|
&& node dist/server.js
|
||||||
186
backend/jest.config.js
Normal file
186
backend/jest.config.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* For a detailed explanation regarding each configuration property, visit:
|
||||||
|
* https://jestjs.io/docs/en/configuration.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
bail: 1,
|
||||||
|
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/tmp/jest_rs",
|
||||||
|
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
collectCoverage: true,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
collectCoverageFrom: ["<rootDir>/src/services/**/*.ts"],
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
coverageDirectory: "coverage",
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Indicates which provider should be used to instrument code for coverage
|
||||||
|
coverageProvider: "v8",
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
coverageReporters: ["text", "lcov"],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: undefined,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: undefined,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files using an array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: undefined,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: undefined,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||||
|
// maxWorkers: "50%",
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "json",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
preset: "ts-jest",
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: undefined,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state between every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: undefined,
|
||||||
|
|
||||||
|
// Automatically restore mock state between every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: undefined,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||||
|
// slowTestThreshold: 5,
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
testEnvironment: "node",
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
testMatch: ["**/__tests__/**/*.spec.ts"]
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: undefined,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jasmine2",
|
||||||
|
|
||||||
|
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||||
|
// testURL: "http://localhost",
|
||||||
|
|
||||||
|
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||||
|
// timers: "real",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: undefined,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/",
|
||||||
|
// "\\.pnp\\.[^\\/]+$"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: undefined,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
};
|
||||||
80
backend/package.json
Normal file
80
backend/package.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc -w",
|
||||||
|
"start": "nodemon dist/server.js",
|
||||||
|
"dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts",
|
||||||
|
"pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all",
|
||||||
|
"test": "NODE_ENV=test jest",
|
||||||
|
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/node": "^5.29.2",
|
||||||
|
"@types/pino": "^6.3.4",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"date-fns": "^2.16.1",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-async-errors": "^3.1.1",
|
||||||
|
"http-graceful-shutdown": "^2.3.2",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"multer": "^1.4.2",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
|
"mysql2": "^2.2.5",
|
||||||
|
"pg": "^8.4.1",
|
||||||
|
"pino": "^6.9.0",
|
||||||
|
"pino-pretty": "^4.3.0",
|
||||||
|
"qrcode-terminal": "^0.12.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"sequelize": "^5.22.3",
|
||||||
|
"sequelize-cli": "^5.5.1",
|
||||||
|
"sequelize-typescript": "^1.1.0",
|
||||||
|
"socket.io": "^3.0.5",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
|
"whatsapp-web.js": "^1.17.1",
|
||||||
|
"yup": "^0.32.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/bluebird": "^3.5.32",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
|
"@types/cors": "^2.8.7",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/factory-girl": "^5.0.2",
|
||||||
|
"@types/faker": "^5.1.3",
|
||||||
|
"@types/jest": "^26.0.15",
|
||||||
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
|
"@types/multer": "^1.4.4",
|
||||||
|
"@types/mustache": "^4.1.2",
|
||||||
|
"@types/node": "^14.11.8",
|
||||||
|
"@types/supertest": "^2.0.10",
|
||||||
|
"@types/uuid": "^8.3.3",
|
||||||
|
"@types/validator": "^13.1.0",
|
||||||
|
"@types/yup": "^0.29.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||||
|
"@typescript-eslint/parser": "^4.4.0",
|
||||||
|
"eslint": "^7.10.0",
|
||||||
|
"eslint-config-airbnb-base": "^14.2.0",
|
||||||
|
"eslint-config-prettier": "^6.12.0",
|
||||||
|
"eslint-import-resolver-typescript": "^2.3.0",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"factory-girl": "^5.0.4",
|
||||||
|
"faker": "^5.1.0",
|
||||||
|
"jest": "^26.6.0",
|
||||||
|
"nodemon": "^2.0.4",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"supertest": "^5.0.0",
|
||||||
|
"ts-jest": "^26.4.1",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.63",
|
||||||
|
"typescript": "4.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
backend/prettier.config.js
Normal file
5
backend/prettier.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: "none",
|
||||||
|
arrowParens: "avoid"
|
||||||
|
};
|
||||||
0
backend/public/.gitkeep
Normal file
0
backend/public/.gitkeep
Normal file
5
backend/src/@types/express.d.ts
vendored
Normal file
5
backend/src/@types/express.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
user: { id: string; profile: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
1
backend/src/@types/qrcode-terminal.d.ts
vendored
Normal file
1
backend/src/@types/qrcode-terminal.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module "qrcode-terminal";
|
||||||
69
backend/src/__tests__/unit/User/AuthUserService.spec.ts
Normal file
69
backend/src/__tests__/unit/User/AuthUserService.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import AppError from "../../../errors/AppError";
|
||||||
|
import AuthUserService from "../../../services/UserServices/AuthUserService";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("Auth", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to login with an existing user", async () => {
|
||||||
|
const password = faker.internet.password();
|
||||||
|
const email = faker.internet.email();
|
||||||
|
|
||||||
|
await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await AuthUserService({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty("token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to login with not registered email", async () => {
|
||||||
|
try {
|
||||||
|
await AuthUserService({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(AppError);
|
||||||
|
expect(err.statusCode).toBe(401);
|
||||||
|
expect(err.message).toBe("ERR_INVALID_CREDENTIALS");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to login with incorret password", async () => {
|
||||||
|
await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: "mail@test.com",
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AuthUserService({
|
||||||
|
email: "mail@test.com",
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(AppError);
|
||||||
|
expect(err.statusCode).toBe(401);
|
||||||
|
expect(err.message).toBe("ERR_INVALID_CREDENTIALS");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
47
backend/src/__tests__/unit/User/CreateUserService.spec.ts
Normal file
47
backend/src/__tests__/unit/User/CreateUserService.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import AppError from "../../../errors/AppError";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("User", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to create a new user", async () => {
|
||||||
|
const user = await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user).toHaveProperty("id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to create a user with duplicated email", async () => {
|
||||||
|
await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: "teste@sameemail.com",
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: "teste@sameemail.com",
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(AppError);
|
||||||
|
expect(err.statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
35
backend/src/__tests__/unit/User/DeleteUserService.spec.ts
Normal file
35
backend/src/__tests__/unit/User/DeleteUserService.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import AppError from "../../../errors/AppError";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import DeleteUserService from "../../../services/UserServices/DeleteUserService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("User", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be delete a existing user", async () => {
|
||||||
|
const { id } = await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(DeleteUserService(id)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("to throw an error if tries to delete a non existing user", async () => {
|
||||||
|
expect(DeleteUserService(faker.random.number())).rejects.toBeInstanceOf(
|
||||||
|
AppError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
34
backend/src/__tests__/unit/User/ListUserService.spec.ts
Normal file
34
backend/src/__tests__/unit/User/ListUserService.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import User from "../../../models/User";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import ListUsersService from "../../../services/UserServices/ListUsersService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("User", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to list users", async () => {
|
||||||
|
await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await ListUsersService({
|
||||||
|
pageNumber: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty("users");
|
||||||
|
expect(response.users[0]).toBeInstanceOf(User);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
backend/src/__tests__/unit/User/ShowUserService.spec.ts
Normal file
39
backend/src/__tests__/unit/User/ShowUserService.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import AppError from "../../../errors/AppError";
|
||||||
|
import User from "../../../models/User";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import ShowUserService from "../../../services/UserServices/ShowUserService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("User", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to find a user", async () => {
|
||||||
|
const newUser = await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await ShowUserService(newUser.id);
|
||||||
|
|
||||||
|
expect(user).toHaveProperty("id");
|
||||||
|
expect(user).toBeInstanceOf(User);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to find a inexisting user", async () => {
|
||||||
|
expect(ShowUserService(faker.random.number())).rejects.toBeInstanceOf(
|
||||||
|
AppError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
68
backend/src/__tests__/unit/User/UpdateUserService.spec.ts
Normal file
68
backend/src/__tests__/unit/User/UpdateUserService.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import faker from "faker";
|
||||||
|
import AppError from "../../../errors/AppError";
|
||||||
|
import CreateUserService from "../../../services/UserServices/CreateUserService";
|
||||||
|
import UpdateUserService from "../../../services/UserServices/UpdateUserService";
|
||||||
|
import { disconnect, truncate } from "../../utils/database";
|
||||||
|
|
||||||
|
describe("User", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await truncate();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to find a user", async () => {
|
||||||
|
const newUser = await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedUser = await UpdateUserService({
|
||||||
|
userId: newUser.id,
|
||||||
|
userData: {
|
||||||
|
name: "New name",
|
||||||
|
email: "newmail@email.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedUser).toHaveProperty("name", "New name");
|
||||||
|
expect(updatedUser).toHaveProperty("email", "newmail@email.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to updated a inexisting user", async () => {
|
||||||
|
const userId = faker.random.number();
|
||||||
|
const userData = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email()
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf(
|
||||||
|
AppError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to updated an user with invalid data", async () => {
|
||||||
|
const newUser = await CreateUserService({
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: faker.internet.email(),
|
||||||
|
password: faker.internet.password()
|
||||||
|
});
|
||||||
|
|
||||||
|
const userId = newUser.id;
|
||||||
|
const userData = {
|
||||||
|
name: faker.name.findName(),
|
||||||
|
email: "test.worgn.email"
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(UpdateUserService({ userId, userData })).rejects.toBeInstanceOf(
|
||||||
|
AppError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
11
backend/src/__tests__/utils/database.ts
Normal file
11
backend/src/__tests__/utils/database.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import database from "../../database";
|
||||||
|
|
||||||
|
const truncate = async (): Promise<void> => {
|
||||||
|
await database.truncate({ force: true, cascade: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = async (): Promise<void> => {
|
||||||
|
return database.connectionManager.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
export { truncate, disconnect };
|
||||||
43
backend/src/app.ts
Normal file
43
backend/src/app.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import "./bootstrap";
|
||||||
|
import "reflect-metadata";
|
||||||
|
import "express-async-errors";
|
||||||
|
import express, { Request, Response, NextFunction } from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
|
import "./database";
|
||||||
|
import uploadConfig from "./config/upload";
|
||||||
|
import AppError from "./errors/AppError";
|
||||||
|
import routes from "./routes";
|
||||||
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
Sentry.init({ dsn: process.env.SENTRY_DSN });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
credentials: true,
|
||||||
|
origin: process.env.FRONTEND_URL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(Sentry.Handlers.requestHandler());
|
||||||
|
app.use("/public", express.static(uploadConfig.directory));
|
||||||
|
app.use(routes);
|
||||||
|
|
||||||
|
app.use(Sentry.Handlers.errorHandler());
|
||||||
|
|
||||||
|
app.use(async (err: Error, req: Request, res: Response, _: NextFunction) => {
|
||||||
|
if (err instanceof AppError) {
|
||||||
|
logger.warn(err);
|
||||||
|
return res.status(err.statusCode).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(err);
|
||||||
|
return res.status(500).json({ error: "Internal server error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
5
backend/src/bootstrap.ts
Normal file
5
backend/src/bootstrap.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: process.env.NODE_ENV === "test" ? ".env.test" : ".env"
|
||||||
|
});
|
||||||
6
backend/src/config/auth.ts
Normal file
6
backend/src/config/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
secret: process.env.JWT_SECRET || "mysecret",
|
||||||
|
expiresIn: "15m",
|
||||||
|
refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret",
|
||||||
|
refreshExpiresIn: "7d"
|
||||||
|
};
|
||||||
15
backend/src/config/database.ts
Normal file
15
backend/src/config/database.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
require("../bootstrap");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
define: {
|
||||||
|
charset: "utf8mb4",
|
||||||
|
collate: "utf8mb4_bin"
|
||||||
|
},
|
||||||
|
dialect: process.env.DB_DIALECT || "mysql",
|
||||||
|
timezone: "-03:00",
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
logging: false
|
||||||
|
};
|
||||||
16
backend/src/config/upload.ts
Normal file
16
backend/src/config/upload.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import path from "path";
|
||||||
|
import multer from "multer";
|
||||||
|
|
||||||
|
const publicFolder = path.resolve(__dirname, "..", "..", "public");
|
||||||
|
export default {
|
||||||
|
directory: publicFolder,
|
||||||
|
|
||||||
|
storage: multer.diskStorage({
|
||||||
|
destination: publicFolder,
|
||||||
|
filename(req, file, cb) {
|
||||||
|
const fileName = new Date().getTime() + path.extname(file.originalname);
|
||||||
|
|
||||||
|
return cb(null, fileName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
111
backend/src/controllers/ApiController.ts
Normal file
111
backend/src/controllers/ApiController.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
import GetDefaultWhatsApp from "../helpers/GetDefaultWhatsApp";
|
||||||
|
import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead";
|
||||||
|
import Message from "../models/Message";
|
||||||
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService";
|
||||||
|
import FindOrCreateTicketService from "../services/TicketServices/FindOrCreateTicketService";
|
||||||
|
import ShowTicketService from "../services/TicketServices/ShowTicketService";
|
||||||
|
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
|
||||||
|
import CheckContactNumber from "../services/WbotServices/CheckNumber";
|
||||||
|
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
|
||||||
|
import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia";
|
||||||
|
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
|
||||||
|
|
||||||
|
type WhatsappData = {
|
||||||
|
whatsappId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageData = {
|
||||||
|
body: string;
|
||||||
|
fromMe: boolean;
|
||||||
|
read: boolean;
|
||||||
|
quotedMsg?: Message;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ContactData {
|
||||||
|
number: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createContact = async (
|
||||||
|
whatsappId: number | undefined,
|
||||||
|
newContact: string
|
||||||
|
) => {
|
||||||
|
await CheckIsValidContact(newContact);
|
||||||
|
|
||||||
|
const validNumber: any = await CheckContactNumber(newContact);
|
||||||
|
|
||||||
|
const profilePicUrl = await GetProfilePicUrl(validNumber);
|
||||||
|
|
||||||
|
const number = validNumber;
|
||||||
|
|
||||||
|
const contactData = {
|
||||||
|
name: `${number}`,
|
||||||
|
number,
|
||||||
|
profilePicUrl,
|
||||||
|
isGroup: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const contact = await CreateOrUpdateContactService(contactData);
|
||||||
|
|
||||||
|
let whatsapp:Whatsapp | null;
|
||||||
|
|
||||||
|
if(whatsappId === undefined) {
|
||||||
|
whatsapp = await GetDefaultWhatsApp();
|
||||||
|
} else {
|
||||||
|
whatsapp = await Whatsapp.findByPk(whatsappId);
|
||||||
|
|
||||||
|
if(whatsapp === null) {
|
||||||
|
throw new AppError(`whatsapp #${whatsappId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTicket = await FindOrCreateTicketService(
|
||||||
|
contact,
|
||||||
|
whatsapp.id,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const ticket = await ShowTicketService(createTicket.id);
|
||||||
|
|
||||||
|
SetTicketMessagesAsRead(ticket);
|
||||||
|
|
||||||
|
return ticket;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const newContact: ContactData = req.body;
|
||||||
|
const { whatsappId }: WhatsappData = req.body;
|
||||||
|
const { body, quotedMsg }: MessageData = req.body;
|
||||||
|
const medias = req.files as Express.Multer.File[];
|
||||||
|
|
||||||
|
newContact.number = newContact.number.replace("-", "").replace(" ", "");
|
||||||
|
|
||||||
|
const schema = Yup.object().shape({
|
||||||
|
number: Yup.string()
|
||||||
|
.required()
|
||||||
|
.matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await schema.validate(newContact);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new AppError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactAndTicket = await createContact(whatsappId, newContact.number);
|
||||||
|
|
||||||
|
if (medias) {
|
||||||
|
await Promise.all(
|
||||||
|
medias.map(async (media: Express.Multer.File) => {
|
||||||
|
await SendWhatsAppMedia({ body, media, ticket: contactAndTicket });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await SendWhatsAppMessage({ body, ticket: contactAndTicket, quotedMsg });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send();
|
||||||
|
};
|
||||||
162
backend/src/controllers/ContactController.ts
Normal file
162
backend/src/controllers/ContactController.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as Yup from "yup";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
|
||||||
|
import ListContactsService from "../services/ContactServices/ListContactsService";
|
||||||
|
import CreateContactService from "../services/ContactServices/CreateContactService";
|
||||||
|
import ShowContactService from "../services/ContactServices/ShowContactService";
|
||||||
|
import UpdateContactService from "../services/ContactServices/UpdateContactService";
|
||||||
|
import DeleteContactService from "../services/ContactServices/DeleteContactService";
|
||||||
|
|
||||||
|
import CheckContactNumber from "../services/WbotServices/CheckNumber"
|
||||||
|
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
|
||||||
|
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
import GetContactService from "../services/ContactServices/GetContactService";
|
||||||
|
|
||||||
|
type IndexQuery = {
|
||||||
|
searchParam: string;
|
||||||
|
pageNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IndexGetContactQuery = {
|
||||||
|
name: string;
|
||||||
|
number: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExtraInfo {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
interface ContactData {
|
||||||
|
name: string;
|
||||||
|
number: string;
|
||||||
|
email?: string;
|
||||||
|
extraInfo?: ExtraInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||||
|
|
||||||
|
const { contacts, count, hasMore } = await ListContactsService({
|
||||||
|
searchParam,
|
||||||
|
pageNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ contacts, count, hasMore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getContact = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { name, number } = req.body as IndexGetContactQuery;
|
||||||
|
|
||||||
|
const contact = await GetContactService({
|
||||||
|
name,
|
||||||
|
number
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const newContact: ContactData = req.body;
|
||||||
|
newContact.number = newContact.number.replace("-", "").replace(" ", "");
|
||||||
|
|
||||||
|
const schema = Yup.object().shape({
|
||||||
|
name: Yup.string().required(),
|
||||||
|
number: Yup.string()
|
||||||
|
.required()
|
||||||
|
.matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await schema.validate(newContact);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckIsValidContact(newContact.number);
|
||||||
|
const validNumber : any = await CheckContactNumber(newContact.number)
|
||||||
|
|
||||||
|
const profilePicUrl = await GetProfilePicUrl(validNumber);
|
||||||
|
|
||||||
|
let name = newContact.name
|
||||||
|
let number = validNumber
|
||||||
|
let email = newContact.email
|
||||||
|
let extraInfo = newContact.extraInfo
|
||||||
|
|
||||||
|
const contact = await CreateContactService({
|
||||||
|
name,
|
||||||
|
number,
|
||||||
|
email,
|
||||||
|
extraInfo,
|
||||||
|
profilePicUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("contact", {
|
||||||
|
action: "create",
|
||||||
|
contact
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { contactId } = req.params;
|
||||||
|
|
||||||
|
const contact = await ShowContactService(contactId);
|
||||||
|
|
||||||
|
return res.status(200).json(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const contactData: ContactData = req.body;
|
||||||
|
|
||||||
|
const schema = Yup.object().shape({
|
||||||
|
name: Yup.string(),
|
||||||
|
number: Yup.string().matches(
|
||||||
|
/^\d+$/,
|
||||||
|
"Invalid number format. Only numbers is allowed."
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await schema.validate(contactData);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckIsValidContact(contactData.number);
|
||||||
|
|
||||||
|
const { contactId } = req.params;
|
||||||
|
|
||||||
|
const contact = await UpdateContactService({ contactData, contactId });
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("contact", {
|
||||||
|
action: "update",
|
||||||
|
contact
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { contactId } = req.params;
|
||||||
|
|
||||||
|
await DeleteContactService(contactId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("contact", {
|
||||||
|
action: "delete",
|
||||||
|
contactId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Contact deleted" });
|
||||||
|
};
|
||||||
9
backend/src/controllers/ImportPhoneContactsController.ts
Normal file
9
backend/src/controllers/ImportPhoneContactsController.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import ImportContactsService from "../services/WbotServices/ImportContactsService";
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const userId:number = parseInt(req.user.id);
|
||||||
|
await ImportContactsService(userId);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "contacts imported" });
|
||||||
|
};
|
||||||
75
backend/src/controllers/MessageController.ts
Normal file
75
backend/src/controllers/MessageController.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
import Message from "../models/Message";
|
||||||
|
|
||||||
|
import ListMessagesService from "../services/MessageServices/ListMessagesService";
|
||||||
|
import ShowTicketService from "../services/TicketServices/ShowTicketService";
|
||||||
|
import DeleteWhatsAppMessage from "../services/WbotServices/DeleteWhatsAppMessage";
|
||||||
|
import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia";
|
||||||
|
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
|
||||||
|
|
||||||
|
type IndexQuery = {
|
||||||
|
pageNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MessageData = {
|
||||||
|
body: string;
|
||||||
|
fromMe: boolean;
|
||||||
|
read: boolean;
|
||||||
|
quotedMsg?: Message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { ticketId } = req.params;
|
||||||
|
const { pageNumber } = req.query as IndexQuery;
|
||||||
|
|
||||||
|
const { count, messages, ticket, hasMore } = await ListMessagesService({
|
||||||
|
pageNumber,
|
||||||
|
ticketId
|
||||||
|
});
|
||||||
|
|
||||||
|
SetTicketMessagesAsRead(ticket);
|
||||||
|
|
||||||
|
return res.json({ count, messages, ticket, hasMore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { ticketId } = req.params;
|
||||||
|
const { body, quotedMsg }: MessageData = req.body;
|
||||||
|
const medias = req.files as Express.Multer.File[];
|
||||||
|
|
||||||
|
const ticket = await ShowTicketService(ticketId);
|
||||||
|
|
||||||
|
SetTicketMessagesAsRead(ticket);
|
||||||
|
|
||||||
|
if (medias) {
|
||||||
|
await Promise.all(
|
||||||
|
medias.map(async (media: Express.Multer.File) => {
|
||||||
|
await SendWhatsAppMedia({ media, ticket });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await SendWhatsAppMessage({ body, ticket, quotedMsg });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { messageId } = req.params;
|
||||||
|
|
||||||
|
const message = await DeleteWhatsAppMessage(messageId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.to(message.ticketId.toString()).emit("appMessage", {
|
||||||
|
action: "update",
|
||||||
|
message
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send();
|
||||||
|
};
|
||||||
69
backend/src/controllers/QueueController.ts
Normal file
69
backend/src/controllers/QueueController.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
import CreateQueueService from "../services/QueueService/CreateQueueService";
|
||||||
|
import DeleteQueueService from "../services/QueueService/DeleteQueueService";
|
||||||
|
import ListQueuesService from "../services/QueueService/ListQueuesService";
|
||||||
|
import ShowQueueService from "../services/QueueService/ShowQueueService";
|
||||||
|
import UpdateQueueService from "../services/QueueService/UpdateQueueService";
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const queues = await ListQueuesService();
|
||||||
|
|
||||||
|
return res.status(200).json(queues);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { name, color, greetingMessage } = req.body;
|
||||||
|
|
||||||
|
const queue = await CreateQueueService({ name, color, greetingMessage });
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("queue", {
|
||||||
|
action: "update",
|
||||||
|
queue
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(queue);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { queueId } = req.params;
|
||||||
|
|
||||||
|
const queue = await ShowQueueService(queueId);
|
||||||
|
|
||||||
|
return res.status(200).json(queue);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { queueId } = req.params;
|
||||||
|
|
||||||
|
const queue = await UpdateQueueService(queueId, req.body);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("queue", {
|
||||||
|
action: "update",
|
||||||
|
queue
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json(queue);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { queueId } = req.params;
|
||||||
|
|
||||||
|
await DeleteQueueService(queueId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("queue", {
|
||||||
|
action: "delete",
|
||||||
|
queueId: +queueId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send();
|
||||||
|
};
|
||||||
117
backend/src/controllers/QuickAnswerController.ts
Normal file
117
backend/src/controllers/QuickAnswerController.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as Yup from "yup";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
|
||||||
|
import ListQuickAnswerService from "../services/QuickAnswerService/ListQuickAnswerService";
|
||||||
|
import CreateQuickAnswerService from "../services/QuickAnswerService/CreateQuickAnswerService";
|
||||||
|
import ShowQuickAnswerService from "../services/QuickAnswerService/ShowQuickAnswerService";
|
||||||
|
import UpdateQuickAnswerService from "../services/QuickAnswerService/UpdateQuickAnswerService";
|
||||||
|
import DeleteQuickAnswerService from "../services/QuickAnswerService/DeleteQuickAnswerService";
|
||||||
|
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
type IndexQuery = {
|
||||||
|
searchParam: string;
|
||||||
|
pageNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface QuickAnswerData {
|
||||||
|
shortcut: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||||
|
|
||||||
|
const { quickAnswers, count, hasMore } = await ListQuickAnswerService({
|
||||||
|
searchParam,
|
||||||
|
pageNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ quickAnswers, count, hasMore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const newQuickAnswer: QuickAnswerData = req.body;
|
||||||
|
|
||||||
|
const QuickAnswerSchema = Yup.object().shape({
|
||||||
|
shortcut: Yup.string().required(),
|
||||||
|
message: Yup.string().required()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await QuickAnswerSchema.validate(newQuickAnswer);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickAnswer = await CreateQuickAnswerService({
|
||||||
|
...newQuickAnswer
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("quickAnswer", {
|
||||||
|
action: "create",
|
||||||
|
quickAnswer
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(quickAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { quickAnswerId } = req.params;
|
||||||
|
|
||||||
|
const quickAnswer = await ShowQuickAnswerService(quickAnswerId);
|
||||||
|
|
||||||
|
return res.status(200).json(quickAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const quickAnswerData: QuickAnswerData = req.body;
|
||||||
|
|
||||||
|
const schema = Yup.object().shape({
|
||||||
|
shortcut: Yup.string(),
|
||||||
|
message: Yup.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await schema.validate(quickAnswerData);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { quickAnswerId } = req.params;
|
||||||
|
|
||||||
|
const quickAnswer = await UpdateQuickAnswerService({
|
||||||
|
quickAnswerData,
|
||||||
|
quickAnswerId
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("quickAnswer", {
|
||||||
|
action: "update",
|
||||||
|
quickAnswer
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(quickAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { quickAnswerId } = req.params;
|
||||||
|
|
||||||
|
await DeleteQuickAnswerService(quickAnswerId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("quickAnswer", {
|
||||||
|
action: "delete",
|
||||||
|
quickAnswerId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Quick Answer deleted" });
|
||||||
|
};
|
||||||
51
backend/src/controllers/SessionController.ts
Normal file
51
backend/src/controllers/SessionController.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
import AuthUserService from "../services/UserServices/AuthUserService";
|
||||||
|
import { SendRefreshToken } from "../helpers/SendRefreshToken";
|
||||||
|
import { RefreshTokenService } from "../services/AuthServices/RefreshTokenService";
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
const { token, serializedUser, refreshToken } = await AuthUserService({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
SendRefreshToken(res, refreshToken);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
token,
|
||||||
|
user: serializedUser
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const token: string = req.cookies.jrt;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, newToken, refreshToken } = await RefreshTokenService(
|
||||||
|
res,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
SendRefreshToken(res, refreshToken);
|
||||||
|
|
||||||
|
return res.json({ token: newToken, user });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
res.clearCookie("jrt");
|
||||||
|
|
||||||
|
return res.send();
|
||||||
|
};
|
||||||
41
backend/src/controllers/SettingController.ts
Normal file
41
backend/src/controllers/SettingController.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
import UpdateSettingService from "../services/SettingServices/UpdateSettingService";
|
||||||
|
import ListSettingsService from "../services/SettingServices/ListSettingsService";
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
if (req.user.profile !== "admin") {
|
||||||
|
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await ListSettingsService();
|
||||||
|
|
||||||
|
return res.status(200).json(settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
if (req.user.profile !== "admin") {
|
||||||
|
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||||
|
}
|
||||||
|
const { settingKey: key } = req.params;
|
||||||
|
const { value } = req.body;
|
||||||
|
|
||||||
|
const setting = await UpdateSettingService({
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("settings", {
|
||||||
|
action: "update",
|
||||||
|
setting
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(setting);
|
||||||
|
};
|
||||||
131
backend/src/controllers/TicketController.ts
Normal file
131
backend/src/controllers/TicketController.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
|
||||||
|
import CreateTicketService from "../services/TicketServices/CreateTicketService";
|
||||||
|
import DeleteTicketService from "../services/TicketServices/DeleteTicketService";
|
||||||
|
import ListTicketsService from "../services/TicketServices/ListTicketsService";
|
||||||
|
import ShowTicketService from "../services/TicketServices/ShowTicketService";
|
||||||
|
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
|
||||||
|
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
|
||||||
|
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||||
|
import formatBody from "../helpers/Mustache";
|
||||||
|
|
||||||
|
type IndexQuery = {
|
||||||
|
searchParam: string;
|
||||||
|
pageNumber: string;
|
||||||
|
status: string;
|
||||||
|
date: string;
|
||||||
|
showAll: string;
|
||||||
|
withUnreadMessages: string;
|
||||||
|
queueIds: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TicketData {
|
||||||
|
contactId: number;
|
||||||
|
status: string;
|
||||||
|
queueId: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const {
|
||||||
|
pageNumber,
|
||||||
|
status,
|
||||||
|
date,
|
||||||
|
searchParam,
|
||||||
|
showAll,
|
||||||
|
queueIds: queueIdsStringified,
|
||||||
|
withUnreadMessages
|
||||||
|
} = req.query as IndexQuery;
|
||||||
|
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
let queueIds: number[] = [];
|
||||||
|
|
||||||
|
if (queueIdsStringified) {
|
||||||
|
queueIds = JSON.parse(queueIdsStringified);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tickets, count, hasMore } = await ListTicketsService({
|
||||||
|
searchParam,
|
||||||
|
pageNumber,
|
||||||
|
status,
|
||||||
|
date,
|
||||||
|
showAll,
|
||||||
|
userId,
|
||||||
|
queueIds,
|
||||||
|
withUnreadMessages
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ tickets, count, hasMore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { contactId, status, userId }: TicketData = req.body;
|
||||||
|
|
||||||
|
const ticket = await CreateTicketService({ contactId, status, userId });
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.to(ticket.status).emit("ticket", {
|
||||||
|
action: "update",
|
||||||
|
ticket
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(ticket);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { ticketId } = req.params;
|
||||||
|
|
||||||
|
const contact = await ShowTicketService(ticketId);
|
||||||
|
|
||||||
|
return res.status(200).json(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { ticketId } = req.params;
|
||||||
|
const ticketData: TicketData = req.body;
|
||||||
|
|
||||||
|
const { ticket } = await UpdateTicketService({
|
||||||
|
ticketData,
|
||||||
|
ticketId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ticket.status === "closed") {
|
||||||
|
const whatsapp = await ShowWhatsAppService(ticket.whatsappId);
|
||||||
|
|
||||||
|
const { farewellMessage } = whatsapp;
|
||||||
|
|
||||||
|
if (farewellMessage) {
|
||||||
|
await SendWhatsAppMessage({
|
||||||
|
body: formatBody(farewellMessage, ticket.contact),
|
||||||
|
ticket
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(ticket);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { ticketId } = req.params;
|
||||||
|
|
||||||
|
const ticket = await DeleteTicketService(ticketId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.to(ticket.status)
|
||||||
|
.to(ticketId)
|
||||||
|
.to("notification")
|
||||||
|
.emit("ticket", {
|
||||||
|
action: "delete",
|
||||||
|
ticketId: +ticketId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "ticket deleted" });
|
||||||
|
};
|
||||||
108
backend/src/controllers/UserController.ts
Normal file
108
backend/src/controllers/UserController.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
|
||||||
|
import CheckSettingsHelper from "../helpers/CheckSettings";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
import CreateUserService from "../services/UserServices/CreateUserService";
|
||||||
|
import ListUsersService from "../services/UserServices/ListUsersService";
|
||||||
|
import UpdateUserService from "../services/UserServices/UpdateUserService";
|
||||||
|
import ShowUserService from "../services/UserServices/ShowUserService";
|
||||||
|
import DeleteUserService from "../services/UserServices/DeleteUserService";
|
||||||
|
|
||||||
|
type IndexQuery = {
|
||||||
|
searchParam: string;
|
||||||
|
pageNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { searchParam, pageNumber } = req.query as IndexQuery;
|
||||||
|
|
||||||
|
const { users, count, hasMore } = await ListUsersService({
|
||||||
|
searchParam,
|
||||||
|
pageNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ users, count, hasMore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { email, password, name, profile, queueIds, whatsappId } = req.body;
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.url === "/signup" &&
|
||||||
|
(await CheckSettingsHelper("userCreation")) === "disabled"
|
||||||
|
) {
|
||||||
|
throw new AppError("ERR_USER_CREATION_DISABLED", 403);
|
||||||
|
} else if (req.url !== "/signup" && req.user.profile !== "admin") {
|
||||||
|
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await CreateUserService({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
profile,
|
||||||
|
queueIds,
|
||||||
|
whatsappId
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("user", {
|
||||||
|
action: "create",
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
const user = await ShowUserService(userId);
|
||||||
|
|
||||||
|
return res.status(200).json(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
if (req.user.profile !== "admin") {
|
||||||
|
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId } = req.params;
|
||||||
|
const userData = req.body;
|
||||||
|
|
||||||
|
const user = await UpdateUserService({ userData, userId });
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("user", {
|
||||||
|
action: "update",
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
if (req.user.profile !== "admin") {
|
||||||
|
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeleteUserService(userId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("user", {
|
||||||
|
action: "delete",
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "User deleted" });
|
||||||
|
};
|
||||||
116
backend/src/controllers/WhatsAppController.ts
Normal file
116
backend/src/controllers/WhatsAppController.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getIO } from "../libs/socket";
|
||||||
|
import { removeWbot } from "../libs/wbot";
|
||||||
|
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
|
||||||
|
|
||||||
|
import CreateWhatsAppService from "../services/WhatsappService/CreateWhatsAppService";
|
||||||
|
import DeleteWhatsAppService from "../services/WhatsappService/DeleteWhatsAppService";
|
||||||
|
import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService";
|
||||||
|
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||||
|
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
|
||||||
|
|
||||||
|
interface WhatsappData {
|
||||||
|
name: string;
|
||||||
|
queueIds: number[];
|
||||||
|
greetingMessage?: string;
|
||||||
|
farewellMessage?: string;
|
||||||
|
status?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const whatsapps = await ListWhatsAppsService();
|
||||||
|
|
||||||
|
return res.status(200).json(whatsapps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
isDefault,
|
||||||
|
greetingMessage,
|
||||||
|
farewellMessage,
|
||||||
|
queueIds
|
||||||
|
}: WhatsappData = req.body;
|
||||||
|
|
||||||
|
const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
isDefault,
|
||||||
|
greetingMessage,
|
||||||
|
farewellMessage,
|
||||||
|
queueIds
|
||||||
|
});
|
||||||
|
|
||||||
|
StartWhatsAppSession(whatsapp);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("whatsapp", {
|
||||||
|
action: "update",
|
||||||
|
whatsapp
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldDefaultWhatsapp) {
|
||||||
|
io.emit("whatsapp", {
|
||||||
|
action: "update",
|
||||||
|
whatsapp: oldDefaultWhatsapp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(whatsapp);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const show = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
|
||||||
|
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||||
|
|
||||||
|
return res.status(200).json(whatsapp);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
const whatsappData = req.body;
|
||||||
|
|
||||||
|
const { whatsapp, oldDefaultWhatsapp } = await UpdateWhatsAppService({
|
||||||
|
whatsappData,
|
||||||
|
whatsappId
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("whatsapp", {
|
||||||
|
action: "update",
|
||||||
|
whatsapp
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldDefaultWhatsapp) {
|
||||||
|
io.emit("whatsapp", {
|
||||||
|
action: "update",
|
||||||
|
whatsapp: oldDefaultWhatsapp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(whatsapp);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
|
||||||
|
await DeleteWhatsAppService(whatsappId);
|
||||||
|
removeWbot(+whatsappId);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
io.emit("whatsapp", {
|
||||||
|
action: "delete",
|
||||||
|
whatsappId: +whatsappId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Whatsapp deleted." });
|
||||||
|
};
|
||||||
40
backend/src/controllers/WhatsAppSessionController.ts
Normal file
40
backend/src/controllers/WhatsAppSessionController.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { getWbot } from "../libs/wbot";
|
||||||
|
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
|
||||||
|
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
|
||||||
|
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
|
||||||
|
|
||||||
|
const store = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||||
|
|
||||||
|
StartWhatsAppSession(whatsapp);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Starting session." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
|
||||||
|
const { whatsapp } = await UpdateWhatsAppService({
|
||||||
|
whatsappId,
|
||||||
|
whatsappData: { session: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
StartWhatsAppSession(whatsapp);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Starting session." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (req: Request, res: Response): Promise<Response> => {
|
||||||
|
const { whatsappId } = req.params;
|
||||||
|
const whatsapp = await ShowWhatsAppService(whatsappId);
|
||||||
|
|
||||||
|
const wbot = getWbot(whatsapp.id);
|
||||||
|
|
||||||
|
wbot.logout();
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Session disconnected." });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { store, remove, update };
|
||||||
36
backend/src/database/index.ts
Normal file
36
backend/src/database/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Sequelize } from "sequelize-typescript";
|
||||||
|
import User from "../models/User";
|
||||||
|
import Setting from "../models/Setting";
|
||||||
|
import Contact from "../models/Contact";
|
||||||
|
import Ticket from "../models/Ticket";
|
||||||
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
import ContactCustomField from "../models/ContactCustomField";
|
||||||
|
import Message from "../models/Message";
|
||||||
|
import Queue from "../models/Queue";
|
||||||
|
import WhatsappQueue from "../models/WhatsappQueue";
|
||||||
|
import UserQueue from "../models/UserQueue";
|
||||||
|
import QuickAnswer from "../models/QuickAnswer";
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const dbConfig = require("../config/database");
|
||||||
|
// import dbConfig from "../config/database";
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(dbConfig);
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
User,
|
||||||
|
Contact,
|
||||||
|
Ticket,
|
||||||
|
Message,
|
||||||
|
Whatsapp,
|
||||||
|
ContactCustomField,
|
||||||
|
Setting,
|
||||||
|
Queue,
|
||||||
|
WhatsappQueue,
|
||||||
|
UserQueue,
|
||||||
|
QuickAnswer
|
||||||
|
];
|
||||||
|
|
||||||
|
sequelize.addModels(models);
|
||||||
|
|
||||||
|
export default sequelize;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Users", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
passwordHash: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Users");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Contacts", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
profilePicUrl: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Contacts");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Tickets", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: "pending",
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
lastMessage: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
contactId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Contacts", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE"
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Users", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE(6),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE(6),
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Tickets");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Messages", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
ack: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
mediaType: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
mediaUrl: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Users", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
},
|
||||||
|
ticketId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Tickets", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE(6),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE(6),
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Messages");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Whatsapps", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
},
|
||||||
|
qrcode: {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
battery: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
plugged: {
|
||||||
|
type: DataTypes.BOOLEAN
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Whatsapps");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("ContactCustomFields", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
contactId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Contacts", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("ContactCustomFields");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Contacts", "email", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: ""
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Contacts", "email");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "userId");
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "userId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Users", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "fromMe", {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "fromMe");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.changeColumn("Tickets", "lastMessage", {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.changeColumn("Tickets", "lastMessage", {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Users", "profile", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "admin"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Users", "profile");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Settings", {
|
||||||
|
key: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Whatsapps", "name", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Whatsapps", "name");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Whatsapps", "default", {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Whatsapps", "default");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Tickets", "whatsappId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Whatsapps", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Tickets", "whatsappId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.renameColumn("Whatsapps", "default", "isDefault");
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.renameColumn("Whatsapps", "isDefault", "default");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "isDeleted", {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "isDeleted");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Users", "tokenVersion", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Users", "tokenVersion");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Tickets", "isGroup", {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Tickets", "isGroup");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Contacts", "isGroup", {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Contacts", "isGroup");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "contactId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Contacts", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "contactId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "vcardContactId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Contacts", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "vcardContactId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "vcardContactId");
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "vcardContactId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Contacts", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "CASCADE"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Whatsapps", "retries", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0,
|
||||||
|
allowNull: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Whatsapps", "retries");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Messages", "quotedMsgId", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
references: { model: "Messages", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Messages", "quotedMsgId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Tickets", "unreadMessages", {
|
||||||
|
type: DataTypes.INTEGER
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Tickets", "unreadMessages");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("Queues", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
greetingMessage: {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("Queues");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Tickets", "queueId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Queues", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Tickets", "queueId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("WhatsappQueues", {
|
||||||
|
whatsappId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
queueId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("WhatsappQueues");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("UserQueues", {
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
queueId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("UserQueues");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Whatsapps", "greetingMessage", {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Whatsapps", "greetingMessage");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.createTable("QuickAnswers", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.dropTable("QuickAnswers");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Whatsapps", "farewellMessage", {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Whatsapps", "farewellMessage");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { QueryInterface, DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.addColumn("Users", "whatsappId", {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
references: { model: "Whatsapps", key: "id" },
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
onDelete: "SET NULL",
|
||||||
|
allowNull: true
|
||||||
|
},);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.removeColumn("Users", "whatsappId");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkInsert(
|
||||||
|
"Settings",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: "userCreation",
|
||||||
|
value: "enabled",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkDelete("Settings", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkInsert(
|
||||||
|
"Users",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "Administrador",
|
||||||
|
email: "admin@whaticket.com",
|
||||||
|
passwordHash: "$2a$08$WaEmpmFDD/XkDqorkpQ42eUZozOqRCPkPcTkmHHMyuTGUOkI8dHsq",
|
||||||
|
profile: "admin",
|
||||||
|
tokenVersion: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkDelete("Users", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkInsert(
|
||||||
|
"Settings",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: "userApiToken",
|
||||||
|
value: uuidv4(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return queryInterface.bulkDelete("Settings", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
12
backend/src/errors/AppError.ts
Normal file
12
backend/src/errors/AppError.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class AppError {
|
||||||
|
public readonly message: string;
|
||||||
|
|
||||||
|
public readonly statusCode: number;
|
||||||
|
|
||||||
|
constructor(message: string, statusCode = 400) {
|
||||||
|
this.message = message;
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppError;
|
||||||
18
backend/src/helpers/CheckContactOpenTickets.ts
Normal file
18
backend/src/helpers/CheckContactOpenTickets.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Op } from "sequelize";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
import Ticket from "../models/Ticket";
|
||||||
|
|
||||||
|
const CheckContactOpenTickets = async (
|
||||||
|
contactId: number,
|
||||||
|
whatsappId: number
|
||||||
|
): Promise<void> => {
|
||||||
|
const ticket = await Ticket.findOne({
|
||||||
|
where: { contactId, whatsappId, status: { [Op.or]: ["open", "pending"] } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ticket) {
|
||||||
|
throw new AppError("ERR_OTHER_OPEN_TICKET");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckContactOpenTickets;
|
||||||
16
backend/src/helpers/CheckSettings.ts
Normal file
16
backend/src/helpers/CheckSettings.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Setting from "../models/Setting";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
const CheckSettings = async (key: string): Promise<string> => {
|
||||||
|
const setting = await Setting.findOne({
|
||||||
|
where: { key }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
throw new AppError("ERR_NO_SETTING_FOUND", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckSettings;
|
||||||
23
backend/src/helpers/CreateTokens.ts
Normal file
23
backend/src/helpers/CreateTokens.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { sign } from "jsonwebtoken";
|
||||||
|
import authConfig from "../config/auth";
|
||||||
|
import User from "../models/User";
|
||||||
|
|
||||||
|
export const createAccessToken = (user: User): string => {
|
||||||
|
const { secret, expiresIn } = authConfig;
|
||||||
|
|
||||||
|
return sign(
|
||||||
|
{ usarname: user.name, profile: user.profile, id: user.id },
|
||||||
|
secret,
|
||||||
|
{
|
||||||
|
expiresIn
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRefreshToken = (user: User): string => {
|
||||||
|
const { refreshSecret, refreshExpiresIn } = authConfig;
|
||||||
|
|
||||||
|
return sign({ id: user.id, tokenVersion: user.tokenVersion }, refreshSecret, {
|
||||||
|
expiresIn: refreshExpiresIn
|
||||||
|
});
|
||||||
|
};
|
||||||
41
backend/src/helpers/Debounce.ts
Normal file
41
backend/src/helpers/Debounce.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
interface Timeout {
|
||||||
|
id: number;
|
||||||
|
timeout: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeouts: Timeout[] = [];
|
||||||
|
|
||||||
|
const findAndClearTimeout = (ticketId: number) => {
|
||||||
|
if (timeouts.length > 0) {
|
||||||
|
const timeoutIndex = timeouts.findIndex(timeout => timeout.id === ticketId);
|
||||||
|
|
||||||
|
if (timeoutIndex !== -1) {
|
||||||
|
clearTimeout(timeouts[timeoutIndex].timeout);
|
||||||
|
timeouts.splice(timeoutIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounce = (
|
||||||
|
func: { (): Promise<void>; (...args: never[]): void },
|
||||||
|
wait: number,
|
||||||
|
ticketId: number
|
||||||
|
) => {
|
||||||
|
return function executedFunction(...args: never[]): void {
|
||||||
|
const later = () => {
|
||||||
|
findAndClearTimeout(ticketId);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
findAndClearTimeout(ticketId);
|
||||||
|
|
||||||
|
const newTimeout = {
|
||||||
|
id: ticketId,
|
||||||
|
timeout: setTimeout(later, wait)
|
||||||
|
};
|
||||||
|
|
||||||
|
timeouts.push(newTimeout);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { debounce };
|
||||||
26
backend/src/helpers/GetDefaultWhatsApp.ts
Normal file
26
backend/src/helpers/GetDefaultWhatsApp.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import AppError from "../errors/AppError";
|
||||||
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
import GetDefaultWhatsAppByUser from "./GetDefaultWhatsAppByUser";
|
||||||
|
|
||||||
|
const GetDefaultWhatsApp = async (
|
||||||
|
userId?: number
|
||||||
|
): Promise<Whatsapp> => {
|
||||||
|
if(userId) {
|
||||||
|
const whatsappByUser = await GetDefaultWhatsAppByUser(userId);
|
||||||
|
if(whatsappByUser !== null) {
|
||||||
|
return whatsappByUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultWhatsapp = await Whatsapp.findOne({
|
||||||
|
where: { isDefault: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!defaultWhatsapp) {
|
||||||
|
throw new AppError("ERR_NO_DEF_WAPP_FOUND");
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultWhatsapp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetDefaultWhatsApp;
|
||||||
20
backend/src/helpers/GetDefaultWhatsAppByUser.ts
Normal file
20
backend/src/helpers/GetDefaultWhatsAppByUser.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import User from "../models/User";
|
||||||
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
const GetDefaultWhatsAppByUser = async (
|
||||||
|
userId: number
|
||||||
|
): Promise<Whatsapp | null> => {
|
||||||
|
const user = await User.findByPk(userId, {include: ["whatsapp"]});
|
||||||
|
if( user === null ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(user.whatsapp !== null) {
|
||||||
|
logger.info(`Found whatsapp linked to user '${user.name}' is '${user.whatsapp.name}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.whatsapp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetDefaultWhatsAppByUser;
|
||||||
18
backend/src/helpers/GetTicketWbot.ts
Normal file
18
backend/src/helpers/GetTicketWbot.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Client as Session } from "whatsapp-web.js";
|
||||||
|
import { getWbot } from "../libs/wbot";
|
||||||
|
import GetDefaultWhatsApp from "./GetDefaultWhatsApp";
|
||||||
|
import Ticket from "../models/Ticket";
|
||||||
|
|
||||||
|
const GetTicketWbot = async (ticket: Ticket): Promise<Session> => {
|
||||||
|
if (!ticket.whatsappId) {
|
||||||
|
const defaultWhatsapp = await GetDefaultWhatsApp(ticket.user.id);
|
||||||
|
|
||||||
|
await ticket.$set("whatsapp", defaultWhatsapp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wbot = getWbot(ticket.whatsappId);
|
||||||
|
|
||||||
|
return wbot;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetTicketWbot;
|
||||||
44
backend/src/helpers/GetWbotMessage.ts
Normal file
44
backend/src/helpers/GetWbotMessage.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Message as WbotMessage } from "whatsapp-web.js";
|
||||||
|
import Ticket from "../models/Ticket";
|
||||||
|
import GetTicketWbot from "./GetTicketWbot";
|
||||||
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
export const GetWbotMessage = async (
|
||||||
|
ticket: Ticket,
|
||||||
|
messageId: string
|
||||||
|
): Promise<WbotMessage> => {
|
||||||
|
const wbot = await GetTicketWbot(ticket);
|
||||||
|
|
||||||
|
const wbotChat = await wbot.getChatById(
|
||||||
|
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`
|
||||||
|
);
|
||||||
|
|
||||||
|
let limit = 20;
|
||||||
|
|
||||||
|
const fetchWbotMessagesGradually = async (): Promise<void | WbotMessage> => {
|
||||||
|
const chatMessages = await wbotChat.fetchMessages({ limit });
|
||||||
|
|
||||||
|
const msgFound = chatMessages.find(msg => msg.id.id === messageId);
|
||||||
|
|
||||||
|
if (!msgFound && limit < 100) {
|
||||||
|
limit += 20;
|
||||||
|
return fetchWbotMessagesGradually();
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgFound;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msgFound = await fetchWbotMessagesGradually();
|
||||||
|
|
||||||
|
if (!msgFound) {
|
||||||
|
throw new Error("Cannot found message within 100 last messages");
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgFound;
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppError("ERR_FETCH_WAPP_MSG");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetWbotMessage;
|
||||||
9
backend/src/helpers/Mustache.ts
Normal file
9
backend/src/helpers/Mustache.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Mustache from "mustache";
|
||||||
|
import Contact from "../models/Contact";
|
||||||
|
|
||||||
|
export default (body: string, contact: Contact): string => {
|
||||||
|
const view = {
|
||||||
|
name: contact ? contact.name : ""
|
||||||
|
};
|
||||||
|
return Mustache.render(body, view);
|
||||||
|
};
|
||||||
5
backend/src/helpers/SendRefreshToken.ts
Normal file
5
backend/src/helpers/SendRefreshToken.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
export const SendRefreshToken = (res: Response, token: string): void => {
|
||||||
|
res.cookie("jrt", token, { httpOnly: true });
|
||||||
|
};
|
||||||
23
backend/src/helpers/SerializeUser.ts
Normal file
23
backend/src/helpers/SerializeUser.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Queue from "../models/Queue";
|
||||||
|
import User from "../models/User";
|
||||||
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
|
||||||
|
interface SerializedUser {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
profile: string;
|
||||||
|
queues: Queue[];
|
||||||
|
whatsapp: Whatsapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SerializeUser = (user: User): SerializedUser => {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
profile: user.profile,
|
||||||
|
queues: user.queues,
|
||||||
|
whatsapp: user.whatsapp
|
||||||
|
};
|
||||||
|
};
|
||||||
12
backend/src/helpers/SerializeWbotMsgId.ts
Normal file
12
backend/src/helpers/SerializeWbotMsgId.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Message from "../models/Message";
|
||||||
|
import Ticket from "../models/Ticket";
|
||||||
|
|
||||||
|
const SerializeWbotMsgId = (ticket: Ticket, message: Message): string => {
|
||||||
|
const serializedMsgId = `${message.fromMe}_${ticket.contact.number}@${
|
||||||
|
ticket.isGroup ? "g" : "c"
|
||||||
|
}.us_${message.id}`;
|
||||||
|
|
||||||
|
return serializedMsgId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SerializeWbotMsgId;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user