From 351d14c2fcdacd7669ad9a8869428f4709b6e740 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 11 Feb 2024 23:38:22 +0100 Subject: [PATCH] change email --- src/controllers/userController.ts | 167 ++++++++++++++++++++++++--- src/models/index.ts | 2 + src/models/userPendingEmailChange.ts | 44 +++++++ src/routes/userRoutes.ts | 7 +- src/utils/constants.ts | 1 + src/utils/utils.ts | 4 +- src/validator/validator.ts | 52 ++++++--- 7 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 src/models/userPendingEmailChange.ts diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 8f8db77..4cbbaa0 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -40,6 +40,7 @@ import fs from "fs-extra"; import rabbitmq from "../rabbitmq/rabbitmq"; import verifyCaptcha from "../utils/recaptcha"; import EmailVerification from "../models/emailVerification"; +import UserPendingEmailChange from "../models/userPendingEmailChange"; export async function SignUp(req: Request, res: Response) { try { @@ -63,7 +64,7 @@ export async function SignUp(req: Request, res: Response) { !language || !isCompanyNameValid(companyName) || !isUsernameValid(username) || - !isEmailValid(email) || + !(await isEmailValid(email)) || !isLanguageCodeValid(language) || rememberMe === undefined || !recaptcha @@ -135,10 +136,7 @@ export async function SignUp(req: Request, res: Response) { }); rabbitmq.sendEmail(email, "dashboardSignUpEmailVerification", language, { - emailVerificationUrl: getEmailVerificationUrl( - String(state), - emailVerificationId - ), + emailVerificationUrl: getEmailVerificationUrl(state, emailVerificationId), }); // create user @@ -175,7 +173,7 @@ export async function Login(req: Request, res: Response) { email = email.toLowerCase(); - if (!isEmailValid(email)) { + if (!(await isEmailValid(email, false))) { return res.status(400).send({ err: "invalid request" }); } @@ -423,7 +421,7 @@ export async function IsEmailAvailable(req: Request, res: Response) { email = email.toLowerCase(); - if (!isEmailValid(email)) { + if (!(await isEmailValid(email))) { return res.status(400).send({ err: "invalid request" }); } @@ -470,9 +468,9 @@ export async function GetUserProfileSettings(req: Request, res: Response) { export async function UpdateUserProfileSettings(req: Request, res: Response) { try { - const { language, analyticsEnabled, username, email } = req.body; + const { language, analyticsEnabled, username } = req.body; - if (!language && analyticsEnabled === undefined && !username && !email) { + if (!language && analyticsEnabled === undefined && !username) { return res.status(400).send({ err: "invalid request" }); } @@ -508,14 +506,6 @@ export async function UpdateUserProfileSettings(req: Request, res: Response) { user.username = username; } - if (email) { - if (!isEmailValid(email)) { - return res.status(400).send({ err: "invalid request" }); - } - - user.email = email; - } - await user.save(); res.status(200).send({ msg: "user profile settings updated" }); @@ -740,7 +730,7 @@ export async function ExportUserAccount(req: Request, res: Response) { if ( !email || !password || - !isEmailValid(email) || + !(await isEmailValid(email)) || !isPasswordValid(password) ) { return res.status(400).send({ err: "invalid request" }); @@ -882,6 +872,85 @@ export async function VerifyEmail(req: Request, res: Response) { res.status(200).send({ msg: "email verified" }); return; + } else if ( + emailVerification.state === + EMAIL_VERIFICATION_STATE.PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION + ) { + const { password } = req.body; + + if (!password) { + return res.status(200).send({ status: "actionRequired" }); + } + + const user = await User.findOne({ + where: { + user_id: emailVerification.user_id, + }, + attributes: ["password"], + }); + + if (!user) { + return res.status(401).send({ err: "unauthorized" }); + } + + const decodedPassword = decodeBase64(password); + + const match = await matchPassword(decodedPassword, user.password); + + if (!match) { + return res.status(400).send({ err: "invalid request" }); + } + + // update user email + + const userPendingEmailChange = await UserPendingEmailChange.findOne({ + where: { + email_verification_id: emailVerificationId, + }, + attributes: ["new_email"], + }); + + if (!userPendingEmailChange) { + return res.status(400).send({ err: "invalid request" }); + } + + // update user email + + await User.update( + { + email: userPendingEmailChange.new_email, + }, + { + where: { + user_id: emailVerification.user_id, + }, + } + ); + + // delete email verification and user pending email change + + await EmailVerification.destroy({ + where: { + email_verification_id: emailVerificationId, + }, + }); + + await UserPendingEmailChange.destroy({ + where: { + email_verification_id: emailVerificationId, + }, + }); + + // delete all sessions of this user by deleting all sessions with this user_id + + await Session.destroy({ + where: { + user_id: emailVerification.user_id, + }, + }); + + res.status(200).send({ msg: "email changed" }); + return; } res.status(400).send({ err: "invalid request" }); @@ -890,3 +959,65 @@ export async function VerifyEmail(req: Request, res: Response) { res.status(500).send({ err: "invalid request" }); } } + +export async function UpdateUserProfileEmail(req: Request, res: Response) { + try { + let { email } = req.body; + + if (!email || !(await isEmailValid(email))) { + return res.status(400).send({ err: "invalid request" }); + } + + email = email.toLowerCase(); + + const session = await getUserSession(req); + + if (!session) { + return res.status(401).send({ err: "unauthorized" }); + } + + const user = await User.findOne({ + where: { + user_id: session.user_id, + }, + attributes: ["language"], + }); + + if (!user) { + return res.status(401).send({ err: "unauthorized" }); + } + + const emailVerificationId = newEmailVerificationId(); + const state = + EMAIL_VERIFICATION_STATE.PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION; + + await EmailVerification.create({ + email_verification_id: emailVerificationId, + user_id: session.user_id, + state: state, + }); + + await UserPendingEmailChange.create({ + email_verification_id: emailVerificationId, + user_id: session.user_id, + new_email: email, + }); + + rabbitmq.sendEmail( + email, + "dashboardUserProfileChangeToNewEmailVerification", + user.language, + { + emailVerificationUrl: getEmailVerificationUrl( + state, + emailVerificationId + ), + } + ); + + res.status(200).send({ msg: "user email updated" }); + } catch (error) { + logger.error(error); + res.status(500).send({ err: "invalid request" }); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 5a6ffe1..a7da773 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -6,6 +6,7 @@ import StoreService from "./storeService"; import StoreServiceActivity from "./storeServiceActivity"; import StoreServiceActivityUsers from "./storeServiceActivityUsers"; import User from "./user"; +import UserPendingEmailChange from "./userPendingEmailChange"; function syncModels() { EmailVerification.sync(); @@ -16,6 +17,7 @@ function syncModels() { StoreServiceActivity.sync(); StoreServiceActivityUsers.sync(); Feedback.sync(); + UserPendingEmailChange.sync(); // UserGoogleTokens.sync(); not needed as it is created by the calendar backend } diff --git a/src/models/userPendingEmailChange.ts b/src/models/userPendingEmailChange.ts new file mode 100644 index 0000000..e083c73 --- /dev/null +++ b/src/models/userPendingEmailChange.ts @@ -0,0 +1,44 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface UserPendingEmailChangeAttributes { + email_verification_id: string; // code that is sent to the user's email + user_id: string; + new_email: string; +} + +class UserPendingEmailChange + extends Model + implements UserPendingEmailChangeAttributes +{ + declare email_verification_id: string; + declare user_id: string; + declare new_email: string; +} + +UserPendingEmailChange.init( + { + // Model attributes are defined here + email_verification_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + }, + new_email: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "user_pending_email_change", + sequelize, // passing the `sequelize` instance is required + createdAt: "created_at", + updatedAt: "updated_at", + } +); + +export default UserPendingEmailChange; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 22b2621..5abe039 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -24,6 +24,11 @@ router.post( sessionProtection, userController.UpdateUserProfilePassword ); +router.post( + "/profile/email", + sessionProtection, + userController.UpdateUserProfileEmail +); router.get( "/profile/sessions", sessionProtection, @@ -45,6 +50,6 @@ router.post( userController.ExportUserAccount ); router.get("/profile/export/:id", userController.GetExportedUserAccount); -router.get("/verify/:state/:emailVerificationId", userController.VerifyEmail); +router.post("/verify/:state/:emailVerificationId", userController.VerifyEmail); export default router; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 45483ca..87bf33e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -74,6 +74,7 @@ export const ACCOUNT_DEMO_DAYS = Number( export enum EMAIL_VERIFICATION_STATE { PENDING_EMAIL_VERIFICATION = 0, // account is created but email is not verified yet + PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION = 1, // user wants to change email, new email is not verified yet } export const DASHBOARD_URL = process.env.DASHBOARD_URL as string; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 771a73b..8c5afee 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -110,8 +110,8 @@ export async function getUserSession(req: Request) { } export function getEmailVerificationUrl( - state: string, + state: number, emailVerificationId: string ) { - return `${DASHBOARD_URL}/verify/${state}/${emailVerificationId}`; + return `${DASHBOARD_URL}/verify/${String(state)}/${emailVerificationId}`; } diff --git a/src/validator/validator.ts b/src/validator/validator.ts index 459ac7a..2687cb5 100644 --- a/src/validator/validator.ts +++ b/src/validator/validator.ts @@ -26,6 +26,7 @@ import { CALENDAR_MAX_EARLIEST_BOOKING_TIME, } from "../utils/constants"; import User from "../models/user"; +import UserPendingEmailChange from "../models/userPendingEmailChange"; // TODO: regex for username export function isUsernameValid(username: string) { @@ -35,8 +36,10 @@ export function isUsernameValid(username: string) { ); } -// TODO: regex for email -export async function isEmailValid(email: string) { +export async function isEmailValid( + email: string, + checkDatabase: boolean = true +) { if ( email.length < EMAIL_MIN_LENGTH || email.length > EMAIL_MAX_LENGTH || @@ -45,19 +48,42 @@ export async function isEmailValid(email: string) { return false; } - // check if email is already taken in the database - - const existingUser = await User.findOne({ - where: { - email: email, - }, - }); - - if (existingUser) { - return false; + if (!checkDatabase) { + return true; } - return true; + try { + // check if email is already taken in the database + + const existingUser = await User.findOne({ + where: { + email: email, + }, + }); + + if (existingUser !== null) { + console.log("existingUser"); + return false; + } + + // check if email is already taken in the pending email change table + + const existingPendingEmailChange = await UserPendingEmailChange.findOne({ + where: { + new_email: email, + }, + }); + + if (existingPendingEmailChange !== null) { + console.log("existingPendingEmailChange"); + return false; + } + + return true; + } catch (err) { + console.warn(err); + return false; + } } // TODO: regex for password