mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
ba0dead96f
Currently direct templates can be created without the required signatures fields for signers. This means that the document can be fully signed by everyone but will ultimately fail the sealing step which leaves the document in an unrecoverable state.
184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
import { prisma } from '@documenso/prisma';
|
|
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
|
import { extractFieldAutoInsertValues } from '../document/send-document';
|
|
import { getTeamSettings } from '../team/get-team-settings';
|
|
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
|
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
|
|
|
export type GetRecipientEnvelopeByTokenOptions = {
|
|
token: string;
|
|
userId?: number;
|
|
accessAuth?: TDocumentAuthMethods;
|
|
};
|
|
|
|
/**
|
|
* Get all the values and details for a direct template envelope that a recipient requires.
|
|
*
|
|
* Do not overexpose any information that the recipient should not have.
|
|
*/
|
|
export const getEnvelopeForDirectTemplateSigning = async ({
|
|
token,
|
|
userId,
|
|
accessAuth,
|
|
}: GetRecipientEnvelopeByTokenOptions): Promise<EnvelopeForSigningResponse> => {
|
|
if (!token) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Missing token',
|
|
});
|
|
}
|
|
|
|
const envelope = await prisma.envelope.findFirst({
|
|
where: {
|
|
type: EnvelopeType.TEMPLATE,
|
|
status: DocumentStatus.DRAFT,
|
|
directLink: {
|
|
enabled: true,
|
|
token,
|
|
},
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
documentMeta: true,
|
|
recipients: {
|
|
include: {
|
|
fields: {
|
|
include: {
|
|
signature: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
signingOrder: 'asc',
|
|
},
|
|
},
|
|
envelopeItems: {
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
},
|
|
team: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
teamEmail: true,
|
|
teamGlobalSettings: {
|
|
select: {
|
|
includeSigningCertificate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
directLink: true,
|
|
},
|
|
});
|
|
|
|
const recipient = (envelope?.recipients || []).find((r) => r.id === envelope?.directLink?.directTemplateRecipientId);
|
|
|
|
if (!envelope || !recipient) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Envelope not found',
|
|
});
|
|
}
|
|
|
|
if (envelope.envelopeItems.length === 0) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Envelope has no items',
|
|
});
|
|
}
|
|
|
|
// Currently not using this since for direct templates "User" access means they just need to be
|
|
// logged in.
|
|
// const documentAccessValid = await isRecipientAuthorized({
|
|
// type: 'ACCESS',
|
|
// documentAuthOptions: envelope.authOptions,
|
|
// recipient,
|
|
// userId,
|
|
// authOptions: accessAuth,
|
|
// });
|
|
|
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
|
documentAuth: envelope.authOptions,
|
|
});
|
|
|
|
// Ensure typesafety when we add more options.
|
|
const documentAccessValid = derivedRecipientAccessAuth.every((auth) =>
|
|
match(auth)
|
|
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(userId))
|
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true)
|
|
.exhaustive(),
|
|
);
|
|
|
|
if (!documentAccessValid) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'Invalid access values',
|
|
});
|
|
}
|
|
|
|
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
|
envelope.recipients,
|
|
envelope.recipients.flatMap((envelopeRecipient) => envelopeRecipient.fields),
|
|
);
|
|
|
|
if (recipientsWithMissingFields.length > 0) {
|
|
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
|
message: 'One or more signers on this direct template are missing a signature field',
|
|
});
|
|
}
|
|
|
|
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
|
|
|
const sender = settings.includeSenderDetails
|
|
? {
|
|
email: envelope.user.email,
|
|
name: envelope.user.name || '',
|
|
}
|
|
: {
|
|
email: envelope.team.teamEmail?.email || '',
|
|
name: envelope.team.name || '',
|
|
};
|
|
|
|
return ZEnvelopeForSigningResponse.parse({
|
|
envelope,
|
|
recipient: {
|
|
...recipient,
|
|
directToken: envelope.directLink?.token || '',
|
|
fields: recipient.fields.map((field) => {
|
|
const autoInsertValue = extractFieldAutoInsertValues(field, recipient);
|
|
|
|
if (!autoInsertValue) {
|
|
return field;
|
|
}
|
|
|
|
return {
|
|
...field,
|
|
inserted: true,
|
|
customText: autoInsertValue.customText,
|
|
};
|
|
}),
|
|
},
|
|
recipientSignature: null,
|
|
isRecipientsTurn: true,
|
|
isCompleted: false,
|
|
isRejected: false,
|
|
isExpired: false,
|
|
sender,
|
|
settings: {
|
|
includeSenderDetails: settings.includeSenderDetails,
|
|
brandingEnabled: settings.brandingEnabled,
|
|
brandingLogo: settings.brandingLogo,
|
|
},
|
|
} satisfies EnvelopeForSigningResponse);
|
|
};
|