send email verification

This commit is contained in:
Puyodead1 2023-01-17 11:12:25 -05:00 committed by Puyodead1
parent ed6c1cbd15
commit 256c7ed8fe
5 changed files with 694 additions and 3 deletions

View File

@ -32859,5 +32859,602 @@
} }
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$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#"
} }
} }

View File

@ -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;

View File

@ -31,7 +31,7 @@ import { ConnectedAccount } from "./ConnectedAccount";
import { Member } from "./Member"; import { Member } from "./Member";
import { UserSettings } from "./UserSettings"; import { UserSettings } from "./UserSettings";
import { Session } from "./Session"; 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 { Request } from "express";
import { SecurityKey } from "./SecurityKey"; import { SecurityKey } from "./SecurityKey";
@ -383,6 +383,30 @@ export class User extends BaseClass {
user.validate(); user.validate();
await Promise.all([user.save(), settings.save()]); 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 () => { setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) { if (Config.get().guild.autoJoin.enabled) {

View File

@ -0,0 +1,4 @@
export interface VerifyEmailSchema {
captcha_key: string | null;
token: string;
}

View File

@ -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 iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256"; const algorithm = "HS256";
return new Promise((res, rej) => { return new Promise((res, rej) => {
jwt.sign( jwt.sign(
{ id: id, iat }, { id: id, email: email, iat },
Config.get().security.jwtSecret, Config.get().security.jwtSecret,
{ {
algorithm, algorithm,