- Finish cleanup

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-22 23:00:31 -05:00
parent 82dc9e1c56
commit 2e7232bb65
27 changed files with 674 additions and 611 deletions

View File

@@ -51,6 +51,12 @@ app.use(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); app.use(bodyParser.urlencoded({limit: "50mb", extended: true}));
app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]}));
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
// Route groupings // Route groupings
app.use('/', require("./server/routes/miscellaneousRoutes")); app.use('/', require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes")); app.use("/notifications", require("./server/routes/notificationsRoutes"));
@@ -85,4 +91,4 @@ main()
}) })
.catch((error) => { .catch((error) => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error); logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error);
}); });

View File

@@ -1,269 +1,269 @@
const path = require("path"); const path = require("path");
require("dotenv").config({ require("dotenv").config({
path: path.resolve( path: path.resolve(
process.cwd(), process.cwd(),
`.env.${process.env.NODE_ENV || "development"}` `.env.${process.env.NODE_ENV || "development"}`
), ),
}); });
const axios = require("axios"); const axios = require("axios");
let nodemailer = require("nodemailer"); let nodemailer = require("nodemailer");
let aws = require("@aws-sdk/client-ses"); let aws = require("@aws-sdk/client-ses");
let { defaultProvider } = require("@aws-sdk/credential-provider-node"); let {defaultProvider} = require("@aws-sdk/credential-provider-node");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client; const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
const ses = new aws.SES({ const ses = new aws.SES({
// The key apiVersion is no longer supported in v3, and can be removed. // The key apiVersion is no longer supported in v3, and can be removed.
// @deprecated The client uses the "latest" apiVersion. // @deprecated The client uses the "latest" apiVersion.
apiVersion: "latest", apiVersion: "latest",
region: "ca-central-1", region: "ca-central-1",
defaultProvider defaultProvider
}); });
let transporter = nodemailer.createTransport({ let transporter = nodemailer.createTransport({
SES: { ses, aws }, SES: {ses, aws},
}); });
exports.sendServerEmail = async function ({ subject, text }) { exports.sendServerEmail = async function ({subject, text}) {
if (process.env.NODE_ENV === undefined) return; if (process.env.NODE_ENV === undefined) return;
try { try {
transporter.sendMail( transporter.sendMail(
{
from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
subject: subject,
text: text,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{ {
Name: "tag_name", from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
Value: "tag_value", to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
subject: subject,
text: text,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
}, },
], (err, info) => {
}, console.log(err || info);
}, }
(err, info) => { );
console.log(err || info); } catch (error) {
} console.log(error);
); logger.log("server-email-failure", "error", null, null, error);
} catch (error) { }
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
}; };
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) { exports.sendTaskEmail = async function ({to, subject, text, attachments}) {
try { try {
transporter.sendMail( transporter.sendMail(
{ {
from: `ImEX Online <noreply@imex.online>`, from: `ImEX Online <noreply@imex.online>`,
to: to, to: to,
subject: subject, subject: subject,
text: text, text: text,
attachments: attachments || null, attachments: attachments || null,
}, },
(err, info) => { (err, info) => {
console.log(err || info); console.log(err || info);
} }
); );
} catch (error) { } catch (error) {
console.log(error); console.log(error);
logger.log("server-email-failure", "error", null, null, error); logger.log("server-email-failure", "error", null, null, error);
} }
}; };
exports.sendEmail = async (req, res) => { exports.sendEmail = async (req, res) => {
logger.log("send-email", "DEBUG", req.user.email, null, { logger.log("send-email", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`, from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email, replyTo: req.body.ReplyTo.Email,
to: req.body.to, to: req.body.to,
cc: req.body.cc, cc: req.body.cc,
subject: req.body.subject, subject: req.body.subject,
}); });
let downloadedMedia = []; let downloadedMedia = [];
if (req.body.media && req.body.media.length > 0) { if (req.body.media && req.body.media.length > 0) {
downloadedMedia = await Promise.all( downloadedMedia = await Promise.all(
req.body.media.map((m) => { req.body.media.map((m) => {
try { try {
return getImage(m); return getImage(m);
} catch (error) { } catch (error) {
logger.log("send-email-error", "ERROR", req.user.email, null, { logger.log("send-email-error", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error,
});
}
})
);
}
transporter.sendMail(
{
from: `${req.body.from.name} <${req.body.from.address}>`, from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email, replyTo: req.body.ReplyTo.Email,
to: req.body.to, to: req.body.to,
cc: req.body.cc, cc: req.body.cc,
subject: req.body.subject, subject: req.body.subject,
error, attachments:
}); [
...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return {
path: a,
};
}),
] || null,
html: req.body.html,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
messageId: info.response,
});
res.json({
success: true, //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({success: false, error: err});
}
} }
})
); );
}
transporter.sendMail(
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
attachments:
[
...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return {
path: a,
};
}),
] || null,
html: req.body.html,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
messageId: info.response,
});
res.json({
success: true, //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({ success: false, error: err });
}
}
);
}; };
async function getImage(imageUrl) { async function getImage(imageUrl) {
let image = await axios.get(imageUrl, { responseType: "arraybuffer" }); let image = await axios.get(imageUrl, {responseType: "arraybuffer"});
let raw = Buffer.from(image.data).toString("base64"); let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw; return "data:" + image.headers["content-type"] + ";base64," + raw;
} }
async function logEmail(req, email) { async function logEmail(req, email) {
try { try {
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, { const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
email: { email: {
to: email.to, to: email.to,
cc: email.cc, cc: email.cc,
subject: email.subject, subject: email.subject,
bodyshopid: req.body.bodyshopid, bodyshopid: req.body.bodyshopid,
useremail: req.user.email, useremail: req.user.email,
contents: req.body.html, contents: req.body.html,
jobid: req.body.jobid, jobid: req.body.jobid,
sesmessageid: email.messageId, sesmessageid: email.messageId,
status: "Sent", status: "Sent",
}, },
}); });
console.log(insertresult); console.log(insertresult);
} catch (error) { } catch (error) {
logger.log("email-log-error", "error", req.user.email, null, { logger.log("email-log-error", "error", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`, from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to, to: req.body.to,
cc: req.body.cc, cc: req.body.cc,
subject: req.body.subject, subject: req.body.subject,
// info, // info,
}); });
} }
} }
exports.emailBounce = async function (req, res, next) { exports.emailBounce = async function (req, res) {
try { try {
const body = JSON.parse(req.body); const body = JSON.parse(req.body);
if (body.Type === "SubscriptionConfirmation") { if (body.Type === "SubscriptionConfirmation") {
logger.log("SNS-message", "DEBUG", "api", null, { logger.log("SNS-message", "DEBUG", "api", null, {
body: req.body, body: req.body,
}); });
}
const message = JSON.parse(body.Message);
if (message.notificationType === "Bounce") {
let replyTo, subject, messageId;
message.mail.headers.forEach((header) => {
if (header.name === "Reply-To") {
replyTo = header.value;
} else if (header.name === "Subject") {
subject = header.value;
} }
}); const message = JSON.parse(body.Message);
messageId = message.mail.messageId; if (message.notificationType === "Bounce") {
if (replyTo === "noreply@imex.online") { let replyTo, subject, messageId;
res.sendStatus(200); message.mail.headers.forEach((header) => {
return; if (header.name === "Reply-To") {
} replyTo = header.value;
//If it's bounced, log it as bounced in audit log. Send an email to the user. } else if (header.name === "Subject") {
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { subject = header.value;
sesid: messageId, }
status: "Bounced", });
context: message.bounce?.bouncedRecipients, messageId = message.mail.messageId;
}); if (replyTo === "noreply@imex.online") {
transporter.sendMail( res.sendStatus(200);
{ return;
from: `ImEX Online <noreply@imex.online>`, }
to: replyTo, //If it's bounced, log it as bounced in audit log. Send an email to the user.
//bcc: "patrick@snapt.ca", const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
subject: `ImEX Online Bounced Email - RE: ${subject}`, sesid: messageId,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. status: "Bounced",
context: message.bounce?.bouncedRecipients,
});
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: replyTo,
//bcc: "patrick@snapt.ca",
subject: `ImEX Online Bounced Email - RE: ${subject}`,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
${body.bounce?.bouncedRecipients.map( ${body.bounce?.bouncedRecipients.map(
(r) => (r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
` `
)} )}
`, `,
}, },
(err, info) => { (err, info) => {
console.log("***", err || info); console.log("***", err || info);
}
);
} }
); } catch (error) {
logger.log("sns-error", "ERROR", "api", null, {
error: JSON.stringify(error),
});
} }
} catch (error) { res.sendStatus(200);
logger.log("sns-error", "ERROR", "api", null, {
error: JSON.stringify(error),
});
}
res.sendStatus(200);
}; };

View File

@@ -1,287 +1,215 @@
var admin = require("firebase-admin"); const admin = require("firebase-admin");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const path = require("path"); const path = require("path");
const { auth } = require("firebase-admin"); const {auth} = require("firebase-admin");
require("dotenv").config({ require("dotenv").config({
path: path.resolve( path: path.resolve(
process.cwd(), process.cwd(),
`.env.${process.env.NODE_ENV || "development"}` `.env.${process.env.NODE_ENV || "development"}`
), ),
}); });
const client = require("../graphql-client/graphql-client").client; const client = require("../graphql-client/graphql-client").client;
var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
const adminEmail = require("../utils/adminEmail");
admin.initializeApp({ admin.initializeApp({
credential: admin.credential.cert(serviceAccount), credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URL, databaseURL: process.env.FIREBASE_DATABASE_URL,
}); });
exports.admin = admin; exports.admin = admin;
const adminEmail = [
"patrick@imex.dev",
//"patrick@imex.test",
"patrick@imex.prod",
"patrick@imexsystems.ca",
"patrick@thinkimex.com",
];
exports.createUser = async (req, res) => { exports.createUser = async (req, res) => {
logger.log("admin-create-user", "ADMIN", req.user.email, null, { logger.log("admin-create-user", "ADMIN", req.user.email, null, {
request: req.body, request: req.body,
ioadmin: true, ioadmin: true,
}); });
const { email, displayName, password, shopid, authlevel } = req.body; const {email, displayName, password, shopid, authlevel} = req.body;
try { try {
const userRecord = await admin const userRecord = await admin
.auth() .auth()
.createUser({ email, displayName, password }); .createUser({email, displayName, password});
// See the UserRecord reference doc for the contents of userRecord. // See the UserRecord reference doc for the contents of userRecord.
const result = await client.request( const result = await client.request(
` `
mutation INSERT_USER($user: users_insert_input!) { mutation INSERT_USER($user: users_insert_input!) {
insert_users_one(object: $user) { insert_users_one(object: $user) {
email email
} }
} }
`, `,
{ {
user: { user: {
email: email.toLowerCase(), email: email.toLowerCase(),
authid: userRecord.uid, authid: userRecord.uid,
associations: { associations: {
data: [{ shopid, authlevel, active: true }], data: [{shopid, authlevel, active: true}],
}, },
}, },
} }
); );
res.json({ userRecord, result }); res.json({userRecord, result});
} catch (error) { } catch (error) {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, { logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error, error,
}); });
res.status(500).json(error); res.status(500).json(error);
} }
}; };
exports.updateUser = (req, res) => { exports.updateUser = (req, res) => {
logger.log("admin-update-user", "ADMIN", req.user.email, null, { logger.log("admin-update-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body, request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.updateUser(
req.body.uid,
req.body.user
// {
// email: "modifiedUser@example.com",
// phoneNumber: "+11234567890",
// emailVerified: true,
// password: "newPassword",
// displayName: "Jane Doe",
// photoURL: "http://www.example.com/12345678/photo.png",
// disabled: true,
// }
)
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
userRecord,
ioadmin: true, ioadmin: true,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
}); });
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.updateUser(
req.body.uid,
req.body.user
// {
// email: "modifiedUser@example.com",
// phoneNumber: "+11234567890",
// emailVerified: true,
// password: "newPassword",
// displayName: "Jane Doe",
// photoURL: "http://www.example.com/12345678/photo.png",
// disabled: true,
// }
)
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
userRecord,
ioadmin: true,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
}; };
exports.getUser = (req, res) => { exports.getUser = (req, res) => {
logger.log("admin-get-user", "ADMIN", req.user.email, null, { logger.log("admin-get-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body, request: req.body,
user: req.user, ioadmin: true,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
}); });
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
}; };
exports.sendNotification = async (req, res) => { exports.sendNotification = async (req, res) => {
setTimeout(() => { setTimeout(() => {
// Send a message to the device corresponding to the provided // Send a message to the device corresponding to the provided
// registration token. // registration token.
admin admin
.messaging() .messaging()
.send({ .send({
topic: "PRD_PATRICK-messaging", topic: "PRD_PATRICK-messaging",
notification: { notification: {
title: `ImEX Online Message - +16049992002`, title: `ImEX Online Message - +16049992002`,
body: "Test Noti.", body: "Test Noti.",
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //imageUrl: "https://thinkimex.com/img/io-fcm.png",
}, },
data: { data: {
type: "messaging-inbound", type: "messaging-inbound",
conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297", conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297",
text: "Hello. ", text: "Hello. ",
image_path: "", image_path: "",
phone_num: "+16049992002", phone_num: "+16049992002",
}, },
}) })
.then((response) => { .then((response) => {
// Response is a message ID string. // Response is a message ID string.
console.log("Successfully sent message:", response); console.log("Successfully sent message:", response);
}) })
.catch((error) => { .catch((error) => {
console.log("Error sending message:", error); console.log("Error sending message:", error);
}); });
res.sendStatus(200); res.sendStatus(200);
}, 500); }, 500);
}; };
exports.subscribe = async (req, res) => { exports.subscribe = async (req, res) => {
const result = await admin const result = await admin
.messaging() .messaging()
.subscribeToTopic( .subscribeToTopic(
req.body.fcm_tokens, req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}` `${req.body.imexshopid}-${req.body.type}`
); );
res.json(result); res.json(result);
}; };
exports.unsubscribe = async (req, res) => { exports.unsubscribe = async (req, res) => {
try { try {
const result = await admin const result = await admin
.messaging() .messaging()
.unsubscribeFromTopic( .unsubscribeFromTopic(
req.body.fcm_tokens, req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}` `${req.body.imexshopid}-${req.body.type}`
); );
res.json(result); res.json(result);
} catch (error) { } catch (error) {
res.sendStatus(500); res.sendStatus(500);
} }
}; };
exports.validateFirebaseIdToken = async (req, res, next) => {
if (
(!req.headers.authorization ||
!req.headers.authorization.startsWith("Bearer ")) &&
!(req.cookies && req.cookies.__session)
) {
console.error("Unauthorized attempt. No authorization provided.");
res.status(403).send("Unauthorized");
return;
}
let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
// console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split("Bearer ")[1];
} else if (req.cookies) {
//console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
} else {
// No cookie
console.error("Unauthorized attempt. No cookie provided.");
logger.log("api-unauthorized-call", "WARN", null, null, {
req,
type: "no-cookie",
});
res.status(403).send("Unauthorized");
return;
}
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
//console.log("ID Token correctly decoded", decodedIdToken);
req.user = decodedIdToken;
next();
return;
} catch (error) {
logger.log("api-unauthorized-call", "WARN", null, null, {
path: req.path,
body: req.body,
type: "unauthroized",
...error,
});
res.status(401).send("Unauthorized");
return;
}
};
exports.validateAdmin = async (req, res, next) => {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
request: req.body,
user: req.user,
});
res.sendStatus(404);
return;
} else {
next();
return;
}
};
//Admin claims code. //Admin claims code.
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1"; // const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";

View File

@@ -13,6 +13,7 @@ async function JobCosting(req, res) {
const { jobid } = req.body; const { jobid } = req.body;
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: { headers: {

View File

@@ -1,5 +1,17 @@
const _ = require("lodash");
const jobLifecycle = (req, res) => { const jobLifecycle = (req, res) => {
return res.status(200).send("jobLifecycle"); const {jobids} = req.body;
return _.isArray(jobids) ?
handleMultipleJobs(jobids, req, res) :
handleSingleJob(jobids, req, res);
}; };
const handleMultipleJobs = (jobIDs, req, res) => {
return res.status(200).send(jobIDs);
}
const handleSingleJob = (req, res) => {
return res.status(200).send(req.body);
}
module.exports = jobLifecycle; module.exports = jobLifecycle;

View File

@@ -9,89 +9,84 @@ const logger = require("../utils/logger");
Dinero.globalRoundingMode = "HALF_EVEN"; Dinero.globalRoundingMode = "HALF_EVEN";
const path = require("path"); const path = require("path");
const client = require("../graphql-client/graphql-client").client; const client = require("../graphql-client/graphql-client").client;
require("dotenv").config({ require("dotenv").config({
path: path.resolve( path: path.resolve(
process.cwd(), process.cwd(),
`.env.${process.env.NODE_ENV || "development"}` `.env.${process.env.NODE_ENV || "development"}`
), ),
}); });
async function StatusTransition(req, res) { async function StatusTransition(req, res) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { const {
res.status(401).send("Unauthorized"); id: jobid,
return; status: value,
} shopid: bodyshopid,
} = req.body.event.data.new;
// return res.sendStatus(200);
const {
id: jobid,
status: value,
shopid: bodyshopid,
} = req.body.event.data.new;
// Create record OPEN on new item, enter state // Create record OPEN on new item, enter state
// If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status
// (Timeline) // (Timeline)
// Final status is exported, there is no end date as there is no further transition (has no end date) // Final status is exported, there is no end date as there is no further transition (has no end date)
try { try {
const { update_transitions } = await client.request( const {update_transitions} = await client.request(
queries.UPDATE_OLD_TRANSITION, queries.UPDATE_OLD_TRANSITION,
{ {
jobid: jobid, jobid: jobid,
existingTransition: { existingTransition: {
end: new Date(), end: new Date(),
next_value: value, next_value: value,
//duration //duration
}, },
} }
); );
let duration = let duration =
update_transitions.affected_rows === 0 update_transitions.affected_rows === 0
? 0 ? 0
: new Date(update_transitions.returning[0].end) - : new Date(update_transitions.returning[0].end) -
new Date(update_transitions.returning[0].start); new Date(update_transitions.returning[0].start);
const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, {
oldTransitionId: oldTransitionId:
update_transitions.affected_rows === 0 update_transitions.affected_rows === 0
? null ? null
: update_transitions.returning[0].id, : update_transitions.returning[0].id,
duration, duration,
newTransition: { newTransition: {
bodyshopid: bodyshopid, bodyshopid: bodyshopid,
jobid: jobid, jobid: jobid,
start: start:
update_transitions.affected_rows === 0 update_transitions.affected_rows === 0
? new Date() ? new Date()
: update_transitions.returning[0].end, : update_transitions.returning[0].end,
prev_value: prev_value:
update_transitions.affected_rows === 0 update_transitions.affected_rows === 0
? null ? null
: update_transitions.returning[0].value, : update_transitions.returning[0].value,
value: value, value: value,
type: "status", type: "status",
}, },
}); });
//Check to see if there is an existing status transition record. //Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null. //Query using Job ID, start is not null, end is null.
//If there is no existing record, this is the start of the transition life cycle. //If there is no existing record, this is the start of the transition life cycle.
// Create the initial transition record. // Create the initial transition record.
//If there is a current status transition record, update it with the end date, duration, and next value. //If there is a current status transition record, update it with the end date, duration, and next value.
res.sendStatus(200); //.json(ret); res.sendStatus(200); //.json(ret);
} catch (error) { } catch (error) {
logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}); });
res.status(400).send(JSON.stringify(error)); res.status(400).send(JSON.stringify(error));
} }
} }
exports.statustransition = StatusTransition; exports.statustransition = StatusTransition;

View File

@@ -0,0 +1,15 @@
/**
* Checks if the event secret is correct
* @param req
* @param res
* @param next
*/
function eventAuthorizationMiddleware(req, res, next) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
return res.status(401).send("Unauthorized");
}
next();
}
module.exports = eventAuthorizationMiddleware;

View File

@@ -0,0 +1,15 @@
const logger = require("../utils/logger");
const adminEmail = require("../utils/adminEmail");
const validateAdminMiddleware = (req, res, next) => {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
request: req.body,
user: req.user,
});
return res.sendStatus(404);
}
next();
};
module.exports = validateAdminMiddleware;

View File

@@ -0,0 +1,59 @@
const logger = require("../utils/logger");
const admin = require("firebase-admin");
const validateFirebaseIdTokenMiddleware = async (req, res, next) => {
if (
(!req.headers.authorization ||
!req.headers.authorization.startsWith("Bearer ")) &&
!(req.cookies && req.cookies.__session)
) {
console.error("Unauthorized attempt. No authorization provided.");
res.status(403).send("Unauthorized");
return;
}
let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
// console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split("Bearer ")[1];
} else if (req.cookies) {
//console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
} else {
// No cookie
console.error("Unauthorized attempt. No cookie provided.");
logger.log("api-unauthorized-call", "WARN", null, null, {
req,
type: "no-cookie",
});
res.status(403).send("Unauthorized");
return;
}
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
//console.log("ID Token correctly decoded", decodedIdToken);
req.user = decodedIdToken;
next();
} catch (error) {
logger.log("api-unauthorized-call", "WARN", null, null, {
path: req.path,
body: req.body,
type: "unauthroized",
...error,
});
res.status(401).send("Unauthorized");
}
};
module.exports = validateFirebaseIdTokenMiddleware;

View File

@@ -15,10 +15,6 @@ const {getClient} = require('../../libs/awsUtils');
async function OpenSearchUpdateHandler(req, res) { async function OpenSearchUpdateHandler(req, res) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
res.status(401).send("Unauthorized");
return;
}
try { try {
const osClient = await getClient(); const osClient = await getClient();

View File

@@ -1,10 +1,12 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const accountQbxml = require('../accounting/qbxml/qbxml'); const {payments, payables, receivables} = require("../accounting/qbxml/qbxml");
router.post('/qbxml/receivables', fb.validateFirebaseIdToken, accountQbxml.receivables); router.use(validateFirebaseIdTokenMiddleware);
router.post('/qbxml/payables', fb.validateFirebaseIdToken, accountQbxml.payables);
router.post('/qbxml/payments', fb.validateFirebaseIdToken, accountQbxml.payments); router.post('/qbxml/receivables', receivables);
router.post('/qbxml/payables', payables);
router.post('/qbxml/payments', payments);
module.exports = router; module.exports = router;

View File

@@ -1,15 +1,18 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const fb = require('../firebase/firebase-handler');
const adm = require('../admin/adminops'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {createAssociation, createShop, updateShop, updateCounter} = require("../admin/adminops");
const validateAdminMiddleware = require("../middleware/validateAdminMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.post('/createassociation', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createAssociation); router.post('/createassociation', validateAdminMiddleware, createAssociation);
router.post('/createshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createShop); router.post('/createshop', validateAdminMiddleware, createShop);
router.post('/updateshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateShop); router.post('/updateshop', validateAdminMiddleware, updateShop);
router.post('/updatecounter', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateCounter); router.post('/updatecounter', validateAdminMiddleware, updateCounter);
router.post('/updateuser', fb.validateFirebaseIdToken, fb.updateUser); router.post('/updateuser', fb.updateUser);
router.post('/getuser', fb.validateFirebaseIdToken, fb.getUser); router.post('/getuser', fb.getUser);
router.post('/createuser', fb.validateFirebaseIdToken, fb.createUser); router.post('/createuser', fb.createUser);
module.exports = router; module.exports = router;

View File

@@ -1,8 +1,10 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler');
const cdkGetMake = require('../cdk/cdk-get-makes'); const cdkGetMake = require('../cdk/cdk-get-makes');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/getvehicles', fb.validateFirebaseIdToken, cdkGetMake.default); router.use(validateFirebaseIdTokenMiddleware);
router.post('/getvehicles', cdkGetMake.default);
module.exports = router; module.exports = router;

View File

@@ -1,9 +1,9 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const data = require('../data/data'); const {autohouse, claimscorp, kaizen} = require('../data/data');
router.post('/ah', data.autohouse); router.post('/ah', autohouse);
router.post('/cc', data.claimscorp); router.post('/cc', claimscorp);
router.post('/kaizen', data.kaizen); router.post('/kaizen', kaizen);
module.exports = router; module.exports = router;

View File

@@ -1,11 +1,11 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const intellipay = require('../intellipay/intellipay'); const {lightbox_credentials, payment_refund, generate_payment_url, postback} = require("../intellipay/intellipay");
router.post('/lightbox_credentials', fb.validateFirebaseIdToken, intellipay.lightbox_credentials); router.post('/lightbox_credentials', validateFirebaseIdTokenMiddleware, lightbox_credentials);
router.post('/payment_refund', fb.validateFirebaseIdToken, intellipay.payment_refund); router.post('/payment_refund', validateFirebaseIdTokenMiddleware, payment_refund);
router.post('/generate_payment_url', fb.validateFirebaseIdToken, intellipay.generate_payment_url); router.post('/generate_payment_url', validateFirebaseIdTokenMiddleware, generate_payment_url);
router.post('/postback', intellipay.postback); router.post('/postback', postback);
module.exports = router; module.exports = router;

View File

@@ -1,15 +1,19 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler');
const job = require('../job/job'); const job = require('../job/job');
const partsScan = require('../parts-scan/parts-scan'); const {partsScan} = require('../parts-scan/parts-scan');
const eventAuthorizationMiddleware = require('../middleware/eventAuthorizationMIddleware');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job");
router.post('/totals', fb.validateFirebaseIdToken, job.totals); router.use(validateFirebaseIdTokenMiddleware);
router.post('/statustransition', fb.validateFirebaseIdToken, job.statustransition);
router.post('/totalsssu', fb.validateFirebaseIdToken, job.totalsSsu); router.post('/totals', totals);
router.post('/costing', fb.validateFirebaseIdToken, job.costing); router.post('/statustransition', eventAuthorizationMiddleware, statustransition);
router.get('/lifecycle', fb.validateFirebaseIdToken, job.lifecycle); router.post('/totalsssu', totalsSsu);
router.post('/costingmulti', fb.validateFirebaseIdToken, job.costingmulti); router.post('/costing', costing);
router.post('/partsscan', fb.validateFirebaseIdToken, partsScan.partsScan); router.get('/lifecycle', lifecycle);
router.post('/costingmulti', costingmulti);
router.post('/partsscan', partsScan);
module.exports = router; module.exports = router;

View File

@@ -1,11 +1,13 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {createSignedUploadURL, downloadFiles, renameKeys, deleteFiles} = require('../media/media');
const media = require('../media/media'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/sign', fb.validateFirebaseIdToken, media.createSignedUploadURL); router.use(validateFirebaseIdTokenMiddleware);
router.post('/download', fb.validateFirebaseIdToken, media.downloadFiles);
router.post('/rename', fb.validateFirebaseIdToken, media.renameKeys); router.post('/sign', createSignedUploadURL);
router.post('/delete', fb.validateFirebaseIdToken, media.deleteFiles); router.post('/download', downloadFiles);
router.post('/rename', renameKeys);
router.post('/delete', deleteFiles);
module.exports = router; module.exports = router;

View File

@@ -3,11 +3,12 @@ const router = express.Router();
const logger = require("../../server/utils/logger"); const logger = require("../../server/utils/logger");
const sendEmail = require("../email/sendemail"); const sendEmail = require("../email/sendemail");
const data = require("../data/data"); const data = require("../data/data");
const fb = require("../firebase/firebase-handler");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const ioevent = require("../ioevent/ioevent"); const ioevent = require("../ioevent/ioevent");
const taskHandler = require("../tasks/tasks"); const taskHandler = require("../tasks/tasks");
const os = require("../opensearch/os-handler"); const os = require("../opensearch/os-handler");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
//Test route to ensure Express is responding. //Test route to ensure Express is responding.
router.get("/test", async function (req, res) { router.get("/test", async function (req, res) {
@@ -29,15 +30,21 @@ router.get("/test", async function (req, res) {
res.status(200).send(`OK - ${commit}`); res.status(200).send(`OK - ${commit}`);
}); });
router.post("/search", fb.validateFirebaseIdToken, os.search); // Search
router.post("/opensearch", os.handler); router.post("/search", validateFirebaseIdTokenMiddleware, os.search);
router.post("/opensearch", eventAuthorizationMiddleware, os.handler);
// IO Events
router.post('/ioevent', ioevent.default); router.post('/ioevent', ioevent.default);
router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail);
// Email
router.post('/sendemail', validateFirebaseIdTokenMiddleware, sendEmail.sendEmail);
router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce);
// Handlers
router.post('/record-handler/arms', data.arms); router.post('/record-handler/arms', data.arms);
router.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskHandler);
module.exports = router; module.exports = router;

View File

@@ -2,9 +2,9 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const multer = require('multer'); const multer = require('multer');
const upload = multer(); const upload = multer();
const fb = require('../firebase/firebase-handler'); const {mixdataUpload} = require('../mixdata/mixdata');
const mixdataUpload = require('../mixdata/mixdata'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/upload', fb.validateFirebaseIdToken, upload.any(), mixdataUpload.mixdataUpload); router.post('/upload', validateFirebaseIdTokenMiddleware, upload.any(), mixdataUpload);
module.exports = router; module.exports = router;

View File

@@ -1,8 +1,11 @@
const express = require('express'); const express = require('express');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {subscribe, unsubscribe} = require("../firebase/firebase-handler");
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler');
router.post('/subscribe', fb.validateFirebaseIdToken, fb.subscribe); router.use(validateFirebaseIdTokenMiddleware);
router.post('/unsubscribe', fb.validateFirebaseIdToken, fb.unsubscribe);
router.post('/subscribe', subscribe);
router.post('/unsubscribe', unsubscribe);
module.exports = router; module.exports = router;

View File

@@ -1,13 +1,13 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {authorize, callback, receivables, payables, payments} = require('../accounting/qbo/qbo');
const qbo = require('../accounting/qbo/qbo'); // Assuming you have a qbo module for handling QuickBooks Online related functionalities const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities
// Define the routes for QuickBooks Online // Define the routes for QuickBooks Online
router.post('/authorize', fb.validateFirebaseIdToken, qbo.authorize); router.post('/authorize', validateFirebaseIdTokenMiddleware, authorize);
router.get('/callback', qbo.callback); router.get('/callback', callback);
router.post('/receivables', fb.validateFirebaseIdToken, qbo.receivables); router.post('/receivables', validateFirebaseIdTokenMiddleware, receivables);
router.post('/payables', fb.validateFirebaseIdToken, qbo.payables); router.post('/payables', validateFirebaseIdTokenMiddleware, payables);
router.post('/payments', fb.validateFirebaseIdToken, qbo.payments); router.post('/payments', validateFirebaseIdTokenMiddleware, payments);
module.exports = router; module.exports = router;

View File

@@ -1,9 +1,9 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {inlinecss} = require('../render/inlinecss');
const inlineCss = require('../render/inlinecss'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
// Define the route for inline CSS rendering // Define the route for inline CSS rendering
router.post('/inlinecss', fb.validateFirebaseIdToken, inlineCss.inlinecss); router.post('/inlinecss', validateFirebaseIdTokenMiddleware, inlinecss);
module.exports = router; module.exports = router;

View File

@@ -1,8 +1,8 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {job} = require('../scheduling/scheduling-job');
const scheduling = require('../scheduling/scheduling-job'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/job', fb.validateFirebaseIdToken, scheduling.job); router.post('/job', validateFirebaseIdTokenMiddleware, job);
module.exports = router; module.exports = router;

View File

@@ -1,17 +1,17 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const twilio = require('twilio'); const twilio = require('twilio');
const fb = require('../firebase/firebase-handler'); const {receive} = require('../sms/receive');
const smsReceive = require('../sms/receive'); const {send} = require('../sms/send');
const smsSend = require('../sms/send'); const {status, markConversationRead} = require('../sms/status');
const smsStatus = require('../sms/status'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
// Twilio Webhook Middleware for production // Twilio Webhook Middleware for production
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }); const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
router.post('/receive', twilioWebhookMiddleware, smsReceive.receive); router.post('/receive', twilioWebhookMiddleware, receive);
router.post('/send', fb.validateFirebaseIdToken, smsSend.send); router.post('/send', validateFirebaseIdTokenMiddleware, send);
router.post('/status', twilioWebhookMiddleware, smsStatus.status); router.post('/status', twilioWebhookMiddleware, status);
router.post('/markConversationRead', fb.validateFirebaseIdToken, smsStatus.markConversationRead); router.post('/markConversationRead', validateFirebaseIdTokenMiddleware, markConversationRead);
module.exports = router; module.exports = router;

View File

@@ -1,8 +1,8 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {techLogin} = require('../tech/tech');
const tech = require('../tech/tech'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/login', fb.validateFirebaseIdToken, tech.techLogin); router.post('/login', validateFirebaseIdTokenMiddleware, techLogin);
module.exports = router; module.exports = router;

View File

@@ -1,9 +1,9 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fb = require('../firebase/firebase-handler'); const {servertime, jsrAuth} = require('../utils/utils');
const utils = require('../utils/utils'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/time', utils.servertime); router.post('/time', servertime);
router.post('/jsr', fb.validateFirebaseIdToken, utils.jsrAuth); router.post('/jsr', validateFirebaseIdTokenMiddleware, jsrAuth);
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,13 @@
/**
* List of admin email addresses
* @type {string[]}
*/
const adminEmail = [
"patrick@imex.dev",
//"patrick@imex.test",
"patrick@imex.prod",
"patrick@imexsystems.ca",
"patrick@thinkimex.com",
];
module.exports = adminEmail;