From ed6c1cbd1521d750bd9ac6823851057d00987332 Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Tue, 17 Jan 2023 09:36:24 -0500
Subject: [PATCH 01/31] Start implementing smtp
---
package.json | 2 ++
src/api/Server.ts | 20 +++++++-------
src/util/config/Config.ts | 2 ++
src/util/config/types/SMTPConfiguration.ts | 7 +++++
src/util/config/types/index.ts | 3 ++-
src/util/util/Email.ts | 31 ++++++++++++++++++++++
6 files changed, 55 insertions(+), 10 deletions(-)
create mode 100644 src/util/config/types/SMTPConfiguration.ts
diff --git a/package.json b/package.json
index 4aef413a..eabc247e 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"@types/node": "^18.7.20",
"@types/node-fetch": "^2.6.2",
"@types/node-os-utils": "^1.3.0",
+ "@types/nodemailer": "^6.4.7",
"@types/probe-image-size": "^7.2.0",
"@types/sharp": "^0.31.0",
"@types/ws": "^8.5.3",
@@ -95,6 +96,7 @@
"node-2fa": "^2.0.3",
"node-fetch": "^2.6.7",
"node-os-utils": "^1.3.7",
+ "nodemailer": "^6.9.0",
"picocolors": "^1.0.0",
"probe-image-size": "^7.2.3",
"proxy-agent": "^5.0.0",
diff --git a/src/api/Server.ts b/src/api/Server.ts
index 7eb4e6f1..aec47818 100644
--- a/src/api/Server.ts
+++ b/src/api/Server.ts
@@ -16,28 +16,29 @@
along with this program. If not, see .
*/
-import "missing-native-js-functions";
-import { Server, ServerOptions } from "lambert-server";
-import { Authentication, CORS } from "./middlewares/";
import {
Config,
+ Email,
initDatabase,
initEvent,
JSONReplacer,
+ registerRoutes,
Sentry,
WebAuthn,
} from "@fosscord/util";
-import { ErrorHandler } from "./middlewares/ErrorHandler";
-import { BodyParser } from "./middlewares/BodyParser";
-import { Router, Request, Response } from "express";
+import { Request, Response, Router } from "express";
+import { Server, ServerOptions } from "lambert-server";
+import "missing-native-js-functions";
+import morgan from "morgan";
import path from "path";
+import { red } from "picocolors";
+import { Authentication, CORS } from "./middlewares/";
+import { BodyParser } from "./middlewares/BodyParser";
+import { ErrorHandler } from "./middlewares/ErrorHandler";
import { initRateLimits } from "./middlewares/RateLimit";
import TestClient from "./middlewares/TestClient";
import { initTranslation } from "./middlewares/Translation";
-import morgan from "morgan";
import { initInstance } from "./util/handlers/Instance";
-import { registerRoutes } from "@fosscord/util";
-import { red } from "picocolors";
export type FosscordServerOptions = ServerOptions;
@@ -63,6 +64,7 @@ export class FosscordServer extends Server {
await initDatabase();
await Config.init();
await initEvent();
+ await Email.init();
await initInstance();
await Sentry.init(this.app);
WebAuthn.init();
diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts
index 122dadb5..583c1489 100644
--- a/src/util/config/Config.ts
+++ b/src/util/config/Config.ts
@@ -35,6 +35,7 @@ import {
RegisterConfiguration,
SecurityConfiguration,
SentryConfiguration,
+ SMTPConfiguration,
TemplateConfiguration,
} from "../config";
@@ -58,4 +59,5 @@ export class ConfigValue {
sentry: SentryConfiguration = new SentryConfiguration();
defaults: DefaultsConfiguration = new DefaultsConfiguration();
external: ExternalTokensConfiguration = new ExternalTokensConfiguration();
+ smtp: SMTPConfiguration = new SMTPConfiguration();
}
diff --git a/src/util/config/types/SMTPConfiguration.ts b/src/util/config/types/SMTPConfiguration.ts
new file mode 100644
index 00000000..e833376a
--- /dev/null
+++ b/src/util/config/types/SMTPConfiguration.ts
@@ -0,0 +1,7 @@
+export class SMTPConfiguration {
+ host: string | null = null;
+ port: number | null = null;
+ secure: boolean | null = null;
+ username: string | null = null;
+ password: string | null = null;
+}
diff --git a/src/util/config/types/index.ts b/src/util/config/types/index.ts
index 523ad186..3d8ed6df 100644
--- a/src/util/config/types/index.ts
+++ b/src/util/config/types/index.ts
@@ -34,5 +34,6 @@ export * from "./RegionConfiguration";
export * from "./RegisterConfiguration";
export * from "./SecurityConfiguration";
export * from "./SentryConfiguration";
-export * from "./TemplateConfiguration";
+export * from "./SMTPConfiguration";
export * from "./subconfigurations";
+export * from "./TemplateConfiguration";
diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts
index 48d8cae1..d45eb9a1 100644
--- a/src/util/util/Email.ts
+++ b/src/util/util/Email.ts
@@ -43,3 +43,34 @@ export function adjustEmail(email?: string): string | undefined {
// return email;
}
+
+export const Email: {
+ transporter: Transporter | null;
+ init: () => Promise;
+} = {
+ transporter: null,
+ init: async function () {
+ const { host, port, secure, username, password } = Config.get().smtp;
+ if (!host || !port || !secure || !username || !password) return;
+ console.log(`[SMTP] connect: ${host}`);
+ this.transporter = nodemailer.createTransport({
+ host,
+ port,
+ secure,
+ auth: {
+ user: username,
+ pass: password,
+ },
+ });
+
+ await this.transporter.verify((error, _) => {
+ if (error) {
+ console.error(`[SMTP] error: ${error}`);
+ this.transporter?.close();
+ this.transporter = null;
+ return;
+ }
+ console.log(`[SMTP] Ready`);
+ });
+ },
+};
From 256c7ed8fefac586590addf4aacae7ffdda0d577 Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Tue, 17 Jan 2023 11:12:25 -0500
Subject: [PATCH 02/31] send email verification
---
assets/schemas.json | 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
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,
From 2cddd7a8de444ee3a618ce6a08f6ae101183a167 Mon Sep 17 00:00:00 2001
From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com>
Date: Thu, 19 Jan 2023 18:49:47 +1100
Subject: [PATCH 03/31] Send different app for /verify
---
assets/client_test/verify.html | 58 ++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 assets/client_test/verify.html
diff --git a/assets/client_test/verify.html b/assets/client_test/verify.html
new file mode 100644
index 00000000..08654323
--- /dev/null
+++ b/assets/client_test/verify.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+ Fosscord Test Client Developer Portal
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From cc6bf066b143841d1745e972385c8c77fb7a12e4 Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Thu, 19 Jan 2023 09:58:49 -0500
Subject: [PATCH 04/31] add missing copyright headers
---
src/api/routes/auth/verify/index.ts | 18 ++++++++++++++++++
src/util/config/types/SMTPConfiguration.ts | 18 ++++++++++++++++++
src/util/schemas/VerifyEmailSchema.ts | 18 ++++++++++++++++++
3 files changed, 54 insertions(+)
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index eae938eb..4c076d09 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -1,3 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
import { route, verifyCaptcha } from "@fosscord/api";
import { Config, FieldErrors, verifyToken } from "@fosscord/util";
import { Request, Response, Router } from "express";
diff --git a/src/util/config/types/SMTPConfiguration.ts b/src/util/config/types/SMTPConfiguration.ts
index e833376a..11eb9e14 100644
--- a/src/util/config/types/SMTPConfiguration.ts
+++ b/src/util/config/types/SMTPConfiguration.ts
@@ -1,3 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
export class SMTPConfiguration {
host: string | null = null;
port: number | null = null;
diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts
index ad170e84..fa6a4c0d 100644
--- a/src/util/schemas/VerifyEmailSchema.ts
+++ b/src/util/schemas/VerifyEmailSchema.ts
@@ -1,3 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
export interface VerifyEmailSchema {
captcha_key: string | null;
token: string;
From a47d80b255f1501e39bebd7ad7e80119c8ed1697 Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Thu, 19 Jan 2023 11:15:12 -0500
Subject: [PATCH 05/31] Email verification works
- Added /auth/verify to authenticated route whitelist
- Updated /auth/verify to properly mark a user as verified, return a response, and fix expiration time check
- Implemented /auth/verify/resend
- Moved verification email sending to a helper method
- Fixed VerifyEmailSchema requiring captcha_key
---
src/api/middlewares/Authentication.ts | 5 +--
src/api/routes/auth/verify/index.ts | 23 +++++++++++--
src/api/routes/auth/verify/resend.ts | 49 +++++++++++++++++++++++++++
src/util/entities/User.ts | 23 ++++---------
src/util/schemas/VerifyEmailSchema.ts | 2 +-
src/util/util/Email.ts | 26 ++++++++++++++
src/util/util/Token.ts | 24 +++++++++++++
7 files changed, 129 insertions(+), 23 deletions(-)
create mode 100644 src/api/routes/auth/verify/resend.ts
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index ea0aa312..f4c33963 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -16,10 +16,10 @@
along with this program. If not, see .
*/
-import { NextFunction, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
import { checkToken, Config, Rights } from "@fosscord/util";
import * as Sentry from "@sentry/node";
+import { NextFunction, Request, Response } from "express";
+import { HTTPError } from "lambert-server";
export const NO_AUTHORIZATION_ROUTES = [
// Authentication routes
@@ -28,6 +28,7 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/location-metadata",
"/auth/mfa/totp",
"/auth/mfa/webauthn",
+ "/auth/verify",
// Routes with a seperate auth system
"/webhooks/",
// Public information endpoints
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index 4c076d09..d61b8d16 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -17,7 +17,11 @@
*/
import { route, verifyCaptcha } from "@fosscord/api";
-import { Config, FieldErrors, verifyToken } from "@fosscord/util";
+import {
+ Config,
+ FieldErrors,
+ verifyTokenEmailVerification,
+} from "@fosscord/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
@@ -43,9 +47,13 @@ router.post(
try {
const { jwtSecret } = Config.get().security;
- const { decoded, user } = await verifyToken(token, jwtSecret);
+ const { decoded, user } = await verifyTokenEmailVerification(
+ token,
+ jwtSecret,
+ );
+
// toksn should last for 24 hours from the time they were issued
- if (decoded.exp < Date.now() / 1000) {
+ if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) {
throw FieldErrors({
token: {
code: "TOKEN_INVALID",
@@ -53,7 +61,16 @@ router.post(
},
});
}
+
+ if (user.verified) return res.send(user);
+
+ // verify email
user.verified = true;
+ await user.save();
+
+ // TODO: invalidate token after use?
+
+ return res.send(user);
} catch (error: any) {
throw new HTTPError(error?.toString(), 400);
}
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
new file mode 100644
index 00000000..0c8c4ed9
--- /dev/null
+++ b/src/api/routes/auth/verify/resend.ts
@@ -0,0 +1,49 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { route } from "@fosscord/api";
+import { Email, User } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+ const user = await User.findOneOrFail({
+ where: { id: req.user_id },
+ select: ["email"],
+ });
+
+ if (!user.email) {
+ // TODO: whats the proper error response for this?
+ throw new HTTPError("User does not have an email address", 400);
+ }
+
+ await Email.sendVerificationEmail(req.user_id, user.email)
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ return res.sendStatus(204);
+ })
+ .catch((e) => {
+ console.error(
+ `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
+ );
+ throw new HTTPError("Failed to send verification email", 500);
+ });
+});
+
+export default router;
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index f39fc19b..66e10297 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -383,28 +383,17 @@ export class User extends BaseClass {
user.validate();
await Promise.all([user.save(), settings.save()]);
- // send verification email
- if (Email.transporter && email) {
- const token = (await generateToken(user.id, email)) as string;
- const link = `http://localhost:3001/verify#token=${token}`;
- const message = {
- from:
- Config.get().general.correspondenceEmail ||
- "noreply@localhost",
- to: email,
- subject: `Verify Email Address for ${
- Config.get().general.instanceName
- }`,
- html: `Please verify your email address by clicking the following link: Verify Email`,
- };
- await Email.transporter
- .sendMail(message)
+ // send verification email if users aren't verified by default and we have an email
+ if (!Config.get().defaults.user.verified && email) {
+ await Email.sendVerificationEmail(user.id, email)
.then((info) => {
console.log("Message sent: %s", info.messageId);
})
.catch((e) => {
- console.error(`Failed to send email to ${email}: ${e}`);
+ console.error(
+ `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
+ );
});
}
diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts
index fa6a4c0d..d94fbbc1 100644
--- a/src/util/schemas/VerifyEmailSchema.ts
+++ b/src/util/schemas/VerifyEmailSchema.ts
@@ -17,6 +17,6 @@
*/
export interface VerifyEmailSchema {
- captcha_key: string | null;
+ captcha_key?: string | null;
token: string;
}
diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts
index d45eb9a1..371ba827 100644
--- a/src/util/util/Email.ts
+++ b/src/util/util/Email.ts
@@ -16,6 +16,10 @@
along with this program. If not, see .
*/
+import nodemailer, { Transporter } from "nodemailer";
+import { Config } from "./Config";
+import { generateToken } from "./Token";
+
export const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -47,6 +51,7 @@ export function adjustEmail(email?: string): string | undefined {
export const Email: {
transporter: Transporter | null;
init: () => Promise;
+ sendVerificationEmail: (id: string, email: string) => Promise;
} = {
transporter: null,
init: async function () {
@@ -73,4 +78,25 @@ export const Email: {
console.log(`[SMTP] Ready`);
});
},
+ sendVerificationEmail: async function (
+ id: string,
+ email: string,
+ ): Promise {
+ if (!this.transporter) return;
+ const token = (await generateToken(id, email)) as string;
+ const instanceUrl =
+ Config.get().general.frontPage || "http://localhost:3001";
+ const link = `${instanceUrl}/verify#token=${token}`;
+ const message = {
+ from:
+ Config.get().general.correspondenceEmail || "noreply@localhost",
+ to: email,
+ subject: `Verify Email Address for ${
+ Config.get().general.instanceName
+ }`,
+ html: `Please verify your email address by clicking the following link: Verify Email`,
+ };
+
+ return this.transporter.sendMail(message);
+ },
};
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index b3ebcc07..e4b1fe41 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -72,6 +72,30 @@ export function checkToken(
});
}
+/**
+ * Puyodead1 (1/19/2023): I made a copy of this function because I didn't want to break anything with the other one.
+ * this version of the function doesn't use select, so we can update the user. with select causes constraint errors.
+ */
+export function verifyTokenEmailVerification(
+ token: string,
+ jwtSecret: string,
+): Promise<{ decoded: any; user: User }> {
+ return new Promise((res, rej) => {
+ jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => {
+ if (err || !decoded) return rej("Invalid Token");
+
+ const user = await User.findOne({
+ where: { id: decoded.id },
+ });
+ if (!user) return rej("Invalid Token");
+ if (user.disabled) return rej("User disabled");
+ if (user.deleted) return rej("User not found");
+
+ return res({ decoded, user });
+ });
+ });
+}
+
export function verifyToken(
token: string,
jwtSecret: string,
From 1f388b17a5c6c0b19b11c9ac06fd223f74e6c2be Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Thu, 19 Jan 2023 11:17:36 -0500
Subject: [PATCH 06/31] change verify.html title
---
assets/client_test/verify.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/client_test/verify.html b/assets/client_test/verify.html
index 08654323..0de6fc6b 100644
--- a/assets/client_test/verify.html
+++ b/assets/client_test/verify.html
@@ -7,7 +7,7 @@
- Fosscord Test Client Developer Portal
+ Fosscord Test Client
From 88d7b89aeb92cfdee63f5d3ef1d235c729fd0a6f Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Thu, 19 Jan 2023 12:04:01 -0500
Subject: [PATCH 07/31] Update package-lock.json
---
package-lock.json | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index ecd455b8..8d7b1db2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
"node-2fa": "^2.0.3",
"node-fetch": "^2.6.7",
"node-os-utils": "^1.3.7",
+ "nodemailer": "^6.9.0",
"picocolors": "^1.0.0",
"probe-image-size": "^7.2.3",
"proxy-agent": "^5.0.0",
@@ -65,6 +66,7 @@
"@types/node": "^18.7.20",
"@types/node-fetch": "^2.6.2",
"@types/node-os-utils": "^1.3.0",
+ "@types/nodemailer": "^6.4.7",
"@types/probe-image-size": "^7.2.0",
"@types/sharp": "^0.31.0",
"@types/ws": "^8.5.3",
@@ -2068,6 +2070,15 @@
"integrity": "sha512-XwVteWQx/XkfRPyaGkw8dEbrCAkoRZ73pI3XznUYIpzbCfpQB3UnDlR5TnmdhetlT889tUJGF8QWo9xrgTpsiA==",
"dev": true
},
+ "node_modules/@types/nodemailer": {
+ "version": "6.4.7",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz",
+ "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/notp": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/notp/-/notp-2.0.2.tgz",
@@ -5822,6 +5833,14 @@
"resolved": "https://registry.npmjs.org/node-os-utils/-/node-os-utils-1.3.7.tgz",
"integrity": "sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q=="
},
+ "node_modules/nodemailer": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
+ "integrity": "sha512-jFaCEGTeT3E/m/5R2MHWiyQH3pSARECRUDM+1hokOYc3lQAAG7ASuy+2jIsYVf+RVa9zePopSQwKNVFH8DKUpA==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
From 0df1ea22cbed1a111925a4d4a1d244ea0837fac7 Mon Sep 17 00:00:00 2001
From: Puyodead1
Date: Thu, 19 Jan 2023 22:33:54 -0500
Subject: [PATCH 08/31] Add an email template for email verification
---
assets/email_templates/verify_email.html | 89 ++++++++++++++++++++++++
1 file changed, 89 insertions(+)
create mode 100644 assets/email_templates/verify_email.html
diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html
new file mode 100644
index 00000000..55825cfc
--- /dev/null
+++ b/assets/email_templates/verify_email.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ Verify Email Address for {instanceName}
+
+
+
+
+
+
+
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:
+
+ 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.
+
- 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.
-
+ 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.
+