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