Refactor email sending + remove email verification if mail sending is not set up

This commit is contained in:
Madeline 2023-08-12 15:36:29 +10:00
parent ecb227105a
commit d18584f8e9
No known key found for this signature in database
GPG Key ID: 1958E017C36F2E47
7 changed files with 174 additions and 281 deletions

View File

@ -1,84 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<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>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"
style="
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<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>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding" style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
" />
<div style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
">
<p style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
It looks like someone tried to log into your {instanceName}
account from a new location. If this is you, follow the link
below to authorize logging in from this location on your
account. If this isn't you, we suggest changing your
password as soon as possible.
</p>
<p>
<strong>IP Address:</strong> {ipAddress}
<br />
<strong>Location:</strong> {locationCity}, {locationRegion},
{locationCountryName}
</p>
<div>
<div
style="
">
Hey {userUsername},
</p>
<p>
It looks like someone tried to log into your {instanceName}
account from a new location. If this is you, follow the link
below to authorize logging in from this location on your
account. If this isn't you, we suggest changing your
password as soon as possible.
</p>
<p>
<strong>IP Address:</strong> {ipAddress}
<br />
<strong>Location:</strong> {locationCity}, {locationRegion},
{locationCountryName}
</p>
<div>
<div style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<a
href="{verifyUrl}"
target="_blank"
style="
">
<a href="{actionUrl}" target="_blank" style="
font-size: 15px;
border: none;
border-radius: 3px;
@ -88,26 +80,23 @@
padding: 15px 19px;
background-color: #0185ff;
border-radius: 5px;
"
>Verify Login</a
>
</div>
<hr />
<div
style="
">Verify Login</a>
</div>
<hr />
<div style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{verifyUrl}" target="_blank" style="word-wrap: break-word;">{verifyUrl}</a>
</div>
">
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{actionUrl}" target="_blank" style="word-wrap: break-word;">{actionUrl}</a>
</div>
</div>
</div>
</body>
</div>
</body>
</html>

View File

@ -1,76 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<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>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"
style="
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<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>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding" style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
" />
<div style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
">
<p style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
Your {instanceName} password can be reset by clicking the
button below. If you did not request a new password, please
ignore this email.
</p>
<div>
<div
style="
">
Hey {userUsername},
</p>
<p>
Your {instanceName} password can be reset by clicking the
button below. If you did not request a new password, please
ignore this email.
</p>
<div>
<div style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<a
href="{passwordResetUrl}"
target="_blank"
style="
">
<a href="{actionUrl}" target="_blank" style="
font-size: 15px;
border: none;
border-radius: 3px;
@ -80,22 +72,19 @@
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Reset Password</a
>
</div>
<hr />
<div style="text-align: center">
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{passwordResetUrl}" target="_blank" style="word-wrap: break-word;"
>{passwordResetUrl}</a
>
</div>
">Reset Password</a>
</div>
<hr />
<div style="text-align: center">
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{actionUrl}" target="_blank" style="word-wrap: break-word;">{actionUrl}</a>
</div>
</div>
</div>
</body>
</div>
</body>
</html>

View File

@ -18,14 +18,13 @@
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
import {
adjustEmail,
Config,
FieldErrors,
generateToken,
generateWebAuthnTicket,
LoginSchema,
User,
WebAuthn,
generateToken,
generateWebAuthnTicket,
} from "@spacebar/util";
import bcrypt from "bcrypt";
import crypto from "crypto";
@ -50,7 +49,6 @@ router.post(
async (req: Request, res: Response) => {
const { login, password, captcha_key, undelete } =
req.body as LoginSchema;
const email = adjustEmail(login);
const config = Config.get();
@ -76,7 +74,7 @@ router.post(
}
const user = await User.findOneOrFail({
where: [{ phone: login }, { email: email }],
where: [{ phone: login }, { email: login }],
select: [
"data",
"id",

View File

@ -30,7 +30,6 @@ import {
RegisterSchema,
User,
ValidRegistrationToken,
adjustEmail,
generateToken,
} from "@spacebar/util";
import bcrypt from "bcrypt";
@ -76,9 +75,6 @@ router.post(
}
}
// email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
const email = adjustEmail(body.email);
// check if registration is allowed
if (!regTokenUsed && !register.allowNewRegistration) {
throw FieldErrors({
@ -161,6 +157,7 @@ router.post(
// TODO: gift_code_sku_id?
// TODO: check password strength
const email = body.email;
if (email) {
// replace all dots and chars after +, if its a gmail.com email
if (!email) {

View File

@ -18,7 +18,6 @@
import { route } from "@spacebar/api";
import {
adjustEmail,
Config,
emitEvent,
FieldErrors,
@ -111,7 +110,6 @@ router.patch(
}
if (body.email) {
body.email = adjustEmail(body.email);
if (!body.email && Config.get().register.email.required)
throw FieldErrors({
email: {

View File

@ -25,14 +25,7 @@ import {
OneToMany,
OneToOne,
} from "typeorm";
import {
Config,
Email,
FieldErrors,
Snowflake,
adjustEmail,
trimSpecial,
} from "..";
import { Config, Email, FieldErrors, Snowflake, trimSpecial } from "..";
import { BitField } from "../util/BitField";
import { BaseClass } from "./BaseClass";
import { ConnectedAccount } from "./ConnectedAccount";
@ -240,18 +233,6 @@ export class User extends BaseClass {
// TODO: I don't like this method?
validate() {
if (this.email) {
this.email = adjustEmail(this.email);
if (!this.email)
throw FieldErrors({
email: { message: "Invalid email", code: "EMAIL_INVALID" },
});
if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g))
throw FieldErrors({
email: { message: "Invalid email", code: "EMAIL_INVALID" },
});
}
if (this.discriminator) {
const discrim = Number(this.discriminator);
if (

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import fs from "node:fs";
import fs from "fs/promises";
import path from "node:path";
import { SentMessageInfo, Transporter } from "nodemailer";
import { User } from "../../entities";
@ -24,8 +24,8 @@ import { Config } from "../Config";
import { generateToken } from "../Token";
import MailGun from "./transports/MailGun";
import MailJet from "./transports/MailJet";
import SendGrid from "./transports/SendGrid";
import SMTP from "./transports/SMTP";
import SendGrid from "./transports/SendGrid";
const ASSET_FOLDER_PATH = path.join(
__dirname,
@ -35,32 +35,11 @@ const ASSET_FOLDER_PATH = path.join(
"..",
"assets",
);
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,}))$/;
export function adjustEmail(email?: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
if (!parts || parts.length < 5) return undefined;
return email;
// // TODO: The below code doesn't actually do anything.
// const domain = parts[5];
// const user = parts[1];
// // TODO: check accounts with uncommon email domains
// if (domain === "gmail.com" || domain === "googlemail.com") {
// // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
// const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
// }
// if (domain === "google.com") {
// // replace .dots and +alternatives -> Google Staff GMail Dot Trick
// const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
// }
// return email;
enum MailTypes {
verify = "verify",
reset = "reset",
pwchange = "pwchange",
}
const transporters: {
@ -76,10 +55,15 @@ export const Email: {
transporter: Transporter | null;
init: () => Promise<void>;
generateLink: (
type: "verify" | "reset",
type: Omit<MailTypes, "pwchange">,
id: string,
email: string,
) => Promise<string>;
sendMail: (
type: MailTypes,
user: User,
email: string,
) => Promise<SentMessageInfo>;
sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>;
sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>;
sendPasswordChanged: (
@ -89,8 +73,7 @@ export const Email: {
doReplacements: (
template: string,
user: User,
emailVerificationUrl?: string,
passwordResetUrl?: string,
actionUrl?: string,
ipInfo?: {
ip: string;
city: string;
@ -119,8 +102,7 @@ export const Email: {
doReplacements: function (
template,
user,
emailVerificationUrl?,
passwordResetUrl?,
actionUrl?,
ipInfo?: {
ip: string;
city: string;
@ -137,8 +119,7 @@ export const Email: {
["{userId}", user.id],
["{phoneNumber}", user.phone?.slice(-4)],
["{userEmail}", user.email],
["{emailVerificationUrl}", emailVerificationUrl],
["{passwordResetUrl}", passwordResetUrl],
["{actionUrl}", actionUrl],
["{ipAddress}", ipInfo?.ip],
["{locationCity}", ipInfo?.city],
["{locationRegion}", ipInfo?.region],
@ -165,32 +146,45 @@ export const Email: {
const link = `${instanceUrl}/${type}#token=${token}`;
return link;
},
/**
* Sends an email to the user with a link to verify their email address
*
* @param type the MailType to send
* @param user the user to address it to
* @param email the email to send it to
* @returns
*/
sendVerifyEmail: async function (user, email) {
sendMail: async function (type, user, email) {
if (!this.transporter) return;
// generate a verification link for the user
const link = await this.generateLink("verify", user.id, email);
const templateNames: { [key in MailTypes]: string } = {
verify: "verify_email.html",
reset: "password_reset_request.html",
pwchange: "password_changed.html",
};
// load the email template
const rawTemplate = fs.readFileSync(
const template = await fs.readFile(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"verify_email.html",
templateNames[type],
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, link);
const html = this.doReplacements(
template,
user,
// password change emails don't have links
type != MailTypes.pwchange
? await this.generateLink(type, user.id, email)
: undefined,
);
// 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 = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
@ -199,78 +193,25 @@ export const Email: {
html,
};
// send the email
return this.transporter.sendMail(message);
},
/**
* Sends an email to the user with a link to verify their email address
*/
sendVerifyEmail: async function (user, email) {
return this.sendMail(MailTypes.verify, user, email);
},
/**
* Sends an email to the user with a link to reset their password
*/
sendResetPassword: async function (user, email) {
if (!this.transporter) return;
// generate a password reset link for the user
const link = await this.generateLink("reset", user.id, email);
// load the email template
const rawTemplate = await fs.promises.readFile(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"password_reset_request.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, undefined, link);
// 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 = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
to: email,
subject,
html,
};
// send the email
return this.transporter.sendMail(message);
return this.sendMail(MailTypes.reset, user, email);
},
/**
* Sends an email to the user notifying them that their password has been changed
*/
sendPasswordChanged: async function (user, email) {
if (!this.transporter) return;
// load the email template
const rawTemplate = await fs.promises.readFile(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"password_changed.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user);
// 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 = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
to: email,
subject,
html,
};
// send the email
return this.transporter.sendMail(message);
return this.sendMail(MailTypes.pwchange, user, email);
},
};