mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
@ -1,26 +1,12 @@
|
||||
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { validateFieldAuth } from '@documenso/lib/server-only/document/validate-field-auth';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
@ -53,21 +39,21 @@ export const signEnvelopeFieldRoute = procedure
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
recipient: {
|
||||
...(recipient.role !== RecipientRole.ASSISTANT
|
||||
...(recipient.role === RecipientRole.ASSISTANT
|
||||
? {
|
||||
id: recipient.id,
|
||||
}
|
||||
: {
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
signingOrder: {
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
}
|
||||
: {
|
||||
id: recipient.id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -82,21 +68,31 @@ export const signEnvelopeFieldRoute = procedure
|
||||
},
|
||||
});
|
||||
|
||||
const { envelope } = field;
|
||||
const { documentMeta } = envelope;
|
||||
|
||||
if (!envelope || !recipient) {
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Document not found for field ${field.id}`,
|
||||
message: `Field ${fieldId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const { envelope } = field;
|
||||
const { documentMeta } = envelope;
|
||||
|
||||
if (envelope.internalVersion !== 2) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Envelope ${envelope.id} is not a version 2 envelope`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldType.SIGNATURE &&
|
||||
recipient.id !== field.recipientId &&
|
||||
recipient.role === RecipientRole.ASSISTANT
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Assistant recipients cannot sign signature fields`,
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldValue.type !== field.type) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Selected values do not match the field values',
|
||||
@ -124,7 +120,6 @@ export const signEnvelopeFieldRoute = procedure
|
||||
});
|
||||
}
|
||||
|
||||
// Todo: Envelopes - Need to auto insert read only fields during sealing.
|
||||
if (field.fieldMeta?.readOnly) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Field ${fieldId} is read only`,
|
||||
@ -136,233 +131,7 @@ export const signEnvelopeFieldRoute = procedure
|
||||
throw new Error(`Field ${fieldId} has no recipientId`);
|
||||
}
|
||||
|
||||
let signatureImageAsBase64: string | null = null;
|
||||
let typedSignature: string | null = null;
|
||||
|
||||
const insertionValues: { customText: string; inserted: boolean } = match(fieldValue)
|
||||
.with({ type: FieldType.EMAIL }, (fieldValue) => {
|
||||
const parsedEmailValue = z.string().email().nullable().safeParse(fieldValue.value);
|
||||
|
||||
if (!parsedEmailValue.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid email',
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedEmailValue.data === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
customText: parsedEmailValue.data,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: P.union(FieldType.NAME, FieldType.INITIALS) }, (fieldValue) => {
|
||||
const parsedGenericStringValue = z.string().min(1).nullable().safeParse(fieldValue.value);
|
||||
|
||||
if (!parsedGenericStringValue.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Value is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedGenericStringValue.data === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
customText: parsedGenericStringValue.data,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.DATE }, (fieldValue) => {
|
||||
if (!fieldValue.value) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
customText: DateTime.now()
|
||||
.setZone(documentMeta.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
|
||||
.toFormat(documentMeta.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.NUMBER }, (fieldValue) => {
|
||||
if (!fieldValue.value) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(
|
||||
fieldValue.value.toString(),
|
||||
numberFieldParsedMeta,
|
||||
true,
|
||||
);
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid number',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value.toString(),
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.TEXT }, (fieldValue) => {
|
||||
if (fieldValue.value === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true);
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid email',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.RADIO }, (fieldValue) => {
|
||||
if (fieldValue.value === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedRadioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateRadioField(fieldValue.value, parsedRadioFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid radio value',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.CHECKBOX }, (fieldValue) => {
|
||||
if (fieldValue.value === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: Envelopes - This won't work.
|
||||
|
||||
const parsedCheckboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const checkboxFieldValues = parsedCheckboxFieldParsedMeta.values || [];
|
||||
|
||||
const { value } = fieldValue;
|
||||
|
||||
const selectedValues = checkboxFieldValues.filter(({ id }) => value.some((v) => v === id));
|
||||
|
||||
if (selectedValues.length !== value.length) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Selected values do not match the checkbox field values',
|
||||
});
|
||||
}
|
||||
|
||||
const errors = validateCheckboxField(
|
||||
selectedValues.map(({ value }) => value),
|
||||
parsedCheckboxFieldParsedMeta,
|
||||
true,
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid checkbox value:' + errors.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: JSON.stringify(fieldValue.value),
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.DROPDOWN }, (fieldValue) => {
|
||||
if (fieldValue.value === null) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true);
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid dropdown value',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.SIGNATURE }, (fieldValue) => {
|
||||
const { value, isBase64 } = fieldValue;
|
||||
|
||||
if (!value) {
|
||||
return {
|
||||
customText: '',
|
||||
inserted: false,
|
||||
};
|
||||
}
|
||||
|
||||
signatureImageAsBase64 = isBase64 ? value : null;
|
||||
typedSignature = !isBase64 ? value : null;
|
||||
|
||||
if (documentMeta.typedSignatureEnabled === false && typedSignature) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Typed signatures are not allowed. Please draw your signature',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customText: '',
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.exhaustive();
|
||||
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
@ -374,6 +143,24 @@ export const signEnvelopeFieldRoute = procedure
|
||||
|
||||
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
|
||||
|
||||
let signatureImageAsBase64 = null;
|
||||
let typedSignature = null;
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
if (fieldValue.type !== FieldType.SIGNATURE) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Field ${fieldId} is not a signature field`,
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldValue.value) {
|
||||
const isBase64 = isBase64Image(fieldValue.value);
|
||||
|
||||
signatureImageAsBase64 = isBase64 ? fieldValue.value : null;
|
||||
typedSignature = !isBase64 ? fieldValue.value : null;
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user