mirror of
https://github.com/documenso/documenso
synced 2026-04-21 21:37:18 +00:00
## Description - Update the rejected certificate so that is it more clear on who rejected the document. - Updated the audit log generation so that the completed audit log is included ### Before <img width="681" height="597" alt="image" src="https://github.com/user-attachments/assets/3dab41c1-c86f-4555-8d50-3d9245be65d5" /> ### After Note that the order of the recipient is different in this case <img width="818" height="769" alt="image" src="https://github.com/user-attachments/assets/71f0ac12-5859-47b4-8980-2420ef949d18" /> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
155 lines
5.6 KiB
TypeScript
155 lines
5.6 KiB
TypeScript
import { PDF } from '@libpdf/core';
|
|
import { i18n } from '@lingui/core';
|
|
import { msg } from '@lingui/core/macro';
|
|
import type { DocumentMeta } from '@prisma/client';
|
|
import type { Envelope, Field, Recipient, Signature } from '@prisma/client';
|
|
import { FieldType } from '@prisma/client';
|
|
import { prop, sortBy } from 'remeda';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
|
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
|
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
import { getTranslations } from '../../utils/i18n';
|
|
import { getDocumentCertificateAuditLogs } from '../document/get-document-certificate-audit-logs';
|
|
import { getOrganisationClaimByTeamId } from '../organisation/get-organisation-claims';
|
|
import { renderCertificate } from './render-certificate';
|
|
|
|
export type GenerateCertificatePdfOptions = {
|
|
/**
|
|
* Note: completedAt is not included since it's not real at this point in time.
|
|
*
|
|
* If we actually need it here in the future, we will need to preserve the
|
|
* completedAt value and pass it to the final `envelope.update` function when
|
|
* the document is initially sealed.
|
|
*/
|
|
envelope: Omit<Envelope, 'completedAt'> & {
|
|
documentMeta: DocumentMeta;
|
|
};
|
|
envelopeOwner: {
|
|
name: string;
|
|
email: string;
|
|
};
|
|
recipients: Recipient[];
|
|
fields: (Pick<Field, 'id' | 'type' | 'secondaryId' | 'recipientId'> & {
|
|
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
|
})[];
|
|
language?: string;
|
|
pageWidth: number;
|
|
pageHeight: number;
|
|
};
|
|
|
|
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
|
|
const { envelope, envelopeOwner, recipients, fields, language, pageWidth, pageHeight } = options;
|
|
|
|
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
|
|
|
|
const [organisationClaim, auditLogs, messages] = await Promise.all([
|
|
getOrganisationClaimByTeamId({ teamId: envelope.teamId }),
|
|
getDocumentCertificateAuditLogs({
|
|
envelopeId: envelope.id,
|
|
}),
|
|
getTranslations(documentLanguage),
|
|
]);
|
|
|
|
i18n.loadAndActivate({
|
|
locale: documentLanguage,
|
|
messages,
|
|
});
|
|
|
|
const payload = {
|
|
recipients: recipients.map((recipient) => {
|
|
const recipientId = recipient.id;
|
|
|
|
const signatureField = fields.find(
|
|
(field) => field.recipientId === recipient.id && field.type === FieldType.SIGNATURE,
|
|
);
|
|
|
|
const emailSent: TDocumentAuditLogBaseSchema | undefined = auditLogs['EMAIL_SENT'].find(
|
|
(log) => log.type === 'EMAIL_SENT' && log.data.recipientId === recipientId,
|
|
);
|
|
|
|
const documentSent: TDocumentAuditLogBaseSchema | undefined = auditLogs['DOCUMENT_SENT'].find(
|
|
(log) => log.type === 'DOCUMENT_SENT',
|
|
);
|
|
|
|
const documentOpened: TDocumentAuditLogBaseSchema | undefined = auditLogs[
|
|
'DOCUMENT_OPENED'
|
|
].find((log) => log.type === 'DOCUMENT_OPENED' && log.data.recipientId === recipientId);
|
|
|
|
const documentRecipientCompleted: TDocumentAuditLogBaseSchema | undefined = auditLogs[
|
|
'DOCUMENT_RECIPIENT_COMPLETED'
|
|
].find(
|
|
(log) =>
|
|
log.type === 'DOCUMENT_RECIPIENT_COMPLETED' && log.data.recipientId === recipientId,
|
|
);
|
|
|
|
const documentRecipientRejected: TDocumentAuditLogBaseSchema | undefined = auditLogs[
|
|
'DOCUMENT_RECIPIENT_REJECTED'
|
|
].find(
|
|
(log) => log.type === 'DOCUMENT_RECIPIENT_REJECTED' && log.data.recipientId === recipientId,
|
|
);
|
|
|
|
const extractedAuthMethods = extractDocumentAuthMethods({
|
|
documentAuth: envelope.authOptions,
|
|
recipientAuth: recipient.authOptions,
|
|
});
|
|
|
|
const insertedAuditLogsWithFieldAuth = sortBy(
|
|
auditLogs.DOCUMENT_FIELD_INSERTED.filter(
|
|
(log) => log.data.recipientId === recipient.id && log.data.fieldSecurity,
|
|
),
|
|
[prop('createdAt'), 'desc'],
|
|
);
|
|
|
|
const actionAuthMethod = insertedAuditLogsWithFieldAuth.at(0)?.data?.fieldSecurity?.type;
|
|
|
|
let authLevel = match(actionAuthMethod)
|
|
.with('ACCOUNT', () => i18n._(msg`Account Re-Authentication`))
|
|
.with('TWO_FACTOR_AUTH', () => i18n._(msg`Two-Factor Re-Authentication`))
|
|
.with('PASSWORD', () => i18n._(msg`Password Re-Authentication`))
|
|
.with('PASSKEY', () => i18n._(msg`Passkey Re-Authentication`))
|
|
.with('EXPLICIT_NONE', () => i18n._(msg`Email`))
|
|
.with(undefined, () => null)
|
|
.exhaustive();
|
|
|
|
if (!authLevel) {
|
|
const accessAuthMethod = extractedAuthMethods.derivedRecipientAccessAuth.at(0);
|
|
|
|
authLevel = match(accessAuthMethod)
|
|
.with('ACCOUNT', () => i18n._(msg`Account Authentication`))
|
|
.with('TWO_FACTOR_AUTH', () => i18n._(msg`Two-Factor Authentication`))
|
|
.with(undefined, () => i18n._(msg`Email`))
|
|
.exhaustive();
|
|
}
|
|
|
|
return {
|
|
id: recipient.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
role: recipient.role,
|
|
signingStatus: recipient.signingStatus,
|
|
signatureField,
|
|
rejectionReason: recipient.rejectionReason,
|
|
authLevel,
|
|
logs: {
|
|
emailed: emailSent ?? null,
|
|
sent: documentSent ?? null,
|
|
opened: documentOpened ?? null,
|
|
completed: documentRecipientCompleted ?? null,
|
|
rejected: documentRecipientRejected ?? null,
|
|
},
|
|
};
|
|
}),
|
|
envelopeOwner,
|
|
qrToken: envelope.qrToken,
|
|
hidePoweredBy: organisationClaim.flags.hidePoweredBy ?? false,
|
|
pageWidth,
|
|
pageHeight,
|
|
i18n,
|
|
};
|
|
|
|
const certificatePages = await renderCertificate(payload);
|
|
|
|
return await PDF.merge(certificatePages);
|
|
};
|