fix: prevent managers from deleting admin invitations (#2636)

This commit is contained in:
David Nguyen 2026-03-20 22:26:59 +11:00 committed by GitHub
parent b2d395e00b
commit ace472c294
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 14 deletions

View file

@ -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={
<span className="text-foreground/80 font-semibold">{row.original.email}</span>
<span className="font-semibold text-foreground/80">{row.original.email}</span>
}
/>
);
@ -129,7 +130,7 @@ export const OrganisationMemberInvitesTable = () => {
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@ -149,17 +150,22 @@ export const OrganisationMemberInvitesTable = () => {
<Trans>Resend</Trans>
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () =>
deleteOrganisationMemberInvitations({
organisationId: organisation.id,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
{isOrganisationRoleWithinUserHierarchy(
organisation.currentOrganisationRole,
row.original.organisationRole,
) && (
<DropdownMenuItem
onClick={async () =>
deleteOrganisationMemberInvitations({
organisationId: organisation.id,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
),

View file

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