+
);
};
diff --git a/apps/web/src/pages/api/v1/[...ts-rest].tsx b/apps/web/src/pages/api/v1/[...ts-rest].tsx
index 0b22d97c6..15b618ebd 100644
--- a/apps/web/src/pages/api/v1/[...ts-rest].tsx
+++ b/apps/web/src/pages/api/v1/[...ts-rest].tsx
@@ -1,227 +1,5 @@
-import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
-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 { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
-import { getUserByApiToken } from '@documenso/lib/server-only/public-api/get-user-by-token';
-import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
-import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
-import { contract } from '@documenso/trpc/api-contract/contract';
-import { createNextRoute, createNextRouter } from '@documenso/trpc/server/public-api/ts-rest';
+import { createNextRouter } from '@documenso/api/next';
+import { ApiContractV1 } from '@documenso/api/v1/contract';
+import { ApiContractV1Implementation } from '@documenso/api/v1/implementation';
-const router = createNextRoute(contract, {
- getDocuments: async (args) => {
- const page = Number(args.query.page) || 1;
- const perPage = Number(args.query.perPage) || 10;
- const { authorization } = args.headers;
- let user;
-
- try {
- user = await getUserByApiToken({ token: authorization });
- } catch (e) {
- return {
- status: 401,
- body: {
- message: e.message,
- },
- };
- }
-
- const { data: documents, totalPages } = await findDocuments({ page, perPage, userId: user.id });
-
- return {
- status: 200,
- body: {
- documents,
- totalPages,
- },
- };
- },
- getDocument: async (args) => {
- const { id: documentId } = args.params;
- const { authorization } = args.headers;
- let user;
-
- try {
- user = await getUserByApiToken({ token: authorization });
- } catch (e) {
- return {
- status: 401,
- body: {
- message: e.message,
- },
- };
- }
-
- try {
- const document = await getDocumentById({ id: Number(documentId), userId: user.id });
-
- return {
- status: 200,
- body: document,
- };
- } catch (e) {
- return {
- status: 404,
- body: {
- message: e.message ?? 'Document not found',
- },
- };
- }
- },
- deleteDocument: async (args) => {
- const { id: documentId } = args.params;
- const { authorization } = args.headers;
-
- let user;
-
- try {
- user = await getUserByApiToken({ token: authorization });
- } catch (e) {
- return {
- status: 401,
- body: {
- message: e.message,
- },
- };
- }
-
- 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 (e) {
- return {
- status: 404,
- body: {
- message: e.message ?? 'Document not found',
- },
- };
- }
- },
- createDocument: async (args) => {
- const { body } = args;
-
- try {
- const { url, key } = await getPresignPostUrl(body.fileName, body.contentType);
-
- return {
- status: 200,
- body: {
- url,
- key,
- },
- };
- } catch (e) {
- return {
- status: 404,
- body: {
- message: e.message ?? 'An error has occured while uploading the file',
- },
- };
- }
- },
- sendDocumentForSigning: async (args) => {
- const { authorization } = args.headers;
- const { id } = args.params;
- const { body } = args;
- let user;
-
- try {
- user = await getUserByApiToken({ token: authorization });
- } catch (e) {
- return {
- status: 401,
- body: {
- message: e.message,
- },
- };
- }
-
- 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 (e) {
- return {
- status: 500,
- body: {
- message: e.message ?? 'An error has occured while sending the document for signing',
- },
- };
- }
- },
-});
-
-export default createNextRouter(contract, router);
+export default createNextRouter(ApiContractV1, ApiContractV1Implementation);
diff --git a/package-lock.json b/package-lock.json
index d244df9e8..148b02096 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -88,6 +88,7 @@
"version": "1.2.3",
"license": "AGPL-3.0",
"dependencies": {
+ "@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
@@ -167,18 +168,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@anatine/zod-openapi": {
- "version": "1.14.2",
- "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-1.14.2.tgz",
- "integrity": "sha512-q0qHfnuNYVKu0Swrnnvfj9971AEyW7c8v9jCOZGCl5ZbyGMNG4RPyJkRcMi/JC8CRfdOe0IDfNm1nNsi2avprg==",
- "dependencies": {
- "ts-deepmerge": "^6.0.3"
- },
- "peerDependencies": {
- "openapi3-ts": "^2.0.0 || ^3.0.0",
- "zod": "^3.20.0"
- }
- },
"node_modules/@aws-crypto/crc32": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
@@ -1776,6 +1765,10 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@documenso/api": {
+ "resolved": "packages/api",
+ "link": true
+ },
"node_modules/@documenso/app-tests": {
"resolved": "packages/app-tests",
"link": true
@@ -14379,22 +14372,6 @@
"node": ">= 14.17.0"
}
},
- "node_modules/openapi3-ts": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz",
- "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==",
- "dependencies": {
- "yaml": "^1.10.2"
- }
- },
- "node_modules/openapi3-ts/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/openid-client": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz",
@@ -17858,14 +17835,6 @@
"typescript": ">=4.2.0"
}
},
- "node_modules/ts-deepmerge": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.0.tgz",
- "integrity": "sha512-2qxI/FZVDPbzh63GwWIZYE7daWKtwXZYuyc8YNq0iTmMUwn4mL0jRLsp6hfFlgbdRSR4x2ppe+E86FnvEpN7Nw==",
- "engines": {
- "node": ">=14.13.1"
- }
- },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -19268,6 +19237,233 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/api": {
+ "name": "@documenso/api",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@documenso/lib": "*",
+ "@documenso/prisma": "*",
+ "@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",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {}
+ },
+ "packages/api/node_modules/@next/env": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz",
+ "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==",
+ "peer": true
+ },
+ "packages/api/node_modules/@next/swc-darwin-arm64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
+ "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-darwin-x64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
+ "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
+ "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
+ "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-x64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
+ "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-x64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz",
+ "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
+ "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
+ "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
+ "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@ts-rest/next": {
+ "version": "3.30.5",
+ "resolved": "https://registry.npmjs.org/@ts-rest/next/-/next-3.30.5.tgz",
+ "integrity": "sha512-NasfUN7SnwcjJNbxvvcemC4fOv4f4IF5I14wVqQODN0HWPokkrta6XLuv0eKQJYdB32AS7VINQhls8Sj1AIN0g==",
+ "peerDependencies": {
+ "@ts-rest/core": "3.30.5",
+ "next": "^12.0.0 || ^13.0.0",
+ "zod": "^3.22.3"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "packages/api/node_modules/next": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz",
+ "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==",
+ "peer": true,
+ "dependencies": {
+ "@next/env": "13.5.6",
+ "@swc/helpers": "0.5.2",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=16.14.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "13.5.6",
+ "@next/swc-darwin-x64": "13.5.6",
+ "@next/swc-linux-arm64-gnu": "13.5.6",
+ "@next/swc-linux-arm64-musl": "13.5.6",
+ "@next/swc-linux-x64-gnu": "13.5.6",
+ "@next/swc-linux-x64-musl": "13.5.6",
+ "@next/swc-win32-arm64-msvc": "13.5.6",
+ "@next/swc-win32-ia32-msvc": "13.5.6",
+ "@next/swc-win32-x64-msvc": "13.5.6"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
"packages/app-tests": {
"name": "@documenso/app-tests",
"version": "1.0.0",
diff --git a/packages/api/index.ts b/packages/api/index.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/packages/api/index.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/api/next.ts b/packages/api/next.ts
new file mode 100644
index 000000000..5ac5aab45
--- /dev/null
+++ b/packages/api/next.ts
@@ -0,0 +1 @@
+export { createNextRouter } from '@ts-rest/next';
diff --git a/packages/api/package.json b/packages/api/package.json
new file mode 100644
index 000000000..9aea9b26f
--- /dev/null
+++ b/packages/api/package.json
@@ -0,0 +1,28 @@
+{
+ "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",
+ "luxon": "^3.4.0",
+ "superjson": "^1.13.1",
+ "ts-pattern": "^5.0.5",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {}
+}
diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json
new file mode 100644
index 000000000..dc21318a7
--- /dev/null
+++ b/packages/api/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@documenso/tsconfig/react-library.json",
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"],
+ "compilerOptions": {
+ "strict": true,
+ }
+}
diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts
new file mode 100644
index 000000000..0f853a020
--- /dev/null
+++ b/packages/api/v1/contract.ts
@@ -0,0 +1,84 @@
+import { initContract } from '@ts-rest/core';
+
+import {
+ ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
+ ZAuthorizationHeadersSchema,
+ ZCreateDocumentMutationSchema,
+ ZDeleteDocumentMutationSchema,
+ ZGetDocumentsQuerySchema,
+ ZSuccessfulDocumentResponseSchema,
+ ZSuccessfulResponseSchema,
+ ZSuccessfulSigningResponseSchema,
+ ZUnsuccessfulResponseSchema,
+ ZUploadDocumentSuccessfulSchema,
+} from './schema';
+
+const c = initContract();
+
+export const ApiContractV1 = c.router(
+ {
+ getDocuments: {
+ method: 'GET',
+ path: '/documents',
+ query: ZGetDocumentsQuerySchema,
+ responses: {
+ 200: ZSuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Get all documents',
+ },
+
+ getDocument: {
+ method: 'GET',
+ path: `/documents/:id`,
+ responses: {
+ 200: ZSuccessfulDocumentResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Get a single document',
+ },
+
+ createDocument: {
+ method: 'POST',
+ path: '/documents',
+ body: ZCreateDocumentMutationSchema,
+ responses: {
+ 200: ZUploadDocumentSuccessfulSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Upload a new document and get a presigned URL',
+ },
+
+ sendDocument: {
+ method: 'PATCH',
+ path: '/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: `/documents/:id`,
+ body: ZDeleteDocumentMutationSchema,
+ responses: {
+ 200: ZSuccessfulDocumentResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Delete a document',
+ },
+ },
+ {
+ baseHeaders: ZAuthorizationHeadersSchema,
+ },
+);
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
new file mode 100644
index 000000000..b317e95d6
--- /dev/null
+++ b/packages/api/v1/implementation.ts
@@ -0,0 +1,178 @@
+import { createNextRoute } from '@ts-rest/next';
+
+import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
+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 { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
+import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
+import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
+
+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 });
+
+ return {
+ status: 200,
+ body: document,
+ };
+ } 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 {
+ const { url, key } = await getPresignPostUrl(body.fileName, body.contentType);
+
+ return {
+ status: 200,
+ body: {
+ url,
+ key,
+ },
+ };
+ } catch (err) {
+ return {
+ status: 404,
+ body: {
+ message: 'An error has occured while uploading the file',
+ },
+ };
+ }
+ }),
+
+ sendDocument: authenticatedMiddleware(async (args, user) => {
+ const { id } = args.params;
+ const { body } = args;
+
+ 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',
+ },
+ };
+ }
+ }),
+});
diff --git a/packages/api/v1/middleware/authenticated.ts b/packages/api/v1/middleware/authenticated.ts
new file mode 100644
index 000000000..3e23029a5
--- /dev/null
+++ b/packages/api/v1/middleware/authenticated.ts
@@ -0,0 +1,37 @@
+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
,
+) => {
+ return async (args: T) => {
+ try {
+ const { authorization: token } = args.req.headers;
+
+ if (!token) {
+ throw new Error('Token was not provided for authenticated middleware');
+ }
+
+ const user = await getUserByApiToken({ token });
+
+ return await handler(args, user);
+ } catch (_err) {
+ return {
+ status: 401,
+ body: {
+ message: 'Unauthorized',
+ },
+ } as const;
+ }
+ };
+};
diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts
new file mode 100644
index 000000000..f4c80ca73
--- /dev/null
+++ b/packages/api/v1/schema.ts
@@ -0,0 +1,87 @@
+import { z } from 'zod';
+
+import { FieldType } from '@documenso/prisma/client';
+
+export const ZGetDocumentsQuerySchema = z.object({
+ page: z.string().optional(),
+ perPage: z.string().optional(),
+});
+
+export type TGetDocumentsQuerySchema = z.infer;
+
+export const ZDeleteDocumentMutationSchema = z.string();
+
+export type TDeleteDocumentMutationSchema = z.infer;
+
+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 type TSuccessfulDocumentResponseSchema = z.infer;
+
+export const ZSendDocumentForSigningMutationSchema = z.object({
+ signerEmail: z.string(),
+ signerName: z.string().optional(),
+ emailSubject: z.string().optional(),
+ emailBody: z.string().optional(),
+ fields: z.array(
+ z.object({
+ fieldType: z.nativeEnum(FieldType),
+ pageNumber: z.number(),
+ pageX: z.number(),
+ pageY: z.number(),
+ pageWidth: z.number(),
+ pageHeight: z.number(),
+ }),
+ ),
+});
+
+export type TSendDocumentForSigningMutationSchema = z.infer<
+ typeof ZSendDocumentForSigningMutationSchema
+>;
+
+export const ZUploadDocumentSuccessfulSchema = z.object({
+ url: z.string(),
+ key: z.string(),
+});
+
+export type TUploadDocumentSuccessfulSchema = z.infer;
+
+export const ZCreateDocumentMutationSchema = z.object({
+ fileName: z.string(),
+ contentType: z.string().default('PDF'),
+});
+
+export type TCreateDocumentMutationSchema = z.infer;
+
+export const ZSuccessfulResponseSchema = z.object({
+ documents: ZSuccessfulDocumentResponseSchema.array(),
+ totalPages: z.number(),
+});
+
+export type TSuccessfulResponseSchema = z.infer;
+
+export const ZSuccessfulSigningResponseSchema = z.object({
+ message: z.string(),
+});
+
+export type TSuccessfulSigningResponseSchema = z.infer;
+
+export const ZUnsuccessfulResponseSchema = z.object({
+ message: z.string(),
+});
+
+export type TUnsuccessfulResponseSchema = z.infer;
+
+export const ZAuthorizationHeadersSchema = z.object({
+ authorization: z.string(),
+});
+
+export type TAuthorizationHeadersSchema = z.infer;
diff --git a/packages/lib/server-only/public-api/get-all-user-tokens.ts b/packages/lib/server-only/public-api/get-all-user-tokens.ts
index d64562b83..1ba31a6cf 100644
--- a/packages/lib/server-only/public-api/get-all-user-tokens.ts
+++ b/packages/lib/server-only/public-api/get-all-user-tokens.ts
@@ -5,7 +5,7 @@ export type GetUserTokensOptions = {
};
export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {
- return prisma.apiToken.findMany({
+ return await prisma.apiToken.findMany({
where: {
userId,
},
@@ -16,5 +16,8 @@ export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {
createdAt: true,
expires: true,
},
+ orderBy: {
+ createdAt: 'desc',
+ },
});
};
diff --git a/packages/trpc/server/api-token-router/schema.ts b/packages/trpc/server/api-token-router/schema.ts
index b615ef3af..c28920b9a 100644
--- a/packages/trpc/server/api-token-router/schema.ts
+++ b/packages/trpc/server/api-token-router/schema.ts
@@ -4,10 +4,16 @@ export const ZGetApiTokenByIdQuerySchema = z.object({
id: z.number().min(1),
});
+export type TGetApiTokenByIdQuerySchema = z.infer;
+
export const ZCreateTokenMutationSchema = z.object({
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
});
+export type TCreateTokenMutationSchema = z.infer;
+
export const ZDeleteTokenByIdMutationSchema = z.object({
id: z.number().min(1),
});
+
+export type TDeleteTokenByIdMutationSchema = z.infer;