From eed7581d0e264ec9acc6e928fa0f77ebb125704d Mon Sep 17 00:00:00 2001 From: Ilango Date: Wed, 29 Oct 2025 02:18:28 +0530 Subject: [PATCH] Updates to change/reset password functionality (#5294) * feat: require old password when changing password * update account settings page - require old password for changing passwords * update profile dropdown - go to /account route for updating account details * Remove all session based on user id after password change * fix: run lint-fix * remove unnecessary error page on account * fix: prevent logout if user provides wrong current password * fix: remove unused user profile page * fix: import --------- Co-authored-by: Henry --- .../server/src/database/entities/index.ts | 4 +- .../database/entities/login-session.entity.ts | 13 + .../middleware/passport/SessionPersistance.ts | 96 ++- .../enterprise/middleware/passport/index.ts | 5 + .../enterprise/services/account.service.ts | 4 + .../src/enterprise/services/user.service.ts | 30 +- packages/server/src/index.ts | 1 + .../Header/ProfileSection/index.jsx | 8 +- packages/ui/src/routes/MainRoutes.jsx | 11 +- packages/ui/src/views/account/UserProfile.jsx | 294 ------- packages/ui/src/views/account/index.jsx | 750 ++++++++++-------- 11 files changed, 546 insertions(+), 670 deletions(-) create mode 100644 packages/server/src/enterprise/database/entities/login-session.entity.ts delete mode 100644 packages/ui/src/views/account/UserProfile.jsx diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index b65ea28b5..ad19b4e2e 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -25,6 +25,7 @@ import { OrganizationUser } from '../../enterprise/database/entities/organizatio import { Workspace } from '../../enterprise/database/entities/workspace.entity' import { WorkspaceUser } from '../../enterprise/database/entities/workspace-user.entity' import { LoginMethod } from '../../enterprise/database/entities/login-method.entity' +import { LoginSession } from '../../enterprise/database/entities/login-session.entity' export const entities = { ChatFlow, @@ -55,5 +56,6 @@ export const entities = { OrganizationUser, Workspace, WorkspaceUser, - LoginMethod + LoginMethod, + LoginSession } diff --git a/packages/server/src/enterprise/database/entities/login-session.entity.ts b/packages/server/src/enterprise/database/entities/login-session.entity.ts new file mode 100644 index 000000000..94ffa7bf2 --- /dev/null +++ b/packages/server/src/enterprise/database/entities/login-session.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm' + +@Entity({ name: 'login_sessions' }) +export class LoginSession { + @PrimaryColumn({ type: 'varchar' }) + sid: string + + @Column({ type: 'text' }) + sess: string + + @Column({ type: 'bigint', nullable: true }) + expire?: number +} diff --git a/packages/server/src/enterprise/middleware/passport/SessionPersistance.ts b/packages/server/src/enterprise/middleware/passport/SessionPersistance.ts index bd21dbae7..afaf3c2f0 100644 --- a/packages/server/src/enterprise/middleware/passport/SessionPersistance.ts +++ b/packages/server/src/enterprise/middleware/passport/SessionPersistance.ts @@ -3,9 +3,13 @@ import { RedisStore } from 'connect-redis' import { getDatabaseSSLFromEnv } from '../../../DataSource' import path from 'path' import { getUserHome } from '../../../utils' +import type { Store } from 'express-session' +import { LoginSession } from '../../database/entities/login-session.entity' +import { getRunningExpressApp } from '../../../utils/getRunningExpressApp' let redisClient: Redis | null = null let redisStore: RedisStore | null = null +let dbStore: Store | null = null export const initializeRedisClientAndStore = (): RedisStore => { if (!redisClient) { @@ -35,6 +39,8 @@ export const initializeRedisClientAndStore = (): RedisStore => { } export const initializeDBClientAndStore: any = () => { + if (dbStore) return dbStore + const databaseType = process.env.DATABASE_TYPE || 'sqlite' switch (databaseType) { case 'mysql': { @@ -51,7 +57,8 @@ export const initializeDBClientAndStore: any = () => { tableName: 'login_sessions' } } - return new MySQLStore(options) + dbStore = new MySQLStore(options) + return dbStore } case 'mariadb': /* TODO: Implement MariaDB session store */ @@ -70,12 +77,13 @@ export const initializeDBClientAndStore: any = () => { database: process.env.DATABASE_NAME, ssl: getDatabaseSSLFromEnv() }) - return new pgSession({ + dbStore = new pgSession({ pool: pgPool, // Connection pool tableName: 'login_sessions', schemaName: 'public', createTableIfMissing: true }) + return dbStore } case 'default': case 'sqlite': { @@ -83,11 +91,93 @@ export const initializeDBClientAndStore: any = () => { const sqlSession = require('connect-sqlite3')(expressSession) let flowisePath = path.join(getUserHome(), '.flowise') const homePath = process.env.DATABASE_PATH ?? flowisePath - return new sqlSession({ + dbStore = new sqlSession({ db: 'database.sqlite', table: 'login_sessions', dir: homePath }) + return dbStore } } } + +const getUserIdFromSession = (session: any): string | undefined => { + try { + const data = typeof session === 'string' ? JSON.parse(session) : session + return data?.passport?.user?.id + } catch { + return undefined + } +} + +export const destroyAllSessionsForUser = async (userId: string): Promise => { + try { + if (redisStore && redisClient) { + const prefix = (redisStore as any)?.prefix ?? 'sess:' + const pattern = `${prefix}*` + const keysToDelete: string[] = [] + const batchSize = 1000 + + const stream = redisClient.scanStream({ + match: pattern, + count: batchSize + }) + + for await (const keysBatch of stream) { + if (keysBatch.length === 0) continue + + const sessions = await redisClient.mget(...keysBatch) + for (let i = 0; i < sessions.length; i++) { + if (getUserIdFromSession(sessions[i]) === userId) { + keysToDelete.push(keysBatch[i]) + } + } + + if (keysToDelete.length >= batchSize) { + const pipeline = redisClient.pipeline() + keysToDelete.splice(0, batchSize).forEach((key) => pipeline.del(key)) + await pipeline.exec() + } + } + + if (keysToDelete.length > 0) { + const pipeline = redisClient.pipeline() + keysToDelete.forEach((key) => pipeline.del(key)) + await pipeline.exec() + } + } else if (dbStore) { + const appServer = getRunningExpressApp() + const dataSource = appServer.AppDataSource + const repository = dataSource.getRepository(LoginSession) + + const databaseType = process.env.DATABASE_TYPE || 'sqlite' + switch (databaseType) { + case 'sqlite': + await repository + .createQueryBuilder() + .delete() + .where(`json_extract(sess, '$.passport.user.id') = :userId`, { userId }) + .execute() + break + case 'mysql': + await repository + .createQueryBuilder() + .delete() + .where(`JSON_EXTRACT(sess, '$.passport.user.id') = :userId`, { userId }) + .execute() + break + case 'postgres': + await repository.createQueryBuilder().delete().where(`sess->'passport'->'user'->>'id' = :userId`, { userId }).execute() + break + default: + console.warn('Unsupported database type:', databaseType) + break + } + } else { + console.warn('Session store not available, skipping session invalidation') + } + } catch (error) { + console.error('Error destroying sessions for user:', error) + throw error + } +} diff --git a/packages/server/src/enterprise/middleware/passport/index.ts b/packages/server/src/enterprise/middleware/passport/index.ts index 8cb0f594a..43abf56de 100644 --- a/packages/server/src/enterprise/middleware/passport/index.ts +++ b/packages/server/src/enterprise/middleware/passport/index.ts @@ -81,6 +81,11 @@ const _initializePassportMiddleware = async (app: express.Application) => { app.use(passport.initialize()) app.use(passport.session()) + if (options.store) { + const appServer = getRunningExpressApp() + appServer.sessionStore = options.store + } + passport.serializeUser((user: any, done) => { done(null, user) }) diff --git a/packages/server/src/enterprise/services/account.service.ts b/packages/server/src/enterprise/services/account.service.ts index a4cb46ad9..f2894f487 100644 --- a/packages/server/src/enterprise/services/account.service.ts +++ b/packages/server/src/enterprise/services/account.service.ts @@ -26,6 +26,7 @@ import { UserErrorMessage, UserService } from './user.service' import { WorkspaceUserErrorMessage, WorkspaceUserService } from './workspace-user.service' import { WorkspaceErrorMessage, WorkspaceService } from './workspace.service' import { sanitizeUser } from '../../utils/sanitize.util' +import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance' type AccountDTO = { user: Partial @@ -576,6 +577,9 @@ export class AccountService { await queryRunner.startTransaction() data.user = await this.userService.saveUser(data.user, queryRunner) await queryRunner.commitTransaction() + + // Invalidate all sessions for this user after password reset + await destroyAllSessionsForUser(user.id as string) } catch (error) { await queryRunner.rollbackTransaction() throw error diff --git a/packages/server/src/enterprise/services/user.service.ts b/packages/server/src/enterprise/services/user.service.ts index 4fe80a04d..991f6644c 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,8 +7,9 @@ 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' +import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance' export const enum UserErrorMessage { EXPIRED_TEMP_TOKEN = 'Expired Temporary Token', @@ -24,7 +24,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 +135,7 @@ export class UserService { return newUser } - public async updateUser(newUserData: Partial & { password?: string }) { + public async updateUser(newUserData: Partial & { oldPassword?: string; newPassword?: string; confirmPassword?: string }) { let queryRunner: QueryRunner | undefined let updatedUser: Partial try { @@ -158,10 +159,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.confirmPassword) { + 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.BAD_REQUEST, UserErrorMessage.INVALID_USER_CREDENTIAL) + } + if (newUserData.newPassword !== newUserData.confirmPassword) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.PASSWORDS_DO_NOT_MATCH) + } + const hash = getHash(newUserData.newPassword) newUserData.credential = hash newUserData.tempToken = '' newUserData.tokenExpiry = undefined @@ -171,6 +180,11 @@ export class UserService { await queryRunner.startTransaction() await this.saveUser(updatedUser, queryRunner) await queryRunner.commitTransaction() + + // Invalidate all sessions for this user if password was changed + if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmPassword) { + await destroyAllSessionsForUser(updatedUser.id as string) + } } catch (error) { if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction() throw error diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1e1d4a3da..e418984df 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -73,6 +73,7 @@ export class App { queueManager: QueueManager redisSubscriber: RedisEventSubscriber usageCacheManager: UsageCacheManager + sessionStore: any constructor() { this.app = express() diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx index 1e47faedd..7b75dc071 100644 --- a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx +++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx @@ -52,7 +52,6 @@ import exportImportApi from '@/api/exportimport' // Hooks import useApi from '@/hooks/useApi' -import { useConfig } from '@/store/context/ConfigContext' import { getErrorMessage } from '@/utils/errorHandler' const dataToExport = [ @@ -215,7 +214,6 @@ const ProfileSection = ({ handleLogout }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) - const { isCloud } = useConfig() const [open, setOpen] = useState(false) const [aboutDialogOpen, setAboutDialogOpen] = useState(false) @@ -500,18 +498,18 @@ const ProfileSection = ({ handleLogout }) => { Version} /> - {isAuthenticated && !currentUser.isSSO && !isCloud && ( + {isAuthenticated && !currentUser.isSSO && ( { setOpen(false) - navigate('/user-profile') + navigate('/account') }} > - Update Profile} /> + Account Settings} /> )} import('@/views/evaluators'))) // account routing const Account = Loadable(lazy(() => import('@/views/account'))) -const UserProfile = Loadable(lazy(() => import('@/views/account/UserProfile'))) // files routing const Files = Loadable(lazy(() => import('@/views/files'))) @@ -294,11 +293,7 @@ const MainRoutes = { }, { path: '/account', - element: ( - - - - ) + element: }, { path: '/users', @@ -308,10 +303,6 @@ const MainRoutes = { ) }, - { - path: '/user-profile', - element: - }, { path: '/roles', element: ( diff --git a/packages/ui/src/views/account/UserProfile.jsx b/packages/ui/src/views/account/UserProfile.jsx deleted file mode 100644 index 1b26d05cc..000000000 --- a/packages/ui/src/views/account/UserProfile.jsx +++ /dev/null @@ -1,294 +0,0 @@ -import { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' - -// material-ui -import { Box, Button, OutlinedInput, Stack, Typography } from '@mui/material' - -// project imports -import ErrorBoundary from '@/ErrorBoundary' -import ViewHeader from '@/layout/MainLayout/ViewHeader' -import { StyledButton } from '@/ui-component/button/StyledButton' -import MainCard from '@/ui-component/cards/MainCard' -import SettingsSection from '@/ui-component/form/settings' -import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' - -// API -import userApi from '@/api/user' -import useApi from '@/hooks/useApi' - -// Store -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' - -// utils -import useNotifier from '@/utils/useNotifier' -import { validatePassword } from '@/utils/validation' - -// Icons -import { IconAlertTriangle, IconX } from '@tabler/icons-react' - -const UserProfile = () => { - useNotifier() - const { error, setError } = useError() - - const dispatch = useDispatch() - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - - const currentUser = useSelector((state) => state.auth.user) - const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) - - const [newPasswordVal, setNewPasswordVal] = useState('') - const [confirmPasswordVal, setConfirmPasswordVal] = useState('') - const [usernameVal, setUsernameVal] = useState('') - const [emailVal, setEmailVal] = useState('') - - const [loading, setLoading] = useState(false) - const [authErrors, setAuthErrors] = useState([]) - - const getUserApi = useApi(userApi.getUserById) - - const validateAndSubmit = async () => { - const validationErrors = [] - setAuthErrors([]) - if (!isAuthenticated) { - validationErrors.push('User is not authenticated') - } - if (currentUser.isSSO) { - validationErrors.push('User is a SSO user, unable to update details') - } - if (!usernameVal) { - validationErrors.push('Name cannot be left blank!') - } - if (!emailVal) { - validationErrors.push('Email cannot be left blank!') - } - if (newPasswordVal || confirmPasswordVal) { - if (newPasswordVal !== confirmPasswordVal) { - validationErrors.push('New Password and Confirm Password do not match') - } - const passwordErrors = validatePassword(newPasswordVal) - if (passwordErrors.length > 0) { - validationErrors.push(...passwordErrors) - } - } - if (validationErrors.length > 0) { - setAuthErrors(validationErrors) - return - } - const body = { - id: currentUser.id, - email: emailVal, - name: usernameVal - } - if (newPasswordVal) body.password = newPasswordVal - setLoading(true) - try { - const updateResponse = await userApi.updateUser(body) - setAuthErrors([]) - setLoading(false) - if (updateResponse.data) { - 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) - setAuthErrors([typeof error.response.data === 'object' ? error.response.data.message : error.response.data]) - enqueueSnackbar({ - message: `Failed to update user details`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - useEffect(() => { - if (getUserApi.data) { - const user = getUserApi.data - setEmailVal(user.email) - setUsernameVal(user.name) - setLoading(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getUserApi.data]) - - useEffect(() => { - if (getUserApi.error) { - setLoading(false) - setError(getUserApi.error) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getUserApi.error]) - - useEffect(() => { - setLoading(true) - getUserApi.request(currentUser.id) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return ( - <> - - {error ? ( - - ) : ( - - - {authErrors && authErrors.length > 0 && ( -
- - - - - -
    - {authErrors.map((msg, key) => ( - -
  • {msg}
  • -
    - ))} -
-
-
-
- )} - - Save - - } - title='Profile' - > - - -
- Email -
-
- setEmailVal(e.target.value)} - value={emailVal} - /> -
- -
- - Full Name * - -
-
- setUsernameVal(e.target.value)} - value={usernameVal} - /> -
- -
- - New Password * - -
-
- setNewPasswordVal(e.target.value)} - value={newPasswordVal} - /> - - - Password must be at least 8 characters long and contain at least one lowercase letter, one - uppercase letter, one digit, and one special character. - - -
- -
- - Confirm Password * - -
-
- setConfirmPasswordVal(e.target.value)} - value={confirmPasswordVal} - /> - - Retype your new password. Must match the password typed above. - -
-
-
-
- )} -
- {loading && } - - ) -} - -export default UserProfile diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index 8a0c78338..b96720773 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -25,7 +25,6 @@ import { import { darken, useTheme } from '@mui/material/styles' // project imports -import ErrorBoundary from '@/ErrorBoundary' import ViewHeader from '@/layout/MainLayout/ViewHeader' import { StyledButton } from '@/ui-component/button/StyledButton' import MainCard from '@/ui-component/cards/MainCard' @@ -48,8 +47,7 @@ import { store } from '@/store' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' 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 ||============================== // @@ -66,12 +64,12 @@ const AccountSettings = () => { const currentUser = useSelector((state) => state.auth.user) const customization = useSelector((state) => state.customization) - const { error, setError } = useError() const { isCloud } = useConfig() const [isLoading, setLoading] = useState(true) const [profileName, setProfileName] = useState('') const [email, setEmail] = useState('') + const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [usage, setUsage] = useState(null) @@ -104,10 +102,19 @@ const AccountSettings = () => { const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource) const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats) const getCurrentUsageApi = useApi(userApi.getCurrentUsage) + const logoutApi = useApi(accountApi.logout) + + useEffect(() => { + if (currentUser) { + getUserByIdApi.request(currentUser.id) + } else { + window.location.href = '/login' + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUser]) useEffect(() => { if (isCloud) { - getUserByIdApi.request(currentUser.id) getPricingPlansApi.request() getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId) getCurrentUsageApi.request() @@ -136,6 +143,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) @@ -219,7 +237,6 @@ const AccountSettings = () => { }) } } catch (error) { - setError(error) enqueueSnackbar({ message: `Failed to update profile: ${ typeof error.response.data === 'object' ? error.response.data.message : error.response.data @@ -241,6 +258,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') } @@ -267,11 +287,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: { @@ -286,7 +312,6 @@ const AccountSettings = () => { }) } } catch (error) { - setError(error) enqueueSnackbar({ message: `Failed to update password: ${ typeof error.response.data === 'object' ? error.response.data.message : error.response.data @@ -388,287 +413,345 @@ const AccountSettings = () => { return ( - {error ? ( - - ) : ( - - - {isLoading && !getUserByIdApi.data ? ( - - - - - - - - - - - - - - + + + {isLoading && !getUserByIdApi.data ? ( + + + + + - ) : ( - <> - - + + + + + + + + + + ) : ( + <> + {isCloud && ( + <> + - {currentPlanTitle && ( - - Current Organization Plan: - - {currentPlanTitle.toUpperCase()} - - - )} - - Update your billing details and subscription - - - - - - - - - - - - - Seats Included in Plan: - - {getAdditionalSeatsQuantityApi.loading ? : includedSeats} + {currentPlanTitle && ( + + Current Organization Plan: + + {currentPlanTitle.toUpperCase()} + + + )} + + Update your billing details and subscription - - - 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) - } + + + + + + + - Add Seats - - - - - - - - - Predictions - - {`${usage?.predictions?.usage || 0} / ${usage?.predictions?.limit || 0}`} - + + Seats Included in Plan: + + {getAdditionalSeatsQuantityApi.loading ? : includedSeats} + + + + Additional Seats Purchased: + + {getAdditionalSeatsQuantityApi.loading ? ( + + ) : ( + purchasedSeats + )} + + + + Occupied Seats: + + {getAdditionalSeatsQuantityApi.loading ? ( + + ) : ( + `${occupiedSeats}/${totalSeats}` + )} + + - - - { - 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 - } + + {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 + - - - Storage - - {`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed( + + + + + + 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 - )}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' - /> + )}%`} + + + + + 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 + )}%`} - {`${storageUsageInPercent.toFixed( - 2 - )}%`} + + + )} + + Save + + } + title='Profile' + > + + + Name + setProfileName(e.target.value)} + value={profileName} + /> - + + Email Address + setEmail(e.target.value)} + value={email} + /> + + + + {!currentUser.isSSO && ( + Save } - title='Profile' + title='Security' > { py: 2 }} > - - Name + + Old Password setProfileName(e.target.value)} - value={profileName} + placeholder='Old Password' + name='oldPassword' + onChange={(e) => setOldPassword(e.target.value)} + value={oldPassword} /> - - Email Address + + New Password setEmail(e.target.value)} - value={email} + placeholder='New Password' + name='newPassword' + onChange={(e) => setNewPassword(e.target.value)} + value={newPassword} + /> + + + Password must be at least 8 characters long and contain at least one lowercase letter, one + uppercase letter, one digit, and one special character. + + + + + Confirm New Password + setConfirmPassword(e.target.value)} + value={confirmPassword} /> - {!currentUser.isSSO && ( - - Save - - } - title='Security' - > - - - New Password - setNewPassword(e.target.value)} - value={newPassword} - /> - - - Password must be at least 8 characters long and contain at least one lowercase letter, - one uppercase letter, one digit, and one special character. - - - - - Confirm Password - setConfirmPassword(e.target.value)} - value={confirmPassword} - /> - - - - )} - - )} - - )} + )} + + )} + {openPricingDialog && isCloud && (