summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/email_templates/new_login_location.html143
-rw-r--r--assets/email_templates/password_reset_request.html125
-rw-r--r--src/api/routes/auth/login.ts8
-rw-r--r--src/api/routes/auth/register.ts5
-rw-r--r--src/api/routes/users/@me/index.ts2
-rw-r--r--src/util/entities/User.ts21
-rw-r--r--src/util/util/email/index.ts151
7 files changed, 174 insertions, 281 deletions
diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html
index e4911c5e..8c188def 100644
--- a/assets/email_templates/new_login_location.html
+++ b/assets/email_templates/new_login_location.html
@@ -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>
-</html>
+	</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/assets/email_templates/password_reset_request.html b/assets/email_templates/password_reset_request.html
index 7de25bf1..df341798 100644
--- a/assets/email_templates/password_reset_request.html
+++ b/assets/email_templates/password_reset_request.html
@@ -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>
-</html>
+	</div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index d3fc1fb4..cc5a2063 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -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",
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index c92ebfe0..de1cbd3d 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -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) {
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index 8fe86265..ad11a428 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -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: {
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 3f1bda05..c6582b00 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -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 (
diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts
index 5effdd2f..619cc5c3 100644
--- a/src/util/util/email/index.ts
+++ b/src/util/util/email/index.ts
@@ -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);
 	},
 };