2023-12-31 13:58:15 +11:00
|
|
|
import { createNextRoute } from '@ts-rest/next';
|
|
|
|
|
2024-02-26 10:01:13 +11:00
|
|
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
2024-05-24 23:36:28 +10:00
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
2024-07-30 08:13:22 +02:00
|
|
|
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';
|
2024-05-10 19:45:19 +07:00
|
|
|
import { AppError } from '@documenso/lib/errors/app-error';
|
2024-02-12 15:16:09 +11:00
|
|
|
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
2024-02-20 19:46:18 +11:00
|
|
|
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
2024-02-12 15:16:09 +11:00
|
|
|
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
2023-12-31 13:58:15 +11:00
|
|
|
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';
|
2024-07-12 14:03:52 +03:00
|
|
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
2023-12-31 13:58:15 +11:00
|
|
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
2024-02-20 19:46:18 +11:00
|
|
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
2024-01-29 22:53:15 +11:00
|
|
|
import { createField } from '@documenso/lib/server-only/field/create-field';
|
|
|
|
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
|
|
|
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
|
|
|
import { updateField } from '@documenso/lib/server-only/field/update-field';
|
2024-04-08 17:01:11 +07:00
|
|
|
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
|
2024-01-29 22:53:15 +11:00
|
|
|
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
|
|
|
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
|
2024-01-22 17:38:02 +11:00
|
|
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
2023-12-31 13:58:15 +11:00
|
|
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
2024-01-29 22:53:15 +11:00
|
|
|
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
2024-05-10 19:45:19 +07:00
|
|
|
import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
|
|
|
|
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
2024-05-07 15:04:12 +07:00
|
|
|
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
|
feat: add more template API endpoints (#1198)
## Description
Update the API endpoint to support more actions for templates
## Changes Made
Add the following endpoints for templates:
- Get template
- Get templates
- Delete template
Get template(s) returns associated recipients and fields.
UI:
- Updated template delete button to have the destructive delete variant
## Testing Performed
Tested endpoints via /api/v1/openapi
Tested deleting templates via UI manually
## Test data
<details>
<summary>Delete template response</summary>
```json
{
"id": 32,
"type": "PRIVATE",
"title": "documenso-supporter-pledge.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxva9b4h0001rrh7v0wdw97h",
"createdAt": "2024-06-26T03:35:45.065Z",
"updatedAt": "2024-06-26T03:35:45.065Z"
}
```
</details>
<details>
<summary>Get template response</summary>
```json
{
"id": 28,
"type": "PRIVATE",
"title": "blank_long.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxu4vyty0003rrr52ue5ee4d",
"createdAt": "2024-06-25T08:17:38.418Z",
"updatedAt": "2024-06-26T03:36:33.890Z",
"templateMeta": {
"id": "clxvaacte0004rrh7s2k910nw",
"subject": "",
"message": "",
"timezone": "Australia/Melbourne",
"dateFormat": "yyyy-MM-dd hh:mm a",
"templateId": 28,
"redirectUrl": ""
},
"directLink": {
"token": "tBJHVFR75sC8m6hPfBTZd",
"enabled": true
},
"templateDocumentData": {
"id": "clxu4vyty0003rrr52ue5ee4d",
"type": "BYTES_64",
"data": "<PDF DATA>"
},
"Field": [
{
"id": 327,
"recipientId": 357,
"type": "SIGNATURE",
"page": 1,
"positionX": "55.8431952662722",
"positionY": "21.39588100686499",
"width": "29.58579881656805",
"height": "6.864988558352403"
},
{
"id": 328,
"recipientId": 357,
"type": "EMAIL",
"page": 1,
"positionX": "28.03254437869823",
"positionY": "72.99771167048056",
"width": "29.58579881656805",
"height": "6.864988558352403"
}
],
"Recipient": [
{
"id": 357,
"email": "direct.link@documenso.com",
"name": "Direct link recipient",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
},
{
"id": 359,
"email": "example@documenso.com",
"name": "Example User",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
}
]
}
```
</details>
<details>
<summary>Get templates response</summary>
```json
{
"templates": [
{
"id": 33,
"type": "PRIVATE",
"title": "documenso-supporter-pledge.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxva9oaj0003rrh7hwdyg60o",
"createdAt": "2024-06-26T03:36:02.130Z",
"updatedAt": "2024-06-26T03:36:02.130Z",
"directLink": null,
"Field": [],
"Recipient": []
},
{
"id": 28,
"type": "PRIVATE",
"title": "blank_long.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxu4vyty0003rrr52ue5ee4d",
"createdAt": "2024-06-25T08:17:38.418Z",
"updatedAt": "2024-06-26T03:36:33.890Z",
"directLink": {
"token": "tBJHVFR75sC8m6hPfBTZd",
"enabled": true
},
"Field": [
{
"id": 327,
"recipientId": 357,
"type": "SIGNATURE",
"page": 1,
"positionX": "55.8431952662722",
"positionY": "21.39588100686499",
"width": "29.58579881656805",
"height": "6.864988558352403"
},
{
"id": 328,
"recipientId": 357,
"type": "EMAIL",
"page": 1,
"positionX": "28.03254437869823",
"positionY": "72.99771167048056",
"width": "29.58579881656805",
"height": "6.864988558352403"
}
],
"Recipient": [
{
"id": 357,
"email": "direct.link@documenso.com",
"name": "Direct link recipient",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
},
{
"id": 359,
"email": "example@documenso.com",
"name": "Example User",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
}
]
}
],
"totalPages": 2
}
```
</details>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added support for team-based template deletion in the dashboard.
- Enhanced API to manage templates, including fetching and deleting
templates by team ID.
- **Bug Fixes**
- Improved error handling for template operations, ensuring better
feedback when templates are not found.
- **Refactor**
- Updated various components and functions to include `teamId` for more
robust template management.
- **Documentation**
- Expanded schema definitions to detail new structures for template and
team interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-27 15:44:16 +10:00
|
|
|
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';
|
2024-08-26 12:37:56 +03:00
|
|
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
2024-02-22 13:39:34 +11:00
|
|
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
2024-04-08 17:01:11 +07:00
|
|
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
2024-05-03 22:25:24 +07:00
|
|
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
2024-04-19 14:04:11 +03:00
|
|
|
import {
|
|
|
|
getPresignGetUrl,
|
|
|
|
getPresignPostUrl,
|
|
|
|
} from '@documenso/lib/universal/upload/server-actions';
|
2024-02-12 15:16:09 +11:00
|
|
|
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
import { ApiContractV1 } from './contract';
|
|
|
|
import { authenticatedMiddleware } from './middleware/authenticated';
|
|
|
|
|
|
|
|
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
2024-02-22 13:39:34 +11:00
|
|
|
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
2023-12-31 13:58:15 +11:00
|
|
|
const page = Number(args.query.page) || 1;
|
|
|
|
const perPage = Number(args.query.perPage) || 10;
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
const { data: documents, totalPages } = await findDocuments({
|
|
|
|
page,
|
|
|
|
perPage,
|
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
});
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
documents,
|
|
|
|
totalPages,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
getDocument: authenticatedMiddleware(async (args, user, team) => {
|
2023-12-31 13:58:15 +11:00
|
|
|
const { id: documentId } = args.params;
|
|
|
|
|
|
|
|
try {
|
2024-02-22 13:39:34 +11:00
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
});
|
|
|
|
|
2024-02-20 19:46:18 +11:00
|
|
|
const recipients = await getRecipientsForDocument({
|
|
|
|
documentId: Number(documentId),
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-02-20 19:46:18 +11:00
|
|
|
userId: user.id,
|
|
|
|
});
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
2024-02-20 19:46:18 +11:00
|
|
|
body: {
|
|
|
|
...document,
|
2024-05-24 23:36:28 +10:00
|
|
|
recipients: recipients.map((recipient) => ({
|
|
|
|
...recipient,
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
|
|
})),
|
2024-02-20 19:46:18 +11:00
|
|
|
},
|
2023-12-31 13:58:15 +11:00
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Document not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2024-04-19 14:04:11 +03:00
|
|
|
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({
|
|
|
|
id: 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.',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
2023-12-31 13:58:15 +11:00
|
|
|
const { id: documentId } = args.params;
|
|
|
|
|
|
|
|
try {
|
2024-02-22 13:39:34 +11:00
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!document) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Document not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
const deletedDocument = await deleteDocument({
|
2024-02-22 13:39:34 +11:00
|
|
|
id: document.id,
|
2023-12-31 13:58:15 +11:00
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2023-12-31 13:58:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: deletedDocument,
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Document not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
2023-12-31 13:58:15 +11:00
|
|
|
const { body } = args;
|
|
|
|
|
|
|
|
try {
|
2024-02-12 15:16:09 +11:00
|
|
|
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
|
|
|
return {
|
|
|
|
status: 500,
|
|
|
|
body: {
|
|
|
|
message: 'Create document is not available without S3 transport.',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-02-26 10:01:13 +11:00
|
|
|
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',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-07-30 08:13:22 +02:00
|
|
|
const dateFormat = body.meta.dateFormat
|
|
|
|
? DATE_FORMATS.find((format) => format.label === body.meta.dateFormat)
|
|
|
|
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
|
|
|
const timezone = body.meta.timezone
|
|
|
|
? TIME_ZONES.find((tz) => tz === body.meta.timezone)
|
|
|
|
: DEFAULT_DOCUMENT_TIME_ZONE;
|
|
|
|
|
|
|
|
const isDateFormatValid = body.meta.dateFormat
|
|
|
|
? DATE_FORMATS.some((format) => format.label === dateFormat?.label)
|
|
|
|
: true;
|
|
|
|
const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
|
|
|
|
|
|
|
if (!isDateFormatValid) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Invalid date format. Please provide a valid date format',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isTimeZoneValid) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Invalid timezone. Please provide a valid timezone',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-02-12 15:16:09 +11:00
|
|
|
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,
|
2024-07-13 16:45:09 +10:00
|
|
|
externalId: body.externalId || null,
|
2024-02-12 15:16:09 +11:00
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-04-08 17:01:11 +07:00
|
|
|
formValues: body.formValues,
|
2024-02-12 15:16:09 +11:00
|
|
|
documentDataId: documentData.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-02-12 15:16:09 +11:00
|
|
|
});
|
|
|
|
|
2024-04-29 12:42:22 +10:00
|
|
|
await upsertDocumentMeta({
|
|
|
|
documentId: document.id,
|
|
|
|
userId: user.id,
|
2024-07-30 08:13:22 +02:00
|
|
|
subject: body.meta.subject,
|
|
|
|
message: body.meta.message,
|
|
|
|
timezone,
|
|
|
|
dateFormat: dateFormat?.value,
|
|
|
|
redirectUrl: body.meta.redirectUrl,
|
2024-04-29 12:42:22 +10:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
|
|
});
|
|
|
|
|
2024-02-12 15:16:09 +11:00
|
|
|
const recipients = await setRecipientsForDocument({
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-02-12 15:16:09 +11:00
|
|
|
documentId: document.id,
|
|
|
|
recipients: body.recipients,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-02-12 15:16:09 +11:00
|
|
|
});
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
2024-02-12 15:16:09 +11:00
|
|
|
uploadUrl: url,
|
|
|
|
documentId: document.id,
|
|
|
|
recipients: recipients.map((recipient) => ({
|
|
|
|
recipientId: recipient.id,
|
2024-02-20 19:46:18 +11:00
|
|
|
name: recipient.name,
|
|
|
|
email: recipient.email,
|
2024-02-12 15:16:09 +11:00
|
|
|
token: recipient.token,
|
|
|
|
role: recipient.role,
|
2024-05-24 23:36:28 +10:00
|
|
|
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
2024-02-12 15:16:09 +11:00
|
|
|
})),
|
2023-12-31 13:58:15 +11:00
|
|
|
},
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'An error has occured while uploading the file',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
feat: add more template API endpoints (#1198)
## Description
Update the API endpoint to support more actions for templates
## Changes Made
Add the following endpoints for templates:
- Get template
- Get templates
- Delete template
Get template(s) returns associated recipients and fields.
UI:
- Updated template delete button to have the destructive delete variant
## Testing Performed
Tested endpoints via /api/v1/openapi
Tested deleting templates via UI manually
## Test data
<details>
<summary>Delete template response</summary>
```json
{
"id": 32,
"type": "PRIVATE",
"title": "documenso-supporter-pledge.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxva9b4h0001rrh7v0wdw97h",
"createdAt": "2024-06-26T03:35:45.065Z",
"updatedAt": "2024-06-26T03:35:45.065Z"
}
```
</details>
<details>
<summary>Get template response</summary>
```json
{
"id": 28,
"type": "PRIVATE",
"title": "blank_long.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxu4vyty0003rrr52ue5ee4d",
"createdAt": "2024-06-25T08:17:38.418Z",
"updatedAt": "2024-06-26T03:36:33.890Z",
"templateMeta": {
"id": "clxvaacte0004rrh7s2k910nw",
"subject": "",
"message": "",
"timezone": "Australia/Melbourne",
"dateFormat": "yyyy-MM-dd hh:mm a",
"templateId": 28,
"redirectUrl": ""
},
"directLink": {
"token": "tBJHVFR75sC8m6hPfBTZd",
"enabled": true
},
"templateDocumentData": {
"id": "clxu4vyty0003rrr52ue5ee4d",
"type": "BYTES_64",
"data": "<PDF DATA>"
},
"Field": [
{
"id": 327,
"recipientId": 357,
"type": "SIGNATURE",
"page": 1,
"positionX": "55.8431952662722",
"positionY": "21.39588100686499",
"width": "29.58579881656805",
"height": "6.864988558352403"
},
{
"id": 328,
"recipientId": 357,
"type": "EMAIL",
"page": 1,
"positionX": "28.03254437869823",
"positionY": "72.99771167048056",
"width": "29.58579881656805",
"height": "6.864988558352403"
}
],
"Recipient": [
{
"id": 357,
"email": "direct.link@documenso.com",
"name": "Direct link recipient",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
},
{
"id": 359,
"email": "example@documenso.com",
"name": "Example User",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
}
]
}
```
</details>
<details>
<summary>Get templates response</summary>
```json
{
"templates": [
{
"id": 33,
"type": "PRIVATE",
"title": "documenso-supporter-pledge.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxva9oaj0003rrh7hwdyg60o",
"createdAt": "2024-06-26T03:36:02.130Z",
"updatedAt": "2024-06-26T03:36:02.130Z",
"directLink": null,
"Field": [],
"Recipient": []
},
{
"id": 28,
"type": "PRIVATE",
"title": "blank_long.pdf",
"userId": 3,
"teamId": null,
"templateDocumentDataId": "clxu4vyty0003rrr52ue5ee4d",
"createdAt": "2024-06-25T08:17:38.418Z",
"updatedAt": "2024-06-26T03:36:33.890Z",
"directLink": {
"token": "tBJHVFR75sC8m6hPfBTZd",
"enabled": true
},
"Field": [
{
"id": 327,
"recipientId": 357,
"type": "SIGNATURE",
"page": 1,
"positionX": "55.8431952662722",
"positionY": "21.39588100686499",
"width": "29.58579881656805",
"height": "6.864988558352403"
},
{
"id": 328,
"recipientId": 357,
"type": "EMAIL",
"page": 1,
"positionX": "28.03254437869823",
"positionY": "72.99771167048056",
"width": "29.58579881656805",
"height": "6.864988558352403"
}
],
"Recipient": [
{
"id": 357,
"email": "direct.link@documenso.com",
"name": "Direct link recipient",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
},
{
"id": 359,
"email": "example@documenso.com",
"name": "Example User",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"role": "SIGNER"
}
]
}
],
"totalPages": 2
}
```
</details>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added support for team-based template deletion in the dashboard.
- Enhanced API to manage templates, including fetching and deleting
templates by team ID.
- **Bug Fixes**
- Improved error handling for template operations, ensuring better
feedback when templates are not found.
- **Refactor**
- Updated various components and functions to include `teamId` for more
robust template management.
- **Documentation**
- Expanded schema definitions to detail new structures for template and
team interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-27 15:44:16 +10:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
} 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 { templates, totalPages } = await findTemplates({
|
|
|
|
page,
|
|
|
|
perPage,
|
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
templates,
|
|
|
|
totalPages,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return AppError.toRestAPIError(err);
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
2024-02-20 19:46:18 +11:00
|
|
|
const { body, params } = args;
|
|
|
|
|
2024-02-26 10:01:13 +11:00
|
|
|
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',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-02-20 19:46:18 +11:00
|
|
|
const templateId = Number(params.templateId);
|
|
|
|
|
|
|
|
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
|
|
|
|
2024-05-07 15:04:12 +07:00
|
|
|
const document = await createDocumentFromTemplateLegacy({
|
2024-02-20 19:46:18 +11:00
|
|
|
templateId,
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-02-20 19:46:18 +11:00
|
|
|
recipients: body.recipients,
|
|
|
|
});
|
|
|
|
|
2024-04-08 17:01:11 +07:00
|
|
|
let documentDataId = document.documentDataId;
|
|
|
|
|
|
|
|
if (body.formValues) {
|
|
|
|
const pdf = await getFile(document.documentData);
|
|
|
|
|
|
|
|
const prefilled = await insertFormValuesInPdf({
|
|
|
|
pdf: Buffer.from(pdf),
|
|
|
|
formValues: body.formValues,
|
|
|
|
});
|
|
|
|
|
2024-05-03 22:25:24 +07:00
|
|
|
const newDocumentData = await putPdfFile({
|
2024-04-08 17:01:11 +07:00
|
|
|
name: fileName,
|
|
|
|
type: 'application/pdf',
|
|
|
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
|
|
|
});
|
|
|
|
|
|
|
|
documentDataId = newDocumentData.id;
|
|
|
|
}
|
|
|
|
|
2024-02-20 19:46:18 +11:00
|
|
|
await updateDocument({
|
|
|
|
documentId: document.id,
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-02-20 19:46:18 +11:00
|
|
|
data: {
|
2024-02-22 13:39:34 +11:00
|
|
|
title: fileName,
|
2024-07-13 16:45:09 +10:00
|
|
|
externalId: body.externalId || null,
|
2024-04-08 17:01:11 +07:00
|
|
|
formValues: body.formValues,
|
|
|
|
documentData: {
|
|
|
|
connect: {
|
|
|
|
id: documentDataId,
|
|
|
|
},
|
|
|
|
},
|
2024-02-20 19:46:18 +11:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (body.meta) {
|
|
|
|
await upsertDocumentMeta({
|
|
|
|
documentId: document.id,
|
|
|
|
userId: user.id,
|
2024-04-29 12:42:22 +10:00
|
|
|
...body.meta,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-02-20 19:46:18 +11:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
documentId: document.id,
|
|
|
|
recipients: document.Recipient.map((recipient) => ({
|
|
|
|
recipientId: recipient.id,
|
|
|
|
name: recipient.name,
|
|
|
|
email: recipient.email,
|
|
|
|
token: recipient.token,
|
|
|
|
role: recipient.role,
|
2024-05-24 23:36:28 +10:00
|
|
|
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
2024-02-20 19:46:18 +11:00
|
|
|
})),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-05-10 19:45:19 +07:00
|
|
|
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
|
|
|
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: CreateDocumentFromTemplateResponse | null = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
document = await createDocumentFromTemplate({
|
|
|
|
templateId,
|
2024-07-13 16:45:09 +10:00
|
|
|
externalId: body.externalId || null,
|
2024-05-10 19:45:19 +07:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
recipients: body.recipients,
|
|
|
|
override: {
|
|
|
|
title: body.title,
|
|
|
|
...body.meta,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
return AppError.toRestAPIError(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (body.formValues) {
|
|
|
|
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
|
|
|
|
|
|
|
|
const pdf = await getFile(document.documentData);
|
|
|
|
|
|
|
|
const prefilled = await insertFormValuesInPdf({
|
|
|
|
pdf: Buffer.from(pdf),
|
|
|
|
formValues: body.formValues,
|
|
|
|
});
|
|
|
|
|
|
|
|
const newDocumentData = await putPdfFile({
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
documentId: document.id,
|
|
|
|
recipients: document.Recipient.map((recipient) => ({
|
|
|
|
recipientId: recipient.id,
|
|
|
|
name: recipient.name,
|
|
|
|
email: recipient.email,
|
|
|
|
token: recipient.token,
|
|
|
|
role: recipient.role,
|
2024-05-24 23:36:28 +10:00
|
|
|
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
2024-05-10 19:45:19 +07:00
|
|
|
})),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
2023-12-31 13:58:15 +11:00
|
|
|
const { id } = args.params;
|
2024-05-24 23:36:28 +10:00
|
|
|
const { sendEmail = true } = args.body ?? {};
|
2023-12-31 13:58:15 +11:00
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
const document = await getDocumentById({ id: Number(id), userId: user.id, teamId: team?.id });
|
2023-12-31 13:58:15 +11:00
|
|
|
|
|
|
|
if (!document) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Document not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
if (document.status === DocumentStatus.COMPLETED) {
|
2023-12-31 13:58:15 +11:00
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
2024-02-22 13:39:34 +11:00
|
|
|
message: 'Document is already complete',
|
2023-12-31 13:58:15 +11:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2024-01-22 17:38:02 +11:00
|
|
|
// await setRecipientsForDocument({
|
|
|
|
// userId: user.id,
|
|
|
|
// documentId: Number(id),
|
|
|
|
// recipients: [
|
|
|
|
// {
|
|
|
|
// email: body.signerEmail,
|
|
|
|
// name: body.signerName ?? '',
|
|
|
|
// },
|
|
|
|
// ],
|
|
|
|
// });
|
|
|
|
|
|
|
|
// await setFieldsForDocument({
|
|
|
|
// documentId: Number(id),
|
|
|
|
// userId: user.id,
|
|
|
|
// fields: body.fields.map((field) => ({
|
|
|
|
// signerEmail: body.signerEmail,
|
|
|
|
// type: field.fieldType,
|
|
|
|
// pageNumber: field.pageNumber,
|
|
|
|
// pageX: field.pageX,
|
|
|
|
// pageY: field.pageY,
|
|
|
|
// pageWidth: field.pageWidth,
|
|
|
|
// pageHeight: field.pageHeight,
|
|
|
|
// })),
|
|
|
|
// });
|
|
|
|
|
|
|
|
// if (body.emailBody || body.emailSubject) {
|
|
|
|
// await upsertDocumentMeta({
|
|
|
|
// documentId: Number(id),
|
|
|
|
// subject: body.emailSubject ?? '',
|
|
|
|
// message: body.emailBody ?? '',
|
|
|
|
// });
|
|
|
|
// }
|
2023-12-31 13:58:15 +11:00
|
|
|
|
2024-05-24 23:36:28 +10:00
|
|
|
const { Recipient: recipients, ...sentDocument } = await sendDocument({
|
2023-12-31 13:58:15 +11:00
|
|
|
documentId: Number(id),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-05-24 23:36:28 +10:00
|
|
|
sendEmail,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2023-12-31 13:58:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
message: 'Document sent for signing successfully',
|
2024-05-24 23:36:28 +10:00
|
|
|
...sentDocument,
|
|
|
|
recipients: recipients.map((recipient) => ({
|
|
|
|
...recipient,
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
|
|
})),
|
2023-12-31 13:58:15 +11:00
|
|
|
},
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 500,
|
|
|
|
body: {
|
|
|
|
message: 'An error has occured while sending the document for signing',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
2024-01-22 17:38:02 +11:00
|
|
|
|
2024-07-12 14:03:52 +03:00
|
|
|
resendDocument: authenticatedMiddleware(async (args, user, team) => {
|
|
|
|
const { id: documentId } = args.params;
|
|
|
|
const { recipients } = args.body;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await resendDocument({
|
|
|
|
userId: user.id,
|
|
|
|
documentId: Number(documentId),
|
|
|
|
recipients,
|
|
|
|
teamId: team?.id,
|
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
message: 'Document resend successfully initiated',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 500,
|
|
|
|
body: {
|
|
|
|
message: 'An error has occured while resending the document',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-22 17:38:02 +11:00
|
|
|
const { id: documentId } = args.params;
|
2024-02-12 15:16:09 +11:00
|
|
|
const { name, email, role } = args.body;
|
2024-01-22 17:38:02 +11:00
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-22 17:38:02 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-22 17:38:02 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email);
|
|
|
|
|
|
|
|
if (recipientAlreadyExists) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Recipient already exists',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const newRecipients = await setRecipientsForDocument({
|
|
|
|
documentId: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-22 17:38:02 +11:00
|
|
|
recipients: [
|
|
|
|
...recipients,
|
|
|
|
{
|
|
|
|
email,
|
|
|
|
name,
|
2024-02-12 15:16:09 +11:00
|
|
|
role,
|
2024-01-22 17:38:02 +11:00
|
|
|
},
|
|
|
|
],
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-01-22 17:38:02 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
|
|
|
|
|
|
|
if (!newRecipient) {
|
|
|
|
throw new Error('Recipient not found');
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
2024-02-12 15:16:09 +11:00
|
|
|
body: {
|
|
|
|
...newRecipient,
|
|
|
|
documentId: Number(documentId),
|
2024-05-24 23:36:28 +10:00
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${newRecipient.token}`,
|
2024-02-12 15:16:09 +11:00
|
|
|
},
|
2024-01-22 17:38:02 +11:00
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return {
|
|
|
|
status: 500,
|
|
|
|
body: {
|
|
|
|
message: 'An error has occured while creating the recipient',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
2024-01-29 22:53:15 +11:00
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-29 22:53:15 +11:00
|
|
|
const { id: documentId, recipientId } = args.params;
|
2024-02-12 15:16:09 +11:00
|
|
|
const { name, email, role } = args.body;
|
2024-01-29 22:53:15 +11:00
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
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),
|
2024-02-22 13:39:34 +11:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
email,
|
|
|
|
name,
|
2024-02-12 15:16:09 +11:00
|
|
|
role,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-01-29 22:53:15 +11:00
|
|
|
}).catch(() => null);
|
|
|
|
|
|
|
|
if (!updatedRecipient) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Recipient not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
2024-02-12 15:16:09 +11:00
|
|
|
body: {
|
|
|
|
...updatedRecipient,
|
|
|
|
documentId: Number(documentId),
|
2024-05-24 23:36:28 +10:00
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${updatedRecipient.token}`,
|
2024-02-12 15:16:09 +11:00
|
|
|
},
|
2024-01-29 22:53:15 +11:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
deleteRecipient: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-29 22:53:15 +11:00
|
|
|
const { id: documentId, recipientId } = args.params;
|
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
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),
|
2024-02-22 13:39:34 +11:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-01-29 22:53:15 +11:00
|
|
|
}).catch(() => null);
|
|
|
|
|
|
|
|
if (!deletedRecipient) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Unable to delete recipient',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
2024-02-12 15:16:09 +11:00
|
|
|
body: {
|
|
|
|
...deletedRecipient,
|
|
|
|
documentId: Number(documentId),
|
2024-05-24 23:36:28 +10:00
|
|
|
signingUrl: '',
|
2024-02-12 15:16:09 +11:00
|
|
|
},
|
2024-01-29 22:53:15 +11:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
createField: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-29 22:53:15 +11:00
|
|
|
const { id: documentId } = args.params;
|
2024-08-26 12:37:56 +03:00
|
|
|
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
|
|
|
|
args.body;
|
|
|
|
|
|
|
|
if (pageNumber <= 0) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Invalid page number',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2024-01-29 22:53:15 +11:00
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
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 getRecipientById({
|
|
|
|
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',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-08-26 12:37:56 +03:00
|
|
|
try {
|
|
|
|
const field = await createField({
|
|
|
|
documentId: Number(documentId),
|
|
|
|
recipientId: Number(recipientId),
|
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
type,
|
|
|
|
pageNumber,
|
|
|
|
pageX,
|
|
|
|
pageY,
|
|
|
|
pageWidth,
|
|
|
|
pageHeight,
|
|
|
|
fieldMeta,
|
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
|
|
});
|
2024-01-29 22:53:15 +11:00
|
|
|
|
2024-08-26 12:37:56 +03:00
|
|
|
const remappedField = {
|
|
|
|
id: field.id,
|
|
|
|
documentId: 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: ZFieldMetaSchema.parse(field.fieldMeta),
|
|
|
|
inserted: field.inserted,
|
|
|
|
};
|
2024-01-29 22:53:15 +11:00
|
|
|
|
2024-08-26 12:37:56 +03:00
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
...remappedField,
|
|
|
|
documentId: Number(documentId),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
return AppError.toRestAPIError(err);
|
|
|
|
}
|
2024-01-29 22:53:15 +11:00
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
updateField: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-29 22:53:15 +11:00
|
|
|
const { id: documentId, fieldId } = args.params;
|
|
|
|
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: Number(documentId),
|
|
|
|
userId: user.id,
|
2024-02-22 13:39:34 +11:00
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
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 getRecipientById({
|
|
|
|
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),
|
2024-02-22 13:39:34 +11:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
documentId: Number(documentId),
|
|
|
|
recipientId: recipientId ? Number(recipientId) : undefined,
|
|
|
|
type,
|
|
|
|
pageNumber,
|
|
|
|
pageX,
|
|
|
|
pageY,
|
|
|
|
pageWidth,
|
|
|
|
pageHeight,
|
2024-02-22 13:39:34 +11:00
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-01-29 22:53:15 +11:00
|
|
|
});
|
|
|
|
|
|
|
|
const remappedField = {
|
2024-02-20 19:46:18 +11:00
|
|
|
id: updatedField.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
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,
|
2024-02-12 15:16:09 +11:00
|
|
|
body: {
|
|
|
|
...remappedField,
|
|
|
|
documentId: Number(documentId),
|
|
|
|
},
|
2024-01-29 22:53:15 +11:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
|
2024-02-22 13:39:34 +11:00
|
|
|
deleteField: authenticatedMiddleware(async (args, user, team) => {
|
2024-01-29 22:53:15 +11:00
|
|
|
const { id: documentId, fieldId } = args.params;
|
|
|
|
|
|
|
|
const document = await getDocumentById({
|
|
|
|
id: 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({
|
2024-07-18 16:45:44 +03:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
fieldId: Number(fieldId),
|
|
|
|
documentId: Number(documentId),
|
|
|
|
}).catch(() => null);
|
|
|
|
|
|
|
|
if (!field) {
|
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: {
|
|
|
|
message: 'Field not found',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const recipient = await getRecipientById({
|
|
|
|
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),
|
2024-02-22 13:39:34 +11:00
|
|
|
userId: user.id,
|
|
|
|
teamId: team?.id,
|
|
|
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
2024-01-29 22:53:15 +11:00
|
|
|
}).catch(() => null);
|
|
|
|
|
|
|
|
if (!deletedField) {
|
|
|
|
return {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: 'Unable to delete field',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const remappedField = {
|
2024-02-20 19:46:18 +11:00
|
|
|
id: deletedField.id,
|
2024-01-29 22:53:15 +11:00
|
|
|
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,
|
2024-02-12 15:16:09 +11:00
|
|
|
body: {
|
|
|
|
...remappedField,
|
|
|
|
documentId: Number(documentId),
|
|
|
|
},
|
2024-01-29 22:53:15 +11:00
|
|
|
};
|
|
|
|
}),
|
2023-12-31 13:58:15 +11:00
|
|
|
});
|