diff --git a/Tombolo/client-reactjs/src/components/login/AuthRoutes.js b/Tombolo/client-reactjs/src/components/login/AuthRoutes.js index b95123be..49fd6459 100644 --- a/Tombolo/client-reactjs/src/components/login/AuthRoutes.js +++ b/Tombolo/client-reactjs/src/components/login/AuthRoutes.js @@ -3,9 +3,9 @@ import BasicLayout from '../common/BasicLayout'; import { Route, Switch } from 'react-router-dom'; const Login = React.lazy(() => import('./login.js')); const Register = React.lazy(() => import('./register.js')); -const ResetPassword = React.lazy(() => import('./ResetPassword.js')); +const ResetPassword = React.lazy(() => import('./ResetPasswordWithToken.js')); const ForgotPassword = React.lazy(() => import('./ForgotPassword.js')); -const resetTempPassword = React.lazy(() => import('./ResetTempPw')); +const resetTempPassword = React.lazy(() => import('./ResetTempPassword.js')); const AuthRoutes = () => { //if traditional login isn't enabled, redirect user to login page @@ -30,9 +30,11 @@ const AuthRoutes = () => { + {/* reset password with self requested token */} - + {/* reset password with temp password from owner/admin registration */} + {/* redirect all other routes hit to login */} diff --git a/Tombolo/client-reactjs/src/components/login/ForgotPassword.js b/Tombolo/client-reactjs/src/components/login/ForgotPassword.js index 5a29d95f..a3be9c46 100644 --- a/Tombolo/client-reactjs/src/components/login/ForgotPassword.js +++ b/Tombolo/client-reactjs/src/components/login/ForgotPassword.js @@ -1,10 +1,17 @@ import React from 'react'; import { Form, Input, Button, Divider, message } from 'antd'; +import { authHeader } from '../common/AuthHeader'; const ForgotPassword = () => { - const onFinish = (values) => { - console.log('Received values:', values); - success(); + const onFinish = async (values) => { + try { + const url = '/api/auth/handlePasswordResetRequest'; + await fetch(url, { headers: authHeader(), method: 'POST', body: JSON.stringify(values) }); + + success(); + } catch (err) { + message.error(err.message); + } }; const [messageApi, contextHolder] = message.useMessage(); diff --git a/Tombolo/client-reactjs/src/components/login/ResetPassword.js b/Tombolo/client-reactjs/src/components/login/ResetPasswordWithToken.js similarity index 73% rename from Tombolo/client-reactjs/src/components/login/ResetPassword.js rename to Tombolo/client-reactjs/src/components/login/ResetPasswordWithToken.js index 28a89867..0f6ec064 100644 --- a/Tombolo/client-reactjs/src/components/login/ResetPassword.js +++ b/Tombolo/client-reactjs/src/components/login/ResetPasswordWithToken.js @@ -2,8 +2,12 @@ import React, { useEffect, useState } from 'react'; import { Form, Input, Button, Divider, message, Popover } from 'antd'; import { useParams } from 'react-router-dom'; import passwordComplexityValidator from '../common/passwordComplexityValidator'; +import { authHeader } from '../common/AuthHeader'; + +import { getDeviceInfo } from './utils'; +import { setUser } from '../common/userStorage'; + const ResetPassword = () => { - const [user, setUser] = useState(null); const [popOverContent, setPopOverContent] = useState(null); //we will get the reset token from the url and test if it is valid to get the user information @@ -29,28 +33,49 @@ const ResetPassword = () => { }); }; + //if there is no token, we will show an error message to the user useEffect(() => { - //check if reset token is valid, if it is, we will get the user ID and store it, if not, we will redirect to the login page - if (user === null && resetToken !== undefined) { - //get user information by reset token + if (resetToken === undefined) { + invalidToken(); + } + }, []); - // get user by reset token route + const onFinish = async (values) => { + try { + const url = '/api/auth/resetPasswordWithToken'; + const password = values.newPassword; + const deviceInfo = getDeviceInfo(); - //if user is found, set user and return - console.log(setUser); + const response = await fetch(url, { + headers: authHeader(), + method: 'POST', + body: JSON.stringify({ password, token: resetToken, deviceInfo }), + }); - //if user is not found, message - invalidToken(); - } + if (!response.ok) { + let json = await response.json(); - if (resetToken === undefined) { - //redirect to login page - } - }, []); + if (json.message) { + message.error(json.message); + } else { + message.error('An undefined error occurred. Please try again later'); + } + return; + } - const onFinish = (values) => { - console.log('Received values:', values); - alert('reset password code fires here'); + if (response.ok) { + message.success('Password reset successfully.'); + let json = await response.json(); + if (json.success === true) { + json.data.isAuthenticated = true; + setUser(json.data); + //reload window + window.location.href = '/'; + } + } + } catch (err) { + message.error(err.message); + } }; useEffect(() => {}, [popOverContent]); @@ -119,7 +144,7 @@ const ResetPassword = () => { - diff --git a/Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx b/Tombolo/client-reactjs/src/components/login/ResetTempPassword.js similarity index 92% rename from Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx rename to Tombolo/client-reactjs/src/components/login/ResetTempPassword.js index ed5eb879..71e9d5d8 100644 --- a/Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx +++ b/Tombolo/client-reactjs/src/components/login/ResetTempPassword.js @@ -36,9 +36,18 @@ function ResetTempPassword() { values.token = resetToken; const result = await resetTempPassword(values); - // Save user token to local storage - setUser(JSON.stringify(result.data)); - window.location.href = '/'; + if (result?.data) { + let user = result.data; + + //set isAuthenticated to true so application loads + user.isAuthenticated = true; + + // Save user token to local storage + setUser(JSON.stringify(user)); + window.location.href = '/'; + } else { + message.error(result.message); + } } catch (err) { message.error(err.message); } finally { diff --git a/Tombolo/client-reactjs/src/components/login/utils.js b/Tombolo/client-reactjs/src/components/login/utils.js index 916d2908..3070c635 100644 --- a/Tombolo/client-reactjs/src/components/login/utils.js +++ b/Tombolo/client-reactjs/src/components/login/utils.js @@ -22,7 +22,7 @@ export const getDeviceInfo = () => { return { os, browser: browserName }; }; -// Make a request to the server to reset the temporary password +// Make a request to the server to reset the temporary password - OWNER REGISTRATION export const resetTempPassword = async (resetData) => { const payload = { method: 'POST', diff --git a/Tombolo/server/controllers/authController.js b/Tombolo/server/controllers/authController.js index 5e3e006d..c67b8264 100644 --- a/Tombolo/server/controllers/authController.js +++ b/Tombolo/server/controllers/authController.js @@ -280,10 +280,10 @@ const verifyEmail = async (req, res) => { } }; -//Reset Temp password -const resetTempPassword = async (req, res) => { +//Reset Password With Token - Self Requested +const resetPasswordWithToken = async (req, res) => { try { - const { tempPassword, password, token, deviceInfo } = req.body; + const { password, token, deviceInfo } = req.body; // From AccountVerificationCodes table findUser ID by code, where code is resetToken const accountVerificationCode = await AccountVerificationCodes.findOne({ @@ -308,9 +308,106 @@ const resetTempPassword = async (req, res) => { throw { status: 404, message: "User not found" }; } - // Compare temp password with hash. - if (!bcrypt.compareSync(tempPassword, user.hash)) { - throw { status: 500, message: "Invalid temporary password" }; + // Hash the new password + const salt = bcrypt.genSaltSync(10); + user.hash = bcrypt.hashSync(password, salt); + user.verifiedUser = true; + user.verifiedAt = new Date(); + user.forcePasswordReset = false; + + // Save user with updated details + await user.save(); + + // Delete the account verification code + await AccountVerificationCodes.destroy({ + where: { code: token }, + }); + + //delete password reset link + await PasswordResetLinks.destroy({ + where: { id: token }, + }); + + //remove all sessions for user before initiating new session + await RefreshTokens.destroy({ + where: { userId: user.id }, + }); + + // Create token id + const tokenId = uuidv4(); + + // Create access jwt + const accessToken = generateAccessToken({ ...user.toJSON(), tokenId }); + + // Generate refresh token + const refreshToken = generateRefreshToken({ tokenId }); + + // Save refresh token to DB + const { iat, exp } = jwt.decode(refreshToken); + + // Save refresh token in DB + await RefreshTokens.create({ + id: tokenId, + userId: user.id, + token: refreshToken, + deviceInfo, + metaData: {}, + iat: new Date(iat * 1000), + exp: new Date(exp * 1000), + }); + + await setTokenCookie(res, accessToken); + + await generateAndSetCSRFToken(req, res, accessToken); + + // User data obj to send to the client + const userObj = { + ...user.toJSON(), + }; + + // remove hash from user object + delete userObj.hash; + + // Success response + res.status(200).json({ + success: true, + message: "Password updated successfully", + data: userObj, + }); + } catch (err) { + logger.error(`Reset Temp Password: ${err.message}`); + res + .status(err.status || 500) + .json({ success: false, message: err.message }); + } +}; + +//Reset Password with Temp Password - Owner/Admin requested +const resetTempPassword = async (req, res) => { + try { + const { password, token, deviceInfo } = req.body; + + // From AccountVerificationCodes table findUser ID by code, where code is resetToken + const accountVerificationCode = await AccountVerificationCodes.findOne({ + where: { code: token }, + }); + + // If accountVerificationCode not found + if (!accountVerificationCode) { + throw { status: 404, message: "Invalid or expired reset token" }; + } + + // If accountVerificationCode has expired + if (new Date() > accountVerificationCode.expiresAt) { + throw { status: 400, message: "Reset token has expired" }; + } + + // Find user by ID + let user = await getAUser({ id: accountVerificationCode.userId }); + + // If user not found + if (!user) { + throw { status: 404, message: "User not found" }; } // Hash the new password @@ -328,6 +425,16 @@ const resetTempPassword = async (req, res) => { where: { code: token }, }); + //delete password reset link + await PasswordResetLinks.destroy({ + where: { id: token }, + }); + + //remove all sessions for user before initiating new session + await RefreshTokens.destroy({ + where: { userId: user.id }, + }); + // Create token id const tokenId = uuidv4(); @@ -508,7 +615,7 @@ const logOutBasicUser = async (req, res) => { } }; -// Fulfill password reset request +// Fulfill password reset request - Self Requested const handlePasswordResetRequest = async (req, res) => { try { // Get user email @@ -549,7 +656,14 @@ const handlePasswordResetRequest = async (req, res) => { // Generate a password reset token const randomId = uuidv4(); - const passwordRestLink = `${process.env.WEB_URL}/reset-password/${randomId}`; + let webUrl = process.env.WEB_URL; + + //cut off last character if it is a slash + if (webUrl[webUrl.length - 1] === "/") { + webUrl = webUrl.slice(0, -1); + } + + const passwordRestLink = `${webUrl}/reset-password/${randomId}`; // Notification subject let subject = "Password Reset Link"; @@ -557,6 +671,9 @@ const handlePasswordResetRequest = async (req, res) => { subject = `${process.env.INSTANCE_NAME} - ${subject}`; } + //include searchableNotificationId in the notification meta data + const searchableNotificationId = uuidv4(); + // Queue notification await NotificationQueue.create({ type: "email", @@ -566,9 +683,12 @@ const handlePasswordResetRequest = async (req, res) => { createdBy: "System", updatedBy: "System", metaData: { - mainRecipients: [email], - subject, - body: "Password reset link", + notificationId: searchableNotificationId, + recipientName: `${user.firstName}`, + notificationOrigin: "Reset Password", + subject: "PasswordResetLink", + mainRecipients: [user.email], + notificationDescription: "Password Reset Link", validForHours: 24, passwordRestLink, }, @@ -583,65 +703,15 @@ const handlePasswordResetRequest = async (req, res) => { expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), }); - // response - res.status(200).json({ success: true }); - } catch (err) { - logger.error(`Reset password: ${err.message}`); - res - .status(err.status || 500) - .json({ success: false, message: err.message }); - } -}; - -// Reset password -const resetPassword = async (req, res) => { - try { - // Get the password reset token - const { token, password } = req.body; - - // Find the password reset token - const passwordResetLink = await PasswordResetLinks.findOne({ - where: { id: token }, - }); - - // Token does not exist - if (!passwordResetLink) { - logger.error(`Reset password: Token ${token} does not exist`); - return res.status(404).json({ - success: false, - message: "Password reset link Invalid or expired", - }); - } - - // Token has expired - if (new Date() > passwordResetLink.expiresAt) { - logger.error(`Reset password: Token ${token} has expired`); - return res - .status(400) - .json({ success: false, message: "Token has expired" }); - } - - // Find the user - const user = await User.findOne({ - where: { id: passwordResetLink.userId }, - }); - - // Hash the new password - const salt = bcrypt.genSaltSync(10); - user.hash = bcrypt.hashSync(password, salt); - - // Save the user - await user.save(); - - // Delete the password link from - PasswordResetLinks.destroy({ - where: { id: token }, + // Create account verification code + await AccountVerificationCodes.create({ + code: randomId, + userId: user.id, + expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), }); // response - res - .status(200) - .json({ success: true, message: "Password updated successfully" }); + res.status(200).json({ success: true }); } catch (err) { logger.error(`Reset password: ${err.message}`); res @@ -809,11 +879,11 @@ const loginOrRegisterAzureUser = async (req, res, next) => { module.exports = { createBasicUser, verifyEmail, + resetPasswordWithToken, resetTempPassword, loginBasicUser, logOutBasicUser, handlePasswordResetRequest, - resetPassword, createApplicationOwner, loginOrRegisterAzureUser, }; diff --git a/Tombolo/server/routes/authRoutes.js b/Tombolo/server/routes/authRoutes.js index 2a3bcc53..80873a94 100644 --- a/Tombolo/server/routes/authRoutes.js +++ b/Tombolo/server/routes/authRoutes.js @@ -18,8 +18,8 @@ const { loginBasicUser, logOutBasicUser, handlePasswordResetRequest, - resetPassword, createApplicationOwner, + resetPasswordWithToken, resetTempPassword, verifyEmail, loginOrRegisterAzureUser, @@ -45,12 +45,16 @@ router.post( validatePasswordResetRequestPayload, handlePasswordResetRequest ); // Reset password -router.post("/resetPassword", validateResetPasswordPayload, resetPassword); // Reset password +router.post( + "/resetPasswordWithToken", + validateResetPasswordPayload, + resetPasswordWithToken +); // Reset Password - Self Requested router.post( "/resetTempPassword", validateResetPasswordPayload, resetTempPassword -); // Complete registration by resetting temporary password +); // Reset Password - Owner/Admin requested through registration flow router.post("/verifyEmail", verifyEmail); // Verify email // TODO - Forgot password route