diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 9380c25f3..1e398ce61 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -158,6 +158,7 @@ export async function sendVerificationEmail( ); }) ).emailVerified; + if (isVerified) { throw new MonkeyError(400, "Email already verified"); } @@ -240,6 +241,13 @@ export async function sendVerificationEmail( return new MonkeyResponse("Email sent", null); } +export async function verifyEmail(req: MonkeyRequest): Promise { + const { uid, email, emailVerified } = req.ctx.decodedToken; + await UserDAL.updateEmail(uid, email, emailVerified); + + return new MonkeyResponse("emailVerify updated.", null); +} + export async function sendForgotPasswordEmail( req: MonkeyRequest ): Promise { @@ -605,7 +613,7 @@ export async function getUser(req: MonkeyRequest): Promise { ); delete relevantUserInfo.customThemes; - //update users emailVerified status if it changed + // soft-migrate user.emailVerified for existing users, update status if it has changed const { email, emailVerified } = req.ctx.decodedToken; if (emailVerified !== undefined && emailVerified !== userInfo.emailVerified) { void addImportantLog( diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index 8e34831a8..e78fec04b 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -120,6 +120,9 @@ export default s.router(usersContract, { handler: async (r) => callController(UserController.sendVerificationEmail)(r), }, + verifyEmail: { + handler: async (r) => callController(UserController.verifyEmail)(r), + }, forgotPasswordEmail: { handler: async (r) => callController(UserController.sendForgotPasswordEmail)(r), diff --git a/frontend/src/email-handler.html b/frontend/src/email-handler.html index 78a4529da..2dee2a9d3 100644 --- a/frontend/src/email-handler.html +++ b/frontend/src/email-handler.html @@ -176,6 +176,8 @@ signInWithEmailAndPassword, } from "firebase/auth"; + import { envConfig } from "./ts/constants/env-config"; + function isPasswordStrong(password) { const hasCapital = !!password.match(/[A-Z]/); const hasNumber = !!password.match(/[\d]/); @@ -189,8 +191,21 @@ function handleVerifyEmail(actionCode, continueUrl) { applyActionCode(Auth, actionCode) - .then((resp) => { + .then(async (resp) => { // Email address has been verified. + const token = + Auth.currentUser !== undefined + ? await Auth.currentUser.getIdToken(true) + : undefined; + const url = envConfig.backendUrl + "/users/verifyEmail"; + + await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); $("main .preloader .icon").html(``); $("main .preloader .text").text( diff --git a/packages/contracts/src/rate-limit/index.ts b/packages/contracts/src/rate-limit/index.ts index 205e3115c..385284575 100644 --- a/packages/contracts/src/rate-limit/index.ts +++ b/packages/contracts/src/rate-limit/index.ts @@ -301,6 +301,11 @@ export const limits = { max: 1, }, + userVerifyEmail: { + window: 15 * 60 * 1000, //15 minutes + max: 1, + }, + userForgotPasswordEmail: { window: "minute", max: 1, diff --git a/packages/contracts/src/users.ts b/packages/contracts/src/users.ts index 5ec48b847..56ed2db0e 100644 --- a/packages/contracts/src/users.ts +++ b/packages/contracts/src/users.ts @@ -872,6 +872,19 @@ export const usersContract = c.router( rateLimit: "userRequestVerificationEmail", }), }, + verifyEmail: { + summary: "verify email", + description: "Verify the user email", + method: "GET", + path: "/verifyEmail", + responses: { + 200: MonkeyResponseSchema, + }, + metadata: meta({ + authenticationOptions: { noCache: true }, + rateLimit: "userVerifyEmail", + }), + }, forgotPasswordEmail: { summary: "send forgot password email", description: "Send a forgot password email",