Initial commit

This commit is contained in:
2023-02-23 18:13:04 -06:00
commit 43c3e1563f
298 changed files with 18530 additions and 0 deletions

5
backend/src/@types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
export interface Request {
user: { id: string; profile: string };
}
}

View File

@@ -0,0 +1 @@
declare module "qrcode-terminal";

View 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");
}
});
});

View 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);
}
});
});

View 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
);
});
});

View 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);
});
});

View 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
);
});
});

View 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
);
});
});

View 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
View 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
View File

@@ -0,0 +1,5 @@
import dotenv from "dotenv";
dotenv.config({
path: process.env.NODE_ENV === "test" ? ".env.test" : ".env"
});

View File

@@ -0,0 +1,6 @@
export default {
secret: process.env.JWT_SECRET || "mysecret",
expiresIn: "15m",
refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret",
refreshExpiresIn: "7d"
};

View 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
};

View 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);
}
})
};

View 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();
};

View 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" });
};

View 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" });
};

View 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();
};

View 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();
};

View 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" });
};

View 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();
};

View 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);
};

View 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" });
};

View 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" });
};

View 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." });
};

View 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 };

View 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;

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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"
});
}
};

View File

@@ -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");
}
};

View File

@@ -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
});
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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"
});
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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", {});
}
};

View File

@@ -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", {});
}
};

View File

@@ -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", {});
}
};

View 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;

View 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;

View 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;

View 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
});
};

View 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 };

View 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;

View 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;

View 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;

View 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;

View 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);
};

View File

@@ -0,0 +1,5 @@
import { Response } from "express";
export const SendRefreshToken = (res: Response, token: string): void => {
res.cookie("jrt", token, { httpOnly: true });
};

View 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
};
};

View 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;

View 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;

View 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;

View 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
View 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);
}
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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