mirror of
https://github.com/cheveguerra/Whaticket.git
synced 2026-04-18 19:49:15 +00:00
Initial commit
This commit is contained in:
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;
|
||||
38
backend/src/helpers/SetTicketMessagesAsRead.ts
Normal file
38
backend/src/helpers/SetTicketMessagesAsRead.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { getIO } from "../libs/socket";
|
||||
import Message from "../models/Message";
|
||||
import Ticket from "../models/Ticket";
|
||||
import { logger } from "../utils/logger";
|
||||
import GetTicketWbot from "./GetTicketWbot";
|
||||
|
||||
const SetTicketMessagesAsRead = async (ticket: Ticket): Promise<void> => {
|
||||
await Message.update(
|
||||
{ read: true },
|
||||
{
|
||||
where: {
|
||||
ticketId: ticket.id,
|
||||
read: false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await ticket.update({ unreadMessages: 0 });
|
||||
|
||||
try {
|
||||
const wbot = await GetTicketWbot(ticket);
|
||||
await wbot.sendSeen(
|
||||
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Could not mark messages as read. Maybe whatsapp session disconnected? Err: ${err}`
|
||||
);
|
||||
}
|
||||
|
||||
const io = getIO();
|
||||
io.to(ticket.status).to("notification").emit("ticket", {
|
||||
action: "updateUnread",
|
||||
ticketId: ticket.id
|
||||
});
|
||||
};
|
||||
|
||||
export default SetTicketMessagesAsRead;
|
||||
17
backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts
Normal file
17
backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Ticket from "../models/Ticket";
|
||||
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
|
||||
|
||||
const UpdateDeletedUserOpenTicketsStatus = async (
|
||||
tickets: Ticket[]
|
||||
): Promise<void> => {
|
||||
tickets.forEach(async t => {
|
||||
const ticketId = t.id.toString();
|
||||
|
||||
await UpdateTicketService({
|
||||
ticketData: { status: "pending" },
|
||||
ticketId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default UpdateDeletedUserOpenTicketsStatus;
|
||||
44
backend/src/libs/socket.ts
Normal file
44
backend/src/libs/socket.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Server as SocketIO } from "socket.io";
|
||||
import { Server } from "http";
|
||||
import AppError from "../errors/AppError";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
let io: SocketIO;
|
||||
|
||||
export const initIO = (httpServer: Server): SocketIO => {
|
||||
io = new SocketIO(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL
|
||||
}
|
||||
});
|
||||
|
||||
io.on("connection", socket => {
|
||||
logger.info("Client Connected");
|
||||
socket.on("joinChatBox", (ticketId: string) => {
|
||||
logger.info("A client joined a ticket channel");
|
||||
socket.join(ticketId);
|
||||
});
|
||||
|
||||
socket.on("joinNotification", () => {
|
||||
logger.info("A client joined notification channel");
|
||||
socket.join("notification");
|
||||
});
|
||||
|
||||
socket.on("joinTickets", (status: string) => {
|
||||
logger.info(`A client joined to ${status} tickets channel.`);
|
||||
socket.join(status);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
logger.info("Client disconnected");
|
||||
});
|
||||
});
|
||||
return io;
|
||||
};
|
||||
|
||||
export const getIO = (): SocketIO => {
|
||||
if (!io) {
|
||||
throw new AppError("Socket IO not initialized");
|
||||
}
|
||||
return io;
|
||||
};
|
||||
155
backend/src/libs/wbot.ts
Normal file
155
backend/src/libs/wbot.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import qrCode from "qrcode-terminal";
|
||||
import { Client, LocalAuth } from "whatsapp-web.js";
|
||||
import { getIO } from "./socket";
|
||||
import Whatsapp from "../models/Whatsapp";
|
||||
import AppError from "../errors/AppError";
|
||||
import { logger } from "../utils/logger";
|
||||
import { handleMessage } from "../services/WbotServices/wbotMessageListener";
|
||||
|
||||
interface Session extends Client {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
const sessions: Session[] = [];
|
||||
|
||||
const syncUnreadMessages = async (wbot: Session) => {
|
||||
const chats = await wbot.getChats();
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const chat of chats) {
|
||||
if (chat.unreadCount > 0) {
|
||||
const unreadMessages = await chat.fetchMessages({
|
||||
limit: chat.unreadCount
|
||||
});
|
||||
|
||||
for (const msg of unreadMessages) {
|
||||
await handleMessage(msg, wbot);
|
||||
}
|
||||
|
||||
await chat.sendSeen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const io = getIO();
|
||||
const sessionName = whatsapp.name;
|
||||
let sessionCfg;
|
||||
|
||||
if (whatsapp && whatsapp.session) {
|
||||
sessionCfg = JSON.parse(whatsapp.session);
|
||||
}
|
||||
|
||||
const args:String = process.env.CHROME_ARGS || "";
|
||||
|
||||
const wbot: Session = new Client({
|
||||
session: sessionCfg,
|
||||
authStrategy: new LocalAuth({clientId: 'bd_'+whatsapp.id}),
|
||||
puppeteer: {
|
||||
executablePath: process.env.CHROME_BIN || undefined,
|
||||
// @ts-ignore
|
||||
browserWSEndpoint: process.env.CHROME_WS || undefined,
|
||||
args: args.split(' ')
|
||||
}
|
||||
});
|
||||
|
||||
wbot.initialize();
|
||||
|
||||
wbot.on("qr", async qr => {
|
||||
logger.info("Session:", sessionName);
|
||||
qrCode.generate(qr, { small: true });
|
||||
await whatsapp.update({ qrcode: qr, status: "qrcode", retries: 0 });
|
||||
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
|
||||
if (sessionIndex === -1) {
|
||||
wbot.id = whatsapp.id;
|
||||
sessions.push(wbot);
|
||||
}
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
});
|
||||
|
||||
wbot.on("authenticated", async session => {
|
||||
logger.info(`Session: ${sessionName} AUTHENTICATED`);
|
||||
});
|
||||
|
||||
wbot.on("auth_failure", async msg => {
|
||||
console.error(
|
||||
`Session: ${sessionName} AUTHENTICATION FAILURE! Reason: ${msg}`
|
||||
);
|
||||
|
||||
if (whatsapp.retries > 1) {
|
||||
await whatsapp.update({ session: "", retries: 0 });
|
||||
}
|
||||
|
||||
const retry = whatsapp.retries;
|
||||
await whatsapp.update({
|
||||
status: "DISCONNECTED",
|
||||
retries: retry + 1
|
||||
});
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
|
||||
reject(new Error("Error starting whatsapp session."));
|
||||
});
|
||||
|
||||
wbot.on("ready", async () => {
|
||||
logger.info(`Session: ${sessionName} READY`);
|
||||
|
||||
await whatsapp.update({
|
||||
status: "CONNECTED",
|
||||
qrcode: "",
|
||||
retries: 0
|
||||
});
|
||||
|
||||
io.emit("whatsappSession", {
|
||||
action: "update",
|
||||
session: whatsapp
|
||||
});
|
||||
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
|
||||
if (sessionIndex === -1) {
|
||||
wbot.id = whatsapp.id;
|
||||
sessions.push(wbot);
|
||||
}
|
||||
|
||||
wbot.sendPresenceAvailable();
|
||||
await syncUnreadMessages(wbot);
|
||||
|
||||
resolve(wbot);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getWbot = (whatsappId: number): Session => {
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
|
||||
|
||||
if (sessionIndex === -1) {
|
||||
throw new AppError("ERR_WAPP_NOT_INITIALIZED");
|
||||
}
|
||||
return sessions[sessionIndex];
|
||||
};
|
||||
|
||||
export const removeWbot = (whatsappId: number): void => {
|
||||
try {
|
||||
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
|
||||
if (sessionIndex !== -1) {
|
||||
sessions[sessionIndex].destroy();
|
||||
sessions.splice(sessionIndex, 1);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
42
backend/src/middleware/isAuth.ts
Normal file
42
backend/src/middleware/isAuth.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { verify } from "jsonwebtoken";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import AppError from "../errors/AppError";
|
||||
import authConfig from "../config/auth";
|
||||
|
||||
interface TokenPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
profile: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
const isAuth = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
|
||||
const [, token] = authHeader.split(" ");
|
||||
|
||||
try {
|
||||
const decoded = verify(token, authConfig.secret);
|
||||
const { id, profile } = decoded as TokenPayload;
|
||||
|
||||
req.user = {
|
||||
id,
|
||||
profile
|
||||
};
|
||||
} catch (err) {
|
||||
throw new AppError(
|
||||
"Invalid token. We'll try to assign a new one on next request",
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export default isAuth;
|
||||
39
backend/src/middleware/isAuthApi.ts
Normal file
39
backend/src/middleware/isAuthApi.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import AppError from "../errors/AppError";
|
||||
import ListSettingByValueService from "../services/SettingServices/ListSettingByValueService";
|
||||
|
||||
const isAuthApi = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
|
||||
const [, token] = authHeader.split(" ");
|
||||
|
||||
try {
|
||||
const getToken = await ListSettingByValueService(token);
|
||||
if (!getToken) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
|
||||
if (getToken.value !== token) {
|
||||
throw new AppError("ERR_SESSION_EXPIRED", 401);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new AppError(
|
||||
"Invalid token. We'll try to assign a new one on next request",
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export default isAuthApi;
|
||||
57
backend/src/models/Contact.ts
Normal file
57
backend/src/models/Contact.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
AllowNull,
|
||||
Unique,
|
||||
Default,
|
||||
HasMany
|
||||
} from "sequelize-typescript";
|
||||
import ContactCustomField from "./ContactCustomField";
|
||||
import Ticket from "./Ticket";
|
||||
|
||||
@Table
|
||||
class Contact extends Model<Contact> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
number: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Default("")
|
||||
@Column
|
||||
email: string;
|
||||
|
||||
@Column
|
||||
profilePicUrl: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isGroup: boolean;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@HasMany(() => Ticket)
|
||||
tickets: Ticket[];
|
||||
|
||||
@HasMany(() => ContactCustomField)
|
||||
extraInfo: ContactCustomField[];
|
||||
}
|
||||
|
||||
export default Contact;
|
||||
41
backend/src/models/ContactCustomField.ts
Normal file
41
backend/src/models/ContactCustomField.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
ForeignKey,
|
||||
BelongsTo
|
||||
} from "sequelize-typescript";
|
||||
import Contact from "./Contact";
|
||||
|
||||
@Table
|
||||
class ContactCustomField extends Model<ContactCustomField> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@Column
|
||||
value: string;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact)
|
||||
contact: Contact;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default ContactCustomField;
|
||||
84
backend/src/models/Message.ts
Normal file
84
backend/src/models/Message.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
DataType,
|
||||
PrimaryKey,
|
||||
Default,
|
||||
BelongsTo,
|
||||
ForeignKey
|
||||
} from "sequelize-typescript";
|
||||
import Contact from "./Contact";
|
||||
import Ticket from "./Ticket";
|
||||
|
||||
@Table
|
||||
class Message extends Model<Message> {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
id: string;
|
||||
|
||||
@Default(0)
|
||||
@Column
|
||||
ack: number;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
read: boolean;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
fromMe: boolean;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
body: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
get mediaUrl(): string | null {
|
||||
if (this.getDataValue("mediaUrl")) {
|
||||
return `${process.env.BACKEND_URL}:${
|
||||
process.env.PROXY_PORT
|
||||
}/public/${this.getDataValue("mediaUrl")}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Column
|
||||
mediaType: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE(6))
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE(6))
|
||||
updatedAt: Date;
|
||||
|
||||
@ForeignKey(() => Message)
|
||||
@Column
|
||||
quotedMsgId: string;
|
||||
|
||||
@BelongsTo(() => Message, "quotedMsgId")
|
||||
quotedMsg: Message;
|
||||
|
||||
@ForeignKey(() => Ticket)
|
||||
@Column
|
||||
ticketId: number;
|
||||
|
||||
@BelongsTo(() => Ticket)
|
||||
ticket: Ticket;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact, "contactId")
|
||||
contact: Contact;
|
||||
}
|
||||
|
||||
export default Message;
|
||||
52
backend/src/models/Queue.ts
Normal file
52
backend/src/models/Queue.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
AllowNull,
|
||||
Unique,
|
||||
BelongsToMany
|
||||
} from "sequelize-typescript";
|
||||
import User from "./User";
|
||||
import UserQueue from "./UserQueue";
|
||||
|
||||
import Whatsapp from "./Whatsapp";
|
||||
import WhatsappQueue from "./WhatsappQueue";
|
||||
|
||||
@Table
|
||||
class Queue extends Model<Queue> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Unique
|
||||
@Column
|
||||
color: string;
|
||||
|
||||
@Column
|
||||
greetingMessage: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@BelongsToMany(() => Whatsapp, () => WhatsappQueue)
|
||||
whatsapps: Array<Whatsapp & { WhatsappQueue: WhatsappQueue }>;
|
||||
|
||||
@BelongsToMany(() => User, () => UserQueue)
|
||||
users: Array<User & { UserQueue: UserQueue }>;
|
||||
}
|
||||
|
||||
export default Queue;
|
||||
32
backend/src/models/QuickAnswer.ts
Normal file
32
backend/src/models/QuickAnswer.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
DataType,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
AutoIncrement
|
||||
} from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
class QuickAnswer extends Model<QuickAnswer> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
shortcut: string;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
message: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default QuickAnswer;
|
||||
26
backend/src/models/Setting.ts
Normal file
26
backend/src/models/Setting.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey
|
||||
} from "sequelize-typescript";
|
||||
|
||||
@Table
|
||||
class Setting extends Model<Setting> {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
key: string;
|
||||
|
||||
@Column
|
||||
value: string;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default Setting;
|
||||
79
backend/src/models/Ticket.ts
Normal file
79
backend/src/models/Ticket.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
ForeignKey,
|
||||
BelongsTo,
|
||||
HasMany,
|
||||
AutoIncrement,
|
||||
Default
|
||||
} from "sequelize-typescript";
|
||||
|
||||
import Contact from "./Contact";
|
||||
import Message from "./Message";
|
||||
import Queue from "./Queue";
|
||||
import User from "./User";
|
||||
import Whatsapp from "./Whatsapp";
|
||||
|
||||
@Table
|
||||
class Ticket extends Model<Ticket> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column({ defaultValue: "pending" })
|
||||
status: string;
|
||||
|
||||
@Column
|
||||
unreadMessages: number;
|
||||
|
||||
@Column
|
||||
lastMessage: string;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
isGroup: boolean;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId: number;
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user: User;
|
||||
|
||||
@ForeignKey(() => Contact)
|
||||
@Column
|
||||
contactId: number;
|
||||
|
||||
@BelongsTo(() => Contact)
|
||||
contact: Contact;
|
||||
|
||||
@ForeignKey(() => Whatsapp)
|
||||
@Column
|
||||
whatsappId: number;
|
||||
|
||||
@BelongsTo(() => Whatsapp)
|
||||
whatsapp: Whatsapp;
|
||||
|
||||
@ForeignKey(() => Queue)
|
||||
@Column
|
||||
queueId: number;
|
||||
|
||||
@BelongsTo(() => Queue)
|
||||
queue: Queue;
|
||||
|
||||
@HasMany(() => Message)
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export default Ticket;
|
||||
83
backend/src/models/User.ts
Normal file
83
backend/src/models/User.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
DataType,
|
||||
BeforeCreate,
|
||||
BeforeUpdate,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
Default,
|
||||
HasMany,
|
||||
BelongsToMany,
|
||||
ForeignKey,
|
||||
BelongsTo
|
||||
} from "sequelize-typescript";
|
||||
import { hash, compare } from "bcryptjs";
|
||||
import Ticket from "./Ticket";
|
||||
import Queue from "./Queue";
|
||||
import UserQueue from "./UserQueue";
|
||||
import Whatsapp from "./Whatsapp";
|
||||
|
||||
@Table
|
||||
class User extends Model<User> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@Column
|
||||
email: string;
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
password: string;
|
||||
|
||||
@Column
|
||||
passwordHash: string;
|
||||
|
||||
@Default(0)
|
||||
@Column
|
||||
tokenVersion: number;
|
||||
|
||||
@Default("admin")
|
||||
@Column
|
||||
profile: string;
|
||||
|
||||
@ForeignKey(() => Whatsapp)
|
||||
@Column
|
||||
whatsappId: number;
|
||||
|
||||
@BelongsTo(() => Whatsapp)
|
||||
whatsapp: Whatsapp;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@HasMany(() => Ticket)
|
||||
tickets: Ticket[];
|
||||
|
||||
@BelongsToMany(() => Queue, () => UserQueue)
|
||||
queues: Queue[];
|
||||
|
||||
@BeforeUpdate
|
||||
@BeforeCreate
|
||||
static hashPassword = async (instance: User): Promise<void> => {
|
||||
if (instance.password) {
|
||||
instance.passwordHash = await hash(instance.password, 8);
|
||||
}
|
||||
};
|
||||
|
||||
public checkPassword = async (password: string): Promise<boolean> => {
|
||||
return compare(password, this.getDataValue("passwordHash"));
|
||||
};
|
||||
}
|
||||
|
||||
export default User;
|
||||
29
backend/src/models/UserQueue.ts
Normal file
29
backend/src/models/UserQueue.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
ForeignKey
|
||||
} from "sequelize-typescript";
|
||||
import Queue from "./Queue";
|
||||
import User from "./User";
|
||||
|
||||
@Table
|
||||
class UserQueue extends Model<UserQueue> {
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId: number;
|
||||
|
||||
@ForeignKey(() => Queue)
|
||||
@Column
|
||||
queueId: number;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export default UserQueue;
|
||||
77
backend/src/models/Whatsapp.ts
Normal file
77
backend/src/models/Whatsapp.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
DataType,
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
Default,
|
||||
AllowNull,
|
||||
HasMany,
|
||||
Unique,
|
||||
BelongsToMany
|
||||
} from "sequelize-typescript";
|
||||
import Queue from "./Queue";
|
||||
import Ticket from "./Ticket";
|
||||
import WhatsappQueue from "./WhatsappQueue";
|
||||
|
||||
@Table
|
||||
class Whatsapp extends Model<Whatsapp> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull
|
||||
@Unique
|
||||
@Column(DataType.TEXT)
|
||||
name: string;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
session: string;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
qrcode: string;
|
||||
|
||||
@Column
|
||||
status: string;
|
||||
|
||||
@Column
|
||||
battery: string;
|
||||
|
||||
@Column
|
||||
plugged: boolean;
|
||||
|
||||
@Column
|
||||
retries: number;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
greetingMessage: string;
|
||||
|
||||
@Column(DataType.TEXT)
|
||||
farewellMessage: string;
|
||||
|
||||
@Default(false)
|
||||
@AllowNull
|
||||
@Column
|
||||
isDefault: boolean;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@HasMany(() => Ticket)
|
||||
tickets: Ticket[];
|
||||
|
||||
@BelongsToMany(() => Queue, () => WhatsappQueue)
|
||||
queues: Array<Queue & { WhatsappQueue: WhatsappQueue }>;
|
||||
|
||||
@HasMany(() => WhatsappQueue)
|
||||
whatsappQueues: WhatsappQueue[];
|
||||
}
|
||||
|
||||
export default Whatsapp;
|
||||
33
backend/src/models/WhatsappQueue.ts
Normal file
33
backend/src/models/WhatsappQueue.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Model,
|
||||
ForeignKey,
|
||||
BelongsTo
|
||||
} from "sequelize-typescript";
|
||||
import Queue from "./Queue";
|
||||
import Whatsapp from "./Whatsapp";
|
||||
|
||||
@Table
|
||||
class WhatsappQueue extends Model<WhatsappQueue> {
|
||||
@ForeignKey(() => Whatsapp)
|
||||
@Column
|
||||
whatsappId: number;
|
||||
|
||||
@ForeignKey(() => Queue)
|
||||
@Column
|
||||
queueId: number;
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
@BelongsTo(() => Queue)
|
||||
queue: Queue;
|
||||
}
|
||||
|
||||
export default WhatsappQueue;
|
||||
14
backend/src/routes/apiRoutes.ts
Normal file
14
backend/src/routes/apiRoutes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import uploadConfig from "../config/upload";
|
||||
|
||||
import * as ApiController from "../controllers/ApiController";
|
||||
import isAuthApi from "../middleware/isAuthApi";
|
||||
|
||||
const upload = multer(uploadConfig);
|
||||
|
||||
const ApiRoutes = express.Router();
|
||||
|
||||
ApiRoutes.post("/send", isAuthApi, upload.array("medias"), ApiController.index);
|
||||
|
||||
export default ApiRoutes;
|
||||
16
backend/src/routes/authRoutes.ts
Normal file
16
backend/src/routes/authRoutes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from "express";
|
||||
import * as SessionController from "../controllers/SessionController";
|
||||
import * as UserController from "../controllers/UserController";
|
||||
import isAuth from "../middleware/isAuth";
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
authRoutes.post("/signup", UserController.store);
|
||||
|
||||
authRoutes.post("/login", SessionController.store);
|
||||
|
||||
authRoutes.post("/refresh_token", SessionController.update);
|
||||
|
||||
authRoutes.delete("/logout", isAuth, SessionController.remove);
|
||||
|
||||
export default authRoutes;
|
||||
27
backend/src/routes/contactRoutes.ts
Normal file
27
backend/src/routes/contactRoutes.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from "express";
|
||||
import isAuth from "../middleware/isAuth";
|
||||
|
||||
import * as ContactController from "../controllers/ContactController";
|
||||
import * as ImportPhoneContactsController from "../controllers/ImportPhoneContactsController";
|
||||
|
||||
const contactRoutes = express.Router();
|
||||
|
||||
contactRoutes.post(
|
||||
"/contacts/import",
|
||||
isAuth,
|
||||
ImportPhoneContactsController.store
|
||||
);
|
||||
|
||||
contactRoutes.get("/contacts", isAuth, ContactController.index);
|
||||
|
||||
contactRoutes.get("/contacts/:contactId", isAuth, ContactController.show);
|
||||
|
||||
contactRoutes.post("/contacts", isAuth, ContactController.store);
|
||||
|
||||
contactRoutes.post("/contact", isAuth, ContactController.getContact);
|
||||
|
||||
contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update);
|
||||
|
||||
contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove);
|
||||
|
||||
export default contactRoutes;
|
||||
29
backend/src/routes/index.ts
Normal file
29
backend/src/routes/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import userRoutes from "./userRoutes";
|
||||
import authRoutes from "./authRoutes";
|
||||
import settingRoutes from "./settingRoutes";
|
||||
import contactRoutes from "./contactRoutes";
|
||||
import ticketRoutes from "./ticketRoutes";
|
||||
import whatsappRoutes from "./whatsappRoutes";
|
||||
import messageRoutes from "./messageRoutes";
|
||||
import whatsappSessionRoutes from "./whatsappSessionRoutes";
|
||||
import queueRoutes from "./queueRoutes";
|
||||
import quickAnswerRoutes from "./quickAnswerRoutes";
|
||||
import apiRoutes from "./apiRoutes";
|
||||
|
||||
const routes = Router();
|
||||
|
||||
routes.use(userRoutes);
|
||||
routes.use("/auth", authRoutes);
|
||||
routes.use(settingRoutes);
|
||||
routes.use(contactRoutes);
|
||||
routes.use(ticketRoutes);
|
||||
routes.use(whatsappRoutes);
|
||||
routes.use(messageRoutes);
|
||||
routes.use(whatsappSessionRoutes);
|
||||
routes.use(queueRoutes);
|
||||
routes.use(quickAnswerRoutes);
|
||||
routes.use("/api/messages", apiRoutes);
|
||||
|
||||
export default routes;
|
||||
23
backend/src/routes/messageRoutes.ts
Normal file
23
backend/src/routes/messageRoutes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import isAuth from "../middleware/isAuth";
|
||||
import uploadConfig from "../config/upload";
|
||||
|
||||
import * as MessageController from "../controllers/MessageController";
|
||||
|
||||
const messageRoutes = Router();
|
||||
|
||||
const upload = multer(uploadConfig);
|
||||
|
||||
messageRoutes.get("/messages/:ticketId", isAuth, MessageController.index);
|
||||
|
||||
messageRoutes.post(
|
||||
"/messages/:ticketId",
|
||||
isAuth,
|
||||
upload.array("medias"),
|
||||
MessageController.store
|
||||
);
|
||||
|
||||
messageRoutes.delete("/messages/:messageId", isAuth, MessageController.remove);
|
||||
|
||||
export default messageRoutes;
|
||||
18
backend/src/routes/queueRoutes.ts
Normal file
18
backend/src/routes/queueRoutes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Router } from "express";
|
||||
import isAuth from "../middleware/isAuth";
|
||||
|
||||
import * as QueueController from "../controllers/QueueController";
|
||||
|
||||
const queueRoutes = Router();
|
||||
|
||||
queueRoutes.get("/queue", isAuth, QueueController.index);
|
||||
|
||||
queueRoutes.post("/queue", isAuth, QueueController.store);
|
||||
|
||||
queueRoutes.get("/queue/:queueId", isAuth, QueueController.show);
|
||||
|
||||
queueRoutes.put("/queue/:queueId", isAuth, QueueController.update);
|
||||
|
||||
queueRoutes.delete("/queue/:queueId", isAuth, QueueController.remove);
|
||||
|
||||
export default queueRoutes;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user