mirror of
https://github.com/documenso/documenso.git
synced 2025-11-24 13:41:30 +10:00
chore: merged api
This commit is contained in:
1
packages/api/index.ts
Normal file
1
packages/api/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/api/next.ts
Normal file
1
packages/api/next.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createNextRouter } from '@ts-rest/next';
|
||||
30
packages/api/package.json
Normal file
30
packages/api/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@documenso/api",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"next.ts",
|
||||
"v1/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@ts-rest/core": "^3.30.5",
|
||||
"@ts-rest/next": "^3.30.5",
|
||||
"@ts-rest/open-api": "^3.33.0",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"luxon": "^3.4.0",
|
||||
"superjson": "^1.13.1",
|
||||
"swagger-ui-react": "^5.11.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
8
packages/api/tsconfig.json
Normal file
8
packages/api/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
}
|
||||
}
|
||||
10
packages/api/v1/api-documentation.tsx
Normal file
10
packages/api/v1/api-documentation.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
|
||||
|
||||
export const OpenApiDocsPage = () => {
|
||||
return <SwaggerUI spec={OpenAPIV1} displayOperationId={true} />;
|
||||
};
|
||||
191
packages/api/v1/contract.ts
Normal file
191
packages/api/v1/contract.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
|
||||
import {
|
||||
ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
|
||||
ZAuthorizationHeadersSchema,
|
||||
ZCreateDocumentFromTemplateMutationResponseSchema,
|
||||
ZCreateDocumentFromTemplateMutationSchema,
|
||||
ZCreateDocumentMutationResponseSchema,
|
||||
ZCreateDocumentMutationSchema,
|
||||
ZCreateFieldMutationSchema,
|
||||
ZCreateRecipientMutationSchema,
|
||||
ZDeleteDocumentMutationSchema,
|
||||
ZDeleteFieldMutationSchema,
|
||||
ZDeleteRecipientMutationSchema,
|
||||
ZGetDocumentsQuerySchema,
|
||||
ZSuccessfulDocumentResponseSchema,
|
||||
ZSuccessfulFieldResponseSchema,
|
||||
ZSuccessfulGetDocumentResponseSchema,
|
||||
ZSuccessfulRecipientResponseSchema,
|
||||
ZSuccessfulResponseSchema,
|
||||
ZSuccessfulSigningResponseSchema,
|
||||
ZUnsuccessfulResponseSchema,
|
||||
ZUpdateFieldMutationSchema,
|
||||
ZUpdateRecipientMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const ApiContractV1 = c.router(
|
||||
{
|
||||
getDocuments: {
|
||||
method: 'GET',
|
||||
path: '/api/v1/documents',
|
||||
query: ZGetDocumentsQuerySchema,
|
||||
responses: {
|
||||
200: ZSuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Get all documents',
|
||||
},
|
||||
|
||||
getDocument: {
|
||||
method: 'GET',
|
||||
path: '/api/v1/documents/:id',
|
||||
responses: {
|
||||
200: ZSuccessfulGetDocumentResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Get a single document',
|
||||
},
|
||||
|
||||
createDocument: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/documents',
|
||||
body: ZCreateDocumentMutationSchema,
|
||||
responses: {
|
||||
200: ZCreateDocumentMutationResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Upload a new document and get a presigned URL',
|
||||
},
|
||||
|
||||
createDocumentFromTemplate: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/templates/:templateId/create-document',
|
||||
body: ZCreateDocumentFromTemplateMutationSchema,
|
||||
responses: {
|
||||
200: ZCreateDocumentFromTemplateMutationResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Upload a new document and get a presigned URL',
|
||||
},
|
||||
|
||||
sendDocument: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/documents/:id/send',
|
||||
body: SendDocumentMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulSigningResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Send a document for signing',
|
||||
},
|
||||
|
||||
deleteDocument: {
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/documents/:id',
|
||||
body: ZDeleteDocumentMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulDocumentResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Delete a document',
|
||||
},
|
||||
|
||||
createRecipient: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/documents/:id/recipients',
|
||||
body: ZCreateRecipientMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulRecipientResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Create a recipient for a document',
|
||||
},
|
||||
|
||||
updateRecipient: {
|
||||
method: 'PATCH',
|
||||
path: '/api/v1/documents/:id/recipients/:recipientId',
|
||||
body: ZUpdateRecipientMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulRecipientResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Update a recipient for a document',
|
||||
},
|
||||
|
||||
deleteRecipient: {
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/documents/:id/recipients/:recipientId',
|
||||
body: ZDeleteRecipientMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulRecipientResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Delete a recipient from a document',
|
||||
},
|
||||
|
||||
createField: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/documents/:id/fields',
|
||||
body: ZCreateFieldMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulFieldResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Create a field for a document',
|
||||
},
|
||||
|
||||
updateField: {
|
||||
method: 'PATCH',
|
||||
path: '/api/v1/documents/:id/fields/:fieldId',
|
||||
body: ZUpdateFieldMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulFieldResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Update a field for a document',
|
||||
},
|
||||
|
||||
deleteField: {
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/documents/:id/fields/:fieldId',
|
||||
body: ZDeleteFieldMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulFieldResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Delete a field from a document',
|
||||
},
|
||||
},
|
||||
{
|
||||
baseHeaders: ZAuthorizationHeadersSchema,
|
||||
},
|
||||
);
|
||||
59
packages/api/v1/examples/01-create-and-send-document.ts
Normal file
59
packages/api/v1/examples/01-create-and-send-document.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const { status, body } = await client.createDocument({
|
||||
body: {
|
||||
title: 'My Document',
|
||||
recipients: [
|
||||
{
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
role: 'SIGNER',
|
||||
},
|
||||
{
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com',
|
||||
role: 'APPROVER',
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
subject: 'Please sign this document',
|
||||
message: 'Hey {signer.name}, please sign the following document: {document.name}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to create document');
|
||||
}
|
||||
|
||||
const { uploadUrl, documentId } = body;
|
||||
|
||||
await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: '<raw-binary-data>',
|
||||
});
|
||||
|
||||
await client.sendDocument({
|
||||
params: {
|
||||
id: documentId.toString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
43
packages/api/v1/examples/02-add-a-field.ts
Normal file
43
packages/api/v1/examples/02-add-a-field.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
const recipientId = 1;
|
||||
|
||||
const { status, body } = await client.createField({
|
||||
params: {
|
||||
id: documentId,
|
||||
},
|
||||
body: {
|
||||
type: 'SIGNATURE',
|
||||
pageHeight: 2.5, // percent of page to occupy in height
|
||||
pageWidth: 5, // percent of page to occupy in width
|
||||
pageX: 10, // percent from left
|
||||
pageY: 10, // percent from top
|
||||
pageNumber: 1,
|
||||
recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to create field');
|
||||
}
|
||||
|
||||
const { id: fieldId } = body;
|
||||
|
||||
console.log(`Field created with id: ${fieldId}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
39
packages/api/v1/examples/03-update-a-field.ts
Normal file
39
packages/api/v1/examples/03-update-a-field.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
const fieldId = '1';
|
||||
|
||||
const { status } = await client.updateField({
|
||||
params: {
|
||||
id: documentId,
|
||||
fieldId,
|
||||
},
|
||||
body: {
|
||||
type: 'SIGNATURE',
|
||||
pageHeight: 2.5, // percent of page to occupy in height
|
||||
pageWidth: 5, // percent of page to occupy in width
|
||||
pageX: 10, // percent from left
|
||||
pageY: 10, // percent from top
|
||||
pageNumber: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to update field');
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
31
packages/api/v1/examples/04-remove-a-field.ts
Normal file
31
packages/api/v1/examples/04-remove-a-field.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
const fieldId = '1';
|
||||
|
||||
const { status } = await client.deleteField({
|
||||
params: {
|
||||
id: documentId,
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to remove field');
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
38
packages/api/v1/examples/05-add-a-recipient.ts
Normal file
38
packages/api/v1/examples/05-add-a-recipient.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
|
||||
const { status, body } = await client.createRecipient({
|
||||
params: {
|
||||
id: documentId,
|
||||
},
|
||||
body: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
role: 'APPROVER',
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to add recipient');
|
||||
}
|
||||
|
||||
const { id: recipientId } = body;
|
||||
|
||||
console.log(`Recipient added with id: ${recipientId}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
34
packages/api/v1/examples/06-update-a-recipient.ts
Normal file
34
packages/api/v1/examples/06-update-a-recipient.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
const recipientId = '1';
|
||||
|
||||
const { status } = await client.updateRecipient({
|
||||
params: {
|
||||
id: documentId,
|
||||
recipientId,
|
||||
},
|
||||
body: {
|
||||
name: 'Johnathon Doe',
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to update recipient');
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
31
packages/api/v1/examples/07-remove-a-recipient.ts
Normal file
31
packages/api/v1/examples/07-remove-a-recipient.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
const recipientId = '1';
|
||||
|
||||
const { status } = await client.deleteRecipient({
|
||||
params: {
|
||||
id: documentId,
|
||||
recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to update recipient');
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
31
packages/api/v1/examples/08-get-a-document.ts
Normal file
31
packages/api/v1/examples/08-get-a-document.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = '1';
|
||||
|
||||
const { status, body } = await client.getDocument({
|
||||
params: {
|
||||
id: documentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to get document');
|
||||
}
|
||||
|
||||
console.log(`Got document with id: ${documentId} and title: ${body.title}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
37
packages/api/v1/examples/09-paginate-all-documents.ts
Normal file
37
packages/api/v1/examples/09-paginate-all-documents.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { initClient } from '@ts-rest/core';
|
||||
|
||||
import { ApiContractV1 } from '../contract';
|
||||
|
||||
const main = async () => {
|
||||
const client = initClient(ApiContractV1, {
|
||||
baseUrl: 'http://localhost:3000/api/v1',
|
||||
baseHeaders: {
|
||||
authorization: 'Bearer <my-token>',
|
||||
},
|
||||
});
|
||||
|
||||
const page = 1;
|
||||
const perPage = 10;
|
||||
|
||||
const { status, body } = await client.getDocuments({
|
||||
query: {
|
||||
page,
|
||||
perPage,
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error('Failed to get documents');
|
||||
}
|
||||
|
||||
for (const document of body.documents) {
|
||||
console.log(`Got document with id: ${document.id} and title: ${document.title}`);
|
||||
}
|
||||
|
||||
console.log(`Total documents: ${body.totalPages * perPage}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
720
packages/api/v1/implementation.ts
Normal file
720
packages/api/v1/implementation.ts
Normal file
@ -0,0 +1,720 @@
|
||||
import { createNextRoute } from '@ts-rest/next';
|
||||
|
||||
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 { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
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';
|
||||
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
||||
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||
|
||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
getDocuments: authenticatedMiddleware(async (args, user) => {
|
||||
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 });
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
documents,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
getDocument: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...document,
|
||||
recipients,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
deleteDocument: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
|
||||
|
||||
const deletedDocument = await deleteDocument({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
status: document.status,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: deletedDocument,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
createDocument: authenticatedMiddleware(async (args, user) => {
|
||||
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 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,
|
||||
userId: user.id,
|
||||
documentDataId: documentData.id,
|
||||
});
|
||||
|
||||
const recipients = await setRecipientsForDocument({
|
||||
userId: user.id,
|
||||
documentId: document.id,
|
||||
recipients: body.recipients,
|
||||
});
|
||||
|
||||
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,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'An error has occured while uploading the file',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const templateId = Number(params.templateId);
|
||||
|
||||
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
||||
|
||||
const document = await createDocumentFromTemplate({
|
||||
templateId,
|
||||
userId: user.id,
|
||||
recipients: body.recipients,
|
||||
});
|
||||
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
data: {
|
||||
title: body.title,
|
||||
},
|
||||
});
|
||||
|
||||
if (body.meta) {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
subject: body.meta.subject,
|
||||
message: body.meta.message,
|
||||
dateFormat: body.meta.dateFormat,
|
||||
timezone: body.meta.timezone,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user) => {
|
||||
const { id } = args.params;
|
||||
|
||||
const document = await getDocumentById({ id: Number(id), userId: user.id });
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (document.status === 'PENDING') {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already waiting for signing',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 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 ?? '',
|
||||
// });
|
||||
// }
|
||||
|
||||
await sendDocument({
|
||||
documentId: Number(id),
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Document sent for signing successfully',
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: 'An error has occured while sending the document for signing',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
|
||||
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 recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
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,
|
||||
recipients: [
|
||||
...recipients,
|
||||
{
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||
|
||||
if (!newRecipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...newRecipient,
|
||||
documentId: Number(documentId),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: 'An error has occured while creating the recipient',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
updateRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId, recipientId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
|
||||
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 updatedRecipient = await updateRecipient({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!updatedRecipient) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Recipient not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...updatedRecipient,
|
||||
documentId: Number(documentId),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
deleteRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId, recipientId } = 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 deletedRecipient = await deleteRecipient({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!deletedRecipient) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Unable to delete recipient',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...deletedRecipient,
|
||||
documentId: Number(documentId),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
createField: authenticatedMiddleware(async (args, user) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
||||
|
||||
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 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 field = await createField({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
});
|
||||
|
||||
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,
|
||||
inserted: field.inserted,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...remappedField,
|
||||
documentId: Number(documentId),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
updateField: authenticatedMiddleware(async (args, user) => {
|
||||
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,
|
||||
});
|
||||
|
||||
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),
|
||||
documentId: Number(documentId),
|
||||
recipientId: recipientId ? Number(recipientId) : undefined,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
});
|
||||
|
||||
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) => {
|
||||
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({
|
||||
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),
|
||||
}).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),
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
41
packages/api/v1/middleware/authenticated.ts
Normal file
41
packages/api/v1/middleware/authenticated.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
|
||||
import { getUserByApiToken } from '@documenso/lib/server-only/public-api/get-user-by-token';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
|
||||
export const authenticatedMiddleware = <
|
||||
T extends {
|
||||
req: NextApiRequest;
|
||||
},
|
||||
R extends {
|
||||
status: number;
|
||||
body: unknown;
|
||||
},
|
||||
>(
|
||||
handler: (args: T, user: User) => Promise<R>,
|
||||
) => {
|
||||
return async (args: T) => {
|
||||
try {
|
||||
const { authorization } = args.req.headers;
|
||||
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
const [token] = (authorization || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Token was not provided for authenticated middleware');
|
||||
}
|
||||
|
||||
const user = await getUserByApiToken({ token });
|
||||
|
||||
return await handler(args, user);
|
||||
} catch (_err) {
|
||||
console.log({ _err });
|
||||
return {
|
||||
status: 401,
|
||||
body: {
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
};
|
||||
};
|
||||
17
packages/api/v1/openapi.ts
Normal file
17
packages/api/v1/openapi.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { generateOpenApi } from '@ts-rest/open-api';
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
|
||||
export const OpenAPIV1 = generateOpenApi(
|
||||
ApiContractV1,
|
||||
{
|
||||
info: {
|
||||
title: 'Documenso API',
|
||||
version: '1.0.0',
|
||||
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
|
||||
},
|
||||
},
|
||||
{
|
||||
setOperationId: true,
|
||||
},
|
||||
);
|
||||
240
packages/api/v1/schema.ts
Normal file
240
packages/api/v1/schema.ts
Normal file
@ -0,0 +1,240 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
FieldType,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
/**
|
||||
* Documents
|
||||
*/
|
||||
export const ZGetDocumentsQuerySchema = z.object({
|
||||
page: z.coerce.number().min(1).optional().default(1),
|
||||
perPage: z.coerce.number().min(1).optional().default(1),
|
||||
});
|
||||
|
||||
export type TGetDocumentsQuerySchema = z.infer<typeof ZGetDocumentsQuerySchema>;
|
||||
|
||||
export const ZDeleteDocumentMutationSchema = null;
|
||||
|
||||
export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema;
|
||||
|
||||
export const ZSuccessfulDocumentResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
userId: z.number(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
documentDataId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
completedAt: z.date().nullable(),
|
||||
});
|
||||
|
||||
export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseSchema.extend({
|
||||
recipients: z.lazy(() => z.array(ZSuccessfulRecipientResponseSchema)),
|
||||
});
|
||||
|
||||
export type TSuccessfulGetDocumentResponseSchema = z.infer<
|
||||
typeof ZSuccessfulGetDocumentResponseSchema
|
||||
>;
|
||||
|
||||
export type TSuccessfulDocumentResponseSchema = z.infer<typeof ZSuccessfulDocumentResponseSchema>;
|
||||
|
||||
export const ZSendDocumentForSigningMutationSchema = null;
|
||||
|
||||
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
|
||||
|
||||
export const ZUploadDocumentSuccessfulSchema = z.object({
|
||||
url: z.string(),
|
||||
key: z.string(),
|
||||
});
|
||||
|
||||
export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSuccessfulSchema>;
|
||||
|
||||
export const ZCreateDocumentMutationSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
}),
|
||||
),
|
||||
meta: z
|
||||
.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
});
|
||||
|
||||
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
|
||||
|
||||
export const ZCreateDocumentMutationResponseSchema = z.object({
|
||||
uploadUrl: z.string().min(1),
|
||||
documentId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TCreateDocumentMutationResponseSchema = z.infer<
|
||||
typeof ZCreateDocumentMutationResponseSchema
|
||||
>;
|
||||
|
||||
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
}),
|
||||
),
|
||||
meta: z
|
||||
.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
|
||||
typeof ZCreateDocumentFromTemplateMutationSchema
|
||||
>;
|
||||
|
||||
export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
|
||||
typeof ZCreateDocumentFromTemplateMutationResponseSchema
|
||||
>;
|
||||
|
||||
export const ZCreateRecipientMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
});
|
||||
|
||||
/**
|
||||
* Recipients
|
||||
*/
|
||||
export type TCreateRecipientMutationSchema = z.infer<typeof ZCreateRecipientMutationSchema>;
|
||||
|
||||
export const ZUpdateRecipientMutationSchema = ZCreateRecipientMutationSchema.partial();
|
||||
|
||||
export type TUpdateRecipientMutationSchema = z.infer<typeof ZUpdateRecipientMutationSchema>;
|
||||
|
||||
export const ZDeleteRecipientMutationSchema = null;
|
||||
|
||||
export type TDeleteRecipientMutationSchema = typeof ZDeleteRecipientMutationSchema;
|
||||
|
||||
export const ZSuccessfulRecipientResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
// !: This handles the fact that we have null documentId's for templates
|
||||
// !: while we won't need the default we must add it to satisfy typescript
|
||||
documentId: z.number().nullish().default(-1),
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
token: z.string(),
|
||||
// !: Not used for now
|
||||
// expired: z.string(),
|
||||
signedAt: z.date().nullable(),
|
||||
readStatus: z.nativeEnum(ReadStatus),
|
||||
signingStatus: z.nativeEnum(SigningStatus),
|
||||
sendStatus: z.nativeEnum(SendStatus),
|
||||
});
|
||||
|
||||
export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecipientResponseSchema>;
|
||||
|
||||
/**
|
||||
* Fields
|
||||
*/
|
||||
export const ZCreateFieldMutationSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
pageNumber: z.number(),
|
||||
pageX: z.number(),
|
||||
pageY: z.number(),
|
||||
pageWidth: z.number(),
|
||||
pageHeight: z.number(),
|
||||
});
|
||||
|
||||
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
|
||||
|
||||
export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial();
|
||||
|
||||
export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>;
|
||||
|
||||
export const ZDeleteFieldMutationSchema = null;
|
||||
|
||||
export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
|
||||
|
||||
export const ZSuccessfulFieldResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
documentId: z.number(),
|
||||
recipientId: z.number(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
pageNumber: z.number(),
|
||||
pageX: z.number(),
|
||||
pageY: z.number(),
|
||||
pageWidth: z.number(),
|
||||
pageHeight: z.number(),
|
||||
customText: z.string(),
|
||||
inserted: z.boolean(),
|
||||
});
|
||||
|
||||
export type TSuccessfulFieldResponseSchema = z.infer<typeof ZSuccessfulFieldResponseSchema>;
|
||||
|
||||
export const ZSuccessfulResponseSchema = z.object({
|
||||
documents: ZSuccessfulDocumentResponseSchema.array(),
|
||||
totalPages: z.number(),
|
||||
});
|
||||
|
||||
export type TSuccessfulResponseSchema = z.infer<typeof ZSuccessfulResponseSchema>;
|
||||
|
||||
export const ZSuccessfulSigningResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export type TSuccessfulSigningResponseSchema = z.infer<typeof ZSuccessfulSigningResponseSchema>;
|
||||
|
||||
/**
|
||||
* General
|
||||
*/
|
||||
export const ZAuthorizationHeadersSchema = z.object({
|
||||
authorization: z.string(),
|
||||
});
|
||||
|
||||
export type TAuthorizationHeadersSchema = z.infer<typeof ZAuthorizationHeadersSchema>;
|
||||
|
||||
export const ZUnsuccessfulResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export type TUnsuccessfulResponseSchema = z.infer<typeof ZUnsuccessfulResponseSchema>;
|
||||
@ -1,5 +1,11 @@
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
export const ONE_SECOND = 1000;
|
||||
export const ONE_MINUTE = ONE_SECOND * 60;
|
||||
export const ONE_HOUR = ONE_MINUTE * 60;
|
||||
export const ONE_DAY = ONE_HOUR * 24;
|
||||
export const ONE_WEEK = ONE_DAY * 7;
|
||||
export const ONE_MONTH = Duration.fromObject({ months: 1 });
|
||||
export const THREE_MONTHS = Duration.fromObject({ months: 3 });
|
||||
export const SIX_MONTHS = Duration.fromObject({ months: 6 });
|
||||
export const ONE_YEAR = Duration.fromObject({ years: 1 });
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { compareSync as bcryptCompareSync, hashSync as bcryptHashSync } from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { SALT_ROUNDS } from '../../constants/auth';
|
||||
|
||||
@ -12,3 +13,7 @@ export const hashSync = (password: string) => {
|
||||
export const compareSync = (password: string, hash: string) => {
|
||||
return bcryptCompareSync(password, hash);
|
||||
};
|
||||
|
||||
export const hashString = (input: string) => {
|
||||
return crypto.createHash('sha512').update(input).digest('hex');
|
||||
};
|
||||
|
||||
41
packages/lib/server-only/field/create-field.ts
Normal file
41
packages/lib/server-only/field/create-field.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export type CreateFieldOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
};
|
||||
|
||||
export const createField = async ({
|
||||
documentId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
}: CreateFieldOptions) => {
|
||||
const field = await prisma.field.create({
|
||||
data: {
|
||||
documentId,
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
},
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
17
packages/lib/server-only/field/delete-field.ts
Normal file
17
packages/lib/server-only/field/delete-field.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type DeleteFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const deleteField = async ({ fieldId, documentId }: DeleteFieldOptions) => {
|
||||
const field = await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
17
packages/lib/server-only/field/get-field-by-id.ts
Normal file
17
packages/lib/server-only/field/get-field-by-id.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetFieldByIdOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({ fieldId, documentId }: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
44
packages/lib/server-only/field/update-field.ts
Normal file
44
packages/lib/server-only/field/update-field.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
recipientId?: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
};
|
||||
|
||||
export const updateField = async ({
|
||||
fieldId,
|
||||
documentId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
}: UpdateFieldOptions) => {
|
||||
const field = await prisma.field.update({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
},
|
||||
data: {
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
},
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
51
packages/lib/server-only/public-api/create-api-token.ts
Normal file
51
packages/lib/server-only/public-api/create-api-token.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
// temporary choice for testing only
|
||||
import * as timeConstants from '../../constants/time';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
type TimeConstants = typeof timeConstants & {
|
||||
[key: string]: number | Duration;
|
||||
};
|
||||
|
||||
type CreateApiTokenInput = {
|
||||
userId: number;
|
||||
tokenName: string;
|
||||
expirationDate: string | null;
|
||||
};
|
||||
|
||||
export const createApiToken = async ({
|
||||
userId,
|
||||
tokenName,
|
||||
expirationDate,
|
||||
}: CreateApiTokenInput) => {
|
||||
const apiToken = `api_${alphaid(16)}`;
|
||||
|
||||
const hashedToken = hashString(apiToken);
|
||||
|
||||
const timeConstantsRecords: TimeConstants = timeConstants;
|
||||
|
||||
const dbToken = await prisma.apiToken.create({
|
||||
data: {
|
||||
token: hashedToken,
|
||||
name: tokenName,
|
||||
userId,
|
||||
expires: expirationDate
|
||||
? DateTime.now().plus(timeConstantsRecords[expirationDate]).toJSDate()
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbToken) {
|
||||
throw new Error('Failed to create the API token');
|
||||
}
|
||||
|
||||
return {
|
||||
id: dbToken.id,
|
||||
token: apiToken,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type DeleteTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const deleteTokenById = async ({ id, userId }: DeleteTokenByIdOptions) => {
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
23
packages/lib/server-only/public-api/get-all-user-tokens.ts
Normal file
23
packages/lib/server-only/public-api/get-all-user-tokens.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetUserTokensOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {
|
||||
return await prisma.apiToken.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
algorithm: true,
|
||||
createdAt: true,
|
||||
expires: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
15
packages/lib/server-only/public-api/get-api-token-by-id.ts
Normal file
15
packages/lib/server-only/public-api/get-api-token-by-id.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetApiTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => {
|
||||
return await prisma.apiToken.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
37
packages/lib/server-only/public-api/get-user-by-token.ts
Normal file
37
packages/lib/server-only/public-api/get-user-by-token.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
export const getUserByApiToken = async ({ token }: { token: string }) => {
|
||||
const hashedToken = hashString(token);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
ApiToken: {
|
||||
some: {
|
||||
token: hashedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
ApiToken: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const retrievedToken = user.ApiToken.find((apiToken) => apiToken.token === hashedToken);
|
||||
|
||||
// This should be impossible but we need to satisfy TypeScript
|
||||
if (!retrievedToken) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
if (retrievedToken.expires && retrievedToken.expires < new Date()) {
|
||||
throw new Error('Expired token');
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
32
packages/lib/server-only/recipient/delete-recipient.ts
Normal file
32
packages/lib/server-only/recipient/delete-recipient.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type DeleteRecipientOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
};
|
||||
|
||||
export const deleteRecipient = async ({ documentId, recipientId }: DeleteRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (recipient.sendStatus !== SendStatus.NOT_SENT) {
|
||||
throw new Error('Can not delete a recipient that has already been sent a document');
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.recipient.delete({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
21
packages/lib/server-only/recipient/get-recipient-by-email.ts
Normal file
21
packages/lib/server-only/recipient/get-recipient-by-email.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetRecipientByEmailOptions = {
|
||||
documentId: number;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const getRecipientByEmail = async ({ documentId, email }: GetRecipientByEmailOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
return recipient;
|
||||
};
|
||||
21
packages/lib/server-only/recipient/get-recipient-by-id.ts
Normal file
21
packages/lib/server-only/recipient/get-recipient-by-id.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetRecipientByIdOptions = {
|
||||
id: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const getRecipientById = async ({ documentId, id }: GetRecipientByIdOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
return recipient;
|
||||
};
|
||||
42
packages/lib/server-only/recipient/update-recipient.ts
Normal file
42
packages/lib/server-only/recipient/update-recipient.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
};
|
||||
|
||||
export const updateRecipient = async ({
|
||||
documentId,
|
||||
recipientId,
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
}: UpdateRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
const updatedRecipient = await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
email: email?.toLowerCase() ?? recipient.email,
|
||||
name: name ?? recipient.name,
|
||||
role: role ?? recipient.role,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedRecipient;
|
||||
};
|
||||
@ -1,14 +1,21 @@
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateDocumentFromTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
import type { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export type CreateDocumentFromTemplateOptions = TCreateDocumentFromTemplateMutationSchema & {
|
||||
export type CreateDocumentFromTemplateOptions = {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
recipients?: {
|
||||
name?: string;
|
||||
email: string;
|
||||
role?: RecipientRole;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const createDocumentFromTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
recipients,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
@ -63,7 +70,11 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
|
||||
include: {
|
||||
Recipient: true,
|
||||
Recipient: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -88,5 +99,34 @@ export const createDocumentFromTemplate = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
if (recipients && recipients.length > 0) {
|
||||
document.Recipient = await Promise.all(
|
||||
recipients.map(async (recipient, index) => {
|
||||
const existingRecipient = document.Recipient.at(index);
|
||||
|
||||
return await prisma.recipient.upsert({
|
||||
where: {
|
||||
documentId_email: {
|
||||
documentId: document.id,
|
||||
email: existingRecipient?.email ?? recipient.email,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
},
|
||||
create: {
|
||||
documentId: document.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
token: nanoid(),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ApiTokenAlgorithm" AS ENUM ('SHA512');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiToken" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"algorithm" "ApiTokenAlgorithm" NOT NULL DEFAULT 'SHA512',
|
||||
"expires" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "ApiToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiToken_token_key" ON "ApiToken"("token");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ApiToken" DROP CONSTRAINT "ApiToken_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ApiToken" ALTER COLUMN "expires" DROP NOT NULL;
|
||||
@ -43,9 +43,9 @@ model User {
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
|
||||
VerificationToken VerificationToken[]
|
||||
Template Template[]
|
||||
VerificationToken VerificationToken[]
|
||||
ApiToken ApiToken[]
|
||||
Template Template[]
|
||||
securityAuditLogs UserSecurityAuditLog[]
|
||||
|
||||
@@index([email])
|
||||
@ -94,6 +94,21 @@ model VerificationToken {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum ApiTokenAlgorithm {
|
||||
SHA512
|
||||
}
|
||||
|
||||
model ApiToken {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
token String @unique
|
||||
algorithm ApiTokenAlgorithm @default(SHA512)
|
||||
expires DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
"@trpc/next": "^10.36.0",
|
||||
"@trpc/react-query": "^10.36.0",
|
||||
"@trpc/server": "^10.36.0",
|
||||
"@ts-rest/core": "^3.30.5",
|
||||
"@ts-rest/next": "^3.30.5",
|
||||
"luxon": "^3.4.0",
|
||||
"superjson": "^1.13.1",
|
||||
"ts-pattern": "^5.0.5",
|
||||
|
||||
81
packages/trpc/server/api-token-router/router.ts
Normal file
81
packages/trpc/server/api-token-router/router.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
|
||||
import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
|
||||
import { getApiTokenById } from '@documenso/lib/server-only/public-api/get-api-token-by-id';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateTokenMutationSchema,
|
||||
ZDeleteTokenByIdMutationSchema,
|
||||
ZGetApiTokenByIdQuerySchema,
|
||||
} from './schema';
|
||||
|
||||
export const apiTokenRouter = router({
|
||||
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
return await getUserTokens({ userId: ctx.user.id });
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to find your API tokens. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getTokenById: authenticatedProcedure
|
||||
.input(ZGetApiTokenByIdQuerySchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id } = input;
|
||||
|
||||
return await getApiTokenById({
|
||||
id,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to find this API token. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
createToken: authenticatedProcedure
|
||||
.input(ZCreateTokenMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { tokenName, expirationDate } = input;
|
||||
|
||||
return await createApiToken({
|
||||
userId: ctx.user.id,
|
||||
tokenName,
|
||||
expirationDate,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to create an API token. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
deleteTokenById: authenticatedProcedure
|
||||
.input(ZDeleteTokenByIdMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id } = input;
|
||||
|
||||
return await deleteTokenById({
|
||||
id,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to delete this API Token. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
20
packages/trpc/server/api-token-router/schema.ts
Normal file
20
packages/trpc/server/api-token-router/schema.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetApiTokenByIdQuerySchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export type TGetApiTokenByIdQuerySchema = z.infer<typeof ZGetApiTokenByIdQuerySchema>;
|
||||
|
||||
export const ZCreateTokenMutationSchema = z.object({
|
||||
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
|
||||
expirationDate: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type TCreateTokenMutationSchema = z.infer<typeof ZCreateTokenMutationSchema>;
|
||||
|
||||
export const ZDeleteTokenByIdMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenByIdMutationSchema>;
|
||||
@ -1,4 +1,5 @@
|
||||
import { adminRouter } from './admin-router/router';
|
||||
import { apiTokenRouter } from './api-token-router/router';
|
||||
import { authRouter } from './auth-router/router';
|
||||
import { cryptoRouter } from './crypto/router';
|
||||
import { documentRouter } from './document-router/router';
|
||||
@ -21,6 +22,7 @@ export const appRouter = router({
|
||||
recipient: recipientRouter,
|
||||
admin: adminRouter,
|
||||
shareLink: shareLinkRouter,
|
||||
apiToken: apiTokenRouter,
|
||||
singleplayer: singleplayerRouter,
|
||||
team: teamRouter,
|
||||
template: templateRouter,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user