diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 58713b4d5..9991f6032 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -21,7 +21,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { projects: z .object({ id: z.string(), - projectRoleSlug: z.string().default(ProjectMembershipRole.Member) + projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member]) }) .array() .optional(), diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 625deacf9..6ab574d64 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -48,7 +48,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projects: [ { id: req.params.projectId, - projectRoleSlug: ProjectMembershipRole.Member + projectRoleSlug: [ProjectMembershipRole.Member] } ] }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index f81719fac..9dad7086e 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -12,6 +12,7 @@ import { SecretKeyEncoding, TableName, TProjectMemberships, + TProjectUserMembershipRolesInsert, TUsers } from "@app/db/schemas"; import { TProjects } from "@app/db/schemas/projects"; @@ -88,7 +89,7 @@ type TOrgServiceFactoryDep = { "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" >; projectUserAdditionalPrivilegeDAL: Pick; - projectRoleDAL: Pick; + projectRoleDAL: Pick; userGroupMembershipDAL: Pick; projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; @@ -652,12 +653,15 @@ export const orgServiceFactory = ({ if (!userWithEncryptionKeyInvitedToProject.length) continue; // validate custom project role - const invitedProjectRole = - invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || ProjectMembershipRole.Member; - const isCustomProjectRole = !Object.values(ProjectMembershipRole).includes( - invitedProjectRole as ProjectMembershipRole + const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [ + ProjectMembershipRole.Member + ]; + + const customProjectRoles = invitedProjectRoles.filter( + (role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) ); - if (isCustomProjectRole) { + const hasCustomRole = Boolean(customProjectRoles.length); + if (hasCustomRole) { if (!plan?.rbac) throw new BadRequestError({ message: @@ -665,6 +669,17 @@ export const orgServiceFactory = ({ }); } + const customRoles = hasCustomRole + ? await projectRoleDAL.find({ + projectId, + $in: { slug: customProjectRoles.map((role) => role) } + }) + : []; + if (customRoles.length !== customProjectRoles.length) + throw new BadRequestError({ message: "Custom role not found" }); + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); if (!ghostUser) { throw new BadRequestError({ @@ -698,7 +713,7 @@ export const orgServiceFactory = ({ userPrivateKey: botPrivateKey, members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ orgMembershipId: userEnc.userId, - projectMembershipRole: invitedProjectRole as ProjectMembershipRole, + projectMembershipRole: ProjectMembershipRole.Admin, userPublicKey: userEnc.publicKey })) }); @@ -712,17 +727,18 @@ export const orgServiceFactory = ({ ); newProjectMemberships.push(...projectMemberships); - let customRoleId: string; - const projectRole = isCustomProjectRole ? ProjectMembershipRole.Custom : invitedProjectRole; - if (isCustomProjectRole) { - const customRole = await projectRoleDAL.findOne({ slug: invitedProjectRole, projectId }); - if (!customRole) throw new BadRequestError({ name: "Invite membership", message: "Project role not found" }); - customRoleId = customRole.id; - } - await projectUserMembershipRoleDAL.insertMany( - projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectRole, customRoleId })), - tx - ); + const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = []; + invitedProjectRoles.forEach((projectRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]); + projectMemberships.forEach((membership) => { + sanitizedProjectMembershipRoles.push({ + projectMembershipId: membership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : projectRole, + customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null + }); + }); + }); + await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); await projectKeyDAL.insertMany( newWsMembers.map((el) => ({ diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index f89a602ad..ef0a5606a 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -34,7 +34,7 @@ export type TInviteUserToOrgDTO = { organizationRoleSlug: string; projects?: { id: string; - projectRoleSlug?: string; + projectRoleSlug?: string[]; }[]; }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 6a6c35d93..6c538811b 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -150,7 +150,7 @@ export type DeletOrgMembershipDTO = { export type AddUserToOrgDTO = { inviteeEmails: string[]; - projects?: { id: string; projectRoleSlug: string }[]; + projects?: { id: string; projectRoleSlug: string[] }[]; organizationRoleSlug: string; organizationId: string; }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 2a615237b..7194432c5 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -129,7 +129,7 @@ export const AddOrgMemberModal = ({ organizationId: currentOrg?.id, inviteeEmails: emails.split(",").map((email) => email.trim()), organizationRoleSlug, - projects: projectIds.map((id) => ({ id, projectRoleSlug })) + projects: projectIds.map((id) => ({ id, projectRoleSlug: [projectRoleSlug] })) }); setCompleteInviteLinks(data?.completeInviteLinks ?? null); diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx index 8a5fe705a..6db92861e 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx @@ -101,9 +101,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { members?.forEach((member) => { wsUserUsernames.set(member.user.username, true); }); - return (orgUsers || []).filter( - ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username) - ); + return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username)); }, [orgUsers, members]); return (