change email

main
alex 2024-02-11 23:38:22 +01:00
parent 5bbcab9fb2
commit 351d14c2fc
7 changed files with 243 additions and 34 deletions

View File

@ -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" });
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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