summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/schemas.json597
-rw-r--r--src/api/routes/auth/verify/index.ts45
-rw-r--r--src/util/entities/User.ts26
-rw-r--r--src/util/schemas/VerifyEmailSchema.ts4
-rw-r--r--src/util/util/Token.ts25
5 files changed, 694 insertions, 3 deletions
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<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#"
     }
 }
\ 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: <a href="${link}">Verify Email</a>`,
+			};
+
+			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,