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);
},
};
|