From 9a60b7b22327c37a56af60e7998b23ad545103b6 Mon Sep 17 00:00:00 2001 From: Vinod Kiran Date: Thu, 19 Jun 2025 23:07:29 +0530 Subject: [PATCH] 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 --- .../controllers/workspace-user.controller.ts | 17 +- .../enterprise/middleware/passport/index.ts | 40 ++-- .../services/workspace-user.service.ts | 24 +- packages/ui/src/api/workspace.js | 6 +- .../workspace/EditWorkspaceUserRoleDialog.jsx | 211 ++++++++++++++++++ .../ui/src/views/workspace/WorkspaceUsers.jsx | 39 ++-- 6 files changed, 280 insertions(+), 57 deletions(-) create mode 100644 packages/ui/src/views/workspace/EditWorkspaceUserRoleDialog.jsx diff --git a/packages/server/src/enterprise/controllers/workspace-user.controller.ts b/packages/server/src/enterprise/controllers/workspace-user.controller.ts index f7af6efb..beab8b1f 100644 --- a/packages/server/src/enterprise/controllers/workspace-user.controller.ts +++ b/packages/server/src/enterprise/controllers/workspace-user.controller.ts @@ -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() } } diff --git a/packages/server/src/enterprise/middleware/passport/index.ts b/packages/server/src/enterprise/middleware/passport/index.ts index b93d2413..e5cd8cb7 100644 --- a/packages/server/src/enterprise/middleware/passport/index.ts +++ b/packages/server/src/enterprise/middleware/passport/index.ts @@ -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) diff --git a/packages/server/src/enterprise/services/workspace-user.service.ts b/packages/server/src/enterprise/services/workspace-user.service.ts index af80b497..2b4f1772 100644 --- a/packages/server/src/enterprise/services/workspace-user.service.ts +++ b/packages/server/src/enterprise/services/workspace-user.service.ts @@ -327,19 +327,22 @@ export class WorkspaceUserService { return newWorkspace } - public async updateWorkspaceUser(newWorkspaserUser: Partial) { - const queryRunner = this.dataSource.createQueryRunner() - await queryRunner.connect() - + public async updateWorkspaceUser(newWorkspaserUser: Partial, 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 } diff --git a/packages/ui/src/api/workspace.js b/packages/ui/src/api/workspace.js index 1ceb99bb..2c771fe0 100644 --- a/packages/ui/src/api/workspace.js +++ b/packages/ui/src/api/workspace.js @@ -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 } diff --git a/packages/ui/src/views/workspace/EditWorkspaceUserRoleDialog.jsx b/packages/ui/src/views/workspace/EditWorkspaceUserRoleDialog.jsx new file mode 100644 index 00000000..5dc6ba20 --- /dev/null +++ b/packages/ui/src/views/workspace/EditWorkspaceUserRoleDialog.jsx @@ -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) => ( + + ) + } + }) + 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) => ( + + ) + } + }) + } + } + + const handleRoleChange = (event, newRole) => { + setSelectedRole(newRole) + } + + const component = show ? ( + + +
+ + {'Change Workspace Role - '} {userEmail || ''} {user.name ? `(${user.name})` : ''} +
+
+ + +
+ + New Role to Assign * + +
+
+ option.label || ''} + options={availableRoles} + renderInput={(params) => } + value={selectedRole} + PopperComponent={StyledPopper} + /> +
+
+ + updateUser()} id='btn_confirmEditUser'> + {dialogProps.confirmButtonName} + + + +
+ ) : null + + return createPortal(component, portalElement) +} + +EditWorkspaceUserRoleDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default EditWorkspaceUserRoleDialog diff --git a/packages/ui/src/views/workspace/WorkspaceUsers.jsx b/packages/ui/src/views/workspace/WorkspaceUsers.jsx index 45db1cca..e1024ca8 100644 --- a/packages/ui/src/views/workspace/WorkspaceUsers.jsx +++ b/packages/ui/src/views/workspace/WorkspaceUsers.jsx @@ -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 = () => { )} + {!item.isOrgOwner && item.status.toUpperCase() === 'ACTIVE' && ( + onEditClick(item)} + > + + + )} ))} @@ -532,14 +540,13 @@ const WorkspaceDetails = () => { onConfirm={onConfirm} > )} - {showEditDialog && ( - setShowEditDialog(false)} + {showWorkspaceUserRoleDialog && ( + setShowWorkspaceUserRoleDialog(false)} onConfirm={onConfirm} - setError={setError} - > + /> )}