Fix template rendering and use verify email template

email html is weird, some stuff isn't supported.
This commit is contained in:
Puyodead1 2023-01-20 10:43:06 -05:00 committed by Puyodead1
parent f337f2e785
commit 689b710c9e
8 changed files with 421 additions and 323 deletions

View File

@ -4,39 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Verify {instanceName} Login from New Location</title> <title>Verify {instanceName} Login from New Location</title>
<style> <style>
* { * {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
}
body {
color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
background-color: #202225;
} }
.btn { p {
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white; color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
} }
.btn:hover { .ExternalClass {
background-color: hsl(22.4, 80%, 50%); width: 100%;
}
.btn:active {
background-color: hsl(22.4, 60%, 50%);
} }
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;">
<img <img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding" alt="Branding"
@ -54,7 +40,7 @@
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 40px 50px; padding: 40px 50px;
background-color: rgba(50, 53, 59, 1); background-color: #32353b;
border-radius: 5px; border-radius: 5px;
" "
> >
@ -66,39 +52,52 @@
line-height: 24px; line-height: 24px;
" "
> >
Hey {username}, Hey {userUsername},
</p> </p>
<p> <p>
It looks like someone tried to log into your {instanceName} It looks like someone tried to log into your {instanceName}
account from a new location. If this is you, follow the link account from a new location. If this is you, follow the link
below to authorize logging in from this location on your below to authorize logging in from this location on your
account. If this isn't you, we suggest changing your password as account. If this isn't you, we suggest changing your
soon as possible. password as soon as possible.
</p> </p>
<p> <p>
<strong>IP Address:</strong> {ip} <strong>IP Address:</strong> {ipAddress}
<br /> <br />
<strong>Location:</strong> {location} <strong>Location:</strong> {locationCity}, {locationRegion},
{locationCountryName}
</p> </p>
<div> <div>
<div <div
style=" style="
display: flex; text-align: center;
justify-content: center; justify-content: center;
padding-bottom: 10px; padding-bottom: 10px;
" "
> >
<a class="btn" href="{verifyUrl}" target="_blank" <a
href="{verifyUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Verify Login</a >Verify Login</a
> >
</div> </div>
<hr /> <hr />
<div <div
style=" style="
display: flex;
justify-content: center;
flex-direction: column;
text-align: center; text-align: center;
justify-content: center;
padding-bottom: 10px;
" "
> >
<p> <p>
@ -109,5 +108,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -4,21 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>{instanceName} Password Changed</title> <title>{instanceName} Password Changed</title>
<style> <style>
* { * {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
}
body {
color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
background-color: #202225; }
p {
color: white;
}
.ExternalClass {
width: 100%;
} }
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;">
<img <img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding" alt="Branding"
@ -36,7 +40,7 @@
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 40px 50px; padding: 40px 50px;
background-color: rgba(50, 53, 59, 1); background-color: #32353b;
border-radius: 5px; border-radius: 5px;
" "
> >
@ -48,7 +52,7 @@
line-height: 24px; line-height: 24px;
" "
> >
Hey {username}, Hey {userUsername},
</p> </p>
<p>Your {instanceName} password has been changed.</p> <p>Your {instanceName} password has been changed.</p>
<p> <p>
@ -56,5 +60,6 @@
password to your {instanceName} account. password to your {instanceName} account.
</p> </p>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -4,39 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Password Reset Request for {instanceName}</title> <title>Password Reset Request for {instanceName}</title>
<style> <style>
* { * {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
}
body {
color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
background-color: #202225;
} }
.btn { p {
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white; color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
} }
.btn:hover { .ExternalClass {
background-color: hsl(22.4, 80%, 50%); width: 100%;
}
.btn:active {
background-color: hsl(22.4, 60%, 50%);
} }
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;">
<img <img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding" alt="Branding"
@ -54,7 +40,7 @@
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 40px 50px; padding: 40px 50px;
background-color: rgba(50, 53, 59, 1); background-color: #32353b;
border-radius: 5px; border-radius: 5px;
" "
> >
@ -66,34 +52,40 @@
line-height: 24px; line-height: 24px;
" "
> >
Hey {username}, Hey {userUsername},
</p> </p>
<p> <p>
Your {instanceName} password can be reset by clicking the button Your {instanceName} password can be reset by clicking the
below. If you did not request a new password, please ignore this button below. If you did not request a new password, please
email. ignore this email.
</p> </p>
<div> <div>
<div <div
style=" style="
display: flex; text-align: center;
justify-content: center; justify-content: center;
padding-bottom: 10px; padding-bottom: 10px;
" "
> >
<a class="btn" href="{passwordResetUrl}" target="_blank" <a
href="{passwordResetUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Reset Password</a >Reset Password</a
> >
</div> </div>
<hr /> <hr />
<div <div style="text-align: center">
style="
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
"
>
<p> <p>
Alternatively, you can directly paste this link into Alternatively, you can directly paste this link into
your browser: your browser:
@ -104,5 +96,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -4,21 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Phone Removed From {instanceName} Account</title> <title>Phone Removed From {instanceName} Account</title>
<style> <style>
* { * {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
}
body {
color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
background-color: #202225; }
p {
color: white;
}
.ExternalClass {
width: 100%;
} }
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;">
<img <img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding" alt="Branding"
@ -36,7 +40,7 @@
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 40px 50px; padding: 40px 50px;
background-color: rgba(50, 53, 59, 1); background-color: #32353b;
border-radius: 5px; border-radius: 5px;
" "
> >
@ -48,7 +52,7 @@
line-height: 24px; line-height: 24px;
" "
> >
Hey {username}, Hey {userUsername},
</p> </p>
<p> <p>
Your phone number ********{phoneNumber} was recently removed Your phone number ********{phoneNumber} was recently removed
@ -60,5 +64,6 @@
{instanceName} account at a time. {instanceName} account at a time.
</p> </p>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -4,39 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Verify Email Address for {instanceName}</title> <title>Verify Email Address for {instanceName}</title>
<style> <style>
* { * {
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
}
body {
color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
background-color: #202225;
} }
.btn { p {
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white; color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
} }
.btn:hover { .ExternalClass {
background-color: hsl(22.4, 80%, 50%); width: 100%;
}
.btn:active {
background-color: hsl(22.4, 60%, 50%);
} }
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;">
<img <img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding" alt="Branding"
@ -54,7 +40,7 @@
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 40px 50px; padding: 40px 50px;
background-color: rgba(50, 53, 59, 1); background-color: #32353b;
border-radius: 5px; border-radius: 5px;
" "
> >
@ -66,43 +52,51 @@
line-height: 24px; line-height: 24px;
" "
> >
Hey {username}, Hey {userUsername},
</p> </p>
<p> <p>
Thanks for registering for an account on {instanceName}! Before Thanks for registering for an account on {instanceName}!
we get started, we just need to confirm that this is you. Click Before we get started, we just need to confirm that this is
below to verify your email address: you. Click below to verify your email address:
</p> </p>
<div> <div>
<div <div
style=" style="
display: flex; text-align: center;
justify-content: center; justify-content: center;
padding-bottom: 10px; padding-bottom: 10px;
" "
> >
<a class="btn" href="{verificationUrl}" target="_blank" <a
class="btn"
href="{emailVerificationUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Verify Email</a >Verify Email</a
> >
</div> </div>
<hr /> <hr />
<div <div style="text-align: center">
style="
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
"
>
<p> <p>
Alternatively, you can directly paste this link into Alternatively, you can directly paste this link into
your browser: your browser:
</p> </p>
<a href="{verificationUrl}" target="_blank" <a href="{emailVerificationUrl}" target="_blank"
>{verificationUrl}</a >{emailVerificationUrl}</a
> >
</div> </div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -33,7 +33,7 @@ router.post("/", route({}), async (req: Request, res: Response) => {
throw new HTTPError("User does not have an email address", 400); throw new HTTPError("User does not have an email address", 400);
} }
await Email.sendVerificationEmail(req.user_id, user.email) await Email.sendVerificationEmail(user, user.email)
.then((info) => { .then((info) => {
console.log("Message sent: %s", info.messageId); console.log("Message sent: %s", info.messageId);
return res.sendStatus(204); return res.sendStatus(204);

View File

@ -386,7 +386,7 @@ export class User extends BaseClass {
// send verification email if users aren't verified by default and we have an email // send verification email if users aren't verified by default and we have an email
if (!Config.get().defaults.user.verified && email) { if (!Config.get().defaults.user.verified && email) {
await Email.sendVerificationEmail(user.id, email) await Email.sendVerificationEmail(user, email)
.then((info) => { .then((info) => {
console.log("Message sent: %s", info.messageId); console.log("Message sent: %s", info.messageId);
}) })

View File

@ -16,10 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import fs from "node:fs";
import path from "node:path";
import nodemailer, { Transporter } from "nodemailer"; import nodemailer, { Transporter } from "nodemailer";
import { User } from "../entities";
import { Config } from "./Config"; import { Config } from "./Config";
import { generateToken } from "./Token"; import { generateToken } from "./Token";
const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
export const EMAIL_REGEX = export const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@ -51,7 +55,20 @@ export function adjustEmail(email?: string): string | undefined {
export const Email: { export const Email: {
transporter: Transporter | null; transporter: Transporter | null;
init: () => Promise<void>; init: () => Promise<void>;
sendVerificationEmail: (id: string, email: string) => Promise<any>; generateVerificationLink: (id: string, email: string) => Promise<string>;
sendVerificationEmail: (user: User, email: string) => Promise<any>;
doReplacements: (
template: string,
user: User,
emailVerificationUrl?: string,
passwordResetUrl?: string,
ipInfo?: {
ip: string;
city: string;
region: string;
country_name: string;
},
) => string;
} = { } = {
transporter: null, transporter: null,
init: async function () { init: async function () {
@ -78,25 +95,109 @@ export const Email: {
console.log(`[SMTP] Ready`); console.log(`[SMTP] Ready`);
}); });
}, },
sendVerificationEmail: async function ( /**
id: string, * Replaces all placeholders in an email template with the correct values
email: string, */
): Promise<any> { doReplacements: function (
if (!this.transporter) return; template: string,
user: User,
emailVerificationUrl?: string,
passwordResetUrl?: string,
ipInfo?: {
ip: string;
city: string;
region: string;
country_name: string;
},
) {
const { instanceName } = Config.get().general;
template = template.replaceAll("{instanceName}", instanceName);
template = template.replaceAll("{userUsername}", user.username);
template = template.replaceAll(
"{userDiscriminator}",
user.discriminator,
);
template = template.replaceAll("{userId}", user.id);
if (user.phone)
template = template.replaceAll(
"{phoneNumber}",
user.phone.slice(-4),
);
if (user.email)
template = template.replaceAll("{userEmail}", user.email);
// template specific replacements
if (emailVerificationUrl)
template = template.replaceAll(
"{emailVerificationUrl}",
emailVerificationUrl,
);
if (passwordResetUrl)
template = template.replaceAll(
"{passwordResetUrl}",
passwordResetUrl,
);
if (ipInfo) {
template = template.replaceAll("{ipAddress}", ipInfo.ip);
template = template.replaceAll("{locationCity}", ipInfo.city);
template = template.replaceAll("{locationRegion}", ipInfo.region);
template = template.replaceAll(
"{locationCountryName}",
ipInfo.country_name,
);
}
return template;
},
/**
*
* @param id user id
* @param email user email
* @returns a verification link for the user
*/
generateVerificationLink: async function (id: string, email: string) {
const token = (await generateToken(id, email)) as string; const token = (await generateToken(id, email)) as string;
const instanceUrl = const instanceUrl =
Config.get().general.frontPage || "http://localhost:3001"; Config.get().general.frontPage || "http://localhost:3001";
const link = `${instanceUrl}/verify#token=${token}`; const link = `${instanceUrl}/verify#token=${token}`;
return link;
},
sendVerificationEmail: async function (
user: User,
email: string,
): Promise<any> {
if (!this.transporter) return;
// generate a verification link for the user
const verificationLink = await this.generateVerificationLink(
user.id,
email,
);
// load the email template
const rawTemplate = fs.readFileSync(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"verify_email.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, verificationLink);
// extract the title from the email template to use as the email subject
const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
// // construct the email
const message = { const message = {
from: from:
Config.get().general.correspondenceEmail || "noreply@localhost", Config.get().general.correspondenceEmail || "noreply@localhost",
to: email, to: email,
subject: `Verify Email Address for ${ subject,
Config.get().general.instanceName html,
}`,
html: `Please verify your email address by clicking the following link: <a href="${link}">Verify Email</a>`,
}; };
// // send the email
return this.transporter.sendMail(message); return this.transporter.sendMail(message);
}, },
}; };