From c97611ade479683f4406c66ae43fe7fc6cdfebdf Mon Sep 17 00:00:00 2001 From: zkldi Date: Thu, 14 Oct 2021 04:10:07 +0100 Subject: [PATCH] Add email verification endpoints and general email verification. --- server/src/external/mongo/db.ts | 3 + server/src/lib/email/formats.ts | 6 ++ .../src/server/router/api/v1/auth/router.ts | 85 ++++++++++++++++++- .../router/api/v1/users/_userID/router.ts | 28 ++++++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 5f944c966..9621bbe3d 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -184,6 +184,8 @@ const db = { "class-achievements": monkDB.get("class-achievements"), "score-blacklist": monkDB.get<{ scoreID: string; userID: integer; score: ScoreDocument }>("score-blacklist"), + "verify-email-codes": + monkDB.get<{ userID: integer; code: string; email: string }>("verify-email-codes"), }; export type StaticDatabases = @@ -218,6 +220,7 @@ export type StaticDatabases = | "user-settings" | "counters" | "score-blacklist" + | "verify-email-codes" | "class-achievements"; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; diff --git a/server/src/lib/email/formats.ts b/server/src/lib/email/formats.ts index ecd5488c6..968ed657f 100644 --- a/server/src/lib/email/formats.ts +++ b/server/src/lib/email/formats.ts @@ -9,6 +9,12 @@ export function EmailFormatResetPassword(username: string, resetCode: string, ip ); } +export function EmailFormatVerifyEmail(username: string, code: string) { + return MainHTMLWrapper( + `Hey ${username}, You need to verify your email before you can use the site.
Click here to verify your email.` + ); +} + export function MainHTMLWrapper(innerHTML: string) { return ` diff --git a/server/src/server/router/api/v1/auth/router.ts b/server/src/server/router/api/v1/auth/router.ts index ac7134507..aee7a549b 100644 --- a/server/src/server/router/api/v1/auth/router.ts +++ b/server/src/server/router/api/v1/auth/router.ts @@ -23,7 +23,7 @@ import CreateLogCtx from "lib/logger/logger"; import prValidate from "server/middleware/prudence-validate"; import { DecrementCounterValue, GetNextCounterValue } from "utils/db"; import { SendEmail } from "lib/email/client"; -import { EmailFormatResetPassword } from "lib/email/formats"; +import { EmailFormatResetPassword, EmailFormatVerifyEmail } from "lib/email/formats"; import { Random20Hex } from "utils/misc"; import { ServerConfig } from "lib/setup/config"; @@ -260,6 +260,16 @@ router.post( MountAuthCookie(req, user!, newSettings); + const resetEmailCode = Random20Hex(); + + await db["verify-email-codes"].insert({ + code: resetEmailCode, + userID: userID, + email: req.body.email, + }); + + await SendEmail(req.body.email, EmailFormatVerifyEmail(user!.username, resetEmailCode)); + return res.status(200).json({ success: true, description: `Successfully created account ${req.body.username}!`, @@ -282,6 +292,79 @@ router.post( } ); +/** + * Verifies the provided email according to the code provided. + * + * @param code - The emailCode set in the /register function. + * + * @name POST /api/v1/auth/verify-email + */ +router.post( + "/verify-email", + prValidate({ + code: "string", + }), + async (req, res) => { + const code = await db["verify-email-codes"].findOne({ + code: req.body.code, + }); + + if (!code) { + return res.status(400).json({ + success: false, + description: `This email code is invalid.`, + }); + } + + await db["verify-email-codes"].remove({ + code: req.body.code, + }); + + return res.status(200).json({ + success: true, + description: `Verified email!`, + body: {}, + }); + } +); + +/** + * Resend a verification email, for when they fall through the + * cracks. + * + * @param email - The email to send a verification email to. + * + * @name POST /api/v1/auth/resend-verify-email + */ +router.post("/resend-verify-email", prValidate({ email: "string" }), async (req, res) => { + // Immediately send a response so the existence of emails + // cannot be timing attacked out. + res.status(200).json({ + success: true, + description: `Sent an email if the email address has not been verified.`, + body: {}, + }); + + const verifyInfo = await db["verify-email-codes"].findOne({ email: req.body.email }); + + if (!verifyInfo) { + logger.warn( + `Attempted to send reset email to ${req.body.email}, but no verifyInfo was set for them.` + ); + return; + } + + const user = await GetUserWithID(verifyInfo.userID); + + if (!user) { + logger.severe(`Email verifyInfo belongs to user that no longer exists?`, verifyInfo); + return; + } + + // Send the email again. + await SendEmail(req.body.email, EmailFormatVerifyEmail(user!.username, verifyInfo.code)); +}); + /** * Logs out the requesting user. * @name POST /api/v1/auth/logout diff --git a/server/src/server/router/api/v1/users/_userID/router.ts b/server/src/server/router/api/v1/users/_userID/router.ts index 317e63150..7fec93ce7 100644 --- a/server/src/server/router/api/v1/users/_userID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/router.ts @@ -203,6 +203,34 @@ router.get("/game-stats", async (req, res) => { }); }); +/** + * Returns whether the user has verified their email or not. + * Requires self-key level permissions. + * + * @name GET /api/v1/users/:userID/is-email-verified + */ +router.get("/is-email-verified", RequireSelfRequestFromUser, async (req, res) => { + const user = req[SYMBOL_TachiData]!.requestedUser!; + + const verifyInfo = await db["verify-email-codes"].findOne({ + userID: user.id, + }); + + if (verifyInfo) { + return res.status(200).json({ + success: true, + description: `User has not verified email.`, + body: false, + }); + } + + return res.status(200).json({ + success: true, + description: `User has verified email.`, + body: true, + }); +}); + router.use("/games/:game/:playtype", gamePTRouter); router.use("/pfp", pfpRouter); router.use("/banner", bannerRouter);