mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
New Feature: Ability to change role for a workspace user. (#4616)
* New Feature: Ability to change role for a workspace user. * reverting some of the changes in workspace-user.service.ts and minor code cleanup in the ui components. * chore: refactor updateWorkspaceUser function queryRunner handling --------- Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { WorkspaceUserService } from '../services/workspace-user.service'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import { WorkspaceUser } from '../database/entities/workspace-user.entity'
|
||||
import { QueryRunner } from 'typeorm'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
import { GeneralErrorMessage } from '../../utils/constants'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import { WorkspaceUser } from '../database/entities/workspace-user.entity'
|
||||
import { WorkspaceUserService } from '../services/workspace-user.service'
|
||||
|
||||
export class WorkspaceUserController {
|
||||
public async create(req: Request, res: Response, next: NextFunction) {
|
||||
@@ -57,12 +58,18 @@ export class WorkspaceUserController {
|
||||
}
|
||||
|
||||
public async update(req: Request, res: Response, next: NextFunction) {
|
||||
let queryRunner: QueryRunner | undefined
|
||||
try {
|
||||
queryRunner = getRunningExpressApp().AppDataSource.createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
const workspaceUserService = new WorkspaceUserService()
|
||||
const workspaceUser = await workspaceUserService.updateWorkspaceUser(req.body)
|
||||
const workspaceUser = await workspaceUserService.updateWorkspaceUser(req.body, queryRunner)
|
||||
return res.status(StatusCodes.OK).json(workspaceUser)
|
||||
} catch (error) {
|
||||
if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction()
|
||||
next(error)
|
||||
} finally {
|
||||
if (queryRunner && !queryRunner.isReleased) await queryRunner.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { HttpStatusCode } from 'axios'
|
||||
import { RedisStore } from 'connect-redis'
|
||||
import express, { NextFunction, Request, Response } from 'express'
|
||||
import session from 'express-session'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import jwt, { JwtPayload, sign } from 'jsonwebtoken'
|
||||
import passport from 'passport'
|
||||
import { VerifiedCallback } from 'passport-jwt'
|
||||
import express, { NextFunction, Request, Response } from 'express'
|
||||
import { ErrorMessage, IAssignedWorkspace, LoggedInUser } from '../../Interface.Enterprise'
|
||||
import { decryptToken, encryptToken, generateSafeCopy } from '../../utils/tempTokenUtils'
|
||||
import jwt, { JwtPayload, sign } from 'jsonwebtoken'
|
||||
import { getAuthStrategy } from './AuthStrategy'
|
||||
import { IdentityManager } from '../../../IdentityManager'
|
||||
import { HttpStatusCode } from 'axios'
|
||||
import { getRunningExpressApp } from '../../../utils/getRunningExpressApp'
|
||||
import session from 'express-session'
|
||||
import { OrganizationService } from '../../services/organization.service'
|
||||
import { AccountService } from '../../services/account.service'
|
||||
import { WorkspaceUser, WorkspaceUserStatus } from '../../database/entities/workspace-user.entity'
|
||||
import { RoleErrorMessage, RoleService } from '../../services/role.service'
|
||||
import { GeneralRole } from '../../database/entities/role.entity'
|
||||
import { RedisStore } from 'connect-redis'
|
||||
import { WorkspaceUserService } from '../../services/workspace-user.service'
|
||||
import { OrganizationUserErrorMessage, OrganizationUserService } from '../../services/organization-user.service'
|
||||
import { InternalFlowiseError } from '../../../errors/internalFlowiseError'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { OrganizationUserStatus } from '../../database/entities/organization-user.entity'
|
||||
import { IdentityManager } from '../../../IdentityManager'
|
||||
import { Platform } from '../../../Interface'
|
||||
import { getRunningExpressApp } from '../../../utils/getRunningExpressApp'
|
||||
import { OrganizationUserStatus } from '../../database/entities/organization-user.entity'
|
||||
import { GeneralRole } from '../../database/entities/role.entity'
|
||||
import { WorkspaceUser, WorkspaceUserStatus } from '../../database/entities/workspace-user.entity'
|
||||
import { ErrorMessage, IAssignedWorkspace, LoggedInUser } from '../../Interface.Enterprise'
|
||||
import { AccountService } from '../../services/account.service'
|
||||
import { OrganizationUserErrorMessage, OrganizationUserService } from '../../services/organization-user.service'
|
||||
import { OrganizationService } from '../../services/organization.service'
|
||||
import { RoleErrorMessage, RoleService } from '../../services/role.service'
|
||||
import { WorkspaceUserService } from '../../services/workspace-user.service'
|
||||
import { decryptToken, encryptToken, generateSafeCopy } from '../../utils/tempTokenUtils'
|
||||
import { getAuthStrategy } from './AuthStrategy'
|
||||
import { initializeDBClientAndStore, initializeRedisClientAndStore } from './SessionPersistance'
|
||||
|
||||
const localStrategy = require('passport-local').Strategy
|
||||
@@ -123,7 +123,7 @@ export const initializeJwtCookieMiddleware = async (app: express.Application, id
|
||||
if (!organizationUser)
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, OrganizationUserErrorMessage.ORGANIZATION_USER_NOT_FOUND)
|
||||
organizationUser.status = OrganizationUserStatus.ACTIVE
|
||||
await workspaceUserService.updateWorkspaceUser(workspaceUser)
|
||||
await workspaceUserService.updateWorkspaceUser(workspaceUser, queryRunner)
|
||||
await organizationUserService.updateOrganizationUser(organizationUser)
|
||||
|
||||
const workspaceUsers = await workspaceUserService.readWorkspaceUserByUserId(organizationUser.userId, queryRunner)
|
||||
|
||||
@@ -327,19 +327,22 @@ export class WorkspaceUserService {
|
||||
return newWorkspace
|
||||
}
|
||||
|
||||
public async updateWorkspaceUser(newWorkspaserUser: Partial<WorkspaceUser>) {
|
||||
const queryRunner = this.dataSource.createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
public async updateWorkspaceUser(newWorkspaserUser: Partial<WorkspaceUser>, queryRunner: QueryRunner) {
|
||||
const { workspaceUser } = await this.readWorkspaceUserByWorkspaceIdUserId(
|
||||
newWorkspaserUser.workspaceId,
|
||||
newWorkspaserUser.userId,
|
||||
queryRunner
|
||||
)
|
||||
if (!workspaceUser) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, WorkspaceUserErrorMessage.WORKSPACE_USER_NOT_FOUND)
|
||||
if (newWorkspaserUser.roleId) {
|
||||
if (newWorkspaserUser.roleId && workspaceUser.role) {
|
||||
const role = await this.roleService.readRoleById(newWorkspaserUser.roleId, queryRunner)
|
||||
if (!role) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, RoleErrorMessage.ROLE_NOT_FOUND)
|
||||
// check if the role is from the same organization
|
||||
if (role.organizationId !== workspaceUser.role.organizationId) {
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, RoleErrorMessage.ROLE_NOT_FOUND)
|
||||
}
|
||||
// delete role, the new role will be created again, with the new roleId (newWorkspaserUser.roleId)
|
||||
if (workspaceUser.role) delete workspaceUser.role
|
||||
}
|
||||
const updatedBy = await this.userService.readUserById(newWorkspaserUser.updatedBy, queryRunner)
|
||||
if (!updatedBy) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, UserErrorMessage.USER_NOT_FOUND)
|
||||
@@ -348,16 +351,7 @@ export class WorkspaceUserService {
|
||||
newWorkspaserUser.createdBy = workspaceUser.createdBy
|
||||
|
||||
let updataWorkspaceUser = queryRunner.manager.merge(WorkspaceUser, workspaceUser, newWorkspaserUser)
|
||||
try {
|
||||
await queryRunner.startTransaction()
|
||||
updataWorkspaceUser = await this.saveWorkspaceUser(updataWorkspaceUser, queryRunner)
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw error
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
updataWorkspaceUser = await this.saveWorkspaceUser(updataWorkspaceUser, queryRunner)
|
||||
|
||||
return updataWorkspaceUser
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ const deleteWorkspace = (id) => client.delete(`/workspace/${id}`)
|
||||
const getSharedWorkspacesForItem = (id) => client.get(`/workspace/shared/${id}`)
|
||||
const setSharedWorkspacesForItem = (id, body) => client.post(`/workspace/shared/${id}`, body)
|
||||
|
||||
const updateWorkspaceUserRole = (body) => client.put(`/workspaceuser`, body)
|
||||
|
||||
export default {
|
||||
getAllWorkspacesByOrganizationId,
|
||||
getWorkspaceById,
|
||||
@@ -26,5 +28,7 @@ export default {
|
||||
linkUsers,
|
||||
switchWorkspace,
|
||||
getSharedWorkspacesForItem,
|
||||
setSharedWorkspacesForItem
|
||||
setSharedWorkspacesForItem,
|
||||
|
||||
updateWorkspaceUserRole
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Material
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Box,
|
||||
Typography,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
styled,
|
||||
Popper
|
||||
} from '@mui/material'
|
||||
|
||||
// Project imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconUser } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import roleApi from '@/api/role'
|
||||
import workspaceApi from '@/api/workspace'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
|
||||
const StyledPopper = styled(Popper)({
|
||||
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
|
||||
borderRadius: '10px',
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
boxSizing: 'border-box',
|
||||
'& ul': {
|
||||
padding: 10,
|
||||
margin: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const EditWorkspaceUserRoleDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [userEmail, setUserEmail] = useState('')
|
||||
const [user, setUser] = useState({})
|
||||
|
||||
const [availableRoles, setAvailableRoles] = useState([])
|
||||
const [selectedRole, setSelectedRole] = useState('')
|
||||
const getAllRolesApi = useApi(roleApi.getAllRolesByOrganizationId)
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllRolesApi.data) {
|
||||
const roles = getAllRolesApi.data.map((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
label: role.name,
|
||||
description: role.description
|
||||
}))
|
||||
setAvailableRoles(roles)
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data && dialogProps.data.role && dialogProps.data.role.name) {
|
||||
const userActiveRole = roles.find((role) => role.name === dialogProps.data.role.name)
|
||||
if (userActiveRole) setSelectedRole(userActiveRole)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllRolesApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.data) {
|
||||
getAllRolesApi.request(currentUser.activeOrganizationId)
|
||||
setUser(dialogProps.data.user)
|
||||
setUserEmail(dialogProps.data.user.email)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setUserEmail('')
|
||||
setUser({})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const updateUser = async () => {
|
||||
try {
|
||||
const saveObj = {
|
||||
userId: user.id,
|
||||
workspaceId: dialogProps.data.workspaceId,
|
||||
organizationId: currentUser.activeOrganizationId,
|
||||
roleId: selectedRole.id,
|
||||
updatedBy: currentUser.id
|
||||
}
|
||||
|
||||
const saveResp = await workspaceApi.updateWorkspaceUserRole(saveObj)
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'WorkspaceUser Details Updated',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm(saveResp.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackbar({
|
||||
message: `Failed to update WorkspaceUser: ${
|
||||
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
|
||||
}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRoleChange = (event, newRole) => {
|
||||
setSelectedRole(newRole)
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconUser style={{ marginRight: '10px' }} />
|
||||
{'Change Workspace Role - '} {userEmail || ''} {user.name ? `(${user.name})` : ''}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
New Role to Assign<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ mt: 1 }}
|
||||
onChange={handleRoleChange}
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
options={availableRoles}
|
||||
renderInput={(params) => <TextField {...params} variant='outlined' placeholder='Select Role' />}
|
||||
value={selectedRole}
|
||||
PopperComponent={StyledPopper}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<StyledButton variant='contained' onClick={() => updateUser()} id='btn_confirmEditUser'>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
EditWorkspaceUserRoleDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default EditWorkspaceUserRoleDialog
|
||||
@@ -29,7 +29,7 @@ import ErrorBoundary from '@/ErrorBoundary'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
import InviteUsersDialog from '@/ui-component/dialog/InviteUsersDialog'
|
||||
import EditUserDialog from '@/views/users/EditUserDialog'
|
||||
import EditWorkspaceUserRoleDialog from '@/views/workspace/EditWorkspaceUserRoleDialog'
|
||||
|
||||
// API
|
||||
import userApi from '@/api/user'
|
||||
@@ -66,8 +66,8 @@ const WorkspaceDetails = () => {
|
||||
|
||||
const [showAddUserDialog, setShowAddUserDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
const [showEditDialog, setShowEditDialog] = useState(false)
|
||||
const [editDialogProps, setEditDialogProps] = useState({})
|
||||
const [showWorkspaceUserRoleDialog, setShowWorkspaceUserRoleDialog] = useState(false)
|
||||
const [workspaceUserRoleDialogProps, setWorkspaceUserRoleDialogProps] = useState({})
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
@@ -152,7 +152,6 @@ const WorkspaceDetails = () => {
|
||||
}
|
||||
|
||||
const editUser = (user) => {
|
||||
// Not used for now
|
||||
const userObj = {
|
||||
...user,
|
||||
assignedRoles: [
|
||||
@@ -161,16 +160,16 @@ const WorkspaceDetails = () => {
|
||||
active: true
|
||||
}
|
||||
],
|
||||
activeWorkspaceId: workspaceId
|
||||
workspaceId: workspaceId
|
||||
}
|
||||
const dialogProp = {
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
confirmButtonName: 'Update Role',
|
||||
data: userObj
|
||||
}
|
||||
setEditDialogProps(dialogProp)
|
||||
setShowEditDialog(true)
|
||||
setWorkspaceUserRoleDialogProps(dialogProp)
|
||||
setShowWorkspaceUserRoleDialog(true)
|
||||
}
|
||||
|
||||
const unlinkUser = async () => {
|
||||
@@ -253,7 +252,7 @@ const WorkspaceDetails = () => {
|
||||
|
||||
const onConfirm = () => {
|
||||
setShowAddUserDialog(false)
|
||||
setShowEditDialog(false)
|
||||
setShowWorkspaceUserRoleDialog(false)
|
||||
getAllUsersByWorkspaceIdApi.request(workspaceId)
|
||||
}
|
||||
|
||||
@@ -511,6 +510,15 @@ const WorkspaceDetails = () => {
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
)}
|
||||
{!item.isOrgOwner && item.status.toUpperCase() === 'ACTIVE' && (
|
||||
<IconButton
|
||||
title='Change Role'
|
||||
color='primary'
|
||||
onClick={() => onEditClick(item)}
|
||||
>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
)}
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
@@ -532,14 +540,13 @@ const WorkspaceDetails = () => {
|
||||
onConfirm={onConfirm}
|
||||
></InviteUsersDialog>
|
||||
)}
|
||||
{showEditDialog && (
|
||||
<EditUserDialog
|
||||
show={showEditDialog}
|
||||
dialogProps={editDialogProps}
|
||||
onCancel={() => setShowEditDialog(false)}
|
||||
{showWorkspaceUserRoleDialog && (
|
||||
<EditWorkspaceUserRoleDialog
|
||||
show={showWorkspaceUserRoleDialog}
|
||||
dialogProps={workspaceUserRoleDialogProps}
|
||||
onCancel={() => setShowWorkspaceUserRoleDialog(false)}
|
||||
onConfirm={onConfirm}
|
||||
setError={setError}
|
||||
></EditUserDialog>
|
||||
/>
|
||||
)}
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user