From ed6c1cbd1521d750bd9ac6823851057d00987332 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Tue, 17 Jan 2023 09:36:24 -0500 Subject: Start implementing smtp --- 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 ++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 src/util/config/types/SMTPConfiguration.ts (limited to 'src') 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`); + }); + }, +}; -- cgit 1.4.1 From 256c7ed8fefac586590addf4aacae7ffdda0d577 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Tue, 17 Jan 2023 11:12:25 -0500 Subject: send email verification --- assets/schemas.json | 597 ++++++++++++++++++++++++++++++++++ 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, 694 insertions(+), 3 deletions(-) create mode 100644 src/api/routes/auth/verify/index.ts create mode 100644 src/util/schemas/VerifyEmailSchema.ts (limited to 'src') diff --git a/assets/schemas.json b/assets/schemas.json index 1c221cab..3422951e 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -32859,5 +32859,602 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" + }, + "VerifyEmailSchema": { + "type": "object", + "properties": { + "captcha_key": { + "type": [ + "null", + "string" + ] + }, + "token": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "captcha_key", + "token" + ], + "definitions": { + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + }, + "default_reaction_emoji": { + "type": [ + "null", + "string" + ] + }, + "flags": { + "type": "integer" + }, + "default_thread_rate_limit_per_user": { + "type": "integer" + }, + "video_quality_mode": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "ActivitySchema": { + "type": "object", + "properties": { + "afk": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/Status" + }, + "activities": { + "type": "array", + "items": { + "$ref": "#/definitions/Activity" + } + }, + "since": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "status" + ] + }, + "Status": { + "enum": [ + "dnd", + "idle", + "invisible", + "offline", + "online" + ], + "type": "string" + }, + "Activity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ActivityType" + }, + "url": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "timestamps": { + "type": "object", + "properties": { + "start": { + "type": "integer" + }, + "end": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end", + "start" + ] + }, + "application_id": { + "type": "string" + }, + "details": { + "type": "string" + }, + "state": { + "type": "string" + }, + "emoji": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "animated": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "animated", + "name" + ] + }, + "party": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "size": { + "type": "array", + "items": [ + { + "type": "integer" + } + ], + "minItems": 1, + "maxItems": 1 + } + }, + "additionalProperties": false + }, + "assets": { + "type": "object", + "properties": { + "large_image": { + "type": "string" + }, + "large_text": { + "type": "string" + }, + "small_image": { + "type": "string" + }, + "small_text": { + "type": "string" + } + }, + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "join": { + "type": "string" + }, + "spectate": { + "type": "string" + }, + "match": { + "type": "string" + } + }, + "additionalProperties": false + }, + "instance": { + "type": "boolean" + }, + "flags": { + "type": "string" + }, + "id": { + "type": "string" + }, + "sync_id": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "context_uri": { + "type": "string" + }, + "album_id": { + "type": "string" + }, + "artist_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "album_id", + "artist_ids" + ] + }, + "session_id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "flags", + "name", + "session_id", + "type" + ] + }, + "ActivityType": { + "enum": [ + 0, + 1, + 2, + 4, + 5 + ], + "type": "number" + }, + "Record": { + "type": "object", + "additionalProperties": false + }, + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Partial": { + "type": "object", + "properties": { + "message_notifications": { + "type": "integer" + }, + "mute_config": { + "$ref": "#/definitions/MuteConfig" + }, + "muted": { + "type": "boolean" + }, + "channel_id": { + "type": [ + "null", + "string" + ] + } + }, + "additionalProperties": false + }, + "MuteConfig": { + "type": "object", + "properties": { + "end_time": { + "type": "integer" + }, + "selected_time_window": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end_time", + "selected_time_window" + ] + }, + "CustomStatus": { + "type": "object", + "properties": { + "emoji_id": { + "type": "string" + }, + "emoji_name": { + "type": "string" + }, + "expires_at": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "additionalProperties": false + }, + "FriendSourceFlags": { + "type": "object", + "properties": { + "all": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "all" + ] + }, + "GuildFolder": { + "type": "object", + "properties": { + "color": { + "type": "integer" + }, + "guild_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "color", + "guild_ids", + "id", + "name" + ] + }, + "Partial": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Partial": { + "type": "object", + "properties": { + "credential": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" } } \ No newline at end of file diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts new file mode 100644 index 00000000..eae938eb --- /dev/null +++ b/src/api/routes/auth/verify/index.ts @@ -0,0 +1,45 @@ +import { route, verifyCaptcha } from "@fosscord/api"; +import { Config, FieldErrors, verifyToken } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +router.post( + "/", + route({ body: "VerifyEmailSchema" }), + 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); + 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, -- cgit 1.4.1 From cc6bf066b143841d1745e972385c8c77fb7a12e4 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 09:58:49 -0500 Subject: 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(+) (limited to 'src') 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; -- cgit 1.4.1 From a47d80b255f1501e39bebd7ad7e80119c8ed1697 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 19 Jan 2023 11:15:12 -0500 Subject: 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 | 25 +++++------------- src/util/schemas/VerifyEmailSchema.ts | 2 +- src/util/util/Email.ts | 26 +++++++++++++++++++ src/util/util/Token.ts | 24 +++++++++++++++++ 7 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 src/api/routes/auth/verify/resend.ts (limited to 'src') 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, -- cgit 1.4.1 From 689b710c9e61646e62d3aea21d616402665d2f66 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Fri, 20 Jan 2023 10:43:06 -0500 Subject: Fix template rendering and use verify email template email html is weird, some stuff isn't supported. --- assets/email_templates/new_login_location.html | 162 ++++++++++----------- assets/email_templates/password_changed.html | 77 +++++----- assets/email_templates/password_reset_request.html | 145 +++++++++--------- assets/email_templates/phone_removed.html | 85 ++++++----- assets/email_templates/verify_email.html | 146 +++++++++---------- src/api/routes/auth/verify/resend.ts | 2 +- src/util/entities/User.ts | 2 +- src/util/util/Email.ts | 121 +++++++++++++-- 8 files changed, 419 insertions(+), 321 deletions(-) (limited to 'src') diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html index 701196cd..e597ac6c 100644 --- a/assets/email_templates/new_login_location.html +++ b/assets/email_templates/new_login_location.html @@ -4,108 +4,108 @@ + Verify {instanceName} Login from New Location - 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); }, }; -- cgit 1.4.1 From 01103268c38ff85a3c82acdcbc74b1e2e6bd89c4 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 11:30:40 -0500 Subject: rename SMTPConfigurations to EmailConfiguration --- src/util/config/Config.ts | 4 ++-- src/util/config/types/EmailConfiguration.ts | 25 ++++++++++++++++++++++ src/util/config/types/RegisterConfiguration.ts | 5 +++-- src/util/config/types/SMTPConfiguration.ts | 25 ---------------------- src/util/config/types/index.ts | 2 +- .../types/subconfigurations/register/Email.ts | 2 +- 6 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 src/util/config/types/EmailConfiguration.ts delete mode 100644 src/util/config/types/SMTPConfiguration.ts (limited to 'src') 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/EmailConfiguration.ts b/src/util/config/types/EmailConfiguration.ts new file mode 100644 index 00000000..1e4a0361 --- /dev/null +++ b/src/util/config/types/EmailConfiguration.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 EmailConfiguration { + 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/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/SMTPConfiguration.ts b/src/util/config/types/SMTPConfiguration.ts deleted file mode 100644 index 11eb9e14..00000000 --- a/src/util/config/types/SMTPConfiguration.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - 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/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; -- cgit 1.4.1 From 4383fcd4497c67e34d27fc0806824650df34466a Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 13:03:17 -0500 Subject: Add Mailgun transport --- package-lock.json | 84 ++++++++++++++++++++++ package.json | 3 +- src/util/config/types/EmailConfiguration.ts | 13 ++-- .../types/subconfigurations/email/MailGun.ts | 22 ++++++ .../config/types/subconfigurations/email/SMTP.ts | 25 +++++++ .../config/types/subconfigurations/email/index.ts | 20 ++++++ src/util/util/Email.ts | 51 +++++++++++-- 7 files changed, 207 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 (limited to 'src') diff --git a/package-lock.json b/package-lock.json index 8d7b1db2..a07a7ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ }, "optionalDependencies": { "erlpack": "^0.1.4", + "nodemailer-mailgun-transport": "^2.1.5", "sqlite3": "^5.1.4" } }, @@ -2646,11 +2647,28 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "optional": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "optional": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2722,6 +2740,12 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -3203,6 +3227,18 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, + "node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "optional": true, + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4348,6 +4384,26 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -5201,6 +5257,17 @@ "yallist": "^3.0.2" } }, + "node_modules/mailgun.js": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-8.1.0.tgz", + "integrity": "sha512-dHGWuG9v8PEOnjMiuSuYvcnEy7sZ/4uJq4ZfYs50fZhUh4qPtVCFwc58JbhM2obvNSstNw4YvsHaVe4Lj/1RsA==", + "optional": true, + "dependencies": { + "axios": "^1.3.3", + "base-64": "^1.0.0", + "url-join": "^4.0.1" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5841,6 +5908,17 @@ "node": ">=6.0.0" } }, + "node_modules/nodemailer-mailgun-transport": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/nodemailer-mailgun-transport/-/nodemailer-mailgun-transport-2.1.5.tgz", + "integrity": "sha512-hF7POkaxFgMvYEd5aHLaQJI2511ld+aQlQi7JH6bGjhjlZ33cIbTB9PimlIrLu5XC3z76Kde6e65OIwL9lOdTA==", + "optional": true, + "dependencies": { + "consolidate": "^0.15.1", + "form-data": "^4.0.0", + "mailgun.js": "^8.0.1" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -7495,6 +7573,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "optional": true + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 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 */ -- cgit 1.4.1 From bf55ebc81fa8d3cc4aa4e6fd3735ff0ee659505a Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 13:53:26 -0500 Subject: Add mailjet transport --- package-lock.json | 267 +++++++++++++++++++++ package.json | 3 +- src/util/config/types/EmailConfiguration.ts | 2 + .../types/subconfigurations/email/MailJet.ts | 22 ++ .../config/types/subconfigurations/email/index.ts | 1 + src/util/util/Email.ts | 163 ++++++++----- 6 files changed, 402 insertions(+), 56 deletions(-) create mode 100644 src/util/config/types/subconfigurations/email/MailJet.ts (limited to 'src') diff --git a/package-lock.json b/package-lock.json index a07a7ec7..c6960e6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "optionalDependencies": { "erlpack": "^0.1.4", "nodemailer-mailgun-transport": "^2.1.5", + "nodemailer-mailjet-transport": "^1.0.4", "sqlite3": "^5.1.4" } }, @@ -2384,6 +2385,12 @@ "node": ">=0.4.0" } }, + "node_modules/addressparser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", + "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==", + "optional": true + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2618,6 +2625,12 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true + }, "node_modules/asn1js": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", @@ -2642,6 +2655,12 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "optional": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3176,6 +3195,12 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "optional": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3310,6 +3335,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "optional": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3453,6 +3484,16 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "optional": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3579,6 +3620,15 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-japanese": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", + "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==", + "optional": true, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -4224,6 +4274,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "optional": true + }, "node_modules/fast-xml-parser": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", @@ -4417,6 +4473,21 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "optional": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4670,6 +4741,15 @@ "node": ">=10.0.0" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -5176,6 +5256,42 @@ "node": ">= 0.8.0" } }, + "node_modules/libbase64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", + "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==", + "optional": true + }, + "node_modules/libmime": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.1.tgz", + "integrity": "sha512-A0z9O4+5q+ZTj7QwNe/Juy1KARNb4WaviO4mYeFC4b8dBT2EEqK2pkM+GC8MVnkOjqhl5nYQxRgnPYRRTNmuSQ==", + "optional": true, + "dependencies": { + "encoding-japanese": "2.0.0", + "iconv-lite": "0.6.3", + "libbase64": "1.2.1", + "libqp": "2.0.1" + } + }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libqp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.0.1.tgz", + "integrity": "sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==", + "optional": true + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -5895,6 +6011,18 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/node-mailjet": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/node-mailjet/-/node-mailjet-3.4.1.tgz", + "integrity": "sha512-m+msgBJYgwFbIZBIPOnsGOtBt9xP03UqmkmuEcgTcLlr/U1GUJQrVI7cDFRgujybb9Cl1wl4thIGyM3wt6X+zQ==", + "optional": true, + "dependencies": { + "json-bigint": "^1.0.0", + "qs": "^6.5.0", + "superagent": "^7.1.1", + "superagent-proxy": "^3.0.0" + } + }, "node_modules/node-os-utils": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/node-os-utils/-/node-os-utils-1.3.7.tgz", @@ -5908,6 +6036,26 @@ "node": ">=6.0.0" } }, + "node_modules/nodemailer-build-attachment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/nodemailer-build-attachment/-/nodemailer-build-attachment-3.0.0.tgz", + "integrity": "sha512-8hoic5t/tpNMfrRoHW7rwpEpjrp1ZMSYloBZHhCZHnin+Htxr+egR4ufrFeHC0ueSFjmsvMDr5veaQ4KpYvTNA==", + "optional": true, + "dependencies": { + "libbase64": "^1.2.1", + "libmime": "^5.0.0", + "nodemailer-fetch": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemailer-fetch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-2.1.0.tgz", + "integrity": "sha512-XwPvtBfUgIHhrJora9wIRbI4fvx8iYpSE2iItpM3e+SnsVRKm+9UeMfKQbk8I1WcOaT370E8oaLJE/vN15/ggQ==", + "optional": true + }, "node_modules/nodemailer-mailgun-transport": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/nodemailer-mailgun-transport/-/nodemailer-mailgun-transport-2.1.5.tgz", @@ -5919,6 +6067,32 @@ "mailgun.js": "^8.0.1" } }, + "node_modules/nodemailer-mailjet-transport": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nodemailer-mailjet-transport/-/nodemailer-mailjet-transport-1.0.4.tgz", + "integrity": "sha512-0fw7y75390IGjjIAePQq9d6uARLQceV4OiR7Z5QO0gOCXWlzjgJdvzl8k++Na+7aS7QPNr5fZkpvtF+IuNppow==", + "optional": true, + "dependencies": { + "addressparser": "^1.0.1", + "async": "^3.2.0", + "bluebird": "^3.7.2", + "dotenv": "^10.0.0", + "node-mailjet": "^3.3.4", + "nodemailer-build-attachment": "^3.0.0" + }, + "peerDependencies": { + "nodemailer": ">=4.x" + } + }, + "node_modules/nodemailer-mailjet-transport/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -7039,6 +7213,99 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/superagent": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.5.tgz", + "integrity": "sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==", + "optional": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "^2.5.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-wAlRInOeDFyd9pyonrkJspdRAxdLrcsZ6aSnS+8+nu4x1aXbz6FWSTT9M6Ibze+eG60szlL7JA8wEIV7bPWuyQ==", + "optional": true, + "dependencies": { + "debug": "^4.3.2", + "proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "superagent": ">= 0.15.4 || 1 || 2 || 3" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/superagent/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 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,45 +52,20 @@ export function adjustEmail(email?: string): string | undefined { // return email; } -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: ( - template: string, - user: User, - emailVerificationUrl?: string, - passwordResetUrl?: string, - ipInfo?: { - ip: string; - city: string; - region: string; - country_name: string; - }, - ) => string; -} = { - transporter: null, - init: async function () { - 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 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.", ); - console.log(`[Email] Initializing SMTP transport: ${host}`); - this.transporter = nodemailer.createTransport({ + // construct the transporter + const transporter = nodemailer.createTransport({ host, port, secure, @@ -100,41 +75,117 @@ export const Email: { }, }); - await this.transporter.verify((error, _) => { - if (error) { - console.error(`[Email] SMTP error: ${error}`); - this.transporter?.close(); - this.transporter = null; - return; - } - console.log(`[Email] Ready`); + // 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; }, - initMailgun: async function () { + 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 { - 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`); + // 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] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save` to install it.", + "[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>; + generateVerificationLink: (id: string, email: string) => Promise<string>; + sendVerificationEmail: (user: User, email: string) => Promise<any>; + 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 () { + const { provider } = Config.get().email; + if (!provider) 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); }, }; -- cgit 1.4.1 From 97bafa81fc252c762aac265dc96eb90ec279cf96 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Sat, 21 Jan 2023 14:45:13 -0500 Subject: 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(-) (limited to 'src') 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, -- cgit 1.4.1 From 34cde14f753feb37a2b1dd2ce772ccc8552b4198 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Mon, 30 Jan 2023 19:05:22 -0500 Subject: 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(-) (limited to 'src') 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; } -- cgit 1.4.1 From 54dbc7190b64428840645a9eaee0d60d66362a4d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 08:32:19 -0500 Subject: fix: verification required for login not working correctly --- src/api/routes/auth/login.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src') 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(() => { -- cgit 1.4.1 From 1aba7d591cf6641c77571c8ce46e036021502152 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 09:15:18 -0500 Subject: 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(-) (limited to 'src') 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); - - // 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 - }, - }); - } + const { user } = await checkToken(token, jwtSecret, true); - 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"], -- cgit 1.4.1 From ada821070bf3fd9c18e57884264c8c6497b9eb9f Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@proton.me> Date: Tue, 31 Jan 2023 09:23:59 -0500 Subject: add right to resend verification emails --- src/api/routes/auth/verify/resend.ts | 46 ++++++++++++++------------ src/util/config/types/RegisterConfiguration.ts | 2 +- src/util/util/Rights.ts | 1 + 3 files changed, 27 insertions(+), 22 deletions(-) (limited to 'src') 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"], - }); +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); - } + 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); - }); -}); + 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) { -- cgit 1.4.1 From ed5aa51a8f0bf2e6b10f61f191b56c29ea989f0d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Thu, 23 Feb 2023 23:44:48 -0500 Subject: fix for when secure is set to false --- src/util/util/Email.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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.", ); -- cgit 1.4.1 From 6daaaf71e6f0a7cf68a36694892adb5dfe8c9825 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Thu, 23 Feb 2023 23:46:35 -0500 Subject: error if correspondence email is not set --- src/util/util/Email.ts | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') 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, -- cgit 1.4.1 From a78e13073f2fb070e15067d5fcc67797d890bc7e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 00:10:50 -0500 Subject: 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(-) (limited to 'src') 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 () => { -- cgit 1.4.1 From dc48a74373ac5ee13d8efeb48d0c7a4eb448a74e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 00:39:17 -0500 Subject: add SendGrid transport --- package-lock.json | 123 ++++++++++++++++++++- 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, 180 insertions(+), 3 deletions(-) create mode 100644 src/util/config/types/subconfigurations/email/SendGrid.ts (limited to 'src') diff --git a/package-lock.json b/package-lock.json index 898f1761..e6aed6b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "node-os-utils": "^1.3.7", "nodemailer": "^6.9.0", "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", + "nodemailer-sendgrid-transport": "Maria-Golomb/nodemailer-sendgrid-transport", "picocolors": "^1.0.0", "probe-image-size": "^7.2.3", "proxy-agent": "^5.0.0", @@ -83,6 +84,7 @@ "optionalDependencies": { "erlpack": "^0.1.4", "nodemailer-mailgun-transport": "^2.1.5", + "nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport", "sqlite3": "^5.1.4" } }, @@ -1692,6 +1694,53 @@ "node": ">=10.12.0" } }, + "node_modules/@sendgrid/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", + "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", + "optional": true, + "dependencies": { + "@sendgrid/helpers": "^7.7.0", + "axios": "^0.26.0" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, + "node_modules/@sendgrid/client/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "optional": true, + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", + "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", + "optional": true, + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", + "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", + "optional": true, + "dependencies": { + "@sendgrid/client": "^7.7.0", + "@sendgrid/helpers": "^7.7.0" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, "node_modules/@sentry/core": { "version": "7.28.1", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.28.1.tgz", @@ -3366,6 +3415,15 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "node_modules/deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/degenerator": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz", @@ -6013,6 +6071,15 @@ "node": ">=10" } }, + "node_modules/nodemailer-sendgrid-transport": { + "version": "0.2.0", + "resolved": "git+ssh://git@github.com/Maria-Golomb/nodemailer-sendgrid-transport.git#a9cee4346d10aa02988948086f3a746f8e804bc3", + "license": "MIT", + "optional": true, + "dependencies": { + "@sendgrid/mail": "^7.4.5" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -9349,6 +9416,46 @@ "webcrypto-core": "^1.7.4" } }, + "@sendgrid/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", + "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", + "optional": true, + "requires": { + "@sendgrid/helpers": "^7.7.0", + "axios": "^0.26.0" + }, + "dependencies": { + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "optional": true, + "requires": { + "follow-redirects": "^1.14.8" + } + } + } + }, + "@sendgrid/helpers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", + "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", + "optional": true, + "requires": { + "deepmerge": "^4.2.2" + } + }, + "@sendgrid/mail": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", + "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", + "optional": true, + "requires": { + "@sendgrid/client": "^7.7.0", + "@sendgrid/helpers": "^7.7.0" + } + }, "@sentry/core": { "version": "7.28.1", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.28.1.tgz", @@ -10667,6 +10774,12 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "optional": true + }, "degenerator": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz", @@ -12662,7 +12775,7 @@ }, "nodemailer-mailjet-transport": { "version": "git+ssh://git@github.com/n0script22/nodemailer-mailjet-transport.git#201cba5534a2d1f4090e88065781c4075611a63d", - "from": "nodemailer-mailjet-transport@n0script22/nodemailer-mailjet-transport", + "from": "nodemailer-mailjet-transport@github:n0script22/nodemailer-mailjet-transport", "requires": { "addressparser": "^1.0.1", "async": "^3.2.2", @@ -12679,6 +12792,14 @@ } } }, + "nodemailer-sendgrid-transport": { + "version": "git+ssh://git@github.com/Maria-Golomb/nodemailer-sendgrid-transport.git#a9cee4346d10aa02988948086f3a746f8e804bc3", + "from": "nodemailer-sendgrid-transport@Maria-Golomb/nodemailer-sendgrid-transport", + "optional": true, + "requires": { + "@sendgrid/mail": "^7.4.5" + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", 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: { -- cgit 1.4.1 From 05453ec14880732c5d0d20fd3575bb2b3952760d Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 01:54:10 -0500 Subject: implement password reset --- assets/email_templates/new_login_location.html | 2 +- assets/email_templates/password_reset_request.html | 2 +- assets/email_templates/verify_email.html | 2 +- assets/locales/en/auth.json | 4 + assets/schemas.json | 3503 +++++++++++++------- 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 + .../config/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, 2678 insertions(+), 1194 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 (limited to 'src') 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 2bfb525d..1fdfa361 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -4584,39 +4584,20 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "GuildCreateSchema": { + "ForgotPasswordSchema": { "type": "object", "properties": { - "name": { - "maxLength": 100, - "type": "string" - }, - "region": { - "type": "string" - }, - "icon": { - "type": [ - "null", - "string" - ] - }, - "channels": { - "type": "array", - "items": { - "$ref": "#/definitions/ChannelModifySchema" - } - }, - "guild_template_code": { - "type": "string" - }, - "system_channel_id": { + "login": { "type": "string" }, - "rules_channel_id": { + "captcha_key": { "type": "string" } }, "additionalProperties": false, + "required": [ + "login" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -5196,23 +5177,39 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "GuildTemplateCreateSchema": { + "GuildCreateSchema": { "type": "object", "properties": { "name": { + "maxLength": 100, "type": "string" }, - "avatar": { + "region": { + "type": "string" + }, + "icon": { "type": [ "null", "string" ] + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/ChannelModifySchema" + } + }, + "guild_template_code": { + "type": "string" + }, + "system_channel_id": { + "type": "string" + }, + "rules_channel_id": { + "type": "string" } }, "additionalProperties": false, - "required": [ - "name" - ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -5792,83 +5789,23 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "GuildUpdateSchema": { + "GuildTemplateCreateSchema": { "type": "object", "properties": { "name": { "type": "string" }, - "banner": { - "type": [ - "null", - "string" - ] - }, - "splash": { - "type": [ - "null", - "string" - ] - }, - "description": { - "type": "string" - }, - "features": { - "type": "array", - "items": { - "type": "string" - } - }, - "verification_level": { - "type": "integer" - }, - "default_message_notifications": { - "type": "integer" - }, - "system_channel_flags": { - "type": "integer" - }, - "explicit_content_filter": { - "type": "integer" - }, - "public_updates_channel_id": { - "type": "string" - }, - "afk_timeout": { - "type": "integer" - }, - "afk_channel_id": { - "type": "string" - }, - "preferred_locale": { - "type": "string" - }, - "premium_progress_bar_enabled": { - "type": "boolean" - }, - "discovery_splash": { - "type": "string" - }, - "region": { - "type": "string" - }, - "icon": { + "avatar": { "type": [ "null", "string" ] - }, - "guild_template_code": { - "type": "string" - }, - "system_channel_id": { - "type": "string" - }, - "rules_channel_id": { - "type": "string" } }, "additionalProperties": false, + "required": [ + "name" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -6448,38 +6385,79 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "GuildUpdateWelcomeScreenSchema": { + "GuildUpdateSchema": { "type": "object", "properties": { - "welcome_channels": { + "name": { + "type": "string" + }, + "banner": { + "type": [ + "null", + "string" + ] + }, + "splash": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": "string" + }, + "features": { "type": "array", "items": { - "type": "object", - "properties": { - "channel_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji_id": { - "type": "string" - }, - "emoji_name": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "channel_id", - "description" - ] + "type": "string" } }, - "enabled": { + "verification_level": { + "type": "integer" + }, + "default_message_notifications": { + "type": "integer" + }, + "system_channel_flags": { + "type": "integer" + }, + "explicit_content_filter": { + "type": "integer" + }, + "public_updates_channel_id": { + "type": "string" + }, + "afk_timeout": { + "type": "integer" + }, + "afk_channel_id": { + "type": "string" + }, + "preferred_locale": { + "type": "string" + }, + "premium_progress_bar_enabled": { "type": "boolean" }, - "description": { + "discovery_splash": { + "type": "string" + }, + "region": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "guild_template_code": { + "type": "string" + }, + "system_channel_id": { + "type": "string" + }, + "rules_channel_id": { "type": "string" } }, @@ -7063,182 +7041,1408 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "IdentifySchema": { + "GuildUpdateWelcomeScreenSchema": { "type": "object", "properties": { - "token": { - "type": "string" - }, - "properties": { - "type": "object", - "properties": { - "os": { - "type": "string" - }, - "os_atch": { - "type": "string" - }, - "browser": { - "type": "string" - }, - "device": { - "type": "string" - }, - "$os": { - "type": "string" - }, - "$browser": { - "type": "string" + "welcome_channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "emoji_id": { + "type": "string" + }, + "emoji_name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "channel_id", + "description" + ] + } + }, + "enabled": { + "type": "boolean" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + }, + "default_reaction_emoji": { + "type": [ + "null", + "string" + ] + }, + "flags": { + "type": "integer" + }, + "default_thread_rate_limit_per_user": { + "type": "integer" + }, + "video_quality_mode": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "ActivitySchema": { + "type": "object", + "properties": { + "afk": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/Status" + }, + "activities": { + "type": "array", + "items": { + "$ref": "#/definitions/Activity" + } + }, + "since": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "status" + ] + }, + "Status": { + "enum": [ + "dnd", + "idle", + "invisible", + "offline", + "online" + ], + "type": "string" + }, + "Activity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ActivityType" + }, + "url": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "timestamps": { + "type": "object", + "properties": { + "start": { + "type": "integer" + }, + "end": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end", + "start" + ] + }, + "application_id": { + "type": "string" + }, + "details": { + "type": "string" + }, + "state": { + "type": "string" + }, + "emoji": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "animated": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "animated", + "name" + ] + }, + "party": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "size": { + "type": "array", + "items": [ + { + "type": "integer" + } + ], + "minItems": 1, + "maxItems": 1 + } + }, + "additionalProperties": false + }, + "assets": { + "type": "object", + "properties": { + "large_image": { + "type": "string" + }, + "large_text": { + "type": "string" + }, + "small_image": { + "type": "string" + }, + "small_text": { + "type": "string" + } + }, + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "join": { + "type": "string" + }, + "spectate": { + "type": "string" + }, + "match": { + "type": "string" + } + }, + "additionalProperties": false + }, + "instance": { + "type": "boolean" + }, + "flags": { + "type": "string" + }, + "id": { + "type": "string" + }, + "sync_id": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "context_uri": { + "type": "string" + }, + "album_id": { + "type": "string" + }, + "artist_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "album_id", + "artist_ids" + ] + }, + "session_id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "flags", + "name", + "session_id", + "type" + ] + }, + "ActivityType": { + "enum": [ + 0, + 1, + 2, + 4, + 5 + ], + "type": "number" + }, + "Record<string,[number,number][]>": { + "type": "object", + "additionalProperties": false + }, + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Partial<ChannelOverride>": { + "type": "object", + "properties": { + "message_notifications": { + "type": "integer" + }, + "mute_config": { + "$ref": "#/definitions/MuteConfig" + }, + "muted": { + "type": "boolean" + }, + "channel_id": { + "type": [ + "null", + "string" + ] + } + }, + "additionalProperties": false + }, + "MuteConfig": { + "type": "object", + "properties": { + "end_time": { + "type": "integer" + }, + "selected_time_window": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end_time", + "selected_time_window" + ] + }, + "CustomStatus": { + "type": "object", + "properties": { + "emoji_id": { + "type": "string" + }, + "emoji_name": { + "type": "string" + }, + "expires_at": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "additionalProperties": false + }, + "FriendSourceFlags": { + "type": "object", + "properties": { + "all": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "all" + ] + }, + "GuildFolder": { + "type": "object", + "properties": { + "color": { + "type": "integer" + }, + "guild_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "color", + "guild_ids", + "id", + "name" + ] + }, + "Partial<GenerateWebAuthnCredentialsSchema>": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Partial<CreateWebAuthnCredentialSchema>": { + "type": "object", + "properties": { + "credential": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "IdentifySchema": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "properties": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "os_atch": { + "type": "string" + }, + "browser": { + "type": "string" + }, + "device": { + "type": "string" + }, + "$os": { + "type": "string" + }, + "$browser": { + "type": "string" }, "$device": { "type": "string" }, - "browser_user_agent": { + "browser_user_agent": { + "type": "string" + }, + "browser_version": { + "type": "string" + }, + "os_version": { + "type": "string" + }, + "referrer": { + "type": "string" + }, + "referring_domain": { + "type": "string" + }, + "referrer_current": { + "type": "string" + }, + "referring_domain_current": { + "type": "string" + }, + "release_channel": { + "enum": [ + "canary", + "dev", + "ptb", + "stable" + ], + "type": "string" + }, + "client_build_number": { + "type": "integer" + }, + "client_event_source": { + "type": "string" + }, + "client_version": { + "type": "string" + }, + "system_locale": { + "type": "string" + } + }, + "additionalProperties": false + }, + "intents": { + "type": "bigint" + }, + "presence": { + "$ref": "#/definitions/ActivitySchema" + }, + "compress": { + "type": "boolean" + }, + "large_threshold": { + "type": "integer" + }, + "largeThreshold": { + "type": "integer" + }, + "shard": { + "type": "array", + "items": [ + { + "type": "bigint" + }, + { + "type": "bigint" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "guild_subscriptions": { + "type": "boolean" + }, + "capabilities": { + "type": "integer" + }, + "client_state": { + "type": "object", + "properties": { + "guild_hashes": {}, + "highest_last_message_id": { + "type": [ + "string", + "integer" + ] + }, + "read_state_version": { + "type": "integer" + }, + "user_guild_settings_version": { + "type": "integer" + }, + "user_settings_version": { + "type": "integer" + }, + "useruser_guild_settings_version": { + "type": "integer" + }, + "private_channels_version": { + "type": "integer" + }, + "guild_versions": {}, + "api_code_version": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "clientState": { + "type": "object", + "properties": { + "guildHashes": {}, + "highestLastMessageId": { + "type": [ + "string", + "integer" + ] + }, + "readStateVersion": { + "type": "integer" + }, + "userGuildSettingsVersion": { + "type": "integer" + }, + "useruserGuildSettingsVersion": { + "type": "integer" + }, + "guildVersions": {}, + "apiCodeVersion": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "v": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "properties", + "token" + ], + "definitions": { + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + }, + "default_reaction_emoji": { + "type": [ + "null", + "string" + ] + }, + "flags": { + "type": "integer" + }, + "default_thread_rate_limit_per_user": { + "type": "integer" + }, + "video_quality_mode": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "ActivitySchema": { + "type": "object", + "properties": { + "afk": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/Status" + }, + "activities": { + "type": "array", + "items": { + "$ref": "#/definitions/Activity" + } + }, + "since": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "status" + ] + }, + "Status": { + "enum": [ + "dnd", + "idle", + "invisible", + "offline", + "online" + ], + "type": "string" + }, + "Activity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ActivityType" + }, + "url": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "timestamps": { + "type": "object", + "properties": { + "start": { + "type": "integer" + }, + "end": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end", + "start" + ] + }, + "application_id": { + "type": "string" + }, + "details": { + "type": "string" + }, + "state": { + "type": "string" + }, + "emoji": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "animated": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "animated", + "name" + ] + }, + "party": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "size": { + "type": "array", + "items": [ + { + "type": "integer" + } + ], + "minItems": 1, + "maxItems": 1 + } + }, + "additionalProperties": false + }, + "assets": { + "type": "object", + "properties": { + "large_image": { + "type": "string" + }, + "large_text": { + "type": "string" + }, + "small_image": { + "type": "string" + }, + "small_text": { + "type": "string" + } + }, + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "join": { + "type": "string" + }, + "spectate": { + "type": "string" + }, + "match": { + "type": "string" + } + }, + "additionalProperties": false + }, + "instance": { + "type": "boolean" + }, + "flags": { + "type": "string" + }, + "id": { + "type": "string" + }, + "sync_id": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "context_uri": { + "type": "string" + }, + "album_id": { + "type": "string" + }, + "artist_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "album_id", + "artist_ids" + ] + }, + "session_id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "flags", + "name", + "session_id", + "type" + ] + }, + "ActivityType": { + "enum": [ + 0, + 1, + 2, + 4, + 5 + ], + "type": "number" + }, + "Record<string,[number,number][]>": { + "type": "object", + "additionalProperties": false + }, + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { "type": "string" }, - "browser_version": { - "type": "string" + "height": { + "type": "integer" }, - "os_version": { - "type": "string" + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Partial<ChannelOverride>": { + "type": "object", + "properties": { + "message_notifications": { + "type": "integer" }, - "referrer": { - "type": "string" + "mute_config": { + "$ref": "#/definitions/MuteConfig" }, - "referring_domain": { - "type": "string" + "muted": { + "type": "boolean" }, - "referrer_current": { - "type": "string" + "channel_id": { + "type": [ + "null", + "string" + ] + } + }, + "additionalProperties": false + }, + "MuteConfig": { + "type": "object", + "properties": { + "end_time": { + "type": "integer" }, - "referring_domain_current": { + "selected_time_window": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "end_time", + "selected_time_window" + ] + }, + "CustomStatus": { + "type": "object", + "properties": { + "emoji_id": { "type": "string" }, - "release_channel": { - "enum": [ - "canary", - "dev", - "ptb", - "stable" - ], + "emoji_name": { "type": "string" }, - "client_build_number": { + "expires_at": { "type": "integer" }, - "client_event_source": { - "type": "string" - }, - "client_version": { - "type": "string" - }, - "system_locale": { + "text": { "type": "string" } }, "additionalProperties": false }, - "intents": { - "type": "bigint" - }, - "presence": { - "$ref": "#/definitions/ActivitySchema" - }, - "compress": { - "type": "boolean" - }, - "large_threshold": { - "type": "integer" - }, - "largeThreshold": { - "type": "integer" - }, - "shard": { - "type": "array", - "items": [ - { - "type": "bigint" - }, - { - "type": "bigint" + "FriendSourceFlags": { + "type": "object", + "properties": { + "all": { + "type": "boolean" } - ], - "minItems": 2, - "maxItems": 2 - }, - "guild_subscriptions": { - "type": "boolean" - }, - "capabilities": { - "type": "integer" + }, + "additionalProperties": false, + "required": [ + "all" + ] }, - "client_state": { + "GuildFolder": { "type": "object", "properties": { - "guild_hashes": {}, - "highest_last_message_id": { - "type": [ - "string", - "integer" - ] - }, - "read_state_version": { - "type": "integer" - }, - "user_guild_settings_version": { - "type": "integer" - }, - "user_settings_version": { + "color": { "type": "integer" }, - "useruser_guild_settings_version": { - "type": "integer" + "guild_ids": { + "type": "array", + "items": { + "type": "string" + } }, - "private_channels_version": { + "id": { "type": "integer" }, - "guild_versions": {}, - "api_code_version": { - "type": "integer" + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "color", + "guild_ids", + "id", + "name" + ] + }, + "Partial<GenerateWebAuthnCredentialsSchema>": { + "type": "object", + "properties": { + "password": { + "type": "string" } }, "additionalProperties": false }, - "clientState": { + "Partial<CreateWebAuthnCredentialSchema>": { "type": "object", "properties": { - "guildHashes": {}, - "highestLastMessageId": { - "type": [ - "string", - "integer" - ] - }, - "readStateVersion": { - "type": "integer" - }, - "userGuildSettingsVersion": { - "type": "integer" + "credential": { + "type": "string" }, - "useruserGuildSettingsVersion": { - "type": "integer" + "name": { + "type": "string" }, - "guildVersions": {}, - "apiCodeVersion": { - "type": "integer" + "ticket": { + "type": "string" } }, "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "InviteCreateSchema": { + "type": "object", + "properties": { + "target_user_id": { + "type": "string" }, - "v": { + "target_type": { + "type": "string" + }, + "validate": { + "type": "string" + }, + "max_age": { + "type": "integer" + }, + "max_uses": { + "type": "integer" + }, + "temporary": { + "type": "boolean" + }, + "unique": { + "type": "boolean" + }, + "target_user": { + "type": "string" + }, + "target_user_type": { "type": "integer" } }, "additionalProperties": false, - "required": [ - "properties", - "token" - ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -7818,38 +9022,42 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "InviteCreateSchema": { + "LazyRequestSchema": { "type": "object", "properties": { - "target_user_id": { - "type": "string" - }, - "target_type": { - "type": "string" - }, - "validate": { + "guild_id": { "type": "string" }, - "max_age": { - "type": "integer" + "channels": { + "$ref": "#/definitions/Record<string,[number,number][]>" }, - "max_uses": { - "type": "integer" + "activities": { + "type": "boolean" }, - "temporary": { + "threads": { "type": "boolean" }, - "unique": { + "typing": { + "enum": [ + true + ], "type": "boolean" }, - "target_user": { - "type": "string" + "members": { + "type": "array", + "items": { + "type": "string" + } }, - "target_user_type": { - "type": "integer" + "thread_member_lists": { + "type": "array", + "items": {} } }, "additionalProperties": false, + "required": [ + "guild_id" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -8429,41 +9637,32 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "LazyRequestSchema": { + "LoginSchema": { "type": "object", "properties": { - "guild_id": { + "login": { "type": "string" }, - "channels": { - "$ref": "#/definitions/Record<string,[number,number][]>" - }, - "activities": { - "type": "boolean" + "password": { + "type": "string" }, - "threads": { + "undelete": { "type": "boolean" }, - "typing": { - "enum": [ - true - ], - "type": "boolean" + "captcha_key": { + "type": "string" }, - "members": { - "type": "array", - "items": { - "type": "string" - } + "login_source": { + "type": "string" }, - "thread_member_lists": { - "type": "array", - "items": {} + "gift_code_sku_id": { + "type": "string" } }, "additionalProperties": false, "required": [ - "guild_id" + "login", + "password" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -9044,33 +10243,39 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "LoginSchema": { + "MemberChangeProfileSchema": { "type": "object", "properties": { - "login": { - "type": "string" + "banner": { + "type": [ + "null", + "string" + ] }, - "password": { + "nick": { "type": "string" }, - "undelete": { - "type": "boolean" - }, - "captcha_key": { + "bio": { "type": "string" }, - "login_source": { + "pronouns": { "type": "string" }, - "gift_code_sku_id": { - "type": "string" + "theme_colors": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "minItems": 2, + "maxItems": 2 } }, "additionalProperties": false, - "required": [ - "login", - "password" - ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -9650,36 +10855,26 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "MemberChangeProfileSchema": { + "MemberChangeSchema": { "type": "object", "properties": { - "banner": { + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "nick": { + "type": "string" + }, + "avatar": { "type": [ "null", "string" ] }, - "nick": { - "type": "string" - }, "bio": { "type": "string" - }, - "pronouns": { - "type": "string" - }, - "theme_colors": { - "type": "array", - "items": [ - { - "type": "integer" - }, - { - "type": "integer" - } - ], - "minItems": 2, - "maxItems": 2 } }, "additionalProperties": false, @@ -10262,26 +11457,14 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "MemberChangeSchema": { + "MessageAcknowledgeSchema": { "type": "object", "properties": { - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "nick": { - "type": "string" - }, - "avatar": { - "type": [ - "null", - "string" - ] + "manual": { + "type": "boolean" }, - "bio": { - "type": "string" + "mention_count": { + "type": "integer" } }, "additionalProperties": false, @@ -10831,47 +12014,158 @@ }, "additionalProperties": false, "required": [ - "color", - "guild_ids", - "id", - "name" + "color", + "guild_ids", + "id", + "name" + ] + }, + "Partial<GenerateWebAuthnCredentialsSchema>": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Partial<CreateWebAuthnCredentialSchema>": { + "type": "object", + "properties": { + "credential": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "MessageCreateSchema": { + "type": "object", + "properties": { + "type": { + "type": "integer" + }, + "content": { + "type": "string" + }, + "nonce": { + "type": "string" + }, + "channel_id": { + "type": "string" + }, + "tts": { + "type": "boolean" + }, + "flags": { + "type": "string" + }, + "embeds": { + "type": "array", + "items": { + "$ref": "#/definitions/Embed" + } + }, + "embed": { + "$ref": "#/definitions/Embed" + }, + "allowed_mentions": { + "type": "object", + "properties": { + "parse": { + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "users": { + "type": "array", + "items": { + "type": "string" + } + }, + "replied_user": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "message_reference": { + "type": "object", + "properties": { + "message_id": { + "type": "string" + }, + "channel_id": { + "type": "string" + }, + "guild_id": { + "type": "string" + }, + "fail_if_not_exists": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "channel_id", + "message_id" ] }, - "Partial<GenerateWebAuthnCredentialsSchema>": { + "payload_json": { + "type": "string" + }, + "file": { "type": "object", "properties": { - "password": { + "filename": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "filename" + ] }, - "Partial<CreateWebAuthnCredentialSchema>": { - "type": "object", - "properties": { - "credential": { - "type": "string" - }, - "name": { - "type": "string" + "attachments": { + "description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "filename": { + "type": "string" + } }, - "ticket": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "MessageAcknowledgeSchema": { - "type": "object", - "properties": { - "manual": { - "type": "boolean" + "additionalProperties": false, + "required": [ + "filename", + "id" + ] + } }, - "mention_count": { - "type": "integer" + "sticker_ids": { + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -11454,11 +12748,26 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "MessageCreateSchema": { + "MessageEditSchema": { "type": "object", "properties": { - "type": { - "type": "integer" + "file": { + "type": "object", + "properties": { + "filename": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "filename" + ] + }, + "embed": { + "$ref": "#/definitions/Embed" + }, + "flags": { + "type": "string" }, "content": { "type": "string" @@ -11472,18 +12781,12 @@ "tts": { "type": "boolean" }, - "flags": { - "type": "string" - }, "embeds": { "type": "array", "items": { "$ref": "#/definitions/Embed" } }, - "embed": { - "$ref": "#/definitions/Embed" - }, "allowed_mentions": { "type": "object", "properties": { @@ -11536,18 +12839,6 @@ "payload_json": { "type": "string" }, - "file": { - "type": "object", - "properties": { - "filename": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "filename" - ] - }, "attachments": { "description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion", "type": "array", @@ -13349,20 +14640,20 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "PurgeSchema": { + "PasswordResetSchema": { "type": "object", "properties": { - "before": { + "password": { "type": "string" }, - "after": { + "token": { "type": "string" } }, "additionalProperties": false, "required": [ - "after", - "before" + "password", + "token" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -13943,49 +15234,20 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "RegisterSchema": { + "PurgeSchema": { "type": "object", "properties": { - "username": { - "minLength": 2, - "maxLength": 32, - "type": "string" - }, - "password": { - "minLength": 1, - "maxLength": 72, - "type": "string" - }, - "consent": { - "type": "boolean" - }, - "email": { - "format": "email", - "type": "string" - }, - "fingerprint": { - "type": "string" - }, - "invite": { - "type": "string" - }, - "date_of_birth": { - "type": "string" - }, - "gift_code_sku_id": { + "before": { "type": "string" }, - "captcha_key": { + "after": { "type": "string" - }, - "promotional_email_opt_in": { - "type": "boolean" } }, "additionalProperties": false, "required": [ - "consent", - "username" + "after", + "before" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -14566,19 +15828,48 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "RelationshipPostSchema": { + "RegisterSchema": { "type": "object", "properties": { - "discriminator": { + "username": { + "minLength": 2, + "maxLength": 32, "type": "string" }, - "username": { + "password": { + "minLength": 1, + "maxLength": 72, + "type": "string" + }, + "consent": { + "type": "boolean" + }, + "email": { + "format": "email", + "type": "string" + }, + "fingerprint": { + "type": "string" + }, + "invite": { + "type": "string" + }, + "date_of_birth": { + "type": "string" + }, + "gift_code_sku_id": { "type": "string" + }, + "captcha_key": { + "type": "string" + }, + "promotional_email_opt_in": { + "type": "boolean" } }, "additionalProperties": false, "required": [ - "discriminator", + "consent", "username" ], "definitions": { @@ -15160,20 +16451,21 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "RelationshipPutSchema": { + "RelationshipPostSchema": { "type": "object", "properties": { - "type": { - "enum": [ - 1, - 2, - 3, - 4 - ], - "type": "number" + "discriminator": { + "type": "string" + }, + "username": { + "type": "string" } }, "additionalProperties": false, + "required": [ + "discriminator", + "username" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -15734,51 +17026,36 @@ } }, "additionalProperties": false - }, - "Partial<CreateWebAuthnCredentialSchema>": { - "type": "object", - "properties": { - "credential": { - "type": "string" - }, - "name": { - "type": "string" - }, - "ticket": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "RoleModifySchema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "permissions": { - "type": "string" - }, - "color": { - "type": "integer" - }, - "hoist": { - "type": "boolean" - }, - "mentionable": { - "type": "boolean" - }, - "position": { - "type": "integer" - }, - "icon": { - "type": "string" - }, - "unicode_emoji": { - "type": "string" + }, + "Partial<CreateWebAuthnCredentialSchema>": { + "type": "object", + "properties": { + "credential": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "RelationshipPutSchema": { + "type": "object", + "properties": { + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "number" } }, "additionalProperties": false, @@ -16361,24 +17638,35 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "RolePositionUpdateSchema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "position": { - "type": "integer" - } + "RoleModifySchema": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "additionalProperties": false, - "required": [ - "id", - "position" - ] + "permissions": { + "type": "string" + }, + "color": { + "type": "integer" + }, + "hoist": { + "type": "boolean" + }, + "mentionable": { + "type": "boolean" + }, + "position": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "unicode_emoji": { + "type": "string" + } }, + "additionalProperties": false, "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -16958,98 +18246,24 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "SelectProtocolSchema": { - "type": "object", - "properties": { - "protocol": { - "enum": [ - "udp", - "webrtc" - ], - "type": "string" - }, - "data": { - "anyOf": [ - { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "mode": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "address", - "mode", - "port" - ] - }, - { - "type": "string" - } - ] - }, - "sdp": { - "type": "string" - }, - "codecs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "enum": [ - "H264", - "VP8", - "VP9", - "opus" - ], - "type": "string" - }, - "type": { - "enum": [ - "audio", - "video" - ], - "type": "string" - }, - "priority": { - "type": "integer" - }, - "payload_type": { - "type": "integer" - }, - "rtx_payload_type": { - "type": [ - "null", - "integer" - ] - } - }, - "additionalProperties": false, - "required": [ - "name", - "payload_type", - "priority", - "type" - ] + "RolePositionUpdateSchema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "position": { + "type": "integer" } }, - "rtc_connection_id": { - "type": "string" - } + "additionalProperties": false, + "required": [ + "id", + "position" + ] }, - "additionalProperties": false, - "required": [ - "data", - "protocol" - ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -17620,28 +18834,106 @@ "name": { "type": "string" }, - "ticket": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "TemplateCreateSchema": { - "type": "object", - "properties": { - "name": { - "type": "string" + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "SelectProtocolSchema": { + "type": "object", + "properties": { + "protocol": { + "enum": [ + "udp", + "webrtc" + ], + "type": "string" + }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "mode": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "address", + "mode", + "port" + ] + }, + { + "type": "string" + } + ] + }, + "sdp": { + "type": "string" + }, + "codecs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "enum": [ + "H264", + "VP8", + "VP9", + "opus" + ], + "type": "string" + }, + "type": { + "enum": [ + "audio", + "video" + ], + "type": "string" + }, + "priority": { + "type": "integer" + }, + "payload_type": { + "type": "integer" + }, + "rtx_payload_type": { + "type": [ + "null", + "integer" + ] + } + }, + "additionalProperties": false, + "required": [ + "name", + "payload_type", + "priority", + "type" + ] + } }, - "description": { + "rtc_connection_id": { "type": "string" } }, "additionalProperties": false, "required": [ - "name" + "data", + "protocol" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -18222,7 +19514,7 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "TemplateModifySchema": { + "TemplateCreateSchema": { "type": "object", "properties": { "name": { @@ -18815,16 +20107,19 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "TotpDisableSchema": { + "TemplateModifySchema": { "type": "object", "properties": { - "code": { + "name": { + "type": "string" + }, + "description": { "type": "string" } }, "additionalProperties": false, "required": [ - "code" + "name" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -19405,22 +20700,16 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "TotpEnableSchema": { + "TotpDisableSchema": { "type": "object", "properties": { - "password": { - "type": "string" - }, "code": { "type": "string" - }, - "secret": { - "type": "string" } }, "additionalProperties": false, "required": [ - "password" + "code" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -20001,32 +21290,22 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "TotpSchema": { + "TotpEnableSchema": { "type": "object", "properties": { - "code": { + "password": { "type": "string" }, - "ticket": { + "code": { "type": "string" }, - "gift_code_sku_id": { - "type": [ - "null", - "string" - ] - }, - "login_source": { - "type": [ - "null", - "string" - ] + "secret": { + "type": "string" } }, "additionalProperties": false, "required": [ - "code", - "ticket" + "password" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -20607,16 +21886,32 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "UserDeleteSchema": { + "TotpSchema": { "type": "object", "properties": { - "user_id": { + "code": { + "type": "string" + }, + "ticket": { "type": "string" + }, + "gift_code_sku_id": { + "type": [ + "null", + "string" + ] + }, + "login_source": { + "type": [ + "null", + "string" + ] } }, "additionalProperties": false, "required": [ - "user_id" + "code", + "ticket" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -21197,66 +22492,17 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "UserGuildSettingsSchema": { + "UserDeleteSchema": { "type": "object", "properties": { - "channel_overrides": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Partial<ChannelOverride>" - } - }, - "version": { - "type": "integer" - }, - "guild_id": { - "type": [ - "null", - "string" - ] - }, - "flags": { - "type": "integer" - }, - "message_notifications": { - "type": "integer" - }, - "mobile_push": { - "type": "boolean" - }, - "mute_config": { - "anyOf": [ - { - "$ref": "#/definitions/MuteConfig" - }, - { - "type": "null" - } - ] - }, - "muted": { - "type": "boolean" - }, - "suppress_everyone": { - "type": "boolean" - }, - "suppress_roles": { - "type": "boolean" - }, - "mute_scheduled_events": { - "type": "boolean" - }, - "hide_muted_channels": { - "type": "boolean" - }, - "notify_highlights": { - "enum": [ - 0 - ], - "type": "number" + "user_id": { + "type": "string" } }, "additionalProperties": false, + "required": [ + "user_id" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -21836,49 +23082,63 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "UserModifySchema": { + "UserGuildSettingsSchema": { "type": "object", "properties": { - "username": { - "minLength": 1, - "maxLength": 100, - "type": "string" + "channel_overrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Partial<ChannelOverride>" + } }, - "avatar": { + "version": { + "type": "integer" + }, + "guild_id": { "type": [ "null", "string" ] }, - "bio": { - "maxLength": 1024, - "type": "string" + "flags": { + "type": "integer" }, - "accent_color": { + "message_notifications": { "type": "integer" }, - "banner": { - "type": [ - "null", - "string" + "mobile_push": { + "type": "boolean" + }, + "mute_config": { + "anyOf": [ + { + "$ref": "#/definitions/MuteConfig" + }, + { + "type": "null" + } ] }, - "password": { - "type": "string" + "muted": { + "type": "boolean" }, - "new_password": { - "type": "string" + "suppress_everyone": { + "type": "boolean" }, - "code": { - "type": "string" + "suppress_roles": { + "type": "boolean" }, - "email": { - "type": "string" + "mute_scheduled_events": { + "type": "boolean" }, - "discriminator": { - "minLength": 4, - "maxLength": 4, - "type": "string" + "hide_muted_channels": { + "type": "boolean" + }, + "notify_highlights": { + "enum": [ + 0 + ], + "type": "number" } }, "additionalProperties": false, @@ -22461,39 +23721,49 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "UserProfileModifySchema": { + "UserModifySchema": { "type": "object", "properties": { - "bio": { + "username": { + "minLength": 1, + "maxLength": 100, "type": "string" }, - "accent_color": { + "avatar": { "type": [ "null", - "integer" + "string" ] }, + "bio": { + "maxLength": 1024, + "type": "string" + }, + "accent_color": { + "type": "integer" + }, "banner": { "type": [ "null", "string" ] }, - "pronouns": { + "password": { "type": "string" }, - "theme_colors": { - "type": "array", - "items": [ - { - "type": "integer" - }, - { - "type": "integer" - } - ], - "minItems": 2, - "maxItems": 2 + "new_password": { + "type": "string" + }, + "code": { + "type": "string" + }, + "email": { + "type": "string" + }, + "discriminator": { + "minLength": 4, + "maxLength": 4, + "type": "string" } }, "additionalProperties": false, @@ -23076,128 +24346,39 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "UserSettingsSchema": { + "UserProfileModifySchema": { "type": "object", "properties": { - "afk_timeout": { - "type": "integer" - }, - "allow_accessibility_detection": { - "type": "boolean" - }, - "animate_emoji": { - "type": "boolean" + "bio": { + "type": "string" }, - "animate_stickers": { - "type": "integer" + "accent_color": { + "type": [ + "null", + "integer" + ] }, - "contact_sync_enabled": { - "type": "boolean" + "banner": { + "type": [ + "null", + "string" + ] }, - "convert_emoticons": { - "type": "boolean" + "pronouns": { + "type": "string" }, - "custom_status": { - "anyOf": [ + "theme_colors": { + "type": "array", + "items": [ { - "$ref": "#/definitions/CustomStatus" + "type": "integer" }, { - "type": "null" + "type": "integer" } - ] - }, - "default_guilds_restricted": { - "type": "boolean" - }, - "detect_platform_accounts": { - "type": "boolean" - }, - "developer_mode": { - "type": "boolean" - }, - "disable_games_tab": { - "type": "boolean" - }, - "enable_tts_command": { - "type": "boolean" - }, - "explicit_content_filter": { - "type": "integer" - }, - "friend_source_flags": { - "$ref": "#/definitions/FriendSourceFlags" - }, - "gateway_connected": { - "type": "boolean" - }, - "gif_auto_play": { - "type": "boolean" - }, - "guild_folders": { - "type": "array", - "items": { - "$ref": "#/definitions/GuildFolder" - } - }, - "guild_positions": { - "type": "array", - "items": { - "type": "string" - } - }, - "inline_attachment_media": { - "type": "boolean" - }, - "inline_embed_media": { - "type": "boolean" - }, - "locale": { - "type": "string" - }, - "message_display_compact": { - "type": "boolean" - }, - "native_phone_integration_enabled": { - "type": "boolean" - }, - "render_embeds": { - "type": "boolean" - }, - "render_reactions": { - "type": "boolean" - }, - "restricted_guilds": { - "type": "array", - "items": { - "type": "string" - } - }, - "show_current_game": { - "type": "boolean" - }, - "status": { - "enum": [ - "dnd", - "idle", - "invisible", - "offline", - "online" ], - "type": "string" - }, - "stream_notifications_enabled": { - "type": "boolean" - }, - "theme": { - "enum": [ - "dark", - "light" - ], - "type": "string" - }, - "timezone_offset": { - "type": "integer" + "minItems": 2, + "maxItems": 2 } }, "additionalProperties": false, @@ -23780,13 +24961,128 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "VanityUrlSchema": { + "UserSettingsSchema": { "type": "object", "properties": { - "code": { - "minLength": 1, - "maxLength": 20, + "afk_timeout": { + "type": "integer" + }, + "allow_accessibility_detection": { + "type": "boolean" + }, + "animate_emoji": { + "type": "boolean" + }, + "animate_stickers": { + "type": "integer" + }, + "contact_sync_enabled": { + "type": "boolean" + }, + "convert_emoticons": { + "type": "boolean" + }, + "custom_status": { + "anyOf": [ + { + "$ref": "#/definitions/CustomStatus" + }, + { + "type": "null" + } + ] + }, + "default_guilds_restricted": { + "type": "boolean" + }, + "detect_platform_accounts": { + "type": "boolean" + }, + "developer_mode": { + "type": "boolean" + }, + "disable_games_tab": { + "type": "boolean" + }, + "enable_tts_command": { + "type": "boolean" + }, + "explicit_content_filter": { + "type": "integer" + }, + "friend_source_flags": { + "$ref": "#/definitions/FriendSourceFlags" + }, + "gateway_connected": { + "type": "boolean" + }, + "gif_auto_play": { + "type": "boolean" + }, + "guild_folders": { + "type": "array", + "items": { + "$ref": "#/definitions/GuildFolder" + } + }, + "guild_positions": { + "type": "array", + "items": { + "type": "string" + } + }, + "inline_attachment_media": { + "type": "boolean" + }, + "inline_embed_media": { + "type": "boolean" + }, + "locale": { + "type": "string" + }, + "message_display_compact": { + "type": "boolean" + }, + "native_phone_integration_enabled": { + "type": "boolean" + }, + "render_embeds": { + "type": "boolean" + }, + "render_reactions": { + "type": "boolean" + }, + "restricted_guilds": { + "type": "array", + "items": { + "type": "string" + } + }, + "show_current_game": { + "type": "boolean" + }, + "status": { + "enum": [ + "dnd", + "idle", + "invisible", + "offline", + "online" + ], + "type": "string" + }, + "stream_notifications_enabled": { + "type": "boolean" + }, + "theme": { + "enum": [ + "dark", + "light" + ], "type": "string" + }, + "timezone_offset": { + "type": "integer" } }, "additionalProperties": false, @@ -24369,55 +25665,16 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "VoiceIdentifySchema": { + "VanityUrlSchema": { "type": "object", "properties": { - "server_id": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "session_id": { - "type": "string" - }, - "token": { + "code": { + "minLength": 1, + "maxLength": 20, "type": "string" - }, - "video": { - "type": "boolean" - }, - "streams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "rid": { - "type": "string" - }, - "quality": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "quality", - "rid", - "type" - ] - } } }, "additionalProperties": false, - "required": [ - "server_id", - "session_id", - "token", - "user_id" - ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ @@ -24997,39 +26254,54 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "VoiceStateUpdateSchema": { + "VoiceIdentifySchema": { "type": "object", "properties": { - "guild_id": { + "server_id": { "type": "string" }, - "channel_id": { + "user_id": { "type": "string" }, - "self_mute": { - "type": "boolean" - }, - "self_deaf": { - "type": "boolean" - }, - "self_video": { - "type": "boolean" - }, - "preferred_region": { + "session_id": { "type": "string" }, - "request_to_speak_timestamp": { - "type": "string", - "format": "date-time" + "token": { + "type": "string" }, - "suppress": { + "video": { "type": "boolean" + }, + "streams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "rid": { + "type": "string" + }, + "quality": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "quality", + "rid", + "type" + ] + } } }, "additionalProperties": false, "required": [ - "self_deaf", - "self_mute" + "server_id", + "session_id", + "token", + "user_id" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -25610,94 +26882,39 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "VoiceVideoSchema": { + "VoiceStateUpdateSchema": { "type": "object", "properties": { - "audio_ssrc": { - "type": "integer" + "guild_id": { + "type": "string" }, - "video_ssrc": { - "type": "integer" + "channel_id": { + "type": "string" }, - "rtx_ssrc": { - "type": "integer" + "self_mute": { + "type": "boolean" }, - "user_id": { + "self_deaf": { + "type": "boolean" + }, + "self_video": { + "type": "boolean" + }, + "preferred_region": { "type": "string" }, - "streams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "enum": [ - "audio", - "video" - ], - "type": "string" - }, - "rid": { - "type": "string" - }, - "ssrc": { - "type": "integer" - }, - "active": { - "type": "boolean" - }, - "quality": { - "type": "integer" - }, - "rtx_ssrc": { - "type": "integer" - }, - "max_bitrate": { - "type": "integer" - }, - "max_framerate": { - "type": "integer" - }, - "max_resolution": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "width": { - "type": "integer" - }, - "height": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "height", - "type", - "width" - ] - } - }, - "additionalProperties": false, - "required": [ - "active", - "max_bitrate", - "max_framerate", - "max_resolution", - "quality", - "rid", - "rtx_ssrc", - "ssrc", - "type" - ] - } + "request_to_speak_timestamp": { + "type": "string", + "format": "date-time" + }, + "suppress": { + "type": "boolean" } }, "additionalProperties": false, "required": [ - "audio_ssrc", - "video_ssrc" + "self_deaf", + "self_mute" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -26269,25 +27486,103 @@ "name": { "type": "string" }, - "ticket": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "GenerateWebAuthnCredentialsSchema": { - "type": "object", - "properties": { - "password": { - "type": "string" + "ticket": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "VoiceVideoSchema": { + "type": "object", + "properties": { + "audio_ssrc": { + "type": "integer" + }, + "video_ssrc": { + "type": "integer" + }, + "rtx_ssrc": { + "type": "integer" + }, + "user_id": { + "type": "string" + }, + "streams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "enum": [ + "audio", + "video" + ], + "type": "string" + }, + "rid": { + "type": "string" + }, + "ssrc": { + "type": "integer" + }, + "active": { + "type": "boolean" + }, + "quality": { + "type": "integer" + }, + "rtx_ssrc": { + "type": "integer" + }, + "max_bitrate": { + "type": "integer" + }, + "max_framerate": { + "type": "integer" + }, + "max_resolution": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "height", + "type", + "width" + ] + } + }, + "additionalProperties": false, + "required": [ + "active", + "max_bitrate", + "max_framerate", + "max_resolution", + "quality", + "rid", + "rtx_ssrc", + "ssrc", + "type" + ] + } } }, "additionalProperties": false, "required": [ - "password" + "audio_ssrc", + "video_ssrc" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -26868,24 +28163,16 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "CreateWebAuthnCredentialSchema": { + "GenerateWebAuthnCredentialsSchema": { "type": "object", "properties": { - "credential": { - "type": "string" - }, - "name": { - "type": "string" - }, - "ticket": { + "password": { "type": "string" } }, "additionalProperties": false, "required": [ - "credential", - "name", - "ticket" + "password" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -27466,14 +28753,24 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "WebAuthnPostSchema": { - "anyOf": [ - { - "$ref": "#/definitions/Partial<GenerateWebAuthnCredentialsSchema>" + "CreateWebAuthnCredentialSchema": { + "type": "object", + "properties": { + "credential": { + "type": "string" }, - { - "$ref": "#/definitions/Partial<CreateWebAuthnCredentialSchema>" + "name": { + "type": "string" + }, + "ticket": { + "type": "string" } + }, + "additionalProperties": false, + "required": [ + "credential", + "name", + "ticket" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -28054,20 +29351,14 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "WebAuthnTotpSchema": { - "type": "object", - "properties": { - "code": { - "type": "string" + "WebAuthnPostSchema": { + "anyOf": [ + { + "$ref": "#/definitions/Partial<GenerateWebAuthnCredentialsSchema>" }, - "ticket": { - "type": "string" + { + "$ref": "#/definitions/Partial<CreateWebAuthnCredentialSchema>" } - }, - "additionalProperties": false, - "required": [ - "code", - "ticket" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -28648,20 +29939,20 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "WebhookCreateSchema": { + "WebAuthnTotpSchema": { "type": "object", "properties": { - "name": { - "maxLength": 80, + "code": { "type": "string" }, - "avatar": { + "ticket": { "type": "string" } }, "additionalProperties": false, "required": [ - "name" + "code", + "ticket" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -29242,20 +30533,20 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "WidgetModifySchema": { + "WebhookCreateSchema": { "type": "object", "properties": { - "enabled": { - "type": "boolean" + "name": { + "maxLength": 80, + "type": "string" }, - "channel_id": { + "avatar": { "type": "string" } }, "additionalProperties": false, "required": [ - "channel_id", - "enabled" + "name" ], "definitions": { "ChannelPermissionOverwriteType": { @@ -29836,125 +31127,21 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "MessageEditSchema": { + "WidgetModifySchema": { "type": "object", "properties": { - "file": { - "type": "object", - "properties": { - "filename": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "filename" - ] - }, - "embed": { - "$ref": "#/definitions/Embed" - }, - "flags": { - "type": "string" - }, - "content": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "channel_id": { - "type": "string" - }, - "tts": { + "enabled": { "type": "boolean" }, - "embeds": { - "type": "array", - "items": { - "$ref": "#/definitions/Embed" - } - }, - "allowed_mentions": { - "type": "object", - "properties": { - "parse": { - "type": "array", - "items": { - "type": "string" - } - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "users": { - "type": "array", - "items": { - "type": "string" - } - }, - "replied_user": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "message_reference": { - "type": "object", - "properties": { - "message_id": { - "type": "string" - }, - "channel_id": { - "type": "string" - }, - "guild_id": { - "type": "string" - }, - "fail_if_not_exists": { - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ - "channel_id", - "message_id" - ] - }, - "payload_json": { + "channel_id": { "type": "string" - }, - "attachments": { - "description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion", - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "filename": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "filename", - "id" - ] - } - }, - "sticker_ids": { - "type": "array", - "items": { - "type": "string" - } } }, "additionalProperties": false, + "required": [ + "channel_id", + "enabled" + ], "definitions": { "ChannelPermissionOverwriteType": { "enum": [ 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"); -- cgit 1.4.1 From ed38d74b3e994f4bd7be5ac22fb167f4169c77a3 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 06:36:57 -0500 Subject: don't return token on register if verification required --- src/api/routes/auth/register.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src') 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) }); }, ); -- cgit 1.4.1 From 91e9d6004066fcb533ae95d2789a1c1b3533d0ac Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 06:52:01 -0500 Subject: first batch of requested changes --- src/api/routes/auth/reset.ts | 57 ++++++++++++++++++------------------- src/api/routes/auth/verify/index.ts | 54 +++++++++++++++++++++++++---------- src/util/util/Email.ts | 11 +++---- 3 files changed, 73 insertions(+), 49 deletions(-) (limited to 'src') 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; - 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 { jwtSecret } = Config.get().security; - const data = { - data: { - hash, - valid_tokens_since: new Date(), + let user; + try { + 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( } } - try { - const { jwtSecret } = Config.get().security; + const { jwtSecret } = Config.get().security; + let user; - const { user } = await checkToken(token, jwtSecret, true); + try { + 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)); + if (user.verified) return res.json(await getToken(user)); - await User.update({ id: user.id }, { verified: true }); + await User.update({ id: user.id }, { verified: true }); - return res.json(await getToken(user)); - } catch (error) { - throw new HTTPError((error as Error).toString(), 400); - } + 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", -- cgit 1.4.1 From 770217b4b20bbc249605bd67bc5b4056621ed1f9 Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 07:02:36 -0500 Subject: simplify replacer function --- src/util/util/Email.ts | 54 +++++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) (limited to 'src') 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; -- cgit 1.4.1 From d3b1fd202622ea10bd5cb89f589786e279f10f5e Mon Sep 17 00:00:00 2001 From: Puyodead1 <puyodead@protonmail.com> Date: Fri, 24 Feb 2023 07:10:56 -0500 Subject: move transporters to their own files --- src/util/util/Email.ts | 397 ----------------------------- src/util/util/email/index.ts | 269 +++++++++++++++++++ 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 +- 8 files changed, 421 insertions(+), 403 deletions(-) delete mode 100644 src/util/util/Email.ts create mode 100644 src/util/util/email/index.ts 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 (limited to 'src') diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts deleted file mode 100644 index 45919f9e..00000000 --- a/src/util/util/Email.ts +++ /dev/null @@ -1,397 +0,0 @@ -/* - 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/>. -*/ - -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"; - -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,}))$/; - -export function adjustEmail(email?: string): string | undefined { - if (!email) return email; - // body parser already checked if it is a valid email - const parts = <RegExpMatchArray>email.match(EMAIL_REGEX); - if (!parts || parts.length < 5) return undefined; - - return email; - // // TODO: The below code doesn't actually do anything. - // const domain = parts[5]; - // const user = parts[1]; - - // // TODO: check accounts with uncommon email domains - // if (domain === "gmail.com" || domain === "googlemail.com") { - // // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator - // const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com"; - // } - - // if (domain === "google.com") { - // // replace .dots and +alternatives -> Google Staff GMail Dot Trick - // const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com"; - // } - - // return email; -} - -const transporters: { - [key: string]: () => Promise<nodemailer.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)); - }, -}; - -export const Email: { - transporter: Transporter | null; - init: () => Promise<void>; - 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>; - 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 () { - const { provider } = Config.get().email; - if (!provider) return; - - const transporterFn = transporters[provider]; - 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 - */ - doReplacements: function ( - template, - user, - emailVerificationUrl?, - passwordResetUrl?, - ipInfo?: { - ip: string; - city: string; - region: string; - country_name: string; - }, - ) { - const { instanceName } = Config.get().general; - - 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; - }, - /** - * - * @param id user id - * @param email user email - */ - 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}/${type}#token=${token}`; - return link; - }, - /** - * 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 link = await this.generateLink("verify", 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, 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 = await fs.promises.readFile( - path.join( - ASSET_FOLDER_PATH, - "email_templates", - "password_reset_request.html", - ), - { encoding: "utf-8" }, - ); - - // replace email template placeholders - const html = this.doReplacements(rawTemplate, user, undefined, link); - - // extract the title from the email template to use as the email subject - const subject = html.match(/<title>(.*)<\/title>/)?.[1] || ""; - - // construct the email - const message = { - from: - Config.get().general.correspondenceEmail || "noreply@localhost", - to: email, - subject, - html, - }; - - // send the email - return this.transporter.sendMail(message); - }, - /** - * Sends an email to the user notifying them that their password has been changed - */ - sendPasswordChanged: async function (user, email) { - if (!this.transporter) return; - - // load the email template - const rawTemplate = await fs.promises.readFile( - path.join( - ASSET_FOLDER_PATH, - "email_templates", - "password_changed.html", - ), - { encoding: "utf-8" }, - ); - - // replace email template placeholders - const html = this.doReplacements(rawTemplate, user); - - // extract the title from the email template to use as the email subject - const subject = html.match(/<title>(.*)<\/title>/)?.[1] || ""; - - // construct the email - const message = { - from: - Config.get().general.correspondenceEmail || "noreply@localhost", - to: email, - subject, - html, - }; - - // send the email - return this.transporter.sendMail(message); - }, -}; diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts new file mode 100644 index 00000000..8c7a848c --- /dev/null +++ b/src/util/util/email/index.ts @@ -0,0 +1,269 @@ +/* + 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/>. +*/ + +import fs from "node:fs"; +import path from "node:path"; +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 = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +export function adjustEmail(email?: string): string | undefined { + if (!email) return email; + // body parser already checked if it is a valid email + const parts = <RegExpMatchArray>email.match(EMAIL_REGEX); + if (!parts || parts.length < 5) return undefined; + + return email; + // // TODO: The below code doesn't actually do anything. + // const domain = parts[5]; + // const user = parts[1]; + + // // TODO: check accounts with uncommon email domains + // if (domain === "gmail.com" || domain === "googlemail.com") { + // // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator + // const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com"; + // } + + // if (domain === "google.com") { + // // replace .dots and +alternatives -> Google Staff GMail Dot Trick + // const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com"; + // } + + // return email; +} + +const transporters: { + [key: string]: () => Promise<Transporter<unknown> | void>; +} = { + smtp: SMTP, + mailgun: MailGun, + mailjet: MailJet, + sendgrid: SendGrid, +}; + +export const Email: { + transporter: Transporter | null; + init: () => Promise<void>; + 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>; + 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 () { + const { provider } = Config.get().email; + if (!provider) return; + + const transporterFn = transporters[provider]; + 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 + */ + doReplacements: function ( + template, + user, + emailVerificationUrl?, + passwordResetUrl?, + ipInfo?: { + ip: string; + city: string; + region: string; + country_name: string; + }, + ) { + const { instanceName } = Config.get().general; + + 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; + }, + /** + * + * @param id user id + * @param email user email + */ + 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}/${type}#token=${token}`; + return link; + }, + /** + * 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 link = await this.generateLink("verify", 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, 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 = await fs.promises.readFile( + path.join( + ASSET_FOLDER_PATH, + "email_templates", + "password_reset_request.html", + ), + { encoding: "utf-8" }, + ); + + // replace email template placeholders + const html = this.doReplacements(rawTemplate, user, undefined, link); + + // extract the title from the email template to use as the email subject + const subject = html.match(/<title>(.*)<\/title>/)?.[1] || ""; + + // construct the email + const message = { + from: + Config.get().general.correspondenceEmail || "noreply@localhost", + to: email, + subject, + html, + }; + + // send the email + return this.transporter.sendMail(message); + }, + /** + * Sends an email to the user notifying them that their password has been changed + */ + sendPasswordChanged: async function (user, email) { + if (!this.transporter) return; + + // load the email template + const rawTemplate = await fs.promises.readFile( + path.join( + ASSET_FOLDER_PATH, + "email_templates", + "password_changed.html", + ), + { encoding: "utf-8" }, + ); + + // replace email template placeholders + const html = this.doReplacements(rawTemplate, user); + + // extract the title from the email template to use as the email subject + const subject = html.match(/<title>(.*)<\/title>/)?.[1] || ""; + + // construct the email + const message = { + from: + Config.get().general.correspondenceEmail || "noreply@localhost", + to: email, + subject, + html, + }; + + // send the email + return this.transporter.sendMail(message); + }, +}; 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"; -- cgit 1.4.1