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);