feat: unlink documents from deleted organization (#2006)

This commit is contained in:
Catalin Pit
2025-12-15 03:17:13 +02:00
committed by GitHub
parent 928edb8645
commit 02f1264eea
8 changed files with 245 additions and 21 deletions
@@ -0,0 +1,52 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { deletedAccountServiceAccount } from '../user/service-accounts/deleted-account';
export type OrphanEnvelopesOptions = {
teamId: number;
};
export const orphanEnvelopes = async ({ teamId }: OrphanEnvelopesOptions) => {
const serviceAccount = await deletedAccountServiceAccount();
// Transfer all inflight and completed envelopes to the service account.
await prisma.envelope.updateMany({
where: {
teamId,
type: EnvelopeType.DOCUMENT,
status: {
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
},
deletedAt: null,
},
data: {
userId: serviceAccount.id,
teamId: serviceAccount.ownedOrganisations[0].teams[0].id,
deletedAt: new Date(),
},
});
// Transfer any remaining deleted envelopes to the service account.
await prisma.envelope.updateMany({
where: {
teamId,
type: EnvelopeType.DOCUMENT,
status: {
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
},
},
data: {
userId: serviceAccount.id,
teamId: serviceAccount.ownedOrganisations[0].teams[0].id,
},
});
// Then delete anything remaining across documents and templates.
await prisma.envelope.deleteMany({
where: {
teamId,
},
});
};
@@ -0,0 +1,20 @@
import { prisma } from '@documenso/prisma';
export type TransferTeamEnvelopesOptions = {
sourceTeamId: number;
targetTeamId: number;
};
export const transferTeamEnvelopes = async ({
sourceTeamId,
targetTeamId,
}: TransferTeamEnvelopesOptions) => {
await prisma.envelope.updateMany({
where: {
teamId: sourceTeamId,
},
data: {
teamId: targetTeamId,
},
});
};
+54 -18
View File
@@ -1,9 +1,7 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
import { orphanEnvelopes } from '../envelope/orphan-envelopes';
export type DeleteUserOptions = {
id: number;
@@ -14,6 +12,30 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
where: {
id,
},
include: {
ownedOrganisations: {
include: {
teams: {
select: {
id: true,
},
},
},
},
organisationMember: {
include: {
organisation: {
include: {
teams: {
select: {
id: true,
},
},
},
},
},
},
},
});
if (!user) {
@@ -22,22 +44,36 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
});
}
const serviceAccount = await deletedAccountServiceAccount();
// Get team IDs from organisations the user owns.
const ownedTeamIds = user.ownedOrganisations.flatMap((org) => org.teams.map((team) => team.id));
// TODO: Send out cancellations for all pending docs
await prisma.envelope.updateMany({
where: {
userId: user.id,
type: EnvelopeType.DOCUMENT,
status: {
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
},
},
data: {
userId: serviceAccount.id,
deletedAt: new Date(),
},
});
// Get team IDs from organisations the user is a member of (but not owner).
const memberTeams = user.organisationMember
.filter((member) => member.organisation.ownerUserId !== user.id)
.flatMap((member) =>
member.organisation.teams.map((team) => ({
teamId: team.id,
orgOwnerId: member.organisation.ownerUserId,
})),
);
// For teams where user is the org owner - orphan their envelopes.
await Promise.all(ownedTeamIds.map(async (teamId) => orphanEnvelopes({ teamId })));
// For teams where user is a member (not owner) - transfer envelopes to team owner.
await Promise.all(
memberTeams.map(async ({ teamId, orgOwnerId }) => {
return prisma.envelope.updateMany({
where: {
userId: user.id,
teamId,
},
data: {
userId: orgOwnerId,
},
});
}),
);
return await prisma.user.delete({
where: {
@@ -5,6 +5,20 @@ export const deletedAccountServiceAccount = async () => {
where: {
email: 'deleted-account@documenso.com',
},
select: {
id: true,
email: true,
ownedOrganisations: {
select: {
id: true,
teams: {
select: {
id: true,
},
},
},
},
},
});
if (!serviceAccount) {