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:
Vinod Kiran
2025-06-19 23:07:29 +05:30
committed by GitHub
parent a107aa7a77
commit 9a60b7b223
6 changed files with 280 additions and 57 deletions
@@ -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
}
+5 -1
View File
@@ -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' }}>&nbsp;*</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 />
</>