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 <hzj94@hotmail.com>
This commit is contained in:
Ilango 2025-10-29 02:18:28 +05:30 committed by GitHub
parent 1ae1638ed9
commit eed7581d0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 546 additions and 670 deletions

View File

@ -25,6 +25,7 @@ import { OrganizationUser } from '../../enterprise/database/entities/organizatio
import { Workspace } from '../../enterprise/database/entities/workspace.entity' import { Workspace } from '../../enterprise/database/entities/workspace.entity'
import { WorkspaceUser } from '../../enterprise/database/entities/workspace-user.entity' import { WorkspaceUser } from '../../enterprise/database/entities/workspace-user.entity'
import { LoginMethod } from '../../enterprise/database/entities/login-method.entity' import { LoginMethod } from '../../enterprise/database/entities/login-method.entity'
import { LoginSession } from '../../enterprise/database/entities/login-session.entity'
export const entities = { export const entities = {
ChatFlow, ChatFlow,
@ -55,5 +56,6 @@ export const entities = {
OrganizationUser, OrganizationUser,
Workspace, Workspace,
WorkspaceUser, WorkspaceUser,
LoginMethod LoginMethod,
LoginSession
} }

View File

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

View File

@ -3,9 +3,13 @@ import { RedisStore } from 'connect-redis'
import { getDatabaseSSLFromEnv } from '../../../DataSource' import { getDatabaseSSLFromEnv } from '../../../DataSource'
import path from 'path' import path from 'path'
import { getUserHome } from '../../../utils' 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 redisClient: Redis | null = null
let redisStore: RedisStore | null = null let redisStore: RedisStore | null = null
let dbStore: Store | null = null
export const initializeRedisClientAndStore = (): RedisStore => { export const initializeRedisClientAndStore = (): RedisStore => {
if (!redisClient) { if (!redisClient) {
@ -35,6 +39,8 @@ export const initializeRedisClientAndStore = (): RedisStore => {
} }
export const initializeDBClientAndStore: any = () => { export const initializeDBClientAndStore: any = () => {
if (dbStore) return dbStore
const databaseType = process.env.DATABASE_TYPE || 'sqlite' const databaseType = process.env.DATABASE_TYPE || 'sqlite'
switch (databaseType) { switch (databaseType) {
case 'mysql': { case 'mysql': {
@ -51,7 +57,8 @@ export const initializeDBClientAndStore: any = () => {
tableName: 'login_sessions' tableName: 'login_sessions'
} }
} }
return new MySQLStore(options) dbStore = new MySQLStore(options)
return dbStore
} }
case 'mariadb': case 'mariadb':
/* TODO: Implement MariaDB session store */ /* TODO: Implement MariaDB session store */
@ -70,12 +77,13 @@ export const initializeDBClientAndStore: any = () => {
database: process.env.DATABASE_NAME, database: process.env.DATABASE_NAME,
ssl: getDatabaseSSLFromEnv() ssl: getDatabaseSSLFromEnv()
}) })
return new pgSession({ dbStore = new pgSession({
pool: pgPool, // Connection pool pool: pgPool, // Connection pool
tableName: 'login_sessions', tableName: 'login_sessions',
schemaName: 'public', schemaName: 'public',
createTableIfMissing: true createTableIfMissing: true
}) })
return dbStore
} }
case 'default': case 'default':
case 'sqlite': { case 'sqlite': {
@ -83,11 +91,93 @@ export const initializeDBClientAndStore: any = () => {
const sqlSession = require('connect-sqlite3')(expressSession) const sqlSession = require('connect-sqlite3')(expressSession)
let flowisePath = path.join(getUserHome(), '.flowise') let flowisePath = path.join(getUserHome(), '.flowise')
const homePath = process.env.DATABASE_PATH ?? flowisePath const homePath = process.env.DATABASE_PATH ?? flowisePath
return new sqlSession({ dbStore = new sqlSession({
db: 'database.sqlite', db: 'database.sqlite',
table: 'login_sessions', table: 'login_sessions',
dir: homePath 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<void> => {
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
}
}

View File

@ -81,6 +81,11 @@ const _initializePassportMiddleware = async (app: express.Application) => {
app.use(passport.initialize()) app.use(passport.initialize())
app.use(passport.session()) app.use(passport.session())
if (options.store) {
const appServer = getRunningExpressApp()
appServer.sessionStore = options.store
}
passport.serializeUser((user: any, done) => { passport.serializeUser((user: any, done) => {
done(null, user) done(null, user)
}) })

View File

@ -26,6 +26,7 @@ import { UserErrorMessage, UserService } from './user.service'
import { WorkspaceUserErrorMessage, WorkspaceUserService } from './workspace-user.service' import { WorkspaceUserErrorMessage, WorkspaceUserService } from './workspace-user.service'
import { WorkspaceErrorMessage, WorkspaceService } from './workspace.service' import { WorkspaceErrorMessage, WorkspaceService } from './workspace.service'
import { sanitizeUser } from '../../utils/sanitize.util' import { sanitizeUser } from '../../utils/sanitize.util'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'
type AccountDTO = { type AccountDTO = {
user: Partial<User> user: Partial<User>
@ -576,6 +577,9 @@ export class AccountService {
await queryRunner.startTransaction() await queryRunner.startTransaction()
data.user = await this.userService.saveUser(data.user, queryRunner) data.user = await this.userService.saveUser(data.user, queryRunner)
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
// Invalidate all sessions for this user after password reset
await destroyAllSessionsForUser(user.id as string)
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw error throw error

View File

@ -1,5 +1,4 @@
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import bcrypt from 'bcryptjs'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Telemetry, TelemetryEventType } from '../../utils/telemetry' import { Telemetry, TelemetryEventType } from '../../utils/telemetry'
@ -8,8 +7,9 @@ import { isInvalidEmail, isInvalidName, isInvalidPassword, isInvalidUUID } from
import { DataSource, ILike, QueryRunner } from 'typeorm' import { DataSource, ILike, QueryRunner } from 'typeorm'
import { generateId } from '../../utils' import { generateId } from '../../utils'
import { GeneralErrorMessage } from '../../utils/constants' 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 { sanitizeUser } from '../../utils/sanitize.util'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'
export const enum UserErrorMessage { export const enum UserErrorMessage {
EXPIRED_TEMP_TOKEN = 'Expired Temporary Token', EXPIRED_TEMP_TOKEN = 'Expired Temporary Token',
@ -24,7 +24,8 @@ export const enum UserErrorMessage {
USER_EMAIL_UNVERIFIED = 'User Email Unverified', USER_EMAIL_UNVERIFIED = 'User Email Unverified',
USER_NOT_FOUND = 'User Not Found', USER_NOT_FOUND = 'User Not Found',
USER_FOUND_MULTIPLE = 'User Found Multiple', 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 { export class UserService {
private telemetry: Telemetry private telemetry: Telemetry
@ -134,7 +135,7 @@ export class UserService {
return newUser return newUser
} }
public async updateUser(newUserData: Partial<User> & { password?: string }) { public async updateUser(newUserData: Partial<User> & { oldPassword?: string; newPassword?: string; confirmPassword?: string }) {
let queryRunner: QueryRunner | undefined let queryRunner: QueryRunner | undefined
let updatedUser: Partial<User> let updatedUser: Partial<User>
try { try {
@ -158,10 +159,18 @@ export class UserService {
this.validateUserStatus(newUserData.status) this.validateUserStatus(newUserData.status)
} }
if (newUserData.password) { if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmPassword) {
const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5')) if (!oldUserData.credential) {
// @ts-ignore throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_CREDENTIAL)
const hash = bcrypt.hashSync(newUserData.password, salt) }
// 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.credential = hash
newUserData.tempToken = '' newUserData.tempToken = ''
newUserData.tokenExpiry = undefined newUserData.tokenExpiry = undefined
@ -171,6 +180,11 @@ export class UserService {
await queryRunner.startTransaction() await queryRunner.startTransaction()
await this.saveUser(updatedUser, queryRunner) await this.saveUser(updatedUser, queryRunner)
await queryRunner.commitTransaction() 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) { } catch (error) {
if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction() if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction()
throw error throw error

View File

@ -73,6 +73,7 @@ export class App {
queueManager: QueueManager queueManager: QueueManager
redisSubscriber: RedisEventSubscriber redisSubscriber: RedisEventSubscriber
usageCacheManager: UsageCacheManager usageCacheManager: UsageCacheManager
sessionStore: any
constructor() { constructor() {
this.app = express() this.app = express()

View File

@ -52,7 +52,6 @@ import exportImportApi from '@/api/exportimport'
// Hooks // Hooks
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
import { getErrorMessage } from '@/utils/errorHandler' import { getErrorMessage } from '@/utils/errorHandler'
const dataToExport = [ const dataToExport = [
@ -215,7 +214,6 @@ const ProfileSection = ({ handleLogout }) => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
const { isCloud } = useConfig()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [aboutDialogOpen, setAboutDialogOpen] = useState(false) const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
@ -500,18 +498,18 @@ const ProfileSection = ({ handleLogout }) => {
</ListItemIcon> </ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Version</Typography>} /> <ListItemText primary={<Typography variant='body2'>Version</Typography>} />
</ListItemButton> </ListItemButton>
{isAuthenticated && !currentUser.isSSO && !isCloud && ( {isAuthenticated && !currentUser.isSSO && (
<ListItemButton <ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }} sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={() => { onClick={() => {
setOpen(false) setOpen(false)
navigate('/user-profile') navigate('/account')
}} }}
> >
<ListItemIcon> <ListItemIcon>
<IconUserEdit stroke={1.5} size='1.3rem' /> <IconUserEdit stroke={1.5} size='1.3rem' />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Update Profile</Typography>} /> <ListItemText primary={<Typography variant='body2'>Account Settings</Typography>} />
</ListItemButton> </ListItemButton>
)} )}
<ListItemButton <ListItemButton

View File

@ -50,7 +50,6 @@ const Evaluators = Loadable(lazy(() => import('@/views/evaluators')))
// account routing // account routing
const Account = Loadable(lazy(() => import('@/views/account'))) const Account = Loadable(lazy(() => import('@/views/account')))
const UserProfile = Loadable(lazy(() => import('@/views/account/UserProfile')))
// files routing // files routing
const Files = Loadable(lazy(() => import('@/views/files'))) const Files = Loadable(lazy(() => import('@/views/files')))
@ -294,11 +293,7 @@ const MainRoutes = {
}, },
{ {
path: '/account', path: '/account',
element: ( element: <Account />
<RequireAuth display={'feat:account'}>
<Account />
</RequireAuth>
)
}, },
{ {
path: '/users', path: '/users',
@ -308,10 +303,6 @@ const MainRoutes = {
</RequireAuth> </RequireAuth>
) )
}, },
{
path: '/user-profile',
element: <UserProfile />
},
{ {
path: '/roles', path: '/roles',
element: ( element: (

View File

@ -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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
} 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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
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 (
<>
<MainCard>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader search={false} title='Settings' />
{authErrors && authErrors.length > 0 && (
<div
style={{
position: 'relative',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
paddingTop: 15,
marginTop: 10,
marginBottom: 10
}}
>
<Box sx={{ p: 2 }}>
<IconAlertTriangle size={25} color='orange' />
</Box>
<Stack flexDirection='column'>
<span style={{ color: 'rgb(116,66,16)' }}>
<ul>
{authErrors.map((msg, key) => (
<strong key={key}>
<li>{msg}</li>
</strong>
))}
</ul>
</span>
</Stack>
</div>
)}
<SettingsSection
action={
<StyledButton variant='contained' style={{ borderRadius: 2, height: 40 }} onClick={validateAndSubmit}>
Save
</StyledButton>
}
title='Profile'
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: gridSpacing,
px: 2.5,
py: 2
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>Email</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
id='email'
type='string'
fullWidth
size='small'
placeholder='Your login Id'
name='name'
onChange={(e) => setEmailVal(e.target.value)}
value={emailVal}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Full Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
id='name'
type='string'
fullWidth
size='small'
placeholder='Your Name'
name='name'
onChange={(e) => setUsernameVal(e.target.value)}
value={usernameVal}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
New Password<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
id='np'
type='password'
fullWidth
size='small'
name='new_password'
onChange={(e) => setNewPasswordVal(e.target.value)}
value={newPasswordVal}
/>
<Typography variant='caption'>
<i>
Password must be at least 8 characters long and contain at least one lowercase letter, one
uppercase letter, one digit, and one special character.
</i>
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Confirm Password<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
id='npc'
type='password'
fullWidth
size='small'
name='new_cnf_password'
onChange={(e) => setConfirmPasswordVal(e.target.value)}
value={confirmPasswordVal}
/>
<Typography variant='caption'>
<i>Retype your new password. Must match the password typed above.</i>
</Typography>
</Box>
</Box>
</SettingsSection>
</Stack>
)}
</MainCard>
{loading && <BackdropLoader open={loading} />}
</>
)
}
export default UserProfile

View File

@ -25,7 +25,6 @@ import {
import { darken, useTheme } from '@mui/material/styles' import { darken, useTheme } from '@mui/material/styles'
// project imports // project imports
import ErrorBoundary from '@/ErrorBoundary'
import ViewHeader from '@/layout/MainLayout/ViewHeader' import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { StyledButton } from '@/ui-component/button/StyledButton' import { StyledButton } from '@/ui-component/button/StyledButton'
import MainCard from '@/ui-component/cards/MainCard' 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 { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
import { gridSpacing } from '@/store/constant' import { gridSpacing } from '@/store/constant'
import { useConfig } from '@/store/context/ConfigContext' import { useConfig } from '@/store/context/ConfigContext'
import { useError } from '@/store/context/ErrorContext' import { logoutSuccess, userProfileUpdated } from '@/store/reducers/authSlice'
import { userProfileUpdated } from '@/store/reducers/authSlice'
// ==============================|| ACCOUNT SETTINGS ||============================== // // ==============================|| ACCOUNT SETTINGS ||============================== //
@ -66,12 +64,12 @@ const AccountSettings = () => {
const currentUser = useSelector((state) => state.auth.user) const currentUser = useSelector((state) => state.auth.user)
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
const { error, setError } = useError()
const { isCloud } = useConfig() const { isCloud } = useConfig()
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true)
const [profileName, setProfileName] = useState('') const [profileName, setProfileName] = useState('')
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('') const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('')
const [usage, setUsage] = useState(null) const [usage, setUsage] = useState(null)
@ -104,10 +102,19 @@ const AccountSettings = () => {
const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource) const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource)
const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats) const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats)
const getCurrentUsageApi = useApi(userApi.getCurrentUsage) 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(() => { useEffect(() => {
if (isCloud) { if (isCloud) {
getUserByIdApi.request(currentUser.id)
getPricingPlansApi.request() getPricingPlansApi.request()
getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId) getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId)
getCurrentUsageApi.request() getCurrentUsageApi.request()
@ -136,6 +143,17 @@ const AccountSettings = () => {
} }
}, [getCurrentUsageApi.data]) }, [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(() => { useEffect(() => {
if (openRemoveSeatsDialog || openAddSeatsDialog) { if (openRemoveSeatsDialog || openAddSeatsDialog) {
setSeatsQuantity(0) setSeatsQuantity(0)
@ -219,7 +237,6 @@ const AccountSettings = () => {
}) })
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to update profile: ${ message: `Failed to update profile: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@ -241,6 +258,9 @@ const AccountSettings = () => {
const savePassword = async () => { const savePassword = async () => {
try { try {
const validationErrors = [] const validationErrors = []
if (!oldPassword) {
validationErrors.push('Old Password cannot be left blank')
}
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
validationErrors.push('New Password and Confirm Password do not match') validationErrors.push('New Password and Confirm Password do not match')
} }
@ -267,11 +287,17 @@ const AccountSettings = () => {
const obj = { const obj = {
id: currentUser.id, id: currentUser.id,
password: newPassword oldPassword,
newPassword,
confirmPassword
} }
const saveProfileResp = await userApi.updateUser(obj) const saveProfileResp = await userApi.updateUser(obj)
if (saveProfileResp.data) { if (saveProfileResp.data) {
store.dispatch(userProfileUpdated(saveProfileResp.data)) store.dispatch(userProfileUpdated(saveProfileResp.data))
setOldPassword('')
setNewPassword('')
setConfirmPassword('')
await logoutApi.request()
enqueueSnackbar({ enqueueSnackbar({
message: 'Password updated', message: 'Password updated',
options: { options: {
@ -286,7 +312,6 @@ const AccountSettings = () => {
}) })
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to update password: ${ message: `Failed to update password: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data typeof error.response.data === 'object' ? error.response.data.message : error.response.data
@ -388,287 +413,345 @@ const AccountSettings = () => {
return ( return (
<MainCard maxWidth='md'> <MainCard maxWidth='md'>
{error ? ( <Stack flexDirection='column' sx={{ gap: 4 }}>
<ErrorBoundary error={error} /> <ViewHeader title='Account Settings' />
) : ( {isLoading && !getUserByIdApi.data ? (
<Stack flexDirection='column' sx={{ gap: 4 }}> <Box display='flex' flexDirection='column' gap={gridSpacing}>
<ViewHeader title='Account Settings' /> <Skeleton width='25%' height={32} />
{isLoading && !getUserByIdApi.data ? ( <Box display='flex' flexDirection='column' gap={2}>
<Box display='flex' flexDirection='column' gap={gridSpacing}> <Skeleton width='20%' />
<Skeleton width='25%' height={32} /> <Skeleton variant='rounded' height={56} />
<Box display='flex' flexDirection='column' gap={2}>
<Skeleton width='20%' />
<Skeleton variant='rounded' height={56} />
</Box>
<Box display='flex' flexDirection='column' gap={2}>
<Skeleton width='20%' />
<Skeleton variant='rounded' height={56} />
</Box>
<Box display='flex' flexDirection='column' gap={2}>
<Skeleton width='20%' />
<Skeleton variant='rounded' height={56} />
</Box>
</Box> </Box>
) : ( <Box display='flex' flexDirection='column' gap={2}>
<> <Skeleton width='20%' />
<SettingsSection title='Subscription & Billing'> <Skeleton variant='rounded' height={56} />
<Box </Box>
sx={{ <Box display='flex' flexDirection='column' gap={2}>
width: '100%', <Skeleton width='20%' />
display: 'grid', <Skeleton variant='rounded' height={56} />
gridTemplateColumns: 'repeat(3, 1fr)' </Box>
}} </Box>
> ) : (
<>
{isCloud && (
<>
<SettingsSection title='Subscription & Billing'>
<Box <Box
sx={{ sx={{
gridColumn: 'span 2 / span 2', width: '100%',
display: 'flex', display: 'grid',
flexDirection: 'column', gridTemplateColumns: 'repeat(3, 1fr)'
alignItems: 'start',
justifyContent: 'center',
gap: 1,
px: 2.5,
py: 2
}} }}
> >
{currentPlanTitle && ( <Box
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Current Organization Plan:</Typography>
<Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
{currentPlanTitle.toUpperCase()}
</Typography>
</Stack>
)}
<Typography
sx={{ opacity: customization.isDarkMode ? 0.7 : 1 }}
variant='body2'
color='text.secondary'
>
Update your billing details and subscription
</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
px: 2.5,
py: 2,
gap: 2
}}
>
<Button
variant='outlined'
endIcon={!isBillingLoading && <IconExternalLink />}
disabled={!currentUser.isOrganizationAdmin || isBillingLoading}
onClick={handleBillingPortalClick}
sx={{ borderRadius: 2, height: 40 }}
>
{isBillingLoading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} color='inherit' />
Loading
</Box>
) : (
'Billing'
)}
</Button>
<Button
variant='contained'
sx={{ sx={{
mr: 1, gridColumn: 'span 2 / span 2',
ml: 2, display: 'flex',
minWidth: 160, flexDirection: 'column',
height: 40, alignItems: 'start',
borderRadius: 15, justifyContent: 'center',
background: (theme) => gap: 1,
`linear-gradient(90deg, ${theme.palette.primary.main} 10%, ${theme.palette.secondary.main} 100%)`, px: 2.5,
color: (theme) => theme.palette.secondary.contrastText, py: 2
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.3s ease',
'&:hover': {
background: (theme) =>
`linear-gradient(90deg, ${darken(theme.palette.primary.main, 0.1)} 10%, ${darken(
theme.palette.secondary.main,
0.1
)} 100%)`,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
}
}} }}
endIcon={<IconSparkles />}
disabled={!currentUser.isOrganizationAdmin}
onClick={() => setOpenPricingDialog(true)}
> >
Change Plan {currentPlanTitle && (
</Button> <Stack sx={{ alignItems: 'center' }} flexDirection='row'>
</Box> <Typography variant='body2'>Current Organization Plan:</Typography>
</Box> <Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
</SettingsSection> {currentPlanTitle.toUpperCase()}
<SettingsSection title='Seats'> </Typography>
<Box </Stack>
sx={{ )}
width: '100%', <Typography
display: 'grid', sx={{ opacity: customization.isDarkMode ? 0.7 : 1 }}
gridTemplateColumns: 'repeat(3, 1fr)' variant='body2'
}} color='text.secondary'
> >
<Box Update your billing details and subscription
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'center',
gap: 1,
px: 2.5,
py: 2
}}
>
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Seats Included in Plan:</Typography>
<Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'>
{getAdditionalSeatsQuantityApi.loading ? <CircularProgress size={16} /> : includedSeats}
</Typography> </Typography>
</Stack> </Box>
<Stack sx={{ alignItems: 'center' }} flexDirection='row'> <Box
<Typography variant='body2'>Additional Seats Purchased:</Typography> sx={{
<Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'> display: 'flex',
{getAdditionalSeatsQuantityApi.loading ? <CircularProgress size={16} /> : purchasedSeats} alignItems: 'center',
</Typography> justifyContent: 'end',
</Stack> px: 2.5,
<Stack sx={{ alignItems: 'center' }} flexDirection='row'> py: 2,
<Typography variant='body2'>Occupied Seats:</Typography> gap: 2
<Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'> }}
{getAdditionalSeatsQuantityApi.loading ? ( >
<CircularProgress size={16} />
) : (
`${occupiedSeats}/${totalSeats}`
)}
</Typography>
</Stack>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
gap: 2,
px: 2.5,
py: 2
}}
>
{getAdditionalSeatsQuantityApi.data?.quantity > 0 && currentPlanTitle.toUpperCase() === 'PRO' && (
<Button <Button
variant='outlined' variant='outlined'
disabled={!currentUser.isOrganizationAdmin || !getAdditionalSeatsQuantityApi.data?.quantity} endIcon={!isBillingLoading && <IconExternalLink />}
onClick={() => { disabled={!currentUser.isOrganizationAdmin || isBillingLoading}
setOpenRemoveSeatsDialog(true) onClick={handleBillingPortalClick}
}}
color='error'
sx={{ borderRadius: 2, height: 40 }} sx={{ borderRadius: 2, height: 40 }}
> >
Remove Seats {isBillingLoading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} color='inherit' />
Loading
</Box>
) : (
'Billing'
)}
</Button> </Button>
)} <Button
<StyledButton variant='contained'
variant='contained' sx={{
disabled={!currentUser.isOrganizationAdmin} mr: 1,
onClick={() => { ml: 2,
if (currentPlanTitle.toUpperCase() === 'PRO') { minWidth: 160,
setOpenAddSeatsDialog(true) height: 40,
} else { borderRadius: 15,
setOpenPricingDialog(true) background: (theme) =>
} `linear-gradient(90deg, ${theme.palette.primary.main} 10%, ${theme.palette.secondary.main} 100%)`,
color: (theme) => theme.palette.secondary.contrastText,
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.3s ease',
'&:hover': {
background: (theme) =>
`linear-gradient(90deg, ${darken(
theme.palette.primary.main,
0.1
)} 10%, ${darken(theme.palette.secondary.main, 0.1)} 100%)`,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
}
}}
endIcon={<IconSparkles />}
disabled={!currentUser.isOrganizationAdmin}
onClick={() => setOpenPricingDialog(true)}
>
Change Plan
</Button>
</Box>
</Box>
</SettingsSection>
<SettingsSection title='Seats'>
<Box
sx={{
width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)'
}}
>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'center',
gap: 1,
px: 2.5,
py: 2
}} }}
title='Add Seats is available only for PRO plan'
sx={{ borderRadius: 2, height: 40 }}
> >
Add Seats <Stack sx={{ alignItems: 'center' }} flexDirection='row'>
</StyledButton> <Typography variant='body2'>Seats Included in Plan:</Typography>
</Box> <Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'>
</Box> {getAdditionalSeatsQuantityApi.loading ? <CircularProgress size={16} /> : includedSeats}
</SettingsSection> </Typography>
<SettingsSection title='Usage'> </Stack>
<Box <Stack sx={{ alignItems: 'center' }} flexDirection='row'>
sx={{ <Typography variant='body2'>Additional Seats Purchased:</Typography>
width: '100%', <Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
display: 'grid', {getAdditionalSeatsQuantityApi.loading ? (
gridTemplateColumns: 'repeat(2, 1fr)' <CircularProgress size={16} />
}} ) : (
> purchasedSeats
<Box sx={{ p: 2.5, borderRight: 1, borderColor: theme.palette.grey[900] + 25 }}> )}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> </Typography>
<Typography variant='h3'>Predictions</Typography> </Stack>
<Typography variant='body2' color='text.secondary'> <Stack sx={{ alignItems: 'center' }} flexDirection='row'>
{`${usage?.predictions?.usage || 0} / ${usage?.predictions?.limit || 0}`} <Typography variant='body2'>Occupied Seats:</Typography>
</Typography> <Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'>
{getAdditionalSeatsQuantityApi.loading ? (
<CircularProgress size={16} />
) : (
`${occupiedSeats}/${totalSeats}`
)}
</Typography>
</Stack>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}> <Box
<Box sx={{ width: '100%', mr: 1 }}> sx={{
<LinearProgress display: 'flex',
sx={{ alignItems: 'center',
height: 10, justifyContent: 'end',
borderRadius: 5, gap: 2,
'& .MuiLinearProgress-bar': { px: 2.5,
backgroundColor: (theme) => { py: 2
if (predictionsUsageInPercent > 90) return theme.palette.error.main }}
if (predictionsUsageInPercent > 75) return theme.palette.warning.main >
if (predictionsUsageInPercent > 50) return theme.palette.success.light {getAdditionalSeatsQuantityApi.data?.quantity > 0 &&
return theme.palette.success.main currentPlanTitle.toUpperCase() === 'PRO' && (
} <Button
variant='outlined'
disabled={
!currentUser.isOrganizationAdmin ||
!getAdditionalSeatsQuantityApi.data?.quantity
} }
}} onClick={() => {
value={predictionsUsageInPercent > 100 ? 100 : predictionsUsageInPercent} setOpenRemoveSeatsDialog(true)
variant='determinate' }}
/> color='error'
</Box> sx={{ borderRadius: 2, height: 40 }}
<Typography variant='body2' color='text.secondary'>{`${predictionsUsageInPercent.toFixed( >
2 Remove Seats
)}%`}</Typography> </Button>
)}
<StyledButton
variant='contained'
disabled={!currentUser.isOrganizationAdmin}
onClick={() => {
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
</StyledButton>
</Box> </Box>
</Box> </Box>
<Box sx={{ p: 2.5 }}> </SettingsSection>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <SettingsSection title='Usage'>
<Typography variant='h3'>Storage</Typography> <Box
<Typography variant='body2' color='text.secondary'> sx={{
{`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed( width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)'
}}
>
<Box sx={{ p: 2.5, borderRight: 1, borderColor: theme.palette.grey[900] + 25 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant='h3'>Predictions</Typography>
<Typography variant='body2' color='text.secondary'>
{`${usage?.predictions?.usage || 0} / ${usage?.predictions?.limit || 0}`}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress
sx={{
height: 10,
borderRadius: 5,
'& .MuiLinearProgress-bar': {
backgroundColor: (theme) => {
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'
/>
</Box>
<Typography variant='body2' color='text.secondary'>{`${predictionsUsageInPercent.toFixed(
2 2
)}MB`} )}%`}</Typography>
</Typography> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}> <Box sx={{ p: 2.5 }}>
<Box sx={{ width: '100%', mr: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<LinearProgress <Typography variant='h3'>Storage</Typography>
sx={{ <Typography variant='body2' color='text.secondary'>
height: 10, {`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed(
borderRadius: 5, 2
'& .MuiLinearProgress-bar': { )}MB`}
backgroundColor: (theme) => { </Typography>
if (storageUsageInPercent > 90) return theme.palette.error.main </Box>
if (storageUsageInPercent > 75) return theme.palette.warning.main <Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
if (storageUsageInPercent > 50) return theme.palette.success.light <Box sx={{ width: '100%', mr: 1 }}>
return theme.palette.success.main <LinearProgress
} sx={{
} height: 10,
}} borderRadius: 5,
value={storageUsageInPercent > 100 ? 100 : storageUsageInPercent} '& .MuiLinearProgress-bar': {
variant='determinate' backgroundColor: (theme) => {
/> 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'
/>
</Box>
<Typography variant='body2' color='text.secondary'>{`${storageUsageInPercent.toFixed(
2
)}%`}</Typography>
</Box> </Box>
<Typography variant='body2' color='text.secondary'>{`${storageUsageInPercent.toFixed(
2
)}%`}</Typography>
</Box> </Box>
</Box> </Box>
</SettingsSection>
</>
)}
<SettingsSection
action={
<StyledButton onClick={saveProfileData} sx={{ borderRadius: 2, height: 40 }} variant='contained'>
Save
</StyledButton>
}
title='Profile'
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: gridSpacing,
px: 2.5,
py: 2
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant='body1'>Name</Typography>
<OutlinedInput
id='name'
type='string'
fullWidth
placeholder='Your Name'
name='name'
onChange={(e) => setProfileName(e.target.value)}
value={profileName}
/>
</Box> </Box>
</SettingsSection> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant='body1'>Email Address</Typography>
<OutlinedInput
id='email'
type='string'
fullWidth
placeholder='Email Address'
name='email'
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
</Box>
</Box>
</SettingsSection>
{!currentUser.isSSO && (
<SettingsSection <SettingsSection
action={ action={
<StyledButton onClick={saveProfileData} sx={{ borderRadius: 2, height: 40 }} variant='contained'> <StyledButton
disabled={!oldPassword || !newPassword || !confirmPassword || newPassword !== confirmPassword}
onClick={savePassword}
sx={{ borderRadius: 2, height: 40 }}
variant='contained'
>
Save Save
</StyledButton> </StyledButton>
} }
title='Profile' title='Security'
> >
<Box <Box
sx={{ sx={{
@ -679,106 +762,75 @@ const AccountSettings = () => {
py: 2 py: 2
}} }}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box
<Typography variant='body1'>Name</Typography> sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>Old Password</Typography>
<OutlinedInput <OutlinedInput
id='name' id='oldPassword'
type='string' type='password'
fullWidth fullWidth
placeholder='Your Name' placeholder='Old Password'
name='name' name='oldPassword'
onChange={(e) => setProfileName(e.target.value)} onChange={(e) => setOldPassword(e.target.value)}
value={profileName} value={oldPassword}
/> />
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box
<Typography variant='body1'>Email Address</Typography> sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>New Password</Typography>
<OutlinedInput <OutlinedInput
id='email' id='newPassword'
type='string' type='password'
fullWidth fullWidth
placeholder='Email Address' placeholder='New Password'
name='email' name='newPassword'
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
value={email} value={newPassword}
/>
<Typography variant='caption'>
<i>
Password must be at least 8 characters long and contain at least one lowercase letter, one
uppercase letter, one digit, and one special character.
</i>
</Typography>
</Box>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>Confirm New Password</Typography>
<OutlinedInput
id='confirmPassword'
type='password'
fullWidth
placeholder='Confirm New Password'
name='confirmPassword'
onChange={(e) => setConfirmPassword(e.target.value)}
value={confirmPassword}
/> />
</Box> </Box>
</Box> </Box>
</SettingsSection> </SettingsSection>
{!currentUser.isSSO && ( )}
<SettingsSection </>
action={ )}
<StyledButton </Stack>
disabled={!newPassword || !confirmPassword || newPassword !== confirmPassword}
onClick={savePassword}
sx={{ borderRadius: 2, height: 40 }}
variant='contained'
>
Save
</StyledButton>
}
title='Security'
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: gridSpacing,
px: 2.5,
py: 2
}}
>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>New Password</Typography>
<OutlinedInput
id='newPassword'
type='password'
fullWidth
placeholder='New Password'
name='newPassword'
onChange={(e) => setNewPassword(e.target.value)}
value={newPassword}
/>
<Typography variant='caption'>
<i>
Password must be at least 8 characters long and contain at least one lowercase letter,
one uppercase letter, one digit, and one special character.
</i>
</Typography>
</Box>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>Confirm Password</Typography>
<OutlinedInput
id='confirmPassword'
type='password'
fullWidth
placeholder='Confirm Password'
name='confirmPassword'
onChange={(e) => setConfirmPassword(e.target.value)}
value={confirmPassword}
/>
</Box>
</Box>
</SettingsSection>
)}
</>
)}
</Stack>
)}
{openPricingDialog && isCloud && ( {openPricingDialog && isCloud && (
<PricingDialog <PricingDialog
open={openPricingDialog} open={openPricingDialog}