mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
d5ce222482
Adds Cloud Signature Consortium (CSC) integration for AES/QES signing against a configured TSP. v1 ships as instance-wide configuration via environment variables, with per-envelope signature level selection, license gating, and an OAuth-driven signing flow (capture + FIFO signers, SAD session, blocking/in-progress recipient pages). Includes signature level compatibility checks (role, signing order, dictate next signer), envelope mutability assertions, Prisma migration for signature level and CSC tables, and docs for the new signing certificate options.
302 lines
7.8 KiB
TypeScript
302 lines
7.8 KiB
TypeScript
import { prisma } from '@documenso/prisma';
|
|
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
|
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
|
import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
|
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
|
|
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
|
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
|
import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
|
import { z } from 'zod';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
|
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
|
|
import { ZRecipientLiteSchema } from '../../types/recipient';
|
|
import { isRecipientExpired } from '../../utils/recipients';
|
|
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
|
import { getTeamSettings } from '../team/get-team-settings';
|
|
|
|
export type GetRecipientEnvelopeByTokenOptions = {
|
|
token: string;
|
|
userId?: number;
|
|
accessAuth?: TDocumentAuthMethods;
|
|
};
|
|
|
|
export const ZEnvelopeForSigningResponse = z.object({
|
|
envelope: EnvelopeSchema.pick({
|
|
type: true,
|
|
status: true,
|
|
id: true,
|
|
secondaryId: true,
|
|
internalVersion: true,
|
|
completedAt: true,
|
|
updatedAt: true,
|
|
deletedAt: true,
|
|
title: true,
|
|
authOptions: true,
|
|
userId: true,
|
|
teamId: true,
|
|
signatureLevel: true,
|
|
}).extend({
|
|
documentMeta: DocumentMetaSchema.pick({
|
|
signingOrder: true,
|
|
distributionMethod: true,
|
|
timezone: true,
|
|
dateFormat: true,
|
|
redirectUrl: true,
|
|
typedSignatureEnabled: true,
|
|
uploadSignatureEnabled: true,
|
|
drawSignatureEnabled: true,
|
|
allowDictateNextSigner: true,
|
|
language: true,
|
|
}),
|
|
recipients: ZRecipientLiteSchema.pick({
|
|
id: true,
|
|
role: true,
|
|
signingStatus: true,
|
|
email: true,
|
|
name: true,
|
|
documentDeletedAt: true,
|
|
expired: true, //!: deprecated Not in use. To be removed in a future migration.
|
|
expiresAt: true,
|
|
expirationNotifiedAt: true,
|
|
signedAt: true,
|
|
authOptions: true,
|
|
signingOrder: true,
|
|
rejectionReason: true,
|
|
})
|
|
.extend({
|
|
fields: ZEnvelopeFieldSchema.extend({
|
|
signature: SignatureSchema.pick({
|
|
signatureImageAsBase64: true,
|
|
typedSignature: true,
|
|
}).nullish(),
|
|
}).array(),
|
|
})
|
|
.array(),
|
|
|
|
envelopeItems: EnvelopeItemSchema.pick({
|
|
envelopeId: true,
|
|
id: true,
|
|
title: true,
|
|
order: true,
|
|
documentDataId: true,
|
|
}).array(),
|
|
|
|
team: TeamSchema.pick({
|
|
id: true,
|
|
name: true,
|
|
}),
|
|
user: UserSchema.pick({
|
|
name: true,
|
|
email: true,
|
|
}),
|
|
}),
|
|
|
|
/**
|
|
* The recipient that is currently signing.
|
|
*/
|
|
recipient: ZRecipientLiteSchema.pick({
|
|
id: true,
|
|
role: true,
|
|
envelopeId: true,
|
|
readStatus: true,
|
|
sendStatus: true,
|
|
signingStatus: true,
|
|
email: true,
|
|
name: true,
|
|
documentDeletedAt: true,
|
|
expiresAt: true,
|
|
expirationNotifiedAt: true,
|
|
signedAt: true,
|
|
authOptions: true,
|
|
token: true,
|
|
signingOrder: true,
|
|
rejectionReason: true,
|
|
}).extend({
|
|
directToken: z.string().nullish(),
|
|
fields: ZFieldSchema.omit({
|
|
documentId: true,
|
|
templateId: true,
|
|
})
|
|
.extend({
|
|
signature: SignatureSchema.nullish(),
|
|
})
|
|
.array(),
|
|
}),
|
|
recipientSignature: SignatureSchema.pick({
|
|
signatureImageAsBase64: true,
|
|
typedSignature: true,
|
|
}).nullable(),
|
|
|
|
isCompleted: z.boolean(),
|
|
isRejected: z.boolean(),
|
|
isExpired: z.boolean(),
|
|
isRecipientsTurn: z.boolean(),
|
|
|
|
sender: z.object({
|
|
email: z.string(),
|
|
name: z.string(),
|
|
}),
|
|
|
|
settings: z.object({
|
|
includeSenderDetails: z.boolean(),
|
|
brandingEnabled: z.boolean(),
|
|
brandingLogo: z.string(),
|
|
}),
|
|
});
|
|
|
|
export type EnvelopeForSigningResponse = z.infer<typeof ZEnvelopeForSigningResponse>;
|
|
|
|
/**
|
|
* Get all the values and details for an envelope that a recipient requires
|
|
* to sign an envelope.
|
|
*
|
|
* Do not overexpose any information that the recipient should not have.
|
|
*/
|
|
export const getEnvelopeForRecipientSigning = 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.DOCUMENT,
|
|
status: {
|
|
not: DocumentStatus.DRAFT,
|
|
},
|
|
recipients: {
|
|
some: {
|
|
token,
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
documentMeta: true,
|
|
recipients: {
|
|
include: {
|
|
fields: {
|
|
include: {
|
|
signature: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
signingOrder: 'asc',
|
|
},
|
|
},
|
|
envelopeItems: true,
|
|
team: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
teamEmail: true,
|
|
teamGlobalSettings: {
|
|
select: {
|
|
includeSigningCertificate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const recipient = (envelope?.recipients || []).find((r) => r.token === token);
|
|
|
|
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',
|
|
});
|
|
}
|
|
|
|
const documentAccessValid = await isRecipientAuthorized({
|
|
type: 'ACCESS',
|
|
documentAuthOptions: envelope.authOptions,
|
|
recipient,
|
|
userId,
|
|
authOptions: accessAuth,
|
|
});
|
|
|
|
if (!documentAccessValid) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'Invalid access values',
|
|
});
|
|
}
|
|
|
|
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
|
|
|
// Get the signature if they have put it in already.
|
|
const recipientSignature = await prisma.signature.findFirst({
|
|
where: {
|
|
field: {
|
|
recipientId: recipient.id,
|
|
envelopeId: envelope.id,
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
recipientId: true,
|
|
signatureImageAsBase64: true,
|
|
typedSignature: true,
|
|
},
|
|
});
|
|
|
|
let isRecipientsTurn = true;
|
|
|
|
const currentRecipientIndex = envelope.recipients.findIndex((r) => r.token === token);
|
|
|
|
if (envelope.documentMeta.signingOrder === DocumentSigningOrder.SEQUENTIAL && currentRecipientIndex !== -1) {
|
|
for (let i = 0; i < currentRecipientIndex; i++) {
|
|
if (envelope.recipients[i].signingStatus !== SigningStatus.SIGNED) {
|
|
isRecipientsTurn = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
recipientSignature,
|
|
isRecipientsTurn,
|
|
isCompleted: recipient.signingStatus === SigningStatus.SIGNED || envelope.status === DocumentStatus.COMPLETED,
|
|
isRejected: recipient.signingStatus === SigningStatus.REJECTED || envelope.status === DocumentStatus.REJECTED,
|
|
isExpired: isRecipientExpired(recipient),
|
|
sender,
|
|
settings: {
|
|
includeSenderDetails: settings.includeSenderDetails,
|
|
brandingEnabled: settings.brandingEnabled,
|
|
brandingLogo: settings.brandingLogo,
|
|
},
|
|
} satisfies EnvelopeForSigningResponse);
|
|
};
|