From d18584f8e9f9423f9d72d837a36a0c3cad6e8d10 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:36:29 +1000 Subject: Refactor email sending + remove email verification if mail sending is not set up --- assets/email_templates/new_login_location.html | 143 +++++++++---------- assets/email_templates/password_reset_request.html | 125 ++++++++--------- src/api/routes/auth/login.ts | 8 +- src/api/routes/auth/register.ts | 5 +- src/api/routes/users/@me/index.ts | 2 - src/util/entities/User.ts | 21 +-- src/util/util/email/index.ts | 151 +++++++-------------- 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 @@ - - - - - - Verify {instanceName} Login from New Location - - - -
- Branding + + + + Verify {instanceName} Login from New Location + + + + + +
+ Branding -
+
-

+

- Hey {userUsername}, -

-

- 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. -

-

- IP Address: {ipAddress} -
- Location: {locationCity}, {locationRegion}, - {locationCountryName} -

-
-
+ Hey {userUsername}, +

+

+ 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. +

+

+ IP Address: {ipAddress} +
+ Location: {locationCity}, {locationRegion}, + {locationCountryName} +

+
+ -
-
Verify Login +
+
+
-

- Alternatively, you can directly paste this link into - your browser: -

- {verifyUrl} -
+ "> +

+ Alternatively, you can directly paste this link into + your browser: +

+ {actionUrl}
- - +
+ + + \ 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 @@ - - - - - - Password Reset Request for {instanceName} - - - -
- Branding + + + + Password Reset Request for {instanceName} + + + + + +
+ Branding -
+
-

+

- Hey {userUsername}, -

-

- Your {instanceName} password can be reset by clicking the - button below. If you did not request a new password, please - ignore this email. -

-
-
+ Hey {userUsername}, +

+

+ Your {instanceName} password can be reset by clicking the + button below. If you did not request a new password, please + ignore this email. +

+
+ -
-
-

- Alternatively, you can directly paste this link into - your browser: -

- {passwordResetUrl} -
+ ">Reset Password +
+
+
+

+ Alternatively, you can directly paste this link into + your browser: +

+ {actionUrl}
- - +
+ + + \ 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 . */ -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 = 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; generateLink: ( - type: "verify" | "reset", + type: Omit, id: string, email: string, ) => Promise; + sendMail: ( + type: MailTypes, + user: User, + email: string, + ) => Promise; sendVerifyEmail: (user: User, email: string) => Promise; sendResetPassword: (user: User, email: string) => Promise; 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>/)?.[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); }, }; -- cgit 1.4.1