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:
parent
1ae1638ed9
commit
eed7581d0e
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: (
|
||||||
|
|
|
||||||
|
|
@ -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' }}> *</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' }}> *</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' }}> *</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
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue