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
11 changed files with 546 additions and 670 deletions
@@ -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
+1 -10
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: (
@@ -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
+401 -349
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,287 +413,345 @@ const AccountSettings = () => {
return (
<MainCard maxWidth='md'>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 4 }}>
<ViewHeader title='Account Settings' />
{isLoading && !getUserByIdApi.data ? (
<Box display='flex' flexDirection='column' gap={gridSpacing}>
<Skeleton width='25%' height={32} />
<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>
<Stack flexDirection='column' sx={{ gap: 4 }}>
<ViewHeader title='Account Settings' />
{isLoading && !getUserByIdApi.data ? (
<Box display='flex' flexDirection='column' gap={gridSpacing}>
<Skeleton width='25%' height={32} />
<Box display='flex' flexDirection='column' gap={2}>
<Skeleton width='20%' />
<Skeleton variant='rounded' height={56} />
</Box>
) : (
<>
<SettingsSection title='Subscription & Billing'>
<Box
sx={{
width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)'
}}
>
<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>
) : (
<>
{isCloud && (
<>
<SettingsSection title='Subscription & Billing'>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'center',
gap: 1,
px: 2.5,
py: 2
width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)'
}}
>
{currentPlanTitle && (
<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'
<Box
sx={{
mr: 1,
ml: 2,
minWidth: 160,
height: 40,
borderRadius: 15,
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)'
}
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'center',
gap: 1,
px: 2.5,
py: 2
}}
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
}}
>
<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}
{currentPlanTitle && (
<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>
</Stack>
<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}
</Typography>
</Stack>
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Occupied Seats:</Typography>
<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' && (
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
px: 2.5,
py: 2,
gap: 2
}}
>
<Button
variant='outlined'
disabled={!currentUser.isOrganizationAdmin || !getAdditionalSeatsQuantityApi.data?.quantity}
onClick={() => {
setOpenRemoveSeatsDialog(true)
}}
color='error'
endIcon={!isBillingLoading && <IconExternalLink />}
disabled={!currentUser.isOrganizationAdmin || isBillingLoading}
onClick={handleBillingPortalClick}
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>
)}
<StyledButton
variant='contained'
disabled={!currentUser.isOrganizationAdmin}
onClick={() => {
if (currentPlanTitle.toUpperCase() === 'PRO') {
setOpenAddSeatsDialog(true)
} else {
setOpenPricingDialog(true)
}
<Button
variant='contained'
sx={{
mr: 1,
ml: 2,
minWidth: 160,
height: 40,
borderRadius: 15,
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
</StyledButton>
</Box>
</Box>
</SettingsSection>
<SettingsSection title='Usage'>
<Box
sx={{
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>
<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>
</Stack>
<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
)}
</Typography>
</Stack>
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Occupied Seats:</Typography>
<Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'>
{getAdditionalSeatsQuantityApi.loading ? (
<CircularProgress size={16} />
) : (
`${occupiedSeats}/${totalSeats}`
)}
</Typography>
</Stack>
</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
}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
gap: 2,
px: 2.5,
py: 2
}}
>
{getAdditionalSeatsQuantityApi.data?.quantity > 0 &&
currentPlanTitle.toUpperCase() === 'PRO' && (
<Button
variant='outlined'
disabled={
!currentUser.isOrganizationAdmin ||
!getAdditionalSeatsQuantityApi.data?.quantity
}
}}
value={predictionsUsageInPercent > 100 ? 100 : predictionsUsageInPercent}
variant='determinate'
/>
</Box>
<Typography variant='body2' color='text.secondary'>{`${predictionsUsageInPercent.toFixed(
2
)}%`}</Typography>
onClick={() => {
setOpenRemoveSeatsDialog(true)
}}
color='error'
sx={{ borderRadius: 2, height: 40 }}
>
Remove Seats
</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 sx={{ p: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant='h3'>Storage</Typography>
<Typography variant='body2' color='text.secondary'>
{`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed(
</SettingsSection>
<SettingsSection title='Usage'>
<Box
sx={{
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
)}MB`}
</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 (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'
/>
)}%`}</Typography>
</Box>
</Box>
<Box sx={{ p: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant='h3'>Storage</Typography>
<Typography variant='body2' color='text.secondary'>
{`${(usage?.storage?.usage || 0).toFixed(2)}MB / ${(usage?.storage?.limit || 0).toFixed(
2
)}MB`}
</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 (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>
<Typography variant='body2' color='text.secondary'>{`${storageUsageInPercent.toFixed(
2
)}%`}</Typography>
</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>
</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
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
</StyledButton>
}
title='Profile'
title='Security'
>
<Box
sx={{
@@ -679,106 +762,75 @@ const AccountSettings = () => {
py: 2
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant='body1'>Name</Typography>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>Old Password</Typography>
<OutlinedInput
id='name'
type='string'
id='oldPassword'
type='password'
fullWidth
placeholder='Your Name'
name='name'
onChange={(e) => setProfileName(e.target.value)}
value={profileName}
placeholder='Old Password'
name='oldPassword'
onChange={(e) => setOldPassword(e.target.value)}
value={oldPassword}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant='body1'>Email Address</Typography>
<Box
sx={{
gridColumn: 'span 2 / span 2',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Typography variant='body1'>New Password</Typography>
<OutlinedInput
id='email'
type='string'
id='newPassword'
type='password'
fullWidth
placeholder='Email Address'
name='email'
onChange={(e) => setEmail(e.target.value)}
value={email}
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 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>
</SettingsSection>
{!currentUser.isSSO && (
<SettingsSection
action={
<StyledButton
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>
)}
)}
</>
)}
</Stack>
{openPricingDialog && isCloud && (
<PricingDialog
open={openPricingDialog}