Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions packages/server/src/enterprise/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { StatusCodes } from 'http-status-codes'
import bcrypt from 'bcryptjs'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Telemetry, TelemetryEventType } from '../../utils/telemetry'
Expand All @@ -8,7 +7,7 @@ import { isInvalidEmail, isInvalidName, isInvalidPassword, isInvalidUUID } from
import { DataSource, ILike, QueryRunner } from 'typeorm'
import { generateId } from '../../utils'
import { GeneralErrorMessage } from '../../utils/constants'
import { getHash } from '../utils/encryption.util'
import { compareHash, getHash } from '../utils/encryption.util'
import { sanitizeUser } from '../../utils/sanitize.util'

export const enum UserErrorMessage {
Expand All @@ -24,7 +23,8 @@ export const enum UserErrorMessage {
USER_EMAIL_UNVERIFIED = 'User Email Unverified',
USER_NOT_FOUND = 'User Not Found',
USER_FOUND_MULTIPLE = 'User Found Multiple',
INCORRECT_USER_EMAIL_OR_CREDENTIALS = 'Incorrect Email or Password'
INCORRECT_USER_EMAIL_OR_CREDENTIALS = 'Incorrect Email or Password',
PASSWORDS_DO_NOT_MATCH = 'Passwords do not match'
}
export class UserService {
private telemetry: Telemetry
Expand Down Expand Up @@ -134,7 +134,7 @@ export class UserService {
return newUser
}

public async updateUser(newUserData: Partial<User> & { password?: string }) {
public async updateUser(newUserData: Partial<User> & { oldPassword?: string; newPassword?: string; confirmPassword?: string }) {
let queryRunner: QueryRunner | undefined
let updatedUser: Partial<User>
try {
Expand All @@ -158,10 +158,18 @@ export class UserService {
this.validateUserStatus(newUserData.status)
}

if (newUserData.password) {
const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5'))
// @ts-ignore
const hash = bcrypt.hashSync(newUserData.password, salt)
if (newUserData.oldPassword && newUserData.newPassword && newUserData.confirmPassword) {
if (!oldUserData.credential) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_CREDENTIAL)
}
// verify old password
if (!compareHash(newUserData.oldPassword, oldUserData.credential)) {
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, UserErrorMessage.INVALID_USER_CREDENTIAL)
}
if (newUserData.newPassword !== newUserData.confirmPassword) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.PASSWORDS_DO_NOT_MATCH)
}
const hash = getHash(newUserData.newPassword)
newUserData.credential = hash
newUserData.tempToken = ''
newUserData.tokenExpiry = undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@
const theme = useTheme()

const customization = useSelector((state) => state.customization)
const { isCloud } = useConfig()

Check warning on line 218 in packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.15.0)

'isCloud' is assigned a value but never used. Allowed unused vars must match /^_/u

const [open, setOpen] = useState(false)
const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
Expand Down Expand Up @@ -500,18 +500,18 @@
</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
Expand Down
6 changes: 1 addition & 5 deletions packages/ui/src/routes/MainRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,7 @@ const MainRoutes = {
},
{
path: '/account',
element: (
<RequireAuth display={'feat:account'}>
<Account />
</RequireAuth>
)
element: <Account />
},
{
path: '/users',
Expand Down
85 changes: 70 additions & 15 deletions packages/ui/src/views/account/UserProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SettingsSection from '@/ui-component/form/settings'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'

// API
import accountApi from '@/api/account.api'
import userApi from '@/api/user'
import useApi from '@/hooks/useApi'

Expand All @@ -21,7 +22,7 @@ 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'
import { logoutSuccess, userProfileUpdated } from '@/store/reducers/authSlice'

// utils
import useNotifier from '@/utils/useNotifier'
Expand All @@ -41,6 +42,7 @@ const UserProfile = () => {
const currentUser = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)

const [oldPasswordVal, setOldPasswordVal] = useState('')
const [newPasswordVal, setNewPasswordVal] = useState('')
const [confirmPasswordVal, setConfirmPasswordVal] = useState('')
const [usernameVal, setUsernameVal] = useState('')
Expand All @@ -50,6 +52,7 @@ const UserProfile = () => {
const [authErrors, setAuthErrors] = useState([])

const getUserApi = useApi(userApi.getUserById)
const logoutApi = useApi(accountApi.logout)

const validateAndSubmit = async () => {
const validationErrors = []
Expand All @@ -67,6 +70,9 @@ const UserProfile = () => {
validationErrors.push('Email cannot be left blank!')
}
if (newPasswordVal || confirmPasswordVal) {
if (!oldPasswordVal) {
validationErrors.push('Old Password cannot be left blank!')
}
if (newPasswordVal !== confirmPasswordVal) {
validationErrors.push('New Password and Confirm Password do not match')
}
Expand All @@ -82,28 +88,49 @@ const UserProfile = () => {
const body = {
id: currentUser.id,
email: emailVal,
name: usernameVal
name: usernameVal,
oldPassword: oldPasswordVal,
newPassword: newPasswordVal,
confirmNewPassword: confirmPasswordVal
}
if (newPasswordVal) body.password = newPasswordVal
setLoading(true)
try {
const updateResponse = await userApi.updateUser(body)
setAuthErrors([])
setLoading(false)
if (updateResponse.data) {
if (oldPasswordVal && newPasswordVal && confirmPasswordVal) {
setOldPasswordVal('')
setNewPasswordVal('')
setConfirmPasswordVal('')
logoutApi.request()
enqueueSnackbar({
message: 'User Details Updated! Logged Out...',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
} else {
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>
)
}
})
}
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)
Expand All @@ -124,6 +151,17 @@ const UserProfile = () => {
}
}

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 (getUserApi.data) {
const user = getUserApi.data
Expand Down Expand Up @@ -238,6 +276,23 @@ const UserProfile = () => {
value={usernameVal}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Old Password<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
id='op'
type='password'
fullWidth
size='small'
name='old_password'
onChange={(e) => setOldPasswordVal(e.target.value)}
value={oldPasswordVal}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Expand Down
Loading