From f5c4f803e8d7bc890a905fc85e17e1301671a833 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 3 Oct 2025 14:26:19 +0530 Subject: [PATCH 1/3] feat: require old password when changing password --- .../src/enterprise/services/user.service.ts | 24 ++++-- packages/ui/src/views/account/UserProfile.jsx | 85 +++++++++++++++---- 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/packages/server/src/enterprise/services/user.service.ts b/packages/server/src/enterprise/services/user.service.ts index 4fe80a04d96..825d1e3de95 100644 --- a/packages/server/src/enterprise/services/user.service.ts +++ b/packages/server/src/enterprise/services/user.service.ts @@ -1,5 +1,4 @@ import { StatusCodes } from 'http-status-codes' -import bcrypt from 'bcryptjs' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { Telemetry, TelemetryEventType } from '../../utils/telemetry' @@ -8,7 +7,7 @@ import { isInvalidEmail, isInvalidName, isInvalidPassword, isInvalidUUID } from import { DataSource, ILike, QueryRunner } from 'typeorm' import { generateId } from '../../utils' import { GeneralErrorMessage } from '../../utils/constants' -import { getHash } from '../utils/encryption.util' +import { compareHash, getHash } from '../utils/encryption.util' import { sanitizeUser } from '../../utils/sanitize.util' export const enum UserErrorMessage { @@ -24,7 +23,8 @@ export const enum UserErrorMessage { USER_EMAIL_UNVERIFIED = 'User Email Unverified', USER_NOT_FOUND = 'User Not Found', USER_FOUND_MULTIPLE = 'User Found Multiple', - INCORRECT_USER_EMAIL_OR_CREDENTIALS = 'Incorrect Email or Password' + INCORRECT_USER_EMAIL_OR_CREDENTIALS = 'Incorrect Email or Password', + PASSWORDS_DO_NOT_MATCH = 'Passwords do not match' } export class UserService { private telemetry: Telemetry @@ -134,7 +134,7 @@ export class UserService { return newUser } - public async updateUser(newUserData: Partial & { password?: string }) { + public async updateUser(newUserData: Partial & { oldPassword?: string; newPassword?: string; confirmNewPassword?: string }) { let queryRunner: QueryRunner | undefined let updatedUser: Partial try { @@ -158,10 +158,18 @@ export class UserService { this.validateUserStatus(newUserData.status) } - if (newUserData.password) { - const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5')) - // @ts-ignore - const hash = bcrypt.hashSync(newUserData.password, salt) + if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmNewPassword) { + if (!oldUserData.credential) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_CREDENTIAL) + } + // verify old password + if (!compareHash(newUserData.oldPassword, oldUserData.credential)) { + throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, UserErrorMessage.INVALID_USER_CREDENTIAL) + } + if (newUserData.newPassword !== newUserData.confirmNewPassword) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.PASSWORDS_DO_NOT_MATCH) + } + const hash = getHash(newUserData.newPassword) newUserData.credential = hash newUserData.tempToken = '' newUserData.tokenExpiry = undefined diff --git a/packages/ui/src/views/account/UserProfile.jsx b/packages/ui/src/views/account/UserProfile.jsx index 1b26d05ccfa..ceffc750c79 100644 --- a/packages/ui/src/views/account/UserProfile.jsx +++ b/packages/ui/src/views/account/UserProfile.jsx @@ -13,6 +13,7 @@ import SettingsSection from '@/ui-component/form/settings' import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' // API +import accountApi from '@/api/account.api' import userApi from '@/api/user' import useApi from '@/hooks/useApi' @@ -21,7 +22,7 @@ import { store } from '@/store' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import { gridSpacing } from '@/store/constant' import { useError } from '@/store/context/ErrorContext' -import { userProfileUpdated } from '@/store/reducers/authSlice' +import { logoutSuccess, userProfileUpdated } from '@/store/reducers/authSlice' // utils import useNotifier from '@/utils/useNotifier' @@ -41,6 +42,7 @@ const UserProfile = () => { const currentUser = useSelector((state) => state.auth.user) const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) + const [oldPasswordVal, setOldPasswordVal] = useState('') const [newPasswordVal, setNewPasswordVal] = useState('') const [confirmPasswordVal, setConfirmPasswordVal] = useState('') const [usernameVal, setUsernameVal] = useState('') @@ -50,6 +52,7 @@ const UserProfile = () => { const [authErrors, setAuthErrors] = useState([]) const getUserApi = useApi(userApi.getUserById) + const logoutApi = useApi(accountApi.logout) const validateAndSubmit = async () => { const validationErrors = [] @@ -67,6 +70,9 @@ const UserProfile = () => { validationErrors.push('Email cannot be left blank!') } if (newPasswordVal || confirmPasswordVal) { + if (!oldPasswordVal) { + validationErrors.push('Old Password cannot be left blank!') + } if (newPasswordVal !== confirmPasswordVal) { validationErrors.push('New Password and Confirm Password do not match') } @@ -82,28 +88,49 @@ const UserProfile = () => { const body = { id: currentUser.id, email: emailVal, - name: usernameVal + name: usernameVal, + oldPassword: oldPasswordVal, + newPassword: newPasswordVal, + confirmNewPassword: confirmPasswordVal } - if (newPasswordVal) body.password = newPasswordVal setLoading(true) try { const updateResponse = await userApi.updateUser(body) setAuthErrors([]) setLoading(false) if (updateResponse.data) { + if (oldPasswordVal && newPasswordVal && confirmPasswordVal) { + setOldPasswordVal('') + setNewPasswordVal('') + setConfirmPasswordVal('') + logoutApi.request() + enqueueSnackbar({ + message: 'User Details Updated! Logged Out...', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + } else { + enqueueSnackbar({ + message: 'User Details Updated!', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + } store.dispatch(userProfileUpdated(updateResponse.data)) - enqueueSnackbar({ - message: 'User Details Updated!', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) } } catch (error) { setLoading(false) @@ -124,6 +151,17 @@ const UserProfile = () => { } } + useEffect(() => { + try { + if (logoutApi.data && logoutApi.data.message === 'logged_out') { + store.dispatch(logoutSuccess()) + window.location.href = logoutApi.data.redirectTo + } + } catch (e) { + console.error(e) + } + }, [logoutApi.data]) + useEffect(() => { if (getUserApi.data) { const user = getUserApi.data @@ -238,6 +276,23 @@ const UserProfile = () => { value={usernameVal} /> + +
+ + Old Password * + +
+
+ setOldPasswordVal(e.target.value)} + value={oldPasswordVal} + /> +
From fe2dccd42e5cde9b5271d3b4d6e8bdc2707a728d Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 6 Oct 2025 13:20:28 +0530 Subject: [PATCH 2/3] update account settings page - require old password for changing passwords --- .../src/enterprise/services/user.service.ts | 6 +-- packages/ui/src/views/account/index.jsx | 51 +++++++++++++++++-- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/server/src/enterprise/services/user.service.ts b/packages/server/src/enterprise/services/user.service.ts index 825d1e3de95..31e51b3e491 100644 --- a/packages/server/src/enterprise/services/user.service.ts +++ b/packages/server/src/enterprise/services/user.service.ts @@ -134,7 +134,7 @@ export class UserService { return newUser } - public async updateUser(newUserData: Partial & { oldPassword?: string; newPassword?: string; confirmNewPassword?: string }) { + public async updateUser(newUserData: Partial & { oldPassword?: string; newPassword?: string; confirmPassword?: string }) { let queryRunner: QueryRunner | undefined let updatedUser: Partial try { @@ -158,7 +158,7 @@ export class UserService { this.validateUserStatus(newUserData.status) } - if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmNewPassword) { + if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmPassword) { if (!oldUserData.credential) { throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_CREDENTIAL) } @@ -166,7 +166,7 @@ export class UserService { if (!compareHash(newUserData.oldPassword, oldUserData.credential)) { throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, UserErrorMessage.INVALID_USER_CREDENTIAL) } - if (newUserData.newPassword !== newUserData.confirmNewPassword) { + if (newUserData.newPassword !== newUserData.confirmPassword) { throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.PASSWORDS_DO_NOT_MATCH) } const hash = getHash(newUserData.newPassword) diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index 6eb3d8f3496..ef960c48873 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -49,7 +49,7 @@ import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackba import { gridSpacing } from '@/store/constant' import { useConfig } from '@/store/context/ConfigContext' import { useError } from '@/store/context/ErrorContext' -import { userProfileUpdated } from '@/store/reducers/authSlice' +import { logoutSuccess, userProfileUpdated } from '@/store/reducers/authSlice' // ==============================|| ACCOUNT SETTINGS ||============================== // @@ -73,6 +73,7 @@ const AccountSettings = () => { const [profileName, setProfileName] = useState('') const [email, setEmail] = useState('') const [migrateEmail, setMigrateEmail] = useState('') + const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [usage, setUsage] = useState(null) @@ -105,6 +106,7 @@ const AccountSettings = () => { const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource) const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats) const getCurrentUsageApi = useApi(userApi.getCurrentUsage) + const logoutApi = useApi(accountApi.logout) useEffect(() => { if (isCloud) { @@ -138,6 +140,17 @@ const AccountSettings = () => { } }, [getCurrentUsageApi.data]) + useEffect(() => { + try { + if (logoutApi.data && logoutApi.data.message === 'logged_out') { + store.dispatch(logoutSuccess()) + window.location.href = logoutApi.data.redirectTo + } + } catch (e) { + console.error(e) + } + }, [logoutApi.data]) + useEffect(() => { if (openRemoveSeatsDialog || openAddSeatsDialog) { setSeatsQuantity(0) @@ -243,6 +256,9 @@ const AccountSettings = () => { const savePassword = async () => { try { const validationErrors = [] + if (!oldPassword) { + validationErrors.push('Old Password cannot be left blank') + } if (newPassword !== confirmPassword) { validationErrors.push('New Password and Confirm Password do not match') } @@ -269,11 +285,17 @@ const AccountSettings = () => { const obj = { id: currentUser.id, - password: newPassword + oldPassword, + newPassword, + confirmPassword } const saveProfileResp = await userApi.updateUser(obj) if (saveProfileResp.data) { store.dispatch(userProfileUpdated(saveProfileResp.data)) + setOldPassword('') + setNewPassword('') + setConfirmPassword('') + await logoutApi.request() enqueueSnackbar({ message: 'Password updated', options: { @@ -711,7 +733,7 @@ const AccountSettings = () => { { py: 2 }} > + + Old Password + setOldPassword(e.target.value)} + value={oldPassword} + /> + { gap: 1 }} > - Confirm Password + Confirm New Password setConfirmPassword(e.target.value)} value={confirmPassword} From 9814474d174239f4b99c55cbbd741100662b7a30 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 6 Oct 2025 14:29:09 +0530 Subject: [PATCH 3/3] update profile dropdown - go to /account route for updating account details --- .../Header/ProfileSection/index.jsx | 6 +- packages/ui/src/routes/MainRoutes.jsx | 6 +- packages/ui/src/views/account/index.jsx | 499 +++++++++--------- 3 files changed, 267 insertions(+), 244 deletions(-) diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx index 1e47faeddfd..bbaf97988dd 100644 --- a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx +++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx @@ -500,18 +500,18 @@ const ProfileSection = ({ handleLogout }) => { Version} /> - {isAuthenticated && !currentUser.isSSO && !isCloud && ( + {isAuthenticated && !currentUser.isSSO && ( { setOpen(false) - navigate('/user-profile') + navigate('/account') }} > - Update Profile} /> + Account Settings} /> )} - - - ) + element: }, { path: '/users', diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index ef960c48873..ad8f66198d0 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -109,8 +109,16 @@ const AccountSettings = () => { const logoutApi = useApi(accountApi.logout) useEffect(() => { - if (isCloud) { + if (currentUser) { getUserByIdApi.request(currentUser.id) + } else { + window.location.href = '/login' + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUser]) + + useEffect(() => { + if (isCloud) { getPricingPlansApi.request() getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId) getCurrentUsageApi.request() @@ -435,257 +443,276 @@ const AccountSettings = () => { ) : ( <> - - - - {currentPlanTitle && ( - - Current Organization Plan: - - {currentPlanTitle.toUpperCase()} - - - )} - - Update your billing details and subscription - - - - - - - - - - - - - Seats Included in Plan: - - {getAdditionalSeatsQuantityApi.loading ? : includedSeats} - - - - Additional Seats Purchased: - - {getAdditionalSeatsQuantityApi.loading ? : purchasedSeats} - - - - Occupied Seats: - - {getAdditionalSeatsQuantityApi.loading ? ( - - ) : ( - `${occupiedSeats}/${totalSeats}` + + {currentPlanTitle && ( + + Current Organization Plan: + + {currentPlanTitle.toUpperCase()} + + )} - - - - - {getAdditionalSeatsQuantityApi.data?.quantity > 0 && currentPlanTitle.toUpperCase() === 'PRO' && ( - - )} - { - if (currentPlanTitle.toUpperCase() === 'PRO') { - setOpenAddSeatsDialog(true) - } else { - setOpenPricingDialog(true) - } - }} - title='Add Seats is available only for PRO plan' - sx={{ borderRadius: 2, height: 40 }} - > - Add Seats - - - - - - - - - Predictions - - {`${usage?.predictions?.usage || 0} / ${usage?.predictions?.limit || 0}`} - - - - - } + disabled={!currentUser.isOrganizationAdmin || isBillingLoading} + onClick={handleBillingPortalClick} + sx={{ borderRadius: 2, height: 40 }} + > + {isBillingLoading ? ( + + + Loading + + ) : ( + 'Billing' + )} + + - {`${predictionsUsageInPercent.toFixed( - 2 - )}%`} - - - - - Storage - - {`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed( - 2 - )}MB`} - - - - { - if (storageUsageInPercent > 90) return theme.palette.error.main - if (storageUsageInPercent > 75) return theme.palette.warning.main - if (storageUsageInPercent > 50) return theme.palette.success.light - return theme.palette.success.main + + + + + + Seats Included in Plan: + + {getAdditionalSeatsQuantityApi.loading ? ( + + ) : ( + includedSeats + )} + + + + Additional Seats Purchased: + + {getAdditionalSeatsQuantityApi.loading ? ( + + ) : ( + purchasedSeats + )} + + + + Occupied Seats: + + {getAdditionalSeatsQuantityApi.loading ? ( + + ) : ( + `${occupiedSeats}/${totalSeats}` + )} + + + + + {getAdditionalSeatsQuantityApi.data?.quantity > 0 && + currentPlanTitle.toUpperCase() === 'PRO' && ( + + )} + { + if (currentPlanTitle.toUpperCase() === 'PRO') { + setOpenAddSeatsDialog(true) + } else { + setOpenPricingDialog(true) } }} - value={storageUsageInPercent > 100 ? 100 : storageUsageInPercent} - variant='determinate' - /> + title='Add Seats is available only for PRO plan' + sx={{ borderRadius: 2, height: 40 }} + > + Add Seats + - {`${storageUsageInPercent.toFixed( - 2 - )}%`} - - - + + + + + + Predictions + + {`${usage?.predictions?.usage || 0} / ${usage?.predictions?.limit || 0}`} + + + + + { + if (predictionsUsageInPercent > 90) return theme.palette.error.main + if (predictionsUsageInPercent > 75) + return theme.palette.warning.main + if (predictionsUsageInPercent > 50) + return theme.palette.success.light + return theme.palette.success.main + } + } + }} + value={predictionsUsageInPercent > 100 ? 100 : predictionsUsageInPercent} + variant='determinate' + /> + + {`${predictionsUsageInPercent.toFixed(2)}%`} + + + + + Storage + + {`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${( + usage?.storage?.limit || 0 + ).toFixed(2)}MB`} + + + + + { + if (storageUsageInPercent > 90) return theme.palette.error.main + if (storageUsageInPercent > 75) return theme.palette.warning.main + if (storageUsageInPercent > 50) return theme.palette.success.light + return theme.palette.success.main + } + } + }} + value={storageUsageInPercent > 100 ? 100 : storageUsageInPercent} + variant='determinate' + /> + + {`${storageUsageInPercent.toFixed( + 2 + )}%`} + + + + + + )}