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

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 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<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.session())
if (options.store) {
const appServer = getRunningExpressApp()
appServer.sessionStore = options.store
}
passport.serializeUser((user: any, done) => {
done(null, user)
})

View File

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

View File

@ -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<User> & { password?: string }) {
public async updateUser(newUserData: Partial<User> & { oldPassword?: string; newPassword?: string; confirmPassword?: string }) {
let queryRunner: QueryRunner | undefined
let updatedUser: Partial<User>
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

View File

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

View File

@ -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 }) => {
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Version</Typography>} />
</ListItemButton>
{isAuthenticated && !currentUser.isSSO && !isCloud && (
{isAuthenticated && !currentUser.isSSO && (
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={() => {
setOpen(false)
navigate('/user-profile')
navigate('/account')
}}
>
<ListItemIcon>
<IconUserEdit stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Update Profile</Typography>} />
<ListItemText primary={<Typography variant='body2'>Account Settings</Typography>} />
</ListItemButton>
)}
<ListItemButton

View File

@ -50,7 +50,6 @@ const Evaluators = Loadable(lazy(() => 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: (
<RequireAuth display={'feat:account'}>
<Account />
</RequireAuth>
)
element: <Account />
},
{
path: '/users',
@ -308,10 +303,6 @@ const MainRoutes = {
</RequireAuth>
)
},
{
path: '/user-profile',
element: <UserProfile />
},
{
path: '/roles',
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'
// 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,9 +413,6 @@ const AccountSettings = () => {
return (
<MainCard maxWidth='md'>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 4 }}>
<ViewHeader title='Account Settings' />
{isLoading && !getUserByIdApi.data ? (
@ -410,6 +432,8 @@ const AccountSettings = () => {
</Box>
</Box>
) : (
<>
{isCloud && (
<>
<SettingsSection title='Subscription & Billing'>
<Box
@ -488,10 +512,10 @@ const AccountSettings = () => {
transition: 'all 0.3s ease',
'&:hover': {
background: (theme) =>
`linear-gradient(90deg, ${darken(theme.palette.primary.main, 0.1)} 10%, ${darken(
theme.palette.secondary.main,
`linear-gradient(90deg, ${darken(
theme.palette.primary.main,
0.1
)} 100%)`,
)} 10%, ${darken(theme.palette.secondary.main, 0.1)} 100%)`,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
}
}}
@ -533,7 +557,11 @@ const AccountSettings = () => {
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Additional Seats Purchased:</Typography>
<Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
{getAdditionalSeatsQuantityApi.loading ? <CircularProgress size={16} /> : purchasedSeats}
{getAdditionalSeatsQuantityApi.loading ? (
<CircularProgress size={16} />
) : (
purchasedSeats
)}
</Typography>
</Stack>
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
@ -557,10 +585,14 @@ const AccountSettings = () => {
py: 2
}}
>
{getAdditionalSeatsQuantityApi.data?.quantity > 0 && currentPlanTitle.toUpperCase() === 'PRO' && (
{getAdditionalSeatsQuantityApi.data?.quantity > 0 &&
currentPlanTitle.toUpperCase() === 'PRO' && (
<Button
variant='outlined'
disabled={!currentUser.isOrganizationAdmin || !getAdditionalSeatsQuantityApi.data?.quantity}
disabled={
!currentUser.isOrganizationAdmin ||
!getAdditionalSeatsQuantityApi.data?.quantity
}
onClick={() => {
setOpenRemoveSeatsDialog(true)
}}
@ -662,6 +694,8 @@ const AccountSettings = () => {
</Box>
</Box>
</SettingsSection>
</>
)}
<SettingsSection
action={
<StyledButton onClick={saveProfileData} sx={{ borderRadius: 2, height: 40 }} variant='contained'>
@ -709,7 +743,7 @@ const AccountSettings = () => {
<SettingsSection
action={
<StyledButton
disabled={!newPassword || !confirmPassword || newPassword !== confirmPassword}
disabled={!oldPassword || !newPassword || !confirmPassword || newPassword !== confirmPassword}
onClick={savePassword}
sx={{ borderRadius: 2, height: 40 }}
variant='contained'
@ -728,6 +762,25 @@ const AccountSettings = () => {
py: 2
}}
>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>Old Password</Typography>
<OutlinedInput
id='oldPassword'
type='password'
fullWidth
placeholder='Old Password'
name='oldPassword'
onChange={(e) => setOldPassword(e.target.value)}
value={oldPassword}
/>
</Box>
<Box
sx={{
gridColumn: 'span 2 / span 2',
@ -748,8 +801,8 @@ const AccountSettings = () => {
/>
<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.
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>
@ -761,12 +814,12 @@ const AccountSettings = () => {
gap: 1
}}
>
<Typography variant='body1'>Confirm Password</Typography>
<Typography variant='body1'>Confirm New Password</Typography>
<OutlinedInput
id='confirmPassword'
type='password'
fullWidth
placeholder='Confirm Password'
placeholder='Confirm New Password'
name='confirmPassword'
onChange={(e) => setConfirmPassword(e.target.value)}
value={confirmPassword}
@ -778,7 +831,6 @@ const AccountSettings = () => {
</>
)}
</Stack>
)}
{openPricingDialog && isCloud && (
<PricingDialog
open={openPricingDialog}