diff options
author | Puyodead1 <puyodead@proton.me> | 2023-01-17 11:12:25 -0500 |
---|---|---|
committer | Puyodead1 <puyodead@protonmail.com> | 2023-02-23 21:35:49 -0500 |
commit | 256c7ed8fefac586590addf4aacae7ffdda0d577 (patch) | |
tree | 93e6229b6f5d2128daaa8143f9b3c232d2fec5be | |
parent | Start implementing smtp (diff) | |
download | server-256c7ed8fefac586590addf4aacae7ffdda0d577.tar.xz |
send email verification
-rw-r--r-- | assets/schemas.json | 597 | ||||
-rw-r--r-- | src/api/routes/auth/verify/index.ts | 45 | ||||
-rw-r--r-- | src/util/entities/User.ts | 26 | ||||
-rw-r--r-- | src/util/schemas/VerifyEmailSchema.ts | 4 | ||||
-rw-r--r-- | src/util/util/Token.ts | 25 |
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, |