From ed6c1cbd1521d750bd9ac6823851057d00987332 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Tue, 17 Jan 2023 09:36:24 -0500 Subject: [PATCH 01/31] Start implementing smtp --- package.json | 2 ++ src/api/Server.ts | 20 +++++++------- src/util/config/Config.ts | 2 ++ src/util/config/types/SMTPConfiguration.ts | 7 +++++ src/util/config/types/index.ts | 3 ++- src/util/util/Email.ts | 31 ++++++++++++++++++++++ 6 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 src/util/config/types/SMTPConfiguration.ts diff --git a/package.json b/package.json index 4aef413a..eabc247e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/node": "^18.7.20", "@types/node-fetch": "^2.6.2", "@types/node-os-utils": "^1.3.0", + "@types/nodemailer": "^6.4.7", "@types/probe-image-size": "^7.2.0", "@types/sharp": "^0.31.0", "@types/ws": "^8.5.3", @@ -95,6 +96,7 @@ "node-2fa": "^2.0.3", "node-fetch": "^2.6.7", "node-os-utils": "^1.3.7", + "nodemailer": "^6.9.0", "picocolors": "^1.0.0", "probe-image-size": "^7.2.3", "proxy-agent": "^5.0.0", diff --git a/src/api/Server.ts b/src/api/Server.ts index 7eb4e6f1..aec47818 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -16,28 +16,29 @@ along with this program. If not, see . */ -import "missing-native-js-functions"; -import { Server, ServerOptions } from "lambert-server"; -import { Authentication, CORS } from "./middlewares/"; import { Config, + Email, initDatabase, initEvent, JSONReplacer, + registerRoutes, Sentry, WebAuthn, } from "@fosscord/util"; -import { ErrorHandler } from "./middlewares/ErrorHandler"; -import { BodyParser } from "./middlewares/BodyParser"; -import { Router, Request, Response } from "express"; +import { Request, Response, Router } from "express"; +import { Server, ServerOptions } from "lambert-server"; +import "missing-native-js-functions"; +import morgan from "morgan"; import path from "path"; +import { red } from "picocolors"; +import { Authentication, CORS } from "./middlewares/"; +import { BodyParser } from "./middlewares/BodyParser"; +import { ErrorHandler } from "./middlewares/ErrorHandler"; import { initRateLimits } from "./middlewares/RateLimit"; import TestClient from "./middlewares/TestClient"; import { initTranslation } from "./middlewares/Translation"; -import morgan from "morgan"; import { initInstance } from "./util/handlers/Instance"; -import { registerRoutes } from "@fosscord/util"; -import { red } from "picocolors"; export type FosscordServerOptions = ServerOptions; @@ -63,6 +64,7 @@ export class FosscordServer extends Server { await initDatabase(); await Config.init(); await initEvent(); + await Email.init(); await initInstance(); await Sentry.init(this.app); WebAuthn.init(); diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts index 122dadb5..583c1489 100644 --- a/src/util/config/Config.ts +++ b/src/util/config/Config.ts @@ -35,6 +35,7 @@ import { RegisterConfiguration, SecurityConfiguration, SentryConfiguration, + SMTPConfiguration, TemplateConfiguration, } from "../config"; @@ -58,4 +59,5 @@ export class ConfigValue { sentry: SentryConfiguration = new SentryConfiguration(); defaults: DefaultsConfiguration = new DefaultsConfiguration(); external: ExternalTokensConfiguration = new ExternalTokensConfiguration(); + smtp: SMTPConfiguration = new SMTPConfiguration(); } diff --git a/src/util/config/types/SMTPConfiguration.ts b/src/util/config/types/SMTPConfiguration.ts new file mode 100644 index 00000000..e833376a --- /dev/null +++ b/src/util/config/types/SMTPConfiguration.ts @@ -0,0 +1,7 @@ +export class SMTPConfiguration { + host: string | null = null; + port: number | null = null; + secure: boolean | null = null; + username: string | null = null; + password: string | null = null; +} diff --git a/src/util/config/types/index.ts b/src/util/config/types/index.ts index 523ad186..3d8ed6df 100644 --- a/src/util/config/types/index.ts +++ b/src/util/config/types/index.ts @@ -34,5 +34,6 @@ export * from "./RegionConfiguration"; export * from "./RegisterConfiguration"; export * from "./SecurityConfiguration"; export * from "./SentryConfiguration"; -export * from "./TemplateConfiguration"; +export * from "./SMTPConfiguration"; export * from "./subconfigurations"; +export * from "./TemplateConfiguration"; diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 48d8cae1..d45eb9a1 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -43,3 +43,34 @@ export function adjustEmail(email?: string): string | undefined { // return email; } + +export const Email: { + transporter: Transporter | null; + init: () => Promise; +} = { + transporter: null, + init: async function () { + const { host, port, secure, username, password } = Config.get().smtp; + if (!host || !port || !secure || !username || !password) return; + console.log(`[SMTP] connect: ${host}`); + this.transporter = nodemailer.createTransport({ + host, + port, + secure, + auth: { + user: username, + pass: password, + }, + }); + + await this.transporter.verify((error, _) => { + if (error) { + console.error(`[SMTP] error: ${error}`); + this.transporter?.close(); + this.transporter = null; + return; + } + console.log(`[SMTP] Ready`); + }); + }, +}; From 256c7ed8fefac586590addf4aacae7ffdda0d577 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Tue, 17 Jan 2023 11:12:25 -0500 Subject: [PATCH 02/31] send email verification --- assets/schemas.json | Bin 1116172 -> 1136521 bytes src/api/routes/auth/verify/index.ts | 45 ++++++++++++++++++++++++++ src/util/entities/User.ts | 26 ++++++++++++++- src/util/schemas/VerifyEmailSchema.ts | 4 +++ src/util/util/Token.ts | 25 ++++++++++++-- 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/api/routes/auth/verify/index.ts create mode 100644 src/util/schemas/VerifyEmailSchema.ts diff --git a/assets/schemas.json b/assets/schemas.json index 1c221caba5fea7557431105827448077afbd7e3f..3422951e81e46cd924c48c75dc1ae39def4ea267 100644 GIT binary patch delta 95 zcmV-l0HFVjkVJ{EMSz3>gaU*Egam{Iga(8Mgb0KQgbIWUgbaiYgbsucgb=sT?E(=9 z3sz-vX=ZsvZDDC_lW`bImwlxKD3?%I3Tc;1o&z|SptuQFmwu%L3%Ap=1NKb<3Vk6| BA@~3Q delta 46 zcmeBN@7goLrJ;qfg{g(Pg{6hHg{_6Xg` { + const { captcha_key, token } = req.body; + + if (captcha_key) { + const { sitekey, service } = Config.get().security.captcha; + const verify = await verifyCaptcha(captcha_key); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + } + + try { + const { jwtSecret } = Config.get().security; + + const { decoded, user } = await verifyToken(token, jwtSecret); + // toksn should last for 24 hours from the time they were issued + if (decoded.exp < Date.now() / 1000) { + throw FieldErrors({ + token: { + code: "TOKEN_INVALID", + message: "Invalid token", // TODO: add translation + }, + }); + } + user.verified = true; + } catch (error: any) { + throw new HTTPError(error?.toString(), 400); + } + }, +); + +export default router; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 7b67c2ac..f39fc19b 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -31,7 +31,7 @@ import { ConnectedAccount } from "./ConnectedAccount"; import { Member } from "./Member"; import { UserSettings } from "./UserSettings"; import { Session } from "./Session"; -import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from ".."; +import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail, Email, generateToken } from ".."; import { Request } from "express"; import { SecurityKey } from "./SecurityKey"; @@ -383,6 +383,30 @@ export class User extends BaseClass { user.validate(); await Promise.all([user.save(), settings.save()]); + // send verification email + if (Email.transporter && email) { + const token = (await generateToken(user.id, email)) as string; + const link = `http://localhost:3001/verify#token=${token}`; + const message = { + from: + Config.get().general.correspondenceEmail || + "noreply@localhost", + to: email, + subject: `Verify Email Address for ${ + Config.get().general.instanceName + }`, + html: `Please verify your email address by clicking the following link: Verify Email`, + }; + + await Email.transporter + .sendMail(message) + .then((info) => { + console.log("Message sent: %s", info.messageId); + }) + .catch((e) => { + console.error(`Failed to send email to ${email}: ${e}`); + }); + } setImmediate(async () => { if (Config.get().guild.autoJoin.enabled) { diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts new file mode 100644 index 00000000..ad170e84 --- /dev/null +++ b/src/util/schemas/VerifyEmailSchema.ts @@ -0,0 +1,4 @@ +export interface VerifyEmailSchema { + captcha_key: string | null; + token: string; +} diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index ca81eaaa..b3ebcc07 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -72,13 +72,34 @@ export function checkToken( }); } -export async function generateToken(id: string) { +export function verifyToken( + token: string, + jwtSecret: string, +): Promise<{ decoded: any; user: User }> { + return new Promise((res, rej) => { + jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { + if (err || !decoded) return rej("Invalid Token"); + + const user = await User.findOne({ + where: { id: decoded.id }, + select: ["data", "bot", "disabled", "deleted", "rights"], + }); + if (!user) return rej("Invalid Token"); + if (user.disabled) return rej("User disabled"); + if (user.deleted) return rej("User not found"); + + return res({ decoded, user }); + }); + }); +} + +export async function generateToken(id: string, email?: string) { const iat = Math.floor(Date.now() / 1000); const algorithm = "HS256"; return new Promise((res, rej) => { jwt.sign( - { id: id, iat }, + { id: id, email: email, iat }, Config.get().security.jwtSecret, { algorithm, From 2cddd7a8de444ee3a618ce6a08f6ae101183a167 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Thu, 19 Jan 2023 18:49:47 +1100 Subject: [PATCH 03/31] Send different app for /verify --- assets/client_test/verify.html | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 assets/client_test/verify.html diff --git a/assets/client_test/verify.html b/assets/client_test/verify.html new file mode 100644 index 00000000..08654323 --- /dev/null +++ b/assets/client_test/verify.html @@ -0,0 +1,58 @@ + + + + + + + + + + Fosscord Test Client Developer Portal + + + + +
+ + + + + + + + \ No newline at end of file From cc6bf066b143841d1745e972385c8c77fb7a12e4 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 09:58:49 -0500 Subject: [PATCH 04/31] add missing copyright headers --- src/api/routes/auth/verify/index.ts | 18 ++++++++++++++++++ src/util/config/types/SMTPConfiguration.ts | 18 ++++++++++++++++++ src/util/schemas/VerifyEmailSchema.ts | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index eae938eb..4c076d09 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -1,3 +1,21 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + import { route, verifyCaptcha } from "@fosscord/api"; import { Config, FieldErrors, verifyToken } from "@fosscord/util"; import { Request, Response, Router } from "express"; diff --git a/src/util/config/types/SMTPConfiguration.ts b/src/util/config/types/SMTPConfiguration.ts index e833376a..11eb9e14 100644 --- a/src/util/config/types/SMTPConfiguration.ts +++ b/src/util/config/types/SMTPConfiguration.ts @@ -1,3 +1,21 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + export class SMTPConfiguration { host: string | null = null; port: number | null = null; diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts index ad170e84..fa6a4c0d 100644 --- a/src/util/schemas/VerifyEmailSchema.ts +++ b/src/util/schemas/VerifyEmailSchema.ts @@ -1,3 +1,21 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + export interface VerifyEmailSchema { captcha_key: string | null; token: string; From a47d80b255f1501e39bebd7ad7e80119c8ed1697 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 11:15:12 -0500 Subject: [PATCH 05/31] Email verification works - Added /auth/verify to authenticated route whitelist - Updated /auth/verify to properly mark a user as verified, return a response, and fix expiration time check - Implemented /auth/verify/resend - Moved verification email sending to a helper method - Fixed VerifyEmailSchema requiring captcha_key --- src/api/middlewares/Authentication.ts | 5 +-- src/api/routes/auth/verify/index.ts | 23 +++++++++++-- src/api/routes/auth/verify/resend.ts | 49 +++++++++++++++++++++++++++ src/util/entities/User.ts | 23 ++++--------- src/util/schemas/VerifyEmailSchema.ts | 2 +- src/util/util/Email.ts | 26 ++++++++++++++ src/util/util/Token.ts | 24 +++++++++++++ 7 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 src/api/routes/auth/verify/resend.ts diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index ea0aa312..f4c33963 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { NextFunction, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; import { checkToken, Config, Rights } from "@fosscord/util"; import * as Sentry from "@sentry/node"; +import { NextFunction, Request, Response } from "express"; +import { HTTPError } from "lambert-server"; export const NO_AUTHORIZATION_ROUTES = [ // Authentication routes @@ -28,6 +28,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "/auth/location-metadata", "/auth/mfa/totp", "/auth/mfa/webauthn", + "/auth/verify", // Routes with a seperate auth system "/webhooks/", // Public information endpoints diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index 4c076d09..d61b8d16 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -17,7 +17,11 @@ */ import { route, verifyCaptcha } from "@fosscord/api"; -import { Config, FieldErrors, verifyToken } from "@fosscord/util"; +import { + Config, + FieldErrors, + verifyTokenEmailVerification, +} from "@fosscord/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); @@ -43,9 +47,13 @@ router.post( try { const { jwtSecret } = Config.get().security; - const { decoded, user } = await verifyToken(token, jwtSecret); + const { decoded, user } = await verifyTokenEmailVerification( + token, + jwtSecret, + ); + // toksn should last for 24 hours from the time they were issued - if (decoded.exp < Date.now() / 1000) { + if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) { throw FieldErrors({ token: { code: "TOKEN_INVALID", @@ -53,7 +61,16 @@ router.post( }, }); } + + if (user.verified) return res.send(user); + + // verify email user.verified = true; + await user.save(); + + // TODO: invalidate token after use? + + return res.send(user); } catch (error: any) { throw new HTTPError(error?.toString(), 400); } diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts new file mode 100644 index 00000000..0c8c4ed9 --- /dev/null +++ b/src/api/routes/auth/verify/resend.ts @@ -0,0 +1,49 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { route } from "@fosscord/api"; +import { Email, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +router.post("/", route({}), async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["email"], + }); + + if (!user.email) { + // TODO: whats the proper error response for this? + throw new HTTPError("User does not have an email address", 400); + } + + await Email.sendVerificationEmail(req.user_id, user.email) + .then((info) => { + console.log("Message sent: %s", info.messageId); + return res.sendStatus(204); + }) + .catch((e) => { + console.error( + `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, + ); + throw new HTTPError("Failed to send verification email", 500); + }); +}); + +export default router; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index f39fc19b..66e10297 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -383,28 +383,17 @@ export class User extends BaseClass { user.validate(); await Promise.all([user.save(), settings.save()]); - // send verification email - if (Email.transporter && email) { - const token = (await generateToken(user.id, email)) as string; - const link = `http://localhost:3001/verify#token=${token}`; - const message = { - from: - Config.get().general.correspondenceEmail || - "noreply@localhost", - to: email, - subject: `Verify Email Address for ${ - Config.get().general.instanceName - }`, - html: `Please verify your email address by clicking the following link: Verify Email`, - }; - await Email.transporter - .sendMail(message) + // send verification email if users aren't verified by default and we have an email + if (!Config.get().defaults.user.verified && email) { + await Email.sendVerificationEmail(user.id, email) .then((info) => { console.log("Message sent: %s", info.messageId); }) .catch((e) => { - console.error(`Failed to send email to ${email}: ${e}`); + console.error( + `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, + ); }); } diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts index fa6a4c0d..d94fbbc1 100644 --- a/src/util/schemas/VerifyEmailSchema.ts +++ b/src/util/schemas/VerifyEmailSchema.ts @@ -17,6 +17,6 @@ */ export interface VerifyEmailSchema { - captcha_key: string | null; + captcha_key?: string | null; token: string; } diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index d45eb9a1..371ba827 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -16,6 +16,10 @@ along with this program. If not, see . */ +import nodemailer, { Transporter } from "nodemailer"; +import { Config } from "./Config"; +import { generateToken } from "./Token"; + 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,}))$/; @@ -47,6 +51,7 @@ export function adjustEmail(email?: string): string | undefined { export const Email: { transporter: Transporter | null; init: () => Promise; + sendVerificationEmail: (id: string, email: string) => Promise; } = { transporter: null, init: async function () { @@ -73,4 +78,25 @@ export const Email: { console.log(`[SMTP] Ready`); }); }, + sendVerificationEmail: async function ( + id: string, + email: string, + ): Promise { + if (!this.transporter) return; + const token = (await generateToken(id, email)) as string; + const instanceUrl = + Config.get().general.frontPage || "http://localhost:3001"; + const link = `${instanceUrl}/verify#token=${token}`; + const message = { + from: + Config.get().general.correspondenceEmail || "noreply@localhost", + to: email, + subject: `Verify Email Address for ${ + Config.get().general.instanceName + }`, + html: `Please verify your email address by clicking the following link: Verify Email`, + }; + + return this.transporter.sendMail(message); + }, }; diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index b3ebcc07..e4b1fe41 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -72,6 +72,30 @@ export function checkToken( }); } +/** + * Puyodead1 (1/19/2023): I made a copy of this function because I didn't want to break anything with the other one. + * this version of the function doesn't use select, so we can update the user. with select causes constraint errors. + */ +export function verifyTokenEmailVerification( + token: string, + jwtSecret: string, +): Promise<{ decoded: any; user: User }> { + return new Promise((res, rej) => { + jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { + if (err || !decoded) return rej("Invalid Token"); + + const user = await User.findOne({ + where: { id: decoded.id }, + }); + if (!user) return rej("Invalid Token"); + if (user.disabled) return rej("User disabled"); + if (user.deleted) return rej("User not found"); + + return res({ decoded, user }); + }); + }); +} + export function verifyToken( token: string, jwtSecret: string, From 1f388b17a5c6c0b19b11c9ac06fd223f74e6c2be Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 11:17:36 -0500 Subject: [PATCH 06/31] change verify.html title --- assets/client_test/verify.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/client_test/verify.html b/assets/client_test/verify.html index 08654323..0de6fc6b 100644 --- a/assets/client_test/verify.html +++ b/assets/client_test/verify.html @@ -7,7 +7,7 @@ - Fosscord Test Client Developer Portal + Fosscord Test Client From 88d7b89aeb92cfdee63f5d3ef1d235c729fd0a6f Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 12:04:01 -0500 Subject: [PATCH 07/31] Update package-lock.json --- package-lock.json | Bin 272837 -> 273528 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecd455b896a2cbc53c6231b7ffb64e3b3a9432a3..8d7b1db285116ba26abd76a989ef88a953c227d5 100644 GIT binary patch delta 384 zcmX?lS>VSFfep)9MDp@eQgahCb5e_xtQ3^u%=9ev3^vba`O74LE;Ct?UATD;`}R5P zjMmz05M7fKCyFtf>6uKQV9IDd{ed1MizLJ_eO-NoeqEpnJ@e^`ZOjTGriIQItF>_8&&}GzVzi!01{kjp8-5(@Z zGMVX3-&no delta 54 zcmV-60LlOO*bv3j5U{5Nv%>`S0)yKJx7!B+J1dtnGXolz&KUzQmrx@C2ZypW0k^U= M0`B{_$T0)3`h%7h=>Px# From 0df1ea22cbed1a111925a4d4a1d244ea0837fac7 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 22:33:54 -0500 Subject: [PATCH 08/31] Add an email template for email verification --- assets/email_templates/verify_email.html | 89 ++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 assets/email_templates/verify_email.html diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html new file mode 100644 index 00000000..55825cfc --- /dev/null +++ b/assets/email_templates/verify_email.html @@ -0,0 +1,89 @@ + + + + + + + Verify Email Address for {instanceName} + + + + + Branding +
+

Hey {username},

+

+ Thanks for registering for an account on {instanceName}! Before + we get started, we just need to confirm that this is you. Click + below to verify your email address: +

+
+ +
+
+

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

+ {verificationUrl} +
+
+
+ + From 292e722f359d7eb5e106f908c07c948d6a10a006 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 22:34:56 -0500 Subject: [PATCH 09/31] update verify email template to add target --- assets/email_templates/verify_email.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html index 55825cfc..041378d9 100644 --- a/assets/email_templates/verify_email.html +++ b/assets/email_templates/verify_email.html @@ -61,7 +61,13 @@ below to verify your email address:

-
+
Verify Email @@ -72,14 +78,14 @@ display: flex; justify-content: center; flex-direction: column; - text-align: center + text-align: center; " >

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

- {verificationUrl}
From 878fd9d1e87924a95a0db0b1c56232df3dd7ca4c Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 22:35:56 -0500 Subject: [PATCH 10/31] Update schemas.json --- assets/schemas.json | Bin 1136521 -> 1137196 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/schemas.json b/assets/schemas.json index 3422951e81e46cd924c48c75dc1ae39def4ea267..2bfb525df9a60fe52e64481415537394ff7bd823 100644 GIT binary patch delta 274 zcmeBN@49A->xRQD+QGgd0nYh(d8x@InfZBo&ZWgA`MHjzB^jxCC7H>IAi<#2;)49V z;?(JZVO(y^xw(nc8|xXxr++A9l%1R)Ai!P(k)M9>DXa5jJ}c>I?2K}g6IcbN^Eoq1 zf|X5wV8AXkJ>HW?V)}+~X4&Zmm)LEmJ7fz;DJACv&C5$s(&17-0!mOLQ-R`>8LdQ^ zfK+3&^mOeVj6Bo-IIuZ1A7*Jk%)$u7OhC*G#2`K^5VHX>I}mdKF((jn0Wmia^8hg~ b5c2^sKM)ISKg=R{h-Leu&CJhznYd~JeUn`x delta 96 zcmZ3p#kF(2>xRQD&5i8sjqHp-%ml>DK+FQftU$~L#2`5iAm#*OE+FOxVjdvo1!6uR d<_BVd?Tze$hghatuVCTXK5-lKH(w^MS^)ED9*O_} From f337f2e785d7aebd52ae7bf30c5b67783d5dc23a Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Fri, 20 Jan 2023 09:18:37 -0500 Subject: [PATCH 11/31] Add other email templates --- .../email_templates/new_login_location.html | 113 ++++++++++++++++++ assets/email_templates/password_changed.html | 60 ++++++++++ .../password_reset_request.html | 108 +++++++++++++++++ assets/email_templates/phone_removed.html | 64 ++++++++++ assets/email_templates/verify_email.html | 15 ++- 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 assets/email_templates/new_login_location.html create mode 100644 assets/email_templates/password_changed.html create mode 100644 assets/email_templates/password_reset_request.html create mode 100644 assets/email_templates/phone_removed.html diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html new file mode 100644 index 00000000..701196cd --- /dev/null +++ b/assets/email_templates/new_login_location.html @@ -0,0 +1,113 @@ + + + + + + + Verify {instanceName} Login from New Location + + + + + Branding +
+

+ Hey {username}, +

+

+ 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: {ip} +
+ Location: {location} +

+
+ +
+
+

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

+ {verifyUrl} +
+
+
+ + diff --git a/assets/email_templates/password_changed.html b/assets/email_templates/password_changed.html new file mode 100644 index 00000000..3f762702 --- /dev/null +++ b/assets/email_templates/password_changed.html @@ -0,0 +1,60 @@ + + + + + + + {instanceName} Password Changed + + + + + Branding +
+

+ Hey {username}, +

+

Your {instanceName} password has been changed.

+

+ If this wasn't done by you, please immediately reset the + password to your {instanceName} account. +

+
+ + diff --git a/assets/email_templates/password_reset_request.html b/assets/email_templates/password_reset_request.html new file mode 100644 index 00000000..fc77b47b --- /dev/null +++ b/assets/email_templates/password_reset_request.html @@ -0,0 +1,108 @@ + + + + + + + Password Reset Request for {instanceName} + + + + + Branding +
+

+ Hey {username}, +

+

+ 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} +
+
+
+ + diff --git a/assets/email_templates/phone_removed.html b/assets/email_templates/phone_removed.html new file mode 100644 index 00000000..1eb52fbe --- /dev/null +++ b/assets/email_templates/phone_removed.html @@ -0,0 +1,64 @@ + + + + + + + Phone Removed From {instanceName} Account + + + + + Branding +
+

+ Hey {username}, +

+

+ Your phone number ********{phoneNumber} was recently removed + from this account and added to a different {instanceName} + account. +

+

+ Please note that your phone number can only be linked to one + {instanceName} account at a time. +

+
+ + diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html index 041378d9..f0c11e52 100644 --- a/assets/email_templates/verify_email.html +++ b/assets/email_templates/verify_email.html @@ -7,6 +7,10 @@ Verify Email Address for {instanceName} - Branding -
-

+ Branding +

- Hey {username}, -

-

- 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: {ip} -
- Location: {location} -

-
-
- Verify Login +

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

+
+
-
-
-
-

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

- {verifyUrl} + Verify Login +
+
+
+

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

+ {verifyUrl} +
diff --git a/assets/email_templates/password_changed.html b/assets/email_templates/password_changed.html index 3f762702..399108a2 100644 --- a/assets/email_templates/password_changed.html +++ b/assets/email_templates/password_changed.html @@ -4,57 +4,62 @@ + {instanceName} Password Changed - Branding -
-

+ Branding +

- Hey {username}, -

-

Your {instanceName} password has been changed.

-

- If this wasn't done by you, please immediately reset the - password to your {instanceName} account. -

+

+ Hey {userUsername}, +

+

Your {instanceName} password has been changed.

+

+ If this wasn't done by you, please immediately reset the + password to your {instanceName} account. +

+
diff --git a/assets/email_templates/password_reset_request.html b/assets/email_templates/password_reset_request.html index fc77b47b..ab8f4d23 100644 --- a/assets/email_templates/password_reset_request.html +++ b/assets/email_templates/password_reset_request.html @@ -4,103 +4,96 @@ + Password Reset Request for {instanceName} - Branding -
-

+ Branding +

- Hey {username}, -

-

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

+ 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} +
diff --git a/assets/email_templates/phone_removed.html b/assets/email_templates/phone_removed.html index 1eb52fbe..65807e29 100644 --- a/assets/email_templates/phone_removed.html +++ b/assets/email_templates/phone_removed.html @@ -4,61 +4,66 @@ + Phone Removed From {instanceName} Account - Branding -
-

+ Branding +

- Hey {username}, -

-

- Your phone number ********{phoneNumber} was recently removed - from this account and added to a different {instanceName} - account. -

-

- Please note that your phone number can only be linked to one - {instanceName} account at a time. -

+

+ Hey {userUsername}, +

+

+ Your phone number ********{phoneNumber} was recently removed + from this account and added to a different {instanceName} + account. +

+

+ Please note that your phone number can only be linked to one + {instanceName} account at a time. +

+
diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html index f0c11e52..604242c4 100644 --- a/assets/email_templates/verify_email.html +++ b/assets/email_templates/verify_email.html @@ -4,103 +4,97 @@ + Verify Email Address for {instanceName} - Branding -
-

+ Branding +

- Hey {username}, -

-

- Thanks for registering for an account on {instanceName}! Before - we get started, we just need to confirm that this is you. Click - below to verify your email address: -

-
- -
-
-

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

- {verificationUrl} +

+ Thanks for registering for an account on {instanceName}! + Before we get started, we just need to confirm that this is + you. Click below to verify your email address: +

+
+ +
+
+

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

+ {emailVerificationUrl} +
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index 0c8c4ed9..d9a9cda5 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -33,7 +33,7 @@ router.post("/", route({}), async (req: Request, res: Response) => { 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) => { console.log("Message sent: %s", info.messageId); return res.sendStatus(204); diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 66e10297..4a399ed9 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -386,7 +386,7 @@ export class User extends BaseClass { // send verification email if users aren't verified by default and we have an email if (!Config.get().defaults.user.verified && email) { - await Email.sendVerificationEmail(user.id, email) + await Email.sendVerificationEmail(user, email) .then((info) => { console.log("Message sent: %s", info.messageId); }) diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 371ba827..9688c3c5 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -16,10 +16,14 @@ along with this program. If not, see . */ +import fs from "node:fs"; +import path from "node:path"; import nodemailer, { Transporter } from "nodemailer"; +import { User } from "../entities"; import { Config } from "./Config"; import { generateToken } from "./Token"; +const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "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,}))$/; @@ -51,7 +55,20 @@ export function adjustEmail(email?: string): string | undefined { export const Email: { transporter: Transporter | null; init: () => Promise; - sendVerificationEmail: (id: string, email: string) => Promise; + generateVerificationLink: (id: string, email: string) => Promise; + sendVerificationEmail: (user: User, email: string) => Promise; + doReplacements: ( + template: string, + user: User, + emailVerificationUrl?: string, + passwordResetUrl?: string, + ipInfo?: { + ip: string; + city: string; + region: string; + country_name: string; + }, + ) => string; } = { transporter: null, init: async function () { @@ -78,25 +95,109 @@ export const Email: { console.log(`[SMTP] Ready`); }); }, - sendVerificationEmail: async function ( - id: string, - email: string, - ): Promise { - if (!this.transporter) return; + /** + * Replaces all placeholders in an email template with the correct values + */ + doReplacements: function ( + 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 instanceUrl = Config.get().general.frontPage || "http://localhost:3001"; const link = `${instanceUrl}/verify#token=${token}`; + return link; + }, + sendVerificationEmail: async function ( + user: User, + email: string, + ): Promise { + 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>/)?.[1] || ""; + + // // construct the email const message = { from: Config.get().general.correspondenceEmail || "noreply@localhost", to: email, - subject: `Verify Email Address for ${ - Config.get().general.instanceName - }`, - html: `Please verify your email address by clicking the following link: <a href="${link}">Verify Email</a>`, + subject, + html, }; + // // send the email return this.transporter.sendMail(message); }, }; From 01103268c38ff85a3c82acdcbc74b1e2e6bd89c4 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 11:30:40 -0500 Subject: [PATCH 13/31] rename SMTPConfigurations to EmailConfiguration --- src/util/config/Config.ts | 4 ++-- .../types/{SMTPConfiguration.ts => EmailConfiguration.ts} | 2 +- src/util/config/types/RegisterConfiguration.ts | 5 +++-- src/util/config/types/index.ts | 2 +- src/util/config/types/subconfigurations/register/Email.ts | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) rename src/util/config/types/{SMTPConfiguration.ts => EmailConfiguration.ts} (96%) diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts index 583c1489..d6f804bf 100644 --- a/src/util/config/Config.ts +++ b/src/util/config/Config.ts @@ -21,6 +21,7 @@ import { CdnConfiguration, ClientConfiguration, DefaultsConfiguration, + EmailConfiguration, EndpointConfiguration, ExternalTokensConfiguration, GeneralConfiguration, @@ -35,7 +36,6 @@ import { RegisterConfiguration, SecurityConfiguration, SentryConfiguration, - SMTPConfiguration, TemplateConfiguration, } from "../config"; @@ -59,5 +59,5 @@ export class ConfigValue { sentry: SentryConfiguration = new SentryConfiguration(); defaults: DefaultsConfiguration = new DefaultsConfiguration(); external: ExternalTokensConfiguration = new ExternalTokensConfiguration(); - smtp: SMTPConfiguration = new SMTPConfiguration(); + email: EmailConfiguration = new EmailConfiguration(); } diff --git a/src/util/config/types/SMTPConfiguration.ts b/src/util/config/types/EmailConfiguration.ts similarity index 96% rename from src/util/config/types/SMTPConfiguration.ts rename to src/util/config/types/EmailConfiguration.ts index 11eb9e14..1e4a0361 100644 --- a/src/util/config/types/SMTPConfiguration.ts +++ b/src/util/config/types/EmailConfiguration.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -export class SMTPConfiguration { +export class EmailConfiguration { host: string | null = null; port: number | null = null; secure: boolean | null = null; diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts index acbaa2d5..689baa85 100644 --- a/src/util/config/types/RegisterConfiguration.ts +++ b/src/util/config/types/RegisterConfiguration.ts @@ -18,12 +18,13 @@ import { DateOfBirthConfiguration, - EmailConfiguration, PasswordConfiguration, + RegistrationEmailConfiguration, } from "."; export class RegisterConfiguration { - email: EmailConfiguration = new EmailConfiguration(); + email: RegistrationEmailConfiguration = + new RegistrationEmailConfiguration(); dateOfBirth: DateOfBirthConfiguration = new DateOfBirthConfiguration(); password: PasswordConfiguration = new PasswordConfiguration(); disabled: boolean = false; diff --git a/src/util/config/types/index.ts b/src/util/config/types/index.ts index 3d8ed6df..1431c128 100644 --- a/src/util/config/types/index.ts +++ b/src/util/config/types/index.ts @@ -20,6 +20,7 @@ export * from "./ApiConfiguration"; export * from "./CdnConfiguration"; export * from "./ClientConfiguration"; export * from "./DefaultsConfiguration"; +export * from "./EmailConfiguration"; export * from "./EndpointConfiguration"; export * from "./ExternalTokensConfiguration"; export * from "./GeneralConfiguration"; @@ -34,6 +35,5 @@ export * from "./RegionConfiguration"; export * from "./RegisterConfiguration"; export * from "./SecurityConfiguration"; export * from "./SentryConfiguration"; -export * from "./SMTPConfiguration"; export * from "./subconfigurations"; export * from "./TemplateConfiguration"; diff --git a/src/util/config/types/subconfigurations/register/Email.ts b/src/util/config/types/subconfigurations/register/Email.ts index 478dc974..4f95caf1 100644 --- a/src/util/config/types/subconfigurations/register/Email.ts +++ b/src/util/config/types/subconfigurations/register/Email.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -export class EmailConfiguration { +export class RegistrationEmailConfiguration { required: boolean = false; allowlist: boolean = false; blocklist: boolean = true; From 4383fcd4497c67e34d27fc0806824650df34466a Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 13:03:17 -0500 Subject: [PATCH 14/31] Add Mailgun transport --- package-lock.json | Bin 273528 -> 276467 bytes package.json | 3 +- src/util/config/types/EmailConfiguration.ts | 13 +++-- .../types/subconfigurations/email/MailGun.ts | 22 ++++++++ .../types/subconfigurations/email/SMTP.ts | 25 +++++++++ .../types/subconfigurations/email/index.ts | 20 +++++++ src/util/util/Email.ts | 51 ++++++++++++++++-- 7 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 src/util/config/types/subconfigurations/email/MailGun.ts create mode 100644 src/util/config/types/subconfigurations/email/SMTP.ts create mode 100644 src/util/config/types/subconfigurations/email/index.ts diff --git a/package-lock.json b/package-lock.json index 8d7b1db285116ba26abd76a989ef88a953c227d5..a07a7ec7553991efe4baebfe83eb6df91b947ec7 100644 GIT binary patch delta 1535 zcmaJ>OQ;)V6lRiJ?Jc&{w)bjVTknl1YD_ZkNpvB3zmu8V%;W(IGnvU`CX<<)d1R7+ z(2L-vQYpdyh%S0@A-Ir=NEW(i5d^ysi<|mf3A&LMx=`HcB=?p+7S87X|8ow^`Mz_$ z@9&#O@7y|i#aMk#H$-W`E1G167KiNUA=~72YuhmGz{?wf*U>N%Cf<}+=khA_w7bHO z6$9*i39b9W7}(tL!H+K@RwNWzo*~2wU;N&;$=XJ15KLGTjpUMqyVdXZ1)-84Yt<1| zY{f7-ok*0(@g&um^7$NZ#g!56(nBWF!E36)OqK0%Erm7+8AUqPA>(Lm%FMZdf7^F* zcKM&DPInDWGu)6ViHa$8Y-`yo5=MwH3~ooD_1Uk!!{DaoUxSbt)I2+X>m2kL<PVWJ zh`$Hrj`5b{-My}by;}$YJMTcxaD6vL=QGLd7_PK)hK!M-8nknYOXEy=YPGSrT;>!u zQOv_Zj?i%ycDnT>Tkh!$jEf3i%SY4A2-BgvLyekhy*8!dQ(2zHe-F<u>_A_wKB762 zpqOCz2ejtL!W8)QeFy<pPeE%C1+L-HCfD4qsAM+9OvP>^Xrc{Wr^d=wT%_t{Or7?} z*=8bJw;M%gt2si(EEnXb<M<$|ij8&!CYd_Z526*cnTV;iRDUE4m_esz?{z?*huGw! zmZ2#kZv*HxXl(`d7@J)?1E1*_`hG(4Sh?>N@=CzY)E{6|4V=_Xt6Y#LvHmbF&^BQf z`J_oX8GGBn2V3l@l*_Y1A*~MJkrCuu1XFSo>=4Toal57$%t#Zatqz>T)o!F0%-PMY zGT1y1Ehl<V*rP@Sf6o0V$RvFMaOykg`26#4p}#I2eqh?m1@wEZgWb~*{QSea-~o6z z<n5(by&#z7m{JzQ3>Yu+Ow?&9sLfRb(QIrZOk-TBNPJKq<P&(BE2Gg7+#ApcHKq70 z5_IGJ%9I=8MX6aIVknACBw|=E&@rUof{X7$F9H~NO<(y1I&q@r2$If8reK(IFK}qp z``(zrKW>1t6X-NJ7xk?I<zN5$?CSL+2SiD5WWZ#gSLPo}zHgiZOBsUQ5B*PoONtLW zwJ*%Dx{%quM<^Pe-OvK8n9DYtY-|*z(^Xw9E6$eF8g=yagtFQZ+$jw4cDH2_aJSuK zd-#yHxn#!e5Yd9rE2D-ms%%+y#l>4AE5~yZUQ#1S%>tVr`A!1(1^<~>7y90p7Z%0> zFD=mo_Jy;bfWz?Jgkx$U)ll?h1`oqC#yAT$e)AoekACviIB*&EA3XA)*~~V6eiB^1 z0<FwGyAuJoVE-ZF!GeVwdD#EYtU{hqf!jyD0rxT#UG4MZj9ebI(jvj<N<34@qXdFz zVvuJvMxhEhQpl=3Rcm8dCoj|!F<KdDd9&mY%_KIZ$f78b5=m6^ZYic!#rl$uh4ZZ6 zr;R1Vg@hKr-Z!0N_Xe~~NB0<*e~tJL{^0kBneBeC2EKrO*zvnedC2Zw!Ho;PXIZt6 z1R0ua7lxCn;-vGjc)G!KRVSh+P};>wvM<EpQo60FIkza4Wii$;<7qja9jkOIg$C;t dK@c#?tWSFBu9<9honlj8Ac)MbA6ofj^FJi!1m6Gv delta 91 zcmV-h0Hpu(?-2Od5U_6vgPjStoe2RKcegHr0Sp1RLWBXo374Od0ScG58vz!#ERO*+ xLzht10T8$7(*gIEmv%4#8<$|L0yDQiZ3209w=LuXRaUn+JOc*Vw@DTRsv&**BEJ9t diff --git a/package.json b/package.json index eabc247e..8a6c0405 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ }, "optionalDependencies": { "erlpack": "^0.1.4", - "sqlite3": "^5.1.4" + "sqlite3": "^5.1.4", + "nodemailer-mailgun-transport": "^2.1.5" } } diff --git a/src/util/config/types/EmailConfiguration.ts b/src/util/config/types/EmailConfiguration.ts index 1e4a0361..34550f4c 100644 --- a/src/util/config/types/EmailConfiguration.ts +++ b/src/util/config/types/EmailConfiguration.ts @@ -16,10 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { + MailGunConfiguration, + SMTPConfiguration, +} from "./subconfigurations/email"; + export class EmailConfiguration { - host: string | null = null; - port: number | null = null; - secure: boolean | null = null; - username: string | null = null; - password: string | null = null; + provider: string | null = null; + smtp: SMTPConfiguration = new SMTPConfiguration(); + mailgun: MailGunConfiguration = new MailGunConfiguration(); } diff --git a/src/util/config/types/subconfigurations/email/MailGun.ts b/src/util/config/types/subconfigurations/email/MailGun.ts new file mode 100644 index 00000000..52cd9069 --- /dev/null +++ b/src/util/config/types/subconfigurations/email/MailGun.ts @@ -0,0 +1,22 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export class MailGunConfiguration { + apiKey: string | null = null; + domain: string | null = null; +} diff --git a/src/util/config/types/subconfigurations/email/SMTP.ts b/src/util/config/types/subconfigurations/email/SMTP.ts new file mode 100644 index 00000000..11eb9e14 --- /dev/null +++ b/src/util/config/types/subconfigurations/email/SMTP.ts @@ -0,0 +1,25 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export class SMTPConfiguration { + host: string | null = null; + port: number | null = null; + secure: boolean | null = null; + username: string | null = null; + password: string | null = null; +} diff --git a/src/util/config/types/subconfigurations/email/index.ts b/src/util/config/types/subconfigurations/email/index.ts new file mode 100644 index 00000000..92fe9184 --- /dev/null +++ b/src/util/config/types/subconfigurations/email/index.ts @@ -0,0 +1,20 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export * from "./MailGun"; +export * from "./SMTP"; diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 9688c3c5..b8019cbd 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -55,6 +55,8 @@ export function adjustEmail(email?: string): string | undefined { export const Email: { transporter: Transporter | null; init: () => Promise<void>; + initSMTP: () => Promise<void>; + initMailgun: () => Promise<void>; generateVerificationLink: (id: string, email: string) => Promise<string>; sendVerificationEmail: (user: User, email: string) => Promise<any>; doReplacements: ( @@ -72,9 +74,22 @@ export const Email: { } = { transporter: null, init: async function () { - const { host, port, secure, username, password } = Config.get().smtp; - if (!host || !port || !secure || !username || !password) return; - console.log(`[SMTP] connect: ${host}`); + const { provider } = Config.get().email; + if (!provider) return; + + if (provider === "smtp") await this.initSMTP(); + else if (provider === "mailgun") await this.initMailgun(); + else throw new Error(`Unknown email provider: ${provider}`); + }, + initSMTP: async function () { + const { host, port, secure, username, password } = + Config.get().email.smtp; + if (!host || !port || !secure || !username || !password) + return console.error( + "[Email] SMTP has not been configured correctly.", + ); + + console.log(`[Email] Initializing SMTP transport: ${host}`); this.transporter = nodemailer.createTransport({ host, port, @@ -87,14 +102,40 @@ export const Email: { await this.transporter.verify((error, _) => { if (error) { - console.error(`[SMTP] error: ${error}`); + console.error(`[Email] SMTP error: ${error}`); this.transporter?.close(); this.transporter = null; return; } - console.log(`[SMTP] Ready`); + console.log(`[Email] Ready`); }); }, + initMailgun: async function () { + const { apiKey, domain } = Config.get().email.mailgun; + if (!apiKey || !domain) + return console.error( + "[Email] Mailgun has not been configured correctly.", + ); + + try { + const mg = require("nodemailer-mailgun-transport"); + const auth = { + auth: { + api_key: apiKey, + domain: domain, + }, + }; + + console.log(`[Email] Initializing Mailgun transport...`); + this.transporter = nodemailer.createTransport(mg(auth)); + console.log(`[Email] Ready`); + } catch { + console.error( + "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save` to install it.", + ); + return; + } + }, /** * Replaces all placeholders in an email template with the correct values */ From bf55ebc81fa8d3cc4aa4e6fd3735ff0ee659505a Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 13:53:26 -0500 Subject: [PATCH 15/31] Add mailjet transport --- package-lock.json | Bin 276467 -> 285969 bytes package.json | 3 +- src/util/config/types/EmailConfiguration.ts | 2 + .../types/subconfigurations/email/MailJet.ts | 22 +++ .../types/subconfigurations/email/index.ts | 1 + src/util/util/Email.ts | 177 ++++++++++++------ 6 files changed, 142 insertions(+), 63 deletions(-) create mode 100644 src/util/config/types/subconfigurations/email/MailJet.ts diff --git a/package-lock.json b/package-lock.json index a07a7ec7553991efe4baebfe83eb6df91b947ec7..c6960e6c80bc2696a7d1e8398e36f8a38cd5ffaf 100644 GIT binary patch delta 4236 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 a`7yK=0q>Df__XyQ8I_@f>pzPARQrGLRDbRO delta 117 zcmV-*0E+*Sx)Jm55U`yIgUboG%LxIBQ@3wk0mC1+F@FKs0=Eo<0Y*xfFVO)7w|&k5 zeiOF|;{lWtx9Il)RuQ+63IeiIw{KqpUKqEbc>>WOx1XB=G)1=u<^s)Qw{%MbGXj%( X2^6=xPy?9(xBOKD=9RZR_XE{sGxso{ diff --git a/package.json b/package.json index 8a6c0405..819b040a 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "optionalDependencies": { "erlpack": "^0.1.4", "sqlite3": "^5.1.4", - "nodemailer-mailgun-transport": "^2.1.5" + "nodemailer-mailgun-transport": "^2.1.5", + "nodemailer-mailjet-transport": "^1.0.4" } } diff --git a/src/util/config/types/EmailConfiguration.ts b/src/util/config/types/EmailConfiguration.ts index 34550f4c..625507f2 100644 --- a/src/util/config/types/EmailConfiguration.ts +++ b/src/util/config/types/EmailConfiguration.ts @@ -18,6 +18,7 @@ import { MailGunConfiguration, + MailJetConfiguration, SMTPConfiguration, } from "./subconfigurations/email"; @@ -25,4 +26,5 @@ export class EmailConfiguration { provider: string | null = null; smtp: SMTPConfiguration = new SMTPConfiguration(); mailgun: MailGunConfiguration = new MailGunConfiguration(); + mailjet: MailJetConfiguration = new MailJetConfiguration(); } diff --git a/src/util/config/types/subconfigurations/email/MailJet.ts b/src/util/config/types/subconfigurations/email/MailJet.ts new file mode 100644 index 00000000..eccda8ac --- /dev/null +++ b/src/util/config/types/subconfigurations/email/MailJet.ts @@ -0,0 +1,22 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export class MailJetConfiguration { + apiKey: string | null = null; + apiSecret: string | null = null; +} diff --git a/src/util/config/types/subconfigurations/email/index.ts b/src/util/config/types/subconfigurations/email/index.ts index 92fe9184..02cc564c 100644 --- a/src/util/config/types/subconfigurations/email/index.ts +++ b/src/util/config/types/subconfigurations/email/index.ts @@ -17,4 +17,5 @@ */ export * from "./MailGun"; +export * from "./MailJet"; export * from "./SMTP"; diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index b8019cbd..8899b3c2 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -52,11 +52,111 @@ export function adjustEmail(email?: string): string | undefined { // return email; } +const transporters = { + smtp: async function () { + // get configuration + const { host, port, secure, username, password } = + Config.get().email.smtp; + + // ensure all required configuration values are set + if (!host || !port || !secure || !username || !password) + return console.error( + "[Email] SMTP has not been configured correctly.", + ); + + // construct the transporter + const transporter = nodemailer.createTransport({ + host, + port, + secure, + auth: { + user: username, + pass: password, + }, + }); + + // verify connection configuration + const verified = await transporter.verify().catch((err) => { + console.error("[Email] SMTP verification failed:", err); + return; + }); + + // if verification failed, return void and don't set transporter + if (!verified) return; + + return transporter; + }, + mailgun: async function () { + // get configuration + const { apiKey, domain } = Config.get().email.mailgun; + + // ensure all required configuration values are set + if (!apiKey || !domain) + return console.error( + "[Email] Mailgun has not been configured correctly.", + ); + + let mg; + try { + // try to import the transporter package + mg = require("nodemailer-mailgun-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + api_key: apiKey, + domain: domain, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(mg(auth)); + }, + mailjet: async function () { + // get configuration + const { apiKey, apiSecret } = Config.get().email.mailjet; + + // ensure all required configuration values are set + if (!apiKey || !apiSecret) + return console.error( + "[Email] Mailjet has not been configured correctly.", + ); + + let mj; + try { + // try to import the transporter package + mj = require("nodemailer-mailjet-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] Mailjet transport is not installed. Please run `npm install nodemailer-mailjet-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + apiKey: apiKey, + apiSecret: apiSecret, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(mj(auth)); + }, +}; + export const Email: { transporter: Transporter | null; init: () => Promise<void>; - initSMTP: () => Promise<void>; - initMailgun: () => Promise<void>; generateVerificationLink: (id: string, email: string) => Promise<string>; sendVerificationEmail: (user: User, email: string) => Promise<any>; doReplacements: ( @@ -77,64 +177,15 @@ export const Email: { const { provider } = Config.get().email; if (!provider) return; - if (provider === "smtp") await this.initSMTP(); - else if (provider === "mailgun") await this.initMailgun(); - else throw new Error(`Unknown email provider: ${provider}`); - }, - initSMTP: async function () { - const { host, port, secure, username, password } = - Config.get().email.smtp; - if (!host || !port || !secure || !username || !password) - return console.error( - "[Email] SMTP has not been configured correctly.", - ); - - console.log(`[Email] Initializing SMTP transport: ${host}`); - this.transporter = nodemailer.createTransport({ - host, - port, - secure, - auth: { - user: username, - pass: password, - }, - }); - - await this.transporter.verify((error, _) => { - if (error) { - console.error(`[Email] SMTP error: ${error}`); - this.transporter?.close(); - this.transporter = null; - return; - } - console.log(`[Email] Ready`); - }); - }, - initMailgun: async function () { - const { apiKey, domain } = Config.get().email.mailgun; - if (!apiKey || !domain) - return console.error( - "[Email] Mailgun has not been configured correctly.", - ); - - try { - const mg = require("nodemailer-mailgun-transport"); - const auth = { - auth: { - api_key: apiKey, - domain: domain, - }, - }; - - console.log(`[Email] Initializing Mailgun transport...`); - this.transporter = nodemailer.createTransport(mg(auth)); - console.log(`[Email] Ready`); - } catch { - console.error( - "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save` to install it.", - ); - return; - } + const transporterFn = + transporters[provider as keyof typeof transporters]; + if (!transporterFn) + return console.error(`[Email] Invalid provider: ${provider}`); + console.log(`[Email] Initializing ${provider} transport...`); + const transporter = await transporterFn(); + if (!transporter) return; + this.transporter = transporter; + console.log(`[Email] ${provider} transport initialized.`); }, /** * Replaces all placeholders in an email template with the correct values @@ -214,6 +265,7 @@ export const Email: { user.id, email, ); + // load the email template const rawTemplate = fs.readFileSync( path.join( @@ -223,13 +275,14 @@ export const Email: { ), { 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 + // construct the email const message = { from: Config.get().general.correspondenceEmail || "noreply@localhost", @@ -238,7 +291,7 @@ export const Email: { html, }; - // // send the email + // send the email return this.transporter.sendMail(message); }, }; From 6b8b42ce9a7e5272e848d22169bdd3433b06ef76 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 13:56:21 -0500 Subject: [PATCH 16/31] Update package-lock.json --- package-lock.json | Bin 285969 -> 520194 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6960e6c80bc2696a7d1e8398e36f8a38cd5ffaf..e19d3e0f42a9be614c5a1229fbcefa5c0d4aaae3 100644 GIT binary patch delta 22926 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 WA11u4Tjsst!fk1^E2cbCT>lsQ5ZQ$Q delta 892 zcmZ|N&uiLX7zc3Fr9;_}vB3sqv=ascCozde9TXNdCK@$HlW0;#7n5qDF~+Dd3S-b6 zcJ1(xzhTFo3OlsD?6%YN4;cF=cGzY9V60>7;f3dYpUda@zR$_Gy$_%6of018jQsvJ zph(t4Ga{1Jn?nob*`F&K10$f^>|2pzXJkq8D002$23g<Jw@sZ-PvfP=4DBH(??kh$ zqLQd&RGOjiVN$2yW)`X!4AAD=L@d`3HL)i)VFJ&PIh(IwaW+94bXG|@qE<wVQpQFZ zPQ?xmLeB$!zdt0K<Is!!!*%Tst%PVSw60%myM1#mEcb42RU!Au56|vAxY%{STmpC9 z^8j$Pt^!^#;$B<=k2YXVMX4M;8+4PM25Wce)@WMI*9l8W3@K*D*l>qe*ka0{YAqxS zD^|v4(qNI~9E>(AjXq-9u&ifC)DUqDQqoOaMXc;_4lRj)8K)mTcihhx-uv#p>4n^% z0bti%z4_Om*H^#;-)f+3ipnrL*1C$EC%8^!YDs*oUl>ezJl@v9fsi&Oux_ZRjygpg z);pj&F*+>Vki|?!RT`5zDC9(_$dN;IBK3uGveKiX2F2W%;@=sLwE5xk@s6$;W9M3k zYpMJ43IOKl;%MW(wO)vg9N2ut)?RZksVWPtWG%{&Qb|s<?P^V9YgWSGgwmkNDY%u! z$6->yVhqZar-}pdlN^pFRYEqQf$hlC)Rc{Li8dCN;tj3an$&K#|8E2SXWwzp0>JUw zy#qME;SV=tp5Uo)vQ^IGDZRjun6A=QTF~-QL5VdnF*Xi|GqBjvyER1U(Wt70bDVRG zW(U16P1RYsWEMd^0hQ4VgY{OkH)FGDVT{e;W$wD<U2yR}xbUow_dK}p#ZP>JH-TS3 CWE^(@ From 97bafa81fc252c762aac265dc96eb90ec279cf96 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 14:45:13 -0500 Subject: [PATCH 17/31] fix linting errors --- src/api/routes/auth/verify/index.ts | 15 +++------ src/util/entities/User.ts | 9 +++++- src/util/util/Email.ts | 12 ++++---- src/util/util/Token.ts | 47 +---------------------------- 4 files changed, 19 insertions(+), 64 deletions(-) diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index d61b8d16..7809bc26 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -17,11 +17,7 @@ */ import { route, verifyCaptcha } from "@fosscord/api"; -import { - Config, - FieldErrors, - verifyTokenEmailVerification, -} from "@fosscord/util"; +import { checkToken, Config, FieldErrors } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); @@ -47,10 +43,7 @@ router.post( try { const { jwtSecret } = Config.get().security; - const { decoded, user } = await verifyTokenEmailVerification( - token, - jwtSecret, - ); + const { decoded, user } = await checkToken(token, jwtSecret); // toksn should last for 24 hours from the time they were issued if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) { @@ -71,8 +64,8 @@ router.post( // TODO: invalidate token after use? return res.send(user); - } catch (error: any) { - throw new HTTPError(error?.toString(), 400); + } catch (error) { + throw new HTTPError((error as Error).toString(), 400); } }, ); diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 4a399ed9..42f74fb4 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -31,7 +31,14 @@ import { ConnectedAccount } from "./ConnectedAccount"; import { Member } from "./Member"; import { UserSettings } from "./UserSettings"; import { Session } from "./Session"; -import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail, Email, generateToken } from ".."; +import { + Config, + FieldErrors, + Snowflake, + trimSpecial, + adjustEmail, + Email, +} from ".."; import { Request } from "express"; import { SecurityKey } from "./SecurityKey"; diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 8899b3c2..cbcc5b60 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -18,7 +18,7 @@ import fs from "node:fs"; import path from "node:path"; -import nodemailer, { Transporter } from "nodemailer"; +import nodemailer, { SentMessageInfo, Transporter } from "nodemailer"; import { User } from "../entities"; import { Config } from "./Config"; import { generateToken } from "./Token"; @@ -158,7 +158,10 @@ export const Email: { transporter: Transporter | null; init: () => Promise<void>; generateVerificationLink: (id: string, email: string) => Promise<string>; - sendVerificationEmail: (user: User, email: string) => Promise<any>; + sendVerificationEmail: ( + user: User, + email: string, + ) => Promise<SentMessageInfo>; doReplacements: ( template: string, user: User, @@ -254,10 +257,7 @@ export const Email: { const link = `${instanceUrl}/verify#token=${token}`; return link; }, - sendVerificationEmail: async function ( - user: User, - email: string, - ): Promise<any> { + sendVerificationEmail: async function (user: User, email: string) { if (!this.transporter) return; // generate a verification link for the user diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index e4b1fe41..12e4a79a 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -72,58 +72,13 @@ export function checkToken( }); } -/** - * Puyodead1 (1/19/2023): I made a copy of this function because I didn't want to break anything with the other one. - * this version of the function doesn't use select, so we can update the user. with select causes constraint errors. - */ -export function verifyTokenEmailVerification( - token: string, - jwtSecret: string, -): Promise<{ decoded: any; user: User }> { - return new Promise((res, rej) => { - jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { - if (err || !decoded) return rej("Invalid Token"); - - const user = await User.findOne({ - where: { id: decoded.id }, - }); - if (!user) return rej("Invalid Token"); - if (user.disabled) return rej("User disabled"); - if (user.deleted) return rej("User not found"); - - return res({ decoded, user }); - }); - }); -} - -export function verifyToken( - token: string, - jwtSecret: string, -): Promise<{ decoded: any; user: User }> { - return new Promise((res, rej) => { - jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { - if (err || !decoded) return rej("Invalid Token"); - - const user = await User.findOne({ - where: { id: decoded.id }, - select: ["data", "bot", "disabled", "deleted", "rights"], - }); - if (!user) return rej("Invalid Token"); - if (user.disabled) return rej("User disabled"); - if (user.deleted) return rej("User not found"); - - return res({ decoded, user }); - }); - }); -} - export async function generateToken(id: string, email?: string) { const iat = Math.floor(Date.now() / 1000); const algorithm = "HS256"; return new Promise((res, rej) => { jwt.sign( - { id: id, email: email, iat }, + { id, iat, email }, Config.get().security.jwtSecret, { algorithm, From 34cde14f753feb37a2b1dd2ce772ccc8552b4198 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Mon, 30 Jan 2023 19:05:22 -0500 Subject: [PATCH 18/31] config: require account verification --- src/api/routes/auth/login.ts | 11 +++++++++++ src/api/routes/auth/verify/index.ts | 8 ++------ src/util/config/types/LoginConfiguration.ts | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 2b97ec10..89d0be69 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -102,6 +102,17 @@ router.post( }); } + // return an error for unverified accounts if verification is required + if (config.login.requireVerification && !user.verified) { + throw FieldErrors({ + login: { + code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL", + message: + "Email verification is required, please check your email.", + }, + }); + } + if (user.mfa_enabled && !user.webauthn_enabled) { // TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy const ticket = crypto.randomBytes(40).toString("hex"); diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index 7809bc26..14cc3f95 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -17,7 +17,7 @@ */ import { route, verifyCaptcha } from "@fosscord/api"; -import { checkToken, Config, FieldErrors } from "@fosscord/util"; +import { checkToken, Config, FieldErrors, User } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); @@ -57,11 +57,7 @@ router.post( if (user.verified) return res.send(user); - // verify email - user.verified = true; - await user.save(); - - // TODO: invalidate token after use? + await User.update({ id: user.id }, { verified: true }); return res.send(user); } catch (error) { diff --git a/src/util/config/types/LoginConfiguration.ts b/src/util/config/types/LoginConfiguration.ts index 862bc185..1d5752fe 100644 --- a/src/util/config/types/LoginConfiguration.ts +++ b/src/util/config/types/LoginConfiguration.ts @@ -18,4 +18,5 @@ export class LoginConfiguration { requireCaptcha: boolean = false; + requireVerification: boolean = false; } From 54dbc7190b64428840645a9eaee0d60d66362a4d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 08:32:19 -0500 Subject: [PATCH 19/31] fix: verification required for login not working correctly --- src/api/routes/auth/login.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 89d0be69..e6616731 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -77,6 +77,7 @@ router.post( "mfa_enabled", "webauthn_enabled", "security_keys", + "verified", ], relations: ["security_keys"], }).catch(() => { From 1aba7d591cf6641c77571c8ce46e036021502152 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 09:15:18 -0500 Subject: [PATCH 20/31] fix: email verification --- src/api/routes/auth/verify/index.ts | 28 ++++++++++++++-------------- src/api/routes/auth/verify/resend.ts | 2 +- src/util/util/Token.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index 14cc3f95..91ff9b93 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -17,11 +17,21 @@ */ import { route, verifyCaptcha } from "@fosscord/api"; -import { checkToken, Config, FieldErrors, User } from "@fosscord/util"; +import { checkToken, Config, generateToken, User } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); +async function getToken(user: User) { + const token = await generateToken(user.id); + + // Notice this will have a different token structure, than discord + // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package + // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png + + return { token }; +} + router.post( "/", route({ body: "VerifyEmailSchema" }), @@ -43,23 +53,13 @@ router.post( try { const { jwtSecret } = Config.get().security; - const { decoded, user } = await checkToken(token, jwtSecret); + const { user } = await checkToken(token, jwtSecret, true); - // toksn should last for 24 hours from the time they were issued - if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) { - throw FieldErrors({ - token: { - code: "TOKEN_INVALID", - message: "Invalid token", // TODO: add translation - }, - }); - } - - if (user.verified) return res.send(user); + if (user.verified) return res.json(await getToken(user)); await User.update({ id: user.id }, { verified: true }); - return res.send(user); + return res.json(await getToken(user)); } catch (error) { throw new HTTPError((error as Error).toString(), 400); } diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index d9a9cda5..a798a3d9 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -25,7 +25,7 @@ const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ where: { id: req.user_id }, - select: ["email"], + select: ["username", "email"], }); if (!user.email) { diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 12e4a79a..e7b2006d 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -27,9 +27,34 @@ export type UserTokenData = { decoded: { id: string; iat: number }; }; +async function checkEmailToken( + decoded: jwt.JwtPayload, +): Promise<UserTokenData> { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (res, rej) => { + if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings. + + const user = await User.findOne({ + where: { + email: decoded.email, + }, + }); + + if (!user) return rej("Invalid Token"); + + if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) + return rej("Invalid Token"); + + // Using as here because we assert `id` and `iat` are in decoded. + // TS just doesn't want to assume its there, though. + return res({ decoded, user } as UserTokenData); + }); +} + export function checkToken( token: string, jwtSecret: string, + isEmailVerification = false, ): Promise<UserTokenData> { return new Promise((res, rej) => { token = token.replace("Bot ", ""); @@ -48,6 +73,8 @@ export function checkToken( ) return rej("Invalid Token"); // will never happen, just for typings. + if (isEmailVerification) return res(checkEmailToken(decoded)); + const user = await User.findOne({ where: { id: decoded.id }, select: ["data", "bot", "disabled", "deleted", "rights"], From ada821070bf3fd9c18e57884264c8c6497b9eb9f Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 09:23:59 -0500 Subject: [PATCH 21/31] add right to resend verification emails --- src/api/routes/auth/verify/resend.ts | 48 ++++++++++--------- .../config/types/RegisterConfiguration.ts | 2 +- src/util/util/Rights.ts | 1 + 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index a798a3d9..d54ddf73 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -22,28 +22,32 @@ import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["username", "email"], - }); - - if (!user.email) { - // TODO: whats the proper error response for this? - throw new HTTPError("User does not have an email address", 400); - } - - await Email.sendVerificationEmail(user, user.email) - .then((info) => { - console.log("Message sent: %s", info.messageId); - return res.sendStatus(204); - }) - .catch((e) => { - console.error( - `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, - ); - throw new HTTPError("Failed to send verification email", 500); +router.post( + "/", + route({ right: "RESEND_VERIFICATION_EMAIL" }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["username", "email"], }); -}); + + if (!user.email) { + // TODO: whats the proper error response for this? + throw new HTTPError("User does not have an email address", 400); + } + + await Email.sendVerificationEmail(user, user.email) + .then((info) => { + console.log("Message sent: %s", info.messageId); + return res.sendStatus(204); + }) + .catch((e) => { + console.error( + `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, + ); + throw new HTTPError("Failed to send verification email", 500); + }); + }, +); export default router; diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts index 689baa85..b8db0077 100644 --- a/src/util/config/types/RegisterConfiguration.ts +++ b/src/util/config/types/RegisterConfiguration.ts @@ -35,5 +35,5 @@ export class RegisterConfiguration { allowMultipleAccounts: boolean = true; blockProxies: boolean = true; incrementingDiscriminators: boolean = false; // random otherwise - defaultRights: string = "312119568366592"; // See `npm run generate:rights` + defaultRights: string = "875069521787904"; // See `npm run generate:rights` } diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts index b48477ed..383f07ec 100644 --- a/src/util/util/Rights.ts +++ b/src/util/util/Rights.ts @@ -93,6 +93,7 @@ export class Rights extends BitField { EDIT_FLAGS: BitFlag(46), // can set others' flags MANAGE_GROUPS: BitFlag(47), // can manage others' groups VIEW_SERVER_STATS: BitFlag(48), // added per @chrischrome's request — can view server stats) + RESEND_VERIFICATION_EMAIL: BitFlag(49), // can resend verification emails (/auth/verify/resend) }; any(permission: RightResolvable, checkOperator = true) { From ed5aa51a8f0bf2e6b10f61f191b56c29ea989f0d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Thu, 23 Feb 2023 23:44:48 -0500 Subject: [PATCH 22/31] fix for when secure is set to false --- src/util/util/Email.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index cbcc5b60..5610b56e 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -59,7 +59,7 @@ const transporters = { Config.get().email.smtp; // ensure all required configuration values are set - if (!host || !port || !secure || !username || !password) + if (!host || !port || secure === null || !username || !password) return console.error( "[Email] SMTP has not been configured correctly.", ); From 6daaaf71e6f0a7cf68a36694892adb5dfe8c9825 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Thu, 23 Feb 2023 23:46:35 -0500 Subject: [PATCH 23/31] error if correspondence email is not set --- src/util/util/Email.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 5610b56e..8575e7b2 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -64,6 +64,11 @@ const transporters = { "[Email] SMTP has not been configured correctly.", ); + if (!Config.get().general.correspondenceEmail) + return console.error( + "[Email] Correspondence email has not been configured! This is used as the sender email address.", + ); + // construct the transporter const transporter = nodemailer.createTransport({ host, From 6131db986fb3d247cedfbe62c5e10bc5b52c7c03 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 00:09:00 -0500 Subject: [PATCH 24/31] use a fixed mailjet transport --- package-lock.json | Bin 520194 -> 519006 bytes package.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e19d3e0f42a9be614c5a1229fbcefa5c0d4aaae3..898f1761f2e7d5ef59d4967924e68885e0a24ef1 100644 GIT binary patch delta 998 zcmbV~T}&KR6vuP#otfJyA1<(rWp}o_9g$S|n%T0zvKokzR+FVA7O-N{1eT?&@@2t* ztk|j##$X%m#66@~yRAkOt3c9+leDx+{o=s~A!)R@v;i>+*c2X2UoefcyHEA0>D$Tq zy66A@-FKGUw|;R?e<whN%H4U|k;q`z>+LnY`atMtq<_%sEjQ1|u8T9mvH%f9$bNT0 z#s!(ggXRwf3Vz-~Vjl3#Fa^Uy<QoRf<3z!yqeOcoS%=1xBm<mwqJaAgaSo$nL<*z^ zzy6VQ+RevTt>y$7wwsF%1(!C+iCj2+hp6}|rN1_%_r_nO!2(DOQw7#Kq*m}dXc59| zbaNM6rpyVp@0km)n^f7GTnG8T(Uo-fQwH0H#R)n>%s{t-bBnZ&q}~qPqB9-oo;ULO zh-oX5VY-Zo_|>y)A2TQRE2!>cV<aUeSrK5eo=LD2WFmN+JPYqU$9^Y;mQ}?A16?)c z<yKUeHm)qx*HeDKdFnG|R_!sbZiL#~e7-#uZQc%}vqJal;p%F=(pOb&gevqZU!`HR zRqpA~O18_gtaV31;ogC;R^!nco^LXZyF1}67cXI6tpw(m#e8rCnV<OJUl;eo@c^;K zj<N0dWsn63ymE@Vp#LsaQQyzzV-Rf=9>uZ=ZV){DC)-O>+VSVV*d&3iDj^HcuCYiu z8@tAGz?KkYn0l3r!q_U?ffpKRCQXNnXIU+IuWy54--eJmh98cIe>m{{kHuPsH^)Vm zC0oajCKYI4vy)wDVW0Bi$_=qU2fNnA&uoy`6mrqKCC=I~J5$<LkHw7=*x}_CD{TQ* zRdl>99q~i#l5{(jg(OI@K!)InLxKwWjPwuO{)yZ1z!fQv;Luem(UP9GaK6dF`H{6s zE#S=r`U|)nR430#B6AY(Y>ByW!^uB*V0eLtf!BGoug4;1ej>Xv>+b&zF8H=v*a4$1 zUiyEQXt6X16uu<b@sOKOM&a+crKEI%YZMMHieAvySq4P%#3#XXn#s_;&UVASGkh3s c&RTZV*?23#@Coq+0?~8)UfPrRdEVaeAD^XE_W%F@ delta 1242 zcmchX{clrc6vy*C&wcLWEgkL3x^6Tbbq=N$;UXKQLy={Uv2N>H&;p&A;ND~}p%}M9 zCvgE24GhF|_Jo(1;Y%=4$P$c4#YBT*VsOdy2aq}E3?Tz1nZy_f65;k<ZO1>rZ_ho? zIp1^6_xa?GpPRR9V&2d<(#d>2DkaV+76|5x*-DwauyM&Eby3OgDFvq#@@Xxc8ITK- zOh^g$gn}3*NjLo6K<sefRg#NMF>;*2r9uAR_kvzhXt}qfaSyq%A{_$tePY7S??_7y z6qyZWBX)U|#B$*5fUJ%32X$f3H0ig&R7$$Oi#pLo>Cew(T7w#)5?a>NHTil{V$)x_ z*cGMGLPKNdT_Wp+Fx16dXiw1G)#;9@U7-8|y_hbAy3gbeaPAOF!fngKQ=^m+xIND8 zDP8DfbPKE)qvz9m=x#jxEB&%LBQcgO;)$<kDbQENB={uEAB31qoKL<*^pFYrE7+41 z{%If{{H>DhB{PiEO8zBab%;k{Bu`{Pl|$H!Q>azU538*)?+$P2QlDB~)!trR-VzLL z^EEH)E?-+4YHA6W)Nfhqjg*G!)wcSI4eLAO?QQ<%o_Oc-4v#0W+2h^3&Kn8_UMg*k zbVPezUhS#w4n(6{W3a7^l|q4^EhH+`_*syY;1NIDPcpWH`X?*Erdsx665?gdgZ_(b zJI(C8cq1``+stR-^d$x)?R^doOtKdZA>G1{)(JB_^%dg~Xb=ye=L+kmF!Z+eNM&8r zV$d`^#jHg42TtrF78qH@3STd}#<1v`WZtWLkG}2vm$_*S6m%u{vJ8O=K9}UT%tq?- z@9~kxjpOojye(UQe*AEfAF>*Aj=UlApnICA_}Wbl*7S2ISft2zDrR(9SSRn#No`#W z8e<V$@Pd5WX-Hq+DL49bY1n|WPE;{!H1!hr!6ZC5_PIQV7^yeEkbes6DyXsY3QMhO zPafqqY`2SNGdN^HV2+S+PZfKNq^@Cj4cny062p=m`tw9K^qpo)Au0(8td&f_@oJWR zpP;#ZlB?kIYqR|i4QSB#;LQ)X11jf=-{D)6aKPZx+W+ARr}(%Rt~$gccsnWF6#6DL z0=_qCa8Kk)7C3Q%KLmk;V%+d*)gcif5L`JUC+pa;!-Bv*vu5R~BVw_RR55IgbAvn3 QxO2jR8lj-~9Z_6&7az#EGXMYp diff --git a/package.json b/package.json index 819b040a..810ae894 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "node-fetch": "^2.6.7", "node-os-utils": "^1.3.7", "nodemailer": "^6.9.0", + "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", "picocolors": "^1.0.0", "probe-image-size": "^7.2.3", "proxy-agent": "^5.0.0", @@ -115,8 +116,7 @@ }, "optionalDependencies": { "erlpack": "^0.1.4", - "sqlite3": "^5.1.4", "nodemailer-mailgun-transport": "^2.1.5", - "nodemailer-mailjet-transport": "^1.0.4" + "sqlite3": "^5.1.4" } } From a78e13073f2fb070e15067d5fcc67797d890bc7e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 00:10:50 -0500 Subject: [PATCH 25/31] don't print anything if email send is successful --- src/api/routes/auth/verify/resend.ts | 3 +-- src/util/entities/User.ts | 34 ++++++++++++---------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index d54ddf73..1cd14f23 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -37,8 +37,7 @@ router.post( } await Email.sendVerificationEmail(user, user.email) - .then((info) => { - console.log("Message sent: %s", info.messageId); + .then(() => { return res.sendStatus(204); }) .catch((e) => { diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 42f74fb4..2947b205 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { Request } from "express"; import { Column, Entity, @@ -24,23 +25,22 @@ import { OneToMany, OneToOne, } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { BitField } from "../util/BitField"; -import { Relationship } from "./Relationship"; -import { ConnectedAccount } from "./ConnectedAccount"; -import { Member } from "./Member"; -import { UserSettings } from "./UserSettings"; -import { Session } from "./Session"; import { + adjustEmail, Config, + Email, FieldErrors, Snowflake, trimSpecial, - adjustEmail, - Email, } from ".."; -import { Request } from "express"; +import { BitField } from "../util/BitField"; +import { BaseClass } from "./BaseClass"; +import { ConnectedAccount } from "./ConnectedAccount"; +import { Member } from "./Member"; +import { Relationship } from "./Relationship"; import { SecurityKey } from "./SecurityKey"; +import { Session } from "./Session"; +import { UserSettings } from "./UserSettings"; export enum PublicUserEnum { username, @@ -393,15 +393,11 @@ export class User extends BaseClass { // send verification email if users aren't verified by default and we have an email if (!Config.get().defaults.user.verified && email) { - await Email.sendVerificationEmail(user, email) - .then((info) => { - console.log("Message sent: %s", info.messageId); - }) - .catch((e) => { - console.error( - `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, - ); - }); + await Email.sendVerificationEmail(user, email).catch((e) => { + console.error( + `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, + ); + }); } setImmediate(async () => { From dc48a74373ac5ee13d8efeb48d0c7a4eb448a74e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 00:39:17 -0500 Subject: [PATCH 26/31] add SendGrid transport --- package-lock.json | Bin 519006 -> 523352 bytes package.json | 3 +- src/util/config/types/EmailConfiguration.ts | 2 ++ .../types/subconfigurations/email/SendGrid.ts | 21 +++++++++++ src/util/util/Email.ts | 34 +++++++++++++++++- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/util/config/types/subconfigurations/email/SendGrid.ts diff --git a/package-lock.json b/package-lock.json index 898f1761f2e7d5ef59d4967924e68885e0a24ef1..e6aed6b2bec95e3e83d610f018868db73a51ffe2 100644 GIT binary patch delta 2435 zcmd5;No*Tc7}j{aG!0Nf)YNW*lRA_@+}Jbr*fUNkbP{h9J6_^7MnW3f&s*#ndpurZ zC2T^<jUHU_I3Pihaz&|f2v-iM5~pxMTv#NKdPpQ<38@uG2s?Ic1&U}7l`@BU@6Fr3 z|NFo9z4`IG$?qRd?x<um$0tF7&E|nHR|Z@%Eo75(qQG+HA}@%V`6<mZD`eR@2cPE) zas4Ak?<!=6(v?SaoL(lK6*HCi{Qs>sn6!H^6gnC@2Bc5qv+&Lbl1nJ5Bb%*ZeBuOZ zrw4GNey&%U>kJsVc_kF6JGJXn4$%GpXJxBG(27+Y*61q3tl8J|i@X+C`Bb<NbNjIr zWj1hJDZXe7CvlK7$4NIJ^KpK?;kTRZq@7PzF@vyLE>>XcO~i!V>~Ed{`63X?9hTl| zZ>+;K*Uk^M9}Qd2^4-sv&H(Wo^2(@LH#@blF-3KY*KgpM4z@@{{d|A5UMTRw2J*7B zJ<km9dJ!CEH;`w$P1Y&Z1bwwcl(1E~fV0AIi_45?%g_#v4cMY93pufov823W*=Dxk z4s(&L=1GvWff&GXzcYnXTC>)YUNQONuCTuBaCulOwaPAo<G3L6u}p~&kdC72=4XFN zhr84t#|RM-9Xlak-ACMuzs-KH)}bHz52Tt9U8d_PkI`XWNJROVejP6~Xgcm{1jSIy zq>TwqZz1A0kQTba+M^-QV#X5paVtfS2_xd^DhQVNh!)40hA(Yout+#<Wjw_iEX||D zP!fP*0SIXTzfeoYnzx3>;qpCH0e?y%;~n5V&9p@g*<s1W6WGc=?2g>qm^;O>URv*K zLRyY4`fQdO<5{RW1EG4&TVGrAmzMPmO|uKDyw8%Uk&#;3>hb&P0xML)C1<t)f>|*v zx*XY3WYJYH6u4Z5h&nvRRMqY-v0c2^2YTsd_}_9FjQEG5;K<j=%zsL!x&P<zuJeyy z4<(=ocZ^aMeD^n*6n^@eM1QeQQajLo9ggVGtD%OWwiJsB1>j6pSC@+VP~BvzF}V^? z;SSS6kw|EHn>!fMlB{43@ac$?&50(~lJ*+QM#joI0(3NIUBjw*zsqA33T29o8+rKE zZMh2We2&PWw;&mZo^4dKHG3b~o4qf&4&amv9XfZC0Qinlp@#0bTnXQ1P*vpdJwexY zk!jYF0Dv$O<|K}>EM~A!l!c%$%A7K?Bw<cbfNF~<VT1<^IsqU4F42w+2<{}kmjI%9 z)KYIH+~{7yEuHz{>{-p^M|Wf|Jh|N<F~wDwKa`w<+eh-zu62AHlNw-V19`67Y+aF0 zhf+_)O5S@$LBf|d5hY6KD7gQLV&cMp{WE1Itz8*5I2*^JAan+uLDTW@&Gu`v<K~(H z`m^9DZP56QlY`W|^q#;_+E)~O1DB2MefX;En|o)(H#A$}mTWKFlK(u@{6$K@2NC%w zd~pf=65iUDU4pjDiU*KaDoz^+`P6LVyh?GoPx0V?<mThs6udhv)2U7gr*AqslV<PQ IxWcgiH%=)m@Bjb+ delta 93 zcmV-j0HXic_#fWZAFvq(vup+01ea+Y1P-&V3P13dfCT{rw;%xl=<J6>?*X?%?*chx zhp<%xx3E<NCEd3#I0eqfmqYIX?T70D2Dj@02k|DCu2BOgw+bl--vPHaEC(XcGEOE6 diff --git a/package.json b/package.json index 810ae894..85039e60 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "node-fetch": "^2.6.7", "node-os-utils": "^1.3.7", "nodemailer": "^6.9.0", - "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", "picocolors": "^1.0.0", "probe-image-size": "^7.2.3", "proxy-agent": "^5.0.0", @@ -117,6 +116,8 @@ "optionalDependencies": { "erlpack": "^0.1.4", "nodemailer-mailgun-transport": "^2.1.5", + "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", + "nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport", "sqlite3": "^5.1.4" } } diff --git a/src/util/config/types/EmailConfiguration.ts b/src/util/config/types/EmailConfiguration.ts index 625507f2..989d59eb 100644 --- a/src/util/config/types/EmailConfiguration.ts +++ b/src/util/config/types/EmailConfiguration.ts @@ -21,10 +21,12 @@ import { MailJetConfiguration, SMTPConfiguration, } from "./subconfigurations/email"; +import { SendGridConfiguration } from "./subconfigurations/email/SendGrid"; export class EmailConfiguration { provider: string | null = null; smtp: SMTPConfiguration = new SMTPConfiguration(); mailgun: MailGunConfiguration = new MailGunConfiguration(); mailjet: MailJetConfiguration = new MailJetConfiguration(); + sendgrid: SendGridConfiguration = new SendGridConfiguration(); } diff --git a/src/util/config/types/subconfigurations/email/SendGrid.ts b/src/util/config/types/subconfigurations/email/SendGrid.ts new file mode 100644 index 00000000..a4755dfb --- /dev/null +++ b/src/util/config/types/subconfigurations/email/SendGrid.ts @@ -0,0 +1,21 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export class SendGridConfiguration { + apiKey: string | null = null; +} diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 8575e7b2..3028b063 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -141,7 +141,7 @@ const transporters = { } catch { // if the package is not installed, log an error and return void so we don't set the transporter console.error( - "[Email] Mailjet transport is not installed. Please run `npm install nodemailer-mailjet-transport --save-optional` to install it.", + "[Email] Mailjet transport is not installed. Please run `npm install n0script22/nodemailer-mailjet-transport --save-optional` to install it.", ); return; } @@ -157,6 +157,38 @@ const transporters = { // create the transporter and return it return nodemailer.createTransport(mj(auth)); }, + sendgrid: async function () { + // get configuration + const { apiKey } = Config.get().email.sendgrid; + + // ensure all required configuration values are set + if (!apiKey) + return console.error( + "[Email] SendGrid has not been configured correctly.", + ); + + let sg; + try { + // try to import the transporter package + sg = require("nodemailer-sendgrid-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] SendGrid transport is not installed. Please run `npm install Maria-Golomb/nodemailer-sendgrid-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + api_key: apiKey, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(sg(auth)); + }, }; export const Email: { From 05453ec14880732c5d0d20fd3575bb2b3952760d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 01:54:10 -0500 Subject: [PATCH 27/31] implement password reset --- .../email_templates/new_login_location.html | 2 +- .../password_reset_request.html | 2 +- assets/email_templates/verify_email.html | 2 +- assets/locales/en/auth.json | 4 + assets/schemas.json | Bin 1137196 -> 1177730 bytes src/api/middlewares/Authentication.ts | 2 + src/api/routes/auth/forgot.ts | 92 +++++++++++++++ src/api/routes/auth/reset.ts | 57 ++++++++++ src/api/routes/auth/verify/resend.ts | 2 +- src/util/config/Config.ts | 3 + .../types/PasswordResetConfiguration.ts | 21 ++++ src/util/config/types/index.ts | 1 + src/util/entities/User.ts | 2 +- src/util/schemas/ForgotPasswordSchema.ts | 22 ++++ src/util/schemas/PasswordResetSchema.ts | 22 ++++ src/util/schemas/index.ts | 20 +--- src/util/util/Email.ts | 106 +++++++++++++++--- src/util/util/Token.ts | 9 ++ 18 files changed, 333 insertions(+), 36 deletions(-) create mode 100644 src/api/routes/auth/forgot.ts create mode 100644 src/api/routes/auth/reset.ts create mode 100644 src/util/config/types/PasswordResetConfiguration.ts create mode 100644 src/util/schemas/ForgotPasswordSchema.ts create mode 100644 src/util/schemas/PasswordResetSchema.ts diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html index e597ac6c..ff262e99 100644 --- a/assets/email_templates/new_login_location.html +++ b/assets/email_templates/new_login_location.html @@ -104,7 +104,7 @@ Alternatively, you can directly paste this link into your browser: </p> - <a href="{verifyUrl}" target="_blank">{verifyUrl}</a> + <a href="{verifyUrl}" target="_blank" style="word-wrap: break-word;">{verifyUrl}</a> </div> </div> </div> diff --git a/assets/email_templates/password_reset_request.html b/assets/email_templates/password_reset_request.html index ab8f4d23..b770e7ba 100644 --- a/assets/email_templates/password_reset_request.html +++ b/assets/email_templates/password_reset_request.html @@ -90,7 +90,7 @@ Alternatively, you can directly paste this link into your browser: </p> - <a href="{passwordResetUrl}" target="_blank" + <a href="{passwordResetUrl}" target="_blank" style="word-wrap: break-word;" >{passwordResetUrl}</a > </div> diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html index 604242c4..481a46d4 100644 --- a/assets/email_templates/verify_email.html +++ b/assets/email_templates/verify_email.html @@ -91,7 +91,7 @@ Alternatively, you can directly paste this link into your browser: </p> - <a href="{emailVerificationUrl}" target="_blank" + <a href="{emailVerificationUrl}" target="_blank" style="word-wrap: break-word;" >{emailVerificationUrl}</a > </div> diff --git a/assets/locales/en/auth.json b/assets/locales/en/auth.json index 2178548e..0521a902 100644 --- a/assets/locales/en/auth.json +++ b/assets/locales/en/auth.json @@ -16,5 +16,9 @@ "USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another", "GUESTS_DISABLED": "Guest users are disabled", "TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later" + }, + "password_reset": { + "EMAIL_DOES_NOT_EXIST": "Email does not exist.", + "INVALID_TOKEN": "Invalid token." } } diff --git a/assets/schemas.json b/assets/schemas.json index 2bfb525df9a60fe52e64481415537394ff7bd823..1fdfa361d0385ea97d1b34446780b3128551e168 100644 GIT binary patch delta 192 zcmZ3p#kJ|9dqWH37N$Spd~W$g>G>rAiN(d``9&#{3+270$1i7bpKiT^#cr~ISQv=M zw*Aq3o)^B;=l@`0ZD)*N0%B$$X4%dd!TRg<^aV3m*|uLP<`;|E{>zz7Wj+tou%Oi9 z)Dp0XecsHn(-*vC*PXs#8#CK<{)Nm&K(phwPu#}**SB5vF&hxG12G2>a{@6J5OV`D Z4-oTimwn8)VM6<>^ZeUiofmNG0szsUP09cO delta 101 zcmZqr=)Pu)YeNg;7N$Sp(+{{av$iWOX98koAZFRFw4C+V>+P2B*_7wEPrS<x#2i4( u3B+7L%nigmK+FroeA_49<zGKx`hn-H?Aw(lvx&vDJLCy$cgPcT>H+{J#w|hs diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index f4c33963..771f0de8 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -29,6 +29,8 @@ export const NO_AUTHORIZATION_ROUTES = [ "/auth/mfa/totp", "/auth/mfa/webauthn", "/auth/verify", + "/auth/forgot", + "/auth/reset", // Routes with a seperate auth system "/webhooks/", // Public information endpoints diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts new file mode 100644 index 00000000..faa43dbb --- /dev/null +++ b/src/api/routes/auth/forgot.ts @@ -0,0 +1,92 @@ +import { getIpAdress, route, verifyCaptcha } from "@fosscord/api"; +import { + Config, + Email, + FieldErrors, + ForgotPasswordSchema, + User, +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +router.post( + "/", + route({ body: "ForgotPasswordSchema" }), + async (req: Request, res: Response) => { + const { login, captcha_key } = req.body as ForgotPasswordSchema; + + const config = Config.get(); + + if ( + config.password_reset.requireCaptcha && + config.security.captcha.enabled + ) { + const { sitekey, service } = config.security.captcha; + if (!captcha_key) { + return res.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + + const ip = getIpAdress(req); + const verify = await verifyCaptcha(captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + } + + const user = await User.findOneOrFail({ + where: [{ phone: login }, { email: login }], + select: ["username", "id", "disabled", "deleted", "email"], + relations: ["security_keys"], + }).catch(() => { + throw FieldErrors({ + login: { + message: req.t("auth:password_reset.EMAIL_DOES_NOT_EXIST"), + code: "EMAIL_DOES_NOT_EXIST", + }, + }); + }); + + if (!user.email) + throw FieldErrors({ + login: { + message: + "This account does not have an email address associated with it.", + code: "NO_EMAIL", + }, + }); + + if (user.deleted) + return res.status(400).json({ + message: "This account is scheduled for deletion.", + code: 20011, + }); + + if (user.disabled) + return res.status(400).json({ + message: req.t("auth:login.ACCOUNT_DISABLED"), + code: 20013, + }); + + return await Email.sendResetPassword(user, user.email) + .then(() => { + return res.sendStatus(204); + }) + .catch((e) => { + console.error( + `Failed to send password reset email to ${user.username}#${user.discriminator}: ${e}`, + ); + throw new HTTPError("Failed to send password reset email", 500); + }); + }, +); + +export default router; diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts new file mode 100644 index 00000000..94053e1a --- /dev/null +++ b/src/api/routes/auth/reset.ts @@ -0,0 +1,57 @@ +import { route } from "@fosscord/api"; +import { + checkToken, + Config, + Email, + FieldErrors, + generateToken, + PasswordResetSchema, + User, +} from "@fosscord/util"; +import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; + +const router = Router(); + +router.post( + "/", + route({ body: "PasswordResetSchema" }), + async (req: Request, res: Response) => { + const { password, token } = req.body as PasswordResetSchema; + + try { + const { jwtSecret } = Config.get().security; + const { user } = await checkToken(token, jwtSecret, true); + + // the salt is saved in the password refer to bcrypt docs + const hash = await bcrypt.hash(password, 12); + + const data = { + data: { + hash, + valid_tokens_since: new Date(), + }, + }; + await User.update({ id: user.id }, data); + + // come on, the user has to have an email to reset their password in the first place + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await Email.sendPasswordChanged(user, user.email!); + + res.json({ token: await generateToken(user.id) }); + } catch (e) { + if ((e as Error).toString() === "Invalid Token") + throw FieldErrors({ + password: { + message: req.t("auth:password_reset.INVALID_TOKEN"), + code: "INVALID_TOKEN", + }, + }); + + throw new HTTPError((e as Error).toString(), 400); + } + }, +); + +export default router; diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index 1cd14f23..918af9a1 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -36,7 +36,7 @@ router.post( throw new HTTPError("User does not have an email address", 400); } - await Email.sendVerificationEmail(user, user.email) + await Email.sendVerifyEmail(user, user.email) .then(() => { return res.sendStatus(204); }) diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts index d6f804bf..c056d454 100644 --- a/src/util/config/Config.ts +++ b/src/util/config/Config.ts @@ -31,6 +31,7 @@ import { LimitsConfiguration, LoginConfiguration, MetricsConfiguration, + PasswordResetConfiguration, RabbitMQConfiguration, RegionConfiguration, RegisterConfiguration, @@ -60,4 +61,6 @@ export class ConfigValue { defaults: DefaultsConfiguration = new DefaultsConfiguration(); external: ExternalTokensConfiguration = new ExternalTokensConfiguration(); email: EmailConfiguration = new EmailConfiguration(); + password_reset: PasswordResetConfiguration = + new PasswordResetConfiguration(); } diff --git a/src/util/config/types/PasswordResetConfiguration.ts b/src/util/config/types/PasswordResetConfiguration.ts new file mode 100644 index 00000000..806d77be --- /dev/null +++ b/src/util/config/types/PasswordResetConfiguration.ts @@ -0,0 +1,21 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export class PasswordResetConfiguration { + requireCaptcha: boolean = false; +} diff --git a/src/util/config/types/index.ts b/src/util/config/types/index.ts index 1431c128..510e19f8 100644 --- a/src/util/config/types/index.ts +++ b/src/util/config/types/index.ts @@ -30,6 +30,7 @@ export * from "./KafkaConfiguration"; export * from "./LimitConfigurations"; export * from "./LoginConfiguration"; export * from "./MetricsConfiguration"; +export * from "./PasswordResetConfiguration"; export * from "./RabbitMQConfiguration"; export * from "./RegionConfiguration"; export * from "./RegisterConfiguration"; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 2947b205..f99a85e7 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -393,7 +393,7 @@ export class User extends BaseClass { // send verification email if users aren't verified by default and we have an email if (!Config.get().defaults.user.verified && email) { - await Email.sendVerificationEmail(user, email).catch((e) => { + await Email.sendVerifyEmail(user, email).catch((e) => { console.error( `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`, ); diff --git a/src/util/schemas/ForgotPasswordSchema.ts b/src/util/schemas/ForgotPasswordSchema.ts new file mode 100644 index 00000000..9a28bd18 --- /dev/null +++ b/src/util/schemas/ForgotPasswordSchema.ts @@ -0,0 +1,22 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export interface ForgotPasswordSchema { + login: string; + captcha_key?: string; +} diff --git a/src/util/schemas/PasswordResetSchema.ts b/src/util/schemas/PasswordResetSchema.ts new file mode 100644 index 00000000..9cc74940 --- /dev/null +++ b/src/util/schemas/PasswordResetSchema.ts @@ -0,0 +1,22 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export interface PasswordResetSchema { + password: string; + token: string; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 194d8571..44909a3a 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +export * from "./AckBulkSchema"; export * from "./ActivitySchema"; export * from "./ApplicationAuthorizeSchema"; export * from "./ApplicationCreateSchema"; @@ -32,6 +33,7 @@ export * from "./CodesVerificationSchema"; export * from "./DmChannelCreateSchema"; export * from "./EmojiCreateSchema"; export * from "./EmojiModifySchema"; +export * from "./ForgotPasswordSchema"; export * from "./GatewayPayloadSchema"; export * from "./GuildCreateSchema"; export * from "./GuildTemplateCreateSchema"; @@ -45,8 +47,10 @@ export * from "./MemberChangeProfileSchema"; export * from "./MemberChangeSchema"; export * from "./MessageAcknowledgeSchema"; export * from "./MessageCreateSchema"; +export * from "./MessageEditSchema"; export * from "./MfaCodesSchema"; export * from "./ModifyGuildStickerSchema"; +export * from "./PasswordResetSchema"; export * from "./PurgeSchema"; export * from "./RegisterSchema"; export * from "./RelationshipPostSchema"; @@ -69,22 +73,6 @@ export * from "./VanityUrlSchema"; export * from "./VoiceIdentifySchema"; export * from "./VoiceStateUpdateSchema"; export * from "./VoiceVideoSchema"; -export * from "./IdentifySchema"; -export * from "./ActivitySchema"; -export * from "./LazyRequestSchema"; -export * from "./GuildUpdateSchema"; -export * from "./ChannelPermissionOverwriteSchema"; -export * from "./UserGuildSettingsSchema"; -export * from "./GatewayPayloadSchema"; -export * from "./RolePositionUpdateSchema"; -export * from "./ChannelReorderSchema"; -export * from "./UserSettingsSchema"; -export * from "./BotModifySchema"; -export * from "./ApplicationModifySchema"; -export * from "./ApplicationCreateSchema"; -export * from "./ApplicationAuthorizeSchema"; -export * from "./AckBulkSchema"; export * from "./WebAuthnSchema"; export * from "./WebhookCreateSchema"; export * from "./WidgetModifySchema"; -export * from "./MessageEditSchema"; diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 3028b063..fa72d9c0 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -194,8 +194,14 @@ const transporters = { export const Email: { transporter: Transporter | null; init: () => Promise<void>; - generateVerificationLink: (id: string, email: string) => Promise<string>; - sendVerificationEmail: ( + generateLink: ( + type: "verify" | "reset", + id: string, + email: string, + ) => Promise<string>; + sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>; + sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>; + sendPasswordChanged: ( user: User, email: string, ) => Promise<SentMessageInfo>; @@ -231,10 +237,10 @@ export const Email: { * Replaces all placeholders in an email template with the correct values */ doReplacements: function ( - template: string, - user: User, - emailVerificationUrl?: string, - passwordResetUrl?: string, + template, + user, + emailVerificationUrl?, + passwordResetUrl?, ipInfo?: { ip: string; city: string; @@ -285,23 +291,22 @@ export const Email: { * * @param id user id * @param email user email - * @returns a verification link for the user */ - generateVerificationLink: async function (id: string, email: string) { + generateLink: async function (type, id, email) { const token = (await generateToken(id, email)) as string; const instanceUrl = Config.get().general.frontPage || "http://localhost:3001"; - const link = `${instanceUrl}/verify#token=${token}`; + const link = `${instanceUrl}/${type}#token=${token}`; return link; }, - sendVerificationEmail: async function (user: User, email: string) { + /** + * Sends an email to the user with a link to verify their email address + */ + sendVerifyEmail: async function (user, email) { if (!this.transporter) return; // generate a verification link for the user - const verificationLink = await this.generateVerificationLink( - user.id, - email, - ); + const link = await this.generateLink("verify", user.id, email); // load the email template const rawTemplate = fs.readFileSync( @@ -314,7 +319,78 @@ export const Email: { ); // replace email template placeholders - const html = this.doReplacements(rawTemplate, user, verificationLink); + const html = this.doReplacements(rawTemplate, user, 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); + }, + /** + * 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 = fs.readFileSync( + 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); + }, + /** + * 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 = fs.readFileSync( + 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] || ""; diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index e7b2006d..ffc442aa 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -38,6 +38,15 @@ async function checkEmailToken( where: { email: decoded.email, }, + select: [ + "email", + "id", + "verified", + "deleted", + "disabled", + "username", + "data", + ], }); if (!user) return rej("Invalid Token"); From ed38d74b3e994f4bd7be5ac22fb167f4169c77a3 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 06:36:57 -0500 Subject: [PATCH 28/31] don't return token on register if verification required --- src/api/routes/auth/register.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 0bf8efae..c941fdf6 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -278,6 +278,17 @@ router.post( await Invite.joinGuild(user.id, body.invite); } + // return an error for unverified accounts if verification is required + if (Config.get().login.requireVerification && !user.verified) { + throw FieldErrors({ + login: { + code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL", + message: + "Email verification is required, please check your email.", + }, + }); + } + return res.json({ token: await generateToken(user.id) }); }, ); From 91e9d6004066fcb533ae95d2789a1c1b3533d0ac Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 06:52:01 -0500 Subject: [PATCH 29/31] first batch of requested changes --- src/api/routes/auth/reset.ts | 57 ++++++++++++++-------------- src/api/routes/auth/verify/index.ts | 58 ++++++++++++++++++++--------- src/util/util/Email.ts | 11 +++--- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts index 94053e1a..9ab25dca 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -10,7 +10,6 @@ import { } from "@fosscord/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; const router = Router(); @@ -20,37 +19,37 @@ router.post( async (req: Request, res: Response) => { const { password, token } = req.body as PasswordResetSchema; + const { jwtSecret } = Config.get().security; + + let user; try { - const { jwtSecret } = Config.get().security; - const { user } = await checkToken(token, jwtSecret, true); - - // the salt is saved in the password refer to bcrypt docs - const hash = await bcrypt.hash(password, 12); - - const data = { - data: { - hash, - valid_tokens_since: new Date(), + const userTokenData = await checkToken(token, jwtSecret, true); + user = userTokenData.user; + } catch { + throw FieldErrors({ + password: { + message: req.t("auth:password_reset.INVALID_TOKEN"), + code: "INVALID_TOKEN", }, - }; - await User.update({ id: user.id }, data); - - // come on, the user has to have an email to reset their password in the first place - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await Email.sendPasswordChanged(user, user.email!); - - res.json({ token: await generateToken(user.id) }); - } catch (e) { - if ((e as Error).toString() === "Invalid Token") - throw FieldErrors({ - password: { - message: req.t("auth:password_reset.INVALID_TOKEN"), - code: "INVALID_TOKEN", - }, - }); - - throw new HTTPError((e as Error).toString(), 400); + }); } + + // the salt is saved in the password refer to bcrypt docs + const hash = await bcrypt.hash(password, 12); + + const data = { + data: { + hash, + valid_tokens_since: new Date(), + }, + }; + await User.update({ id: user.id }, data); + + // come on, the user has to have an email to reset their password in the first place + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await Email.sendPasswordChanged(user, user.email!); + + res.json({ token: await generateToken(user.id) }); }, ); diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index 91ff9b93..cdbd371a 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -16,10 +16,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { route, verifyCaptcha } from "@fosscord/api"; -import { checkToken, Config, generateToken, User } from "@fosscord/util"; +import { getIpAdress, route, verifyCaptcha } from "@fosscord/api"; +import { + checkToken, + Config, + FieldErrors, + generateToken, + User, +} from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; const router = Router(); async function getToken(user: User) { @@ -38,9 +43,21 @@ router.post( async (req: Request, res: Response) => { const { captcha_key, token } = req.body; - if (captcha_key) { - const { sitekey, service } = Config.get().security.captcha; - const verify = await verifyCaptcha(captcha_key); + const config = Config.get(); + + if (config.register.requireCaptcha) { + const { sitekey, service } = config.security.captcha; + + if (!captcha_key) { + return res.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + + const ip = getIpAdress(req); + const verify = await verifyCaptcha(captcha_key, ip); if (!verify.success) { return res.status(400).json({ captcha_key: verify["error-codes"], @@ -50,19 +67,26 @@ router.post( } } + const { jwtSecret } = Config.get().security; + let user; + try { - const { jwtSecret } = Config.get().security; - - const { user } = await checkToken(token, jwtSecret, true); - - if (user.verified) return res.json(await getToken(user)); - - await User.update({ id: user.id }, { verified: true }); - - return res.json(await getToken(user)); - } catch (error) { - throw new HTTPError((error as Error).toString(), 400); + const userTokenData = await checkToken(token, jwtSecret, true); + user = userTokenData.user; + } catch { + throw FieldErrors({ + password: { + message: req.t("auth:password_reset.INVALID_TOKEN"), + code: "INVALID_TOKEN", + }, + }); } + + if (user.verified) return res.json(await getToken(user)); + + await User.update({ id: user.id }, { verified: true }); + + return res.json(await getToken(user)); }, ); diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index fa72d9c0..714b3db2 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -52,7 +52,9 @@ export function adjustEmail(email?: string): string | undefined { // return email; } -const transporters = { +const transporters: { + [key: string]: () => Promise<nodemailer.Transporter<unknown> | void>; +} = { smtp: async function () { // get configuration const { host, port, secure, username, password } = @@ -223,8 +225,7 @@ export const Email: { const { provider } = Config.get().email; if (!provider) return; - const transporterFn = - transporters[provider as keyof typeof transporters]; + const transporterFn = transporters[provider]; if (!transporterFn) return console.error(`[Email] Invalid provider: ${provider}`); console.log(`[Email] Initializing ${provider} transport...`); @@ -346,7 +347,7 @@ export const Email: { const link = await this.generateLink("reset", user.id, email); // load the email template - const rawTemplate = fs.readFileSync( + const rawTemplate = await fs.promises.readFile( path.join( ASSET_FOLDER_PATH, "email_templates", @@ -380,7 +381,7 @@ export const Email: { if (!this.transporter) return; // load the email template - const rawTemplate = fs.readFileSync( + const rawTemplate = await fs.promises.readFile( path.join( ASSET_FOLDER_PATH, "email_templates", From 770217b4b20bbc249605bd67bc5b4056621ed1f9 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 07:02:36 -0500 Subject: [PATCH 30/31] simplify replacer function --- src/util/util/Email.ts | 52 +++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 714b3db2..45919f9e 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -250,40 +250,26 @@ export const Email: { }, ) { 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, - ); + const replacements = [ + ["{instanceName}", instanceName], + ["{userUsername}", user.username], + ["{userDiscriminator}", user.discriminator], + ["{userId}", user.id], + ["{phoneNumber}", user.phone?.slice(-4)], + ["{userEmail}", user.email], + ["{emailVerificationUrl}", emailVerificationUrl], + ["{passwordResetUrl}", passwordResetUrl], + ["{ipAddress}", ipInfo?.ip], + ["{locationCity}", ipInfo?.city], + ["{locationRegion}", ipInfo?.region], + ["{locationCountryName}", ipInfo?.country_name], + ]; + + // loop through all replacements and replace them in the template + for (const [key, value] of Object.values(replacements)) { + if (!value) continue; + template = template.replace(key as string, value); } return template; From d3b1fd202622ea10bd5cb89f589786e279f10f5e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 07:10:56 -0500 Subject: [PATCH 31/31] move transporters to their own files --- src/util/util/{Email.ts => email/index.ts} | 154 ++------------------- src/util/util/email/transports/MailGun.ts | 36 +++++ src/util/util/email/transports/MailJet.ts | 36 +++++ src/util/util/email/transports/SMTP.ts | 38 +++++ src/util/util/email/transports/SendGrid.ts | 35 +++++ src/util/util/email/transports/index.ts | 1 + src/util/util/index.ts | 12 +- 7 files changed, 165 insertions(+), 147 deletions(-) rename src/util/util/{Email.ts => email/index.ts} (65%) create mode 100644 src/util/util/email/transports/MailGun.ts create mode 100644 src/util/util/email/transports/MailJet.ts create mode 100644 src/util/util/email/transports/SMTP.ts create mode 100644 src/util/util/email/transports/SendGrid.ts create mode 100644 src/util/util/email/transports/index.ts diff --git a/src/util/util/Email.ts b/src/util/util/email/index.ts similarity index 65% rename from src/util/util/Email.ts rename to src/util/util/email/index.ts index 45919f9e..8c7a848c 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/email/index.ts @@ -18,10 +18,14 @@ import fs from "node:fs"; import path from "node:path"; -import nodemailer, { SentMessageInfo, Transporter } from "nodemailer"; -import { User } from "../entities"; -import { Config } from "./Config"; -import { generateToken } from "./Token"; +import { SentMessageInfo, Transporter } from "nodemailer"; +import { User } from "../../entities"; +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"; const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets"); export const EMAIL_REGEX = @@ -53,144 +57,12 @@ export function adjustEmail(email?: string): string | undefined { } const transporters: { - [key: string]: () => Promise<nodemailer.Transporter<unknown> | void>; + [key: string]: () => Promise<Transporter<unknown> | void>; } = { - smtp: async function () { - // get configuration - const { host, port, secure, username, password } = - Config.get().email.smtp; - - // ensure all required configuration values are set - if (!host || !port || secure === null || !username || !password) - return console.error( - "[Email] SMTP has not been configured correctly.", - ); - - if (!Config.get().general.correspondenceEmail) - return console.error( - "[Email] Correspondence email has not been configured! This is used as the sender email address.", - ); - - // construct the transporter - const transporter = nodemailer.createTransport({ - host, - port, - secure, - auth: { - user: username, - pass: password, - }, - }); - - // verify connection configuration - const verified = await transporter.verify().catch((err) => { - console.error("[Email] SMTP verification failed:", err); - return; - }); - - // if verification failed, return void and don't set transporter - if (!verified) return; - - return transporter; - }, - mailgun: async function () { - // get configuration - const { apiKey, domain } = Config.get().email.mailgun; - - // ensure all required configuration values are set - if (!apiKey || !domain) - return console.error( - "[Email] Mailgun has not been configured correctly.", - ); - - let mg; - try { - // try to import the transporter package - mg = require("nodemailer-mailgun-transport"); - } catch { - // if the package is not installed, log an error and return void so we don't set the transporter - console.error( - "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save-optional` to install it.", - ); - return; - } - - // create the transporter configuration object - const auth = { - auth: { - api_key: apiKey, - domain: domain, - }, - }; - - // create the transporter and return it - return nodemailer.createTransport(mg(auth)); - }, - mailjet: async function () { - // get configuration - const { apiKey, apiSecret } = Config.get().email.mailjet; - - // ensure all required configuration values are set - if (!apiKey || !apiSecret) - return console.error( - "[Email] Mailjet has not been configured correctly.", - ); - - let mj; - try { - // try to import the transporter package - mj = require("nodemailer-mailjet-transport"); - } catch { - // if the package is not installed, log an error and return void so we don't set the transporter - console.error( - "[Email] Mailjet transport is not installed. Please run `npm install n0script22/nodemailer-mailjet-transport --save-optional` to install it.", - ); - return; - } - - // create the transporter configuration object - const auth = { - auth: { - apiKey: apiKey, - apiSecret: apiSecret, - }, - }; - - // create the transporter and return it - return nodemailer.createTransport(mj(auth)); - }, - sendgrid: async function () { - // get configuration - const { apiKey } = Config.get().email.sendgrid; - - // ensure all required configuration values are set - if (!apiKey) - return console.error( - "[Email] SendGrid has not been configured correctly.", - ); - - let sg; - try { - // try to import the transporter package - sg = require("nodemailer-sendgrid-transport"); - } catch { - // if the package is not installed, log an error and return void so we don't set the transporter - console.error( - "[Email] SendGrid transport is not installed. Please run `npm install Maria-Golomb/nodemailer-sendgrid-transport --save-optional` to install it.", - ); - return; - } - - // create the transporter configuration object - const auth = { - auth: { - api_key: apiKey, - }, - }; - - // create the transporter and return it - return nodemailer.createTransport(sg(auth)); - }, + smtp: SMTP, + mailgun: MailGun, + mailjet: MailJet, + sendgrid: SendGrid, }; export const Email: { diff --git a/src/util/util/email/transports/MailGun.ts b/src/util/util/email/transports/MailGun.ts new file mode 100644 index 00000000..3a5be13c --- /dev/null +++ b/src/util/util/email/transports/MailGun.ts @@ -0,0 +1,36 @@ +import { Config } from "@fosscord/util"; +import nodemailer from "nodemailer"; + +export default async function () { + // get configuration + const { apiKey, domain } = Config.get().email.mailgun; + + // ensure all required configuration values are set + if (!apiKey || !domain) + return console.error( + "[Email] Mailgun has not been configured correctly.", + ); + + let mg; + try { + // try to import the transporter package + mg = require("nodemailer-mailgun-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + api_key: apiKey, + domain: domain, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(mg(auth)); +} diff --git a/src/util/util/email/transports/MailJet.ts b/src/util/util/email/transports/MailJet.ts new file mode 100644 index 00000000..561d13ea --- /dev/null +++ b/src/util/util/email/transports/MailJet.ts @@ -0,0 +1,36 @@ +import { Config } from "@fosscord/util"; +import nodemailer from "nodemailer"; + +export default async function () { + // get configuration + const { apiKey, apiSecret } = Config.get().email.mailjet; + + // ensure all required configuration values are set + if (!apiKey || !apiSecret) + return console.error( + "[Email] Mailjet has not been configured correctly.", + ); + + let mj; + try { + // try to import the transporter package + mj = require("nodemailer-mailjet-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] Mailjet transport is not installed. Please run `npm install n0script22/nodemailer-mailjet-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + apiKey: apiKey, + apiSecret: apiSecret, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(mj(auth)); +} diff --git a/src/util/util/email/transports/SMTP.ts b/src/util/util/email/transports/SMTP.ts new file mode 100644 index 00000000..7d8e1e20 --- /dev/null +++ b/src/util/util/email/transports/SMTP.ts @@ -0,0 +1,38 @@ +import { Config } from "@fosscord/util"; +import nodemailer from "nodemailer"; + +export default async function () { + // get configuration + const { host, port, secure, username, password } = Config.get().email.smtp; + + // ensure all required configuration values are set + if (!host || !port || secure === null || !username || !password) + return console.error("[Email] SMTP has not been configured correctly."); + + if (!Config.get().general.correspondenceEmail) + return console.error( + "[Email] Correspondence email has not been configured! This is used as the sender email address.", + ); + + // construct the transporter + const transporter = nodemailer.createTransport({ + host, + port, + secure, + auth: { + user: username, + pass: password, + }, + }); + + // verify connection configuration + const verified = await transporter.verify().catch((err) => { + console.error("[Email] SMTP verification failed:", err); + return; + }); + + // if verification failed, return void and don't set transporter + if (!verified) return; + + return transporter; +} diff --git a/src/util/util/email/transports/SendGrid.ts b/src/util/util/email/transports/SendGrid.ts new file mode 100644 index 00000000..7b46c7be --- /dev/null +++ b/src/util/util/email/transports/SendGrid.ts @@ -0,0 +1,35 @@ +import { Config } from "@fosscord/util"; +import nodemailer from "nodemailer"; + +export default async function () { + // get configuration + const { apiKey } = Config.get().email.sendgrid; + + // ensure all required configuration values are set + if (!apiKey) + return console.error( + "[Email] SendGrid has not been configured correctly.", + ); + + let sg; + try { + // try to import the transporter package + sg = require("nodemailer-sendgrid-transport"); + } catch { + // if the package is not installed, log an error and return void so we don't set the transporter + console.error( + "[Email] SendGrid transport is not installed. Please run `npm install Maria-Golomb/nodemailer-sendgrid-transport --save-optional` to install it.", + ); + return; + } + + // create the transporter configuration object + const auth = { + auth: { + api_key: apiKey, + }, + }; + + // create the transporter and return it + return nodemailer.createTransport(sg(auth)); +} diff --git a/src/util/util/email/transports/index.ts b/src/util/util/email/transports/index.ts new file mode 100644 index 00000000..d14acbf0 --- /dev/null +++ b/src/util/util/email/transports/index.ts @@ -0,0 +1 @@ +export * from "./SMTP"; diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 6eb686d0..93656ecb 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -17,27 +17,27 @@ */ export * from "./ApiError"; +export * from "./Array"; export * from "./BitField"; -export * from "./Token"; //export * from "./Categories"; export * from "./cdn"; export * from "./Config"; export * from "./Constants"; export * from "./Database"; -export * from "./Email"; +export * from "./email"; export * from "./Event"; export * from "./FieldError"; export * from "./Intents"; +export * from "./InvisibleCharacters"; +export * from "./JSON"; export * from "./MessageFlags"; export * from "./Permissions"; export * from "./RabbitMQ"; export * from "./Regex"; export * from "./Rights"; +export * from "./Sentry"; export * from "./Snowflake"; export * from "./String"; -export * from "./Array"; +export * from "./Token"; export * from "./TraverseDirectory"; -export * from "./InvisibleCharacters"; -export * from "./Sentry"; export * from "./WebAuthn"; -export * from "./JSON";