Skip to content

Commit

Permalink
feat: mailer
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivo committed Apr 28, 2023
1 parent dd0ee45 commit accd472
Show file tree
Hide file tree
Showing 10 changed files with 829 additions and 3 deletions.
463 changes: 463 additions & 0 deletions apps/client/emails/template.html

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"cookies-next": "^2.1.1",
"date-fns": "^2.29.3",
"github-slugger": "^2.0.0",
"handlebars": "^4.7.7",
"i18next": "^22.4.14",
"lucide-react": "^0.172.0",
"next": "13.3.0",
Expand All @@ -40,6 +41,7 @@
"next-i18next": "^13.2.2",
"next-sitemap": "^4.0.7",
"nextjs-progressbar": "^0.0.16",
"nodemailer": "^6.9.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
Expand All @@ -55,6 +57,7 @@
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/nodemailer": "^6.4.7",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@types/react-transition-group": "^4.4.5",
Expand Down
65 changes: 65 additions & 0 deletions apps/client/src/pages/api/cron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";

import { prisma } from "@app/lib/prisma";
import sendMail from "@app/utils/send-mail";

const emailsDir = path.resolve(process.cwd(), "emails");

export default async function sendEmail(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const emailFile = readFileSync(path.join(emailsDir, "template.html"), {
encoding: "utf8"
});

Handlebars.registerHelper("breaklines", function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, "<br>");
return new Handlebars.SafeString(text);
});

const emailTemplate = Handlebars.compile(emailFile);

const awaitingCount = await prisma.user.count({
where: {
active: false
}
});

const admins = await prisma.user.findMany({
where: {
role: "ADMIN"
}
});

const to = admins.map((admin) => admin.email);

try {
const result = await sendMail({
to,
subject: `There are ${awaitingCount} sign up requests awaiting your approval`,
html: emailTemplate({
base_url: process.env.NEXTAUTH_URL,
preheader: "There are people awaiting to use VD",
content:
"Hey there, Admin.\n\nThere are some sign up requests awaiting to be reviewed by you.",
showButton: true,
buttonLink: `${process.env.NEXTAUTH_URL}/admin/signup`,
buttonText: "Go to Visual Dynamics",
showPostButtonText: true,
postButtonText: `Remember: When you approve or reject someone they'll receive an automated email about this decision.`,
email: to
})
});
return res.status(200).json({ success: result });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
}
}
50 changes: 50 additions & 0 deletions apps/client/src/pages/api/mailer/dynamics/failed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";

import sendMail from "@app/utils/send-mail";

const emailsDir = path.resolve(process.cwd(), "emails");

export default async function sendEmail(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const { to, dynamicType, dynamicMolecule } = req.query;
const emailFile = readFileSync(path.join(emailsDir, "template.html"), {
encoding: "utf8"
});

Handlebars.registerHelper("breaklines", function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, "<br>");
return new Handlebars.SafeString(text);
});

const emailTemplate = Handlebars.compile(emailFile);

try {
const result = await sendMail({
to: String(to),
subject: "Your dynamic has failed.",
html: emailTemplate({
base_url: process.env.NEXTAUTH_URL,
preheader:
"An error has occurred during the execution of your dynamic.",
content: `Your ${dynamicType} dynamic has failed.\n\nThe ${dynamicType} - ${dynamicMolecule} dynamic you left running has failed.\n\nPlease access VD and check the logs provided, if you're sure it's a bug in our software, please contact us.`,
showButton: true,
buttonLink: process.env.NEXTAUTH_URL,
buttonText: "Go to Visual Dynamics",
showPostButtonText: false,
email: to
})
});
return res.status(200).json({ success: result });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
}
}
49 changes: 49 additions & 0 deletions apps/client/src/pages/api/mailer/dynamics/success.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";

import sendMail from "@app/utils/send-mail";

const emailsDir = path.resolve(process.cwd(), "emails");

export default async function sendEmail(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const { to, dynamicType, dynamicMolecule } = req.query;
const emailFile = readFileSync(path.join(emailsDir, "template.html"), {
encoding: "utf8"
});

Handlebars.registerHelper("breaklines", function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, "<br>");
return new Handlebars.SafeString(text);
});

const emailTemplate = Handlebars.compile(emailFile);

try {
const result = await sendMail({
to: String(to),
subject: "Your dynamic has ended.",
html: emailTemplate({
base_url: process.env.NEXTAUTH_URL,
preheader: "The dynamic you left running has ended.",
content: `Your ${dynamicType} dynamic has ended.\n\nThe ${dynamicType} - ${dynamicMolecule} dynamic you left running has ended.\n\nPlease access VD to download the figure graphics, raw data and more.`,
showButton: true,
buttonLink: process.env.NEXTAUTH_URL,
buttonText: "Go to Visual Dynamics",
showPostButtonText: false,
email: to
})
});
return res.status(200).json({ success: result });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
}
}
52 changes: 52 additions & 0 deletions apps/client/src/pages/api/mailer/user/approved.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";

import sendMail from "@app/utils/send-mail";

const emailsDir = path.resolve(process.cwd(), "emails");

export default async function sendEmail(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const { to } = req.query;
const emailFile = readFileSync(path.join(emailsDir, "template.html"), {
encoding: "utf8"
});

Handlebars.registerHelper("breaklines", function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, "<br>");
return new Handlebars.SafeString(text);
});

const emailTemplate = Handlebars.compile(emailFile);

try {
const result = await sendMail({
to: String(to),
subject: "You can now start and track your dynamics",
html: emailTemplate({
base_url: process.env.NEXTAUTH_URL,
preheader: "We're thrilled to have you with us!",
content:
"Welcome to Visual Dynamics.\nWe're thrilled to tell you that your account has been validated and now you can start a dynamic and develop something incredible.",
showButton: true,
buttonLink: `${process.env.NEXTAUTH_URL}/signin`,
buttonText: "Go to Visual Dynamics",
showPostButtonText: true,
postButtonText: `You can sign in to VD using this email: ${to} and the password you provided on sign up.`,
signin_url: `${process.env.NEXTAUTH_URL}/signin`,
email: to
})
});
return res.status(200).json({ success: result });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
}
}
49 changes: 49 additions & 0 deletions apps/client/src/pages/api/mailer/user/rejected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";

import sendMail from "@app/utils/send-mail";

const emailsDir = path.resolve(process.cwd(), "emails");

export default async function sendEmail(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const { to } = req.query;
const emailFile = readFileSync(path.join(emailsDir, "template.html"), {
encoding: "utf8"
});

Handlebars.registerHelper("breaklines", function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, "<br>");
return new Handlebars.SafeString(text);
});

const emailTemplate = Handlebars.compile(emailFile);

try {
const result = await sendMail({
to: String(to),
subject: "Your request to join VD has been rejected.",
html: emailTemplate({
base_url: process.env.NEXTAUTH_URL,
preheader:
"We're sorry, but at this moment we couldn't allow your access.",
content:
"We couldn't verify and approve your account.\nWe're sorry about this, but at this moment we couldn't allow your access to VD.\n\nThis could have happened for various reasons, the most common being the usage of a non-institutional e-mail address.\n\nIf you think this is an error on our part, don't hesitate to contact us.",
showButton: false,
showPostButtonText: false,
email: to
})
});
return res.status(200).json({ success: result });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
}
}
37 changes: 37 additions & 0 deletions apps/client/src/utils/send-mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.EMAIL_NO_REPLY,
pass: process.env.EMAIL_PASS
}
});

type SendMailProps = {
to: string | string[];
subject: string;
html: string;
};

const sendMail = async ({ to, subject, html }: SendMailProps) => {
const mailOptions = {
from: `"⚛️ Visual Dynamics" ${process.env.EMAIL_NO_REPLY}`,
to,
subject,
html
};

try {
const result = await transporter.sendMail(mailOptions);
console.log(result);
return true;
} catch (error) {
console.error(error);
return false;
}
};

export default sendMail;
8 changes: 8 additions & 0 deletions apps/client/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"crons": [
{
"path": "/api/cron",
"schedule": "0 16 * * *"
}
]
}

0 comments on commit accd472

Please sign in to comment.