From 449d9b0ffe4fe27628269767850d03647f88ba8f Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 5 Jan 2026 23:57:59 -0300 Subject: [PATCH] Fix add member to project not using the organization default role --- .../routes/v1/project-membership-router.ts | 3 +-- .../membership-user-service.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index dc76efa927..016ddbe375 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -2,7 +2,6 @@ import { z } from "zod"; import { AccessScope, - OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, @@ -275,7 +274,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider orgId: req.permission.orgId }, data: { - roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }], + roles: [], usernames: usernamesAndEmails } }); diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index 59772a603d..cb4e4d9c94 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -1,5 +1,6 @@ import { AccessScope, + OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, TemporaryPermissionMode, @@ -157,13 +158,22 @@ export const membershipUserServiceFactory = ({ const { scopeData, data } = dto; const factory = scopeFactory[scopeData.scope]; - const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + const orgDetails = await orgDAL.findById(dto.permission.orgId); + + // If roles array is empty and scope is Organization, use org's default role + let rolesToUse = data.roles; + if (data.roles.length === 0 && scopeData.scope === AccessScope.Organization) { + const defaultRole = orgDetails.defaultMembershipRole || OrgMembershipRole.NoAccess; + rolesToUse = [{ isTemporary: false, role: defaultRole }]; + } + + const hasNoPermanentRole = rolesToUse.every((el) => el.isTemporary); if (hasNoPermanentRole) { throw new BadRequestError({ message: "User must have at least one permanent role" }); } - const isInvalidTemporaryRole = data.roles.some((el) => { + const isInvalidTemporaryRole = rolesToUse.some((el) => { if (el.isTemporary) { if (!el.temporaryAccessStartTime || !el.temporaryRange) { return true; @@ -188,7 +198,6 @@ export const membershipUserServiceFactory = ({ }); if (existingMemberships.length === users.length) return { memberships: [] }; - const orgDetails = await orgDAL.findById(dto.permission.orgId); const isSubOrganization = Boolean(orgDetails.rootOrgId); const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id)); @@ -212,7 +221,7 @@ export const membershipUserServiceFactory = ({ }; }); - const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const customInputRoles = rolesToUse.filter((el) => factory.isCustomRole(el.role)); const hasCustomRole = customInputRoles.length > 0; if (hasCustomRole) { const plan = await licenseService.getPlan(scopeData.orgId); @@ -241,7 +250,7 @@ export const membershipUserServiceFactory = ({ const roleDocs: TMembershipRolesInsert[] = []; docs.forEach((membership) => { - data.roles.forEach((membershipRole) => { + rolesToUse.forEach((membershipRole) => { const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); if (membershipRole.isTemporary) { const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null;