diff --git a/apps/remix/app/components/tables/organisation-member-invites-table.tsx b/apps/remix/app/components/tables/organisation-member-invites-table.tsx index ba7fbf030..5ce733df6 100644 --- a/apps/remix/app/components/tables/organisation-member-invites-table.tsx +++ b/apps/remix/app/components/tables/organisation-member-invites-table.tsx @@ -11,6 +11,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-upda import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params'; +import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations'; import { trpc } from '@documenso/trpc/react'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; @@ -108,7 +109,7 @@ export const OrganisationMemberInvitesTable = () => { avatarClass="h-12 w-12" avatarFallback={row.original.email.slice(0, 1).toUpperCase()} primaryText={ - {row.original.email} + {row.original.email} } /> ); @@ -129,7 +130,7 @@ export const OrganisationMemberInvitesTable = () => { cell: ({ row }) => ( - + @@ -149,17 +150,22 @@ export const OrganisationMemberInvitesTable = () => { Resend - - deleteOrganisationMemberInvitations({ - organisationId: organisation.id, - invitationIds: [row.original.id], - }) - } - > - - Remove - + {isOrganisationRoleWithinUserHierarchy( + organisation.currentOrganisationRole, + row.original.organisationRole, + ) && ( + + deleteOrganisationMemberInvitations({ + organisationId: organisation.id, + invitationIds: [row.original.id], + }) + } + > + + Remove + + )} ), diff --git a/packages/trpc/server/organisation-router/delete-organisation-member-invites.ts b/packages/trpc/server/organisation-router/delete-organisation-member-invites.ts index 2e07e1c98..b65b01504 100644 --- a/packages/trpc/server/organisation-router/delete-organisation-member-invites.ts +++ b/packages/trpc/server/organisation-router/delete-organisation-member-invites.ts @@ -1,8 +1,12 @@ import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity'; import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles'; import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing'; -import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations'; +import { + buildOrganisationWhereQuery, + isOrganisationRoleWithinUserHierarchy, +} from '@documenso/lib/utils/organisations'; import { prisma } from '@documenso/prisma'; import { authenticatedProcedure } from '../trpc'; @@ -52,6 +56,41 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure throw new AppError(AppErrorCode.NOT_FOUND); } + const currentOrganisationMemberRole = await getMemberOrganisationRole({ + organisationId: organisation.id, + reference: { + type: 'User', + id: userId, + }, + }); + + const invitesToDelete = await prisma.organisationMemberInvite.findMany({ + where: { + id: { + in: invitationIds, + }, + organisationId: organisation.id, + }, + select: { + id: true, + organisationRole: true, + }, + }); + + const hasUnauthorizedRoleAccess = invitesToDelete.some( + (invite) => + !isOrganisationRoleWithinUserHierarchy( + currentOrganisationMemberRole, + invite.organisationRole, + ), + ); + + if (hasUnauthorizedRoleAccess) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'User does not have permission to delete invitations for higher roles', + }); + } + const { organisationClaim } = organisation; const subscription = validateIfSubscriptionIsRequired(organisation.subscription);