Files
sign/packages/api/v1/implementation.ts
David Nguyen 25bb6ffe77 fix: imports
2025-03-03 14:49:28 +11:00

1620 lines
42 KiB
TypeScript

import type { Prisma } from '@prisma/client';
import { DocumentDataType, DocumentStatus, SigningStatus, TeamMemberRole } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import '@documenso/lib/constants/time-zones';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { updateField } from '@documenso/lib/server-only/field/update-field';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZFieldMetaSchema,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import {
getPresignGetUrl,
getPresignPostUrl,
} from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { ApiContractV1 } from './contract';
import { authenticatedMiddleware } from './middleware/authenticated';
export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
getDocuments: authenticatedMiddleware(async (args, user, team) => {
const page = Number(args.query.page) || 1;
const perPage = Number(args.query.perPage) || 10;
const { data: documents, totalPages } = await findDocuments({
page,
perPage,
userId: user.id,
teamId: team?.id,
});
return {
status: 200,
body: {
documents,
totalPages,
},
};
}),
getDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
try {
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const fields = await getFieldsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const parsedMetaFields = fields.map((field) => {
let parsedMetaOrNull = null;
if (field.fieldMeta) {
const result = ZFieldMetaSchema.safeParse(field.fieldMeta);
if (!result.success) {
throw new Error('Field meta parsing failed for field ' + field.id);
}
parsedMetaOrNull = result.data;
}
return {
...field,
fieldMeta: parsedMetaOrNull,
};
});
return {
status: 200,
body: {
...document,
recipients: recipients.map((recipient) => ({
...recipient,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
fields: parsedMetaFields,
},
};
} catch (err) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
}),
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
return {
status: 500,
body: {
message: 'Please make sure the storage transport is set to S3.',
},
};
}
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document || !document.documentDataId) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (DocumentDataType.S3_PATH !== document.documentData.type) {
return {
status: 400,
body: {
message: 'Invalid document data type',
},
};
}
if (document.status !== DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is not completed yet.',
},
};
}
const { url } = await getPresignGetUrl(document.documentData.data);
return {
status: 200,
body: { downloadUrl: url },
};
} catch (err) {
return {
status: 500,
body: {
message: 'Error downloading the document. Please try again.',
},
};
}
}),
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
try {
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
const deletedDocument = await deleteDocument({
id: document.id,
userId: user.id,
teamId: team?.id,
requestMetadata: metadata,
});
return {
status: 200,
body: deletedDocument,
};
} catch (err) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
}),
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body } = args;
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
return {
status: 500,
body: {
message: 'Create document is not available without S3 transport.',
},
};
}
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const dateFormat = body.meta.dateFormat
? DATE_FORMATS.find((format) => format.value === body.meta.dateFormat)
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
if (body.meta.dateFormat && !dateFormat) {
return {
status: 400,
body: {
message: 'Invalid date format. Please provide a valid date format',
},
};
}
const timezone = body.meta.timezone
? TIME_ZONES.find((tz) => tz === body.meta.timezone)
: DEFAULT_DOCUMENT_TIME_ZONE;
const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
if (!isTimeZoneValid) {
return {
status: 400,
body: {
message: 'Invalid timezone. Please provide a valid timezone',
},
};
}
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const documentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const document = await createDocument({
title: body.title,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
formValues: body.formValues,
documentDataId: documentData.id,
requestMetadata: metadata,
});
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
subject: body.meta.subject,
message: body.meta.message,
timezone,
dateFormat: dateFormat?.value,
redirectUrl: body.meta.redirectUrl,
signingOrder: body.meta.signingOrder,
language: body.meta.language,
typedSignatureEnabled: body.meta.typedSignatureEnabled,
distributionMethod: body.meta.distributionMethod,
emailSettings: body.meta.emailSettings,
requestMetadata: metadata,
});
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
...body.authOptions,
},
requestMetadata: metadata,
});
}
const { recipients } = await setDocumentRecipients({
userId: user.id,
teamId: team?.id,
documentId: document.id,
recipients: body.recipients,
requestMetadata: metadata,
});
return {
status: 200,
body: {
uploadUrl: url,
documentId: document.id,
recipients: recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
} catch (err) {
return {
status: 404,
body: {
message: 'An error has occured while uploading the file',
},
};
}
}),
deleteTemplate: authenticatedMiddleware(async (args, user, team) => {
const { id: templateId } = args.params;
try {
const deletedTemplate = await deleteTemplate({
id: Number(templateId),
userId: user.id,
teamId: team?.id,
});
return {
status: 200,
body: deletedTemplate,
};
} catch (err) {
return {
status: 404,
body: {
message: 'Template not found',
},
};
}
}),
getTemplate: authenticatedMiddleware(async (args, user, team) => {
const { id: templateId } = args.params;
try {
const template = await getTemplateById({
id: Number(templateId),
userId: user.id,
teamId: team?.id,
});
return {
status: 200,
body: {
...template,
Field: template.fields.map((field) => ({
...field,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
})),
Recipient: template.recipients,
},
};
} catch (err) {
return AppError.toRestAPIError(err);
}
}),
getTemplates: authenticatedMiddleware(async (args, user, team) => {
const page = Number(args.query.page) || 1;
const perPage = Number(args.query.perPage) || 10;
try {
const { data: templates, totalPages } = await findTemplates({
page,
perPage,
userId: user.id,
teamId: team?.id,
});
return {
status: 200,
body: {
templates: templates.map((template) => ({
...template,
Field: template.fields.map((field) => ({
...field,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
})),
Recipient: template.recipients,
})),
totalPages,
},
};
} catch (err) {
return AppError.toRestAPIError(err);
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const document = await createDocumentFromTemplateLegacy({
templateId,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
externalId: body.externalId || null,
formValues: body.formValues,
documentData: {
connect: {
id: documentDataId,
},
},
},
});
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
document = await createDocumentFromTemplate({
templateId,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
override: {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
formValues: body.formValues,
documentData: {
connect: {
id: newDocumentData.id,
},
},
},
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { sendEmail, sendCompletionEmails } = args.body;
try {
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already complete',
},
};
}
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
// Update document email settings if sendCompletionEmails is provided
if (typeof sendCompletionEmails === 'boolean') {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
emailSettings: {
...emailSettings,
documentCompleted: sendCompletionEmails,
ownerDocumentCompleted: sendCompletionEmails,
},
requestMetadata: metadata,
});
}
const { recipients, ...sentDocument } = await sendDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
sendEmail,
requestMetadata: metadata,
});
return {
status: 200,
body: {
message: 'Document sent for signing successfully',
...sentDocument,
recipients: recipients.map((recipient) => ({
...recipient,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
} catch (err) {
return {
status: 500,
body: {
message: 'An error has occured while sending the document for signing',
},
};
}
}),
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
try {
await resendDocument({
userId: user.id,
documentId: Number(documentId),
recipients,
teamId: team?.id,
requestMetadata: metadata,
});
return {
status: 200,
body: {
message: 'Document resend successfully initiated',
},
};
} catch (err) {
return {
status: 500,
body: {
message: 'An error has occured while resending the document',
},
};
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email);
if (recipientAlreadyExists) {
return {
status: 400,
body: {
message: 'Recipient already exists',
},
};
}
try {
const { recipients: newRecipients } = await setDocumentRecipients({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
recipients: [
...recipients.map(({ email, name }) => ({
email,
name,
role,
})),
{
email,
name,
role,
signingOrder,
actionAuth: authOptions?.actionAuth ?? null,
},
],
requestMetadata: metadata,
});
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
if (!newRecipient) {
throw new Error('Recipient not found');
}
return {
status: 200,
body: {
...newRecipient,
documentId: Number(documentId),
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${newRecipient.token}`,
},
};
} catch (err) {
return {
status: 500,
body: {
message: 'An error has occured while creating the recipient',
},
};
}
}),
updateRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId, recipientId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const updatedRecipient = await updateRecipient({
documentId: Number(documentId),
recipientId: Number(recipientId),
userId: user.id,
teamId: team?.id,
email,
name,
role,
signingOrder,
actionAuth: authOptions?.actionAuth,
requestMetadata: metadata.requestMetadata,
}).catch(() => null);
if (!updatedRecipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
return {
status: 200,
body: {
...updatedRecipient,
documentId: Number(documentId),
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${updatedRecipient.token}`,
},
};
}),
deleteRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId, recipientId } = args.params;
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const deletedRecipient = await deleteRecipient({
documentId: Number(documentId),
recipientId: Number(recipientId),
userId: user.id,
teamId: team?.id,
requestMetadata: metadata.requestMetadata,
}).catch(() => null);
if (!deletedRecipient) {
return {
status: 400,
body: {
message: 'Unable to delete recipient',
},
};
}
return {
status: 200,
body: {
...deletedRecipient,
documentId: Number(documentId),
signingUrl: '',
},
};
}),
createField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const fields = Array.isArray(args.body) ? args.body : [args.body];
const document = await prisma.document.findFirst({
select: { id: true, status: true },
where: {
id: Number(documentId),
...(team?.id
? {
team: {
id: team.id,
members: { some: { userId: user.id } },
},
}
: {
userId: user.id,
teamId: null,
}),
},
});
if (!document) {
return {
status: 404,
body: { message: 'Document not found' },
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: { message: 'Document is already completed' },
};
}
try {
const createdFields = await prisma.$transaction(async (tx) => {
return Promise.all(
fields.map(async (fieldData) => {
const {
recipientId,
type,
pageNumber,
pageWidth,
pageHeight,
pageX,
pageY,
fieldMeta,
} = fieldData;
if (pageNumber <= 0) {
throw new Error('Invalid page number');
}
const recipient = await getRecipientByIdV1Api({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
throw new Error('Recipient not found');
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
throw new Error('Recipient has already signed the document');
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
type,
);
if (advancedField && !fieldMeta) {
throw new Error(
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
);
}
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
throw new Error('Field meta type does not match the field type');
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: undefined,
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: undefined,
}))
.exhaustive();
if (!result.success) {
throw new Error('Field meta parsing failed');
}
const field = await tx.field.create({
data: {
documentId: Number(documentId),
recipientId: Number(recipientId),
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
},
include: {
recipient: true,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: 'FIELD_CREATED',
documentId: Number(documentId),
user: {
id: team?.id ?? user.id,
email: team?.name ?? user.email,
name: team ? '' : user.name,
},
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.recipient?.email ?? '',
fieldRecipientId: recipientId,
fieldType: field.type,
},
requestMetadata: metadata.requestMetadata,
}),
});
return {
id: field.id,
documentId: Number(field.documentId),
recipientId: field.recipientId ?? -1,
type: field.type,
pageNumber: field.page,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
customText: field.customText,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
inserted: field.inserted,
};
}),
);
});
return {
status: 200,
body: {
fields: createdFields,
documentId: Number(documentId),
},
};
} catch (err) {
return AppError.toRestAPIError(err);
}
}),
updateField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId, fieldId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
args.body;
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const recipient = await getRecipientByIdV1Api({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
const updatedField = await updateField({
fieldId: Number(fieldId),
userId: user.id,
teamId: team?.id,
documentId: Number(documentId),
recipientId: recipientId ? Number(recipientId) : undefined,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
requestMetadata: metadata.requestMetadata,
fieldMeta: fieldMeta ? ZFieldMetaSchema.parse(fieldMeta) : undefined,
});
const remappedField = {
id: updatedField.id,
documentId: updatedField.documentId,
recipientId: updatedField.recipientId ?? -1,
type: updatedField.type,
pageNumber: updatedField.page,
pageX: Number(updatedField.positionX),
pageY: Number(updatedField.positionY),
pageWidth: Number(updatedField.width),
pageHeight: Number(updatedField.height),
customText: updatedField.customText,
inserted: updatedField.inserted,
};
return {
status: 200,
body: {
...remappedField,
documentId: Number(documentId),
},
};
}),
deleteField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId, fieldId } = args.params;
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const field = await getFieldById({
userId: user.id,
teamId: team?.id,
fieldId: Number(fieldId),
documentId: Number(documentId),
}).catch(() => null);
if (!field) {
return {
status: 404,
body: {
message: 'Field not found',
},
};
}
const recipient = await getRecipientByIdV1Api({
id: Number(field.recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (recipient?.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
const deletedField = await deleteField({
documentId: Number(documentId),
fieldId: Number(fieldId),
userId: user.id,
teamId: team?.id,
requestMetadata: metadata.requestMetadata,
}).catch(() => null);
if (!deletedField) {
return {
status: 400,
body: {
message: 'Unable to delete field',
},
};
}
const remappedField = {
id: deletedField.id,
documentId: deletedField.documentId,
recipientId: deletedField.recipientId ?? -1,
type: deletedField.type,
pageNumber: deletedField.page,
pageX: Number(deletedField.positionX),
pageY: Number(deletedField.positionY),
pageWidth: Number(deletedField.width),
pageHeight: Number(deletedField.height),
customText: deletedField.customText,
inserted: deletedField.inserted,
};
return {
status: 200,
body: {
...remappedField,
documentId: Number(documentId),
},
};
}),
findTeamMembers: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId } = args.params;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const members = await prisma.teamMember.findMany({
where: {
teamId: team.id,
},
include: {
user: true,
},
});
return {
status: 200,
body: {
members: members.map((member) => ({
id: member.id,
email: member.user.email,
role: member.role,
})),
},
};
}),
inviteTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId } = args.params;
const { email, role } = args.body;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const hasAlreadyBeenInvited = await prisma.teamMember.findFirst({
where: {
teamId: team.id,
user: {
email,
},
},
});
if (hasAlreadyBeenInvited) {
return {
status: 400,
body: {
message: 'This user has already been invited to the team',
},
};
}
await createTeamMemberInvites({
userId: user.id,
userName: user.name ?? '',
teamId: team.id,
invitations: [
{
email,
role,
},
],
});
return {
status: 200,
body: {
message: 'An invite has been sent to the member',
},
};
}),
updateTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId, memberId } = args.params;
const { role } = args.body;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const member = await prisma.teamMember.findFirst({
where: {
id: Number(memberId),
teamId: team.id,
},
});
if (!member) {
return {
status: 404,
body: {
message: 'The provided member id does not exist.',
},
};
}
const updatedMember = await prisma.teamMember.update({
where: {
id: member.id,
},
data: {
role,
},
include: {
user: true,
},
});
return {
status: 200,
body: {
id: updatedMember.id,
email: updatedMember.user.email,
role: updatedMember.role,
},
};
}),
removeTeamMember: authenticatedMiddleware(async (args, user, team) => {
const { id: teamId, memberId } = args.params;
if (team?.id !== Number(teamId)) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const self = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId: team.id,
},
});
if (self?.role !== TeamMemberRole.ADMIN) {
return {
status: 403,
body: {
message: 'You are not authorized to perform actions against this team.',
},
};
}
const member = await prisma.teamMember.findFirst({
where: {
id: Number(memberId),
teamId: Number(teamId),
},
include: {
user: true,
},
});
if (!member) {
return {
status: 404,
body: {
message: 'Member not found',
},
};
}
if (team.ownerUserId === member.userId) {
return {
status: 403,
body: {
message: 'You cannot remove the owner of the team',
},
};
}
if (member.userId === user.id) {
return {
status: 403,
body: {
message: 'You cannot remove yourself from the team',
},
};
}
await deleteTeamMembers({
userId: user.id,
teamId: team.id,
teamMemberIds: [member.id],
});
return {
status: 200,
body: {
id: member.id,
email: member.user.email,
role: member.role,
},
};
}),
});
const updateDocument = async ({
documentId,
userId,
teamId,
data,
}: {
documentId: number;
data: Prisma.DocumentUpdateInput;
userId: number;
teamId?: number;
}) => {
return await prisma.document.update({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
data: {
...data,
},
});
};