diff --git a/apps/web/src/pages/api/v1/[...ts-rest].tsx b/apps/web/src/pages/api/v1/[...ts-rest].tsx index 15b618ebd..095936cf0 100644 --- a/apps/web/src/pages/api/v1/[...ts-rest].tsx +++ b/apps/web/src/pages/api/v1/[...ts-rest].tsx @@ -1,5 +1,17 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + import { createNextRouter } from '@documenso/api/next'; import { ApiContractV1 } from '@documenso/api/v1/contract'; import { ApiContractV1Implementation } from '@documenso/api/v1/implementation'; -export default createNextRouter(ApiContractV1, ApiContractV1Implementation); +const nextRouteHandler = createNextRouter(ApiContractV1, ApiContractV1Implementation, { + responseValidation: false, +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // TODO: Dirty hack to make ts-rest handler work with next.js in a more intuitive way. + req.query['ts-rest'] = Array.isArray(req.query['ts-rest']) ? req.query['ts-rest'] : []; // Make `ts-rest` an array. + req.query['ts-rest'].unshift('api', 'v1'); // Prepend our base path to the array. + + return await nextRouteHandler(req, res); +} diff --git a/package-lock.json b/package-lock.json index 8aa403089..decf0485d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6718,8 +6718,7 @@ "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "devOptional": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/ramda": { "version": "0.29.9", @@ -6733,7 +6732,6 @@ "version": "18.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz", "integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6757,14 +6755,21 @@ "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==" }, + "node_modules/@types/swagger-ui-react": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-4.18.3.tgz", + "integrity": "sha512-Mo/R7IjDVwtiFPs84pWvh5pI9iyNGBjmfielxqbOh2Jv+8WVSDVe8Nu25kb5BOuV2xmGS3o33jr6nwDJMBcX+Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", @@ -8729,8 +8734,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "devOptional": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/d3-array": { "version": "3.2.4", @@ -20739,6 +20743,7 @@ "@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", diff --git a/packages/api/package.json b/packages/api/package.json index 1d9b5c159..aebb09c9b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -20,6 +20,7 @@ "@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", diff --git a/packages/api/v1/api-documentation.tsx b/packages/api/v1/api-documentation.tsx index 6f8062271..6082d2d7f 100644 --- a/packages/api/v1/api-documentation.tsx +++ b/packages/api/v1/api-documentation.tsx @@ -1,3 +1,5 @@ +'use client'; + import SwaggerUI from 'swagger-ui-react'; import 'swagger-ui-react/swagger-ui.css'; diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts index 0f853a020..438fa9cee 100644 --- a/packages/api/v1/contract.ts +++ b/packages/api/v1/contract.ts @@ -4,9 +4,11 @@ import { ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema, ZAuthorizationHeadersSchema, ZCreateDocumentMutationSchema, + ZCreateRecipientMutationSchema, ZDeleteDocumentMutationSchema, ZGetDocumentsQuerySchema, ZSuccessfulDocumentResponseSchema, + ZSuccessfulRecipientResponseSchema, ZSuccessfulResponseSchema, ZSuccessfulSigningResponseSchema, ZUnsuccessfulResponseSchema, @@ -19,7 +21,7 @@ export const ApiContractV1 = c.router( { getDocuments: { method: 'GET', - path: '/documents', + path: '/api/v1/documents', query: ZGetDocumentsQuerySchema, responses: { 200: ZSuccessfulResponseSchema, @@ -31,7 +33,7 @@ export const ApiContractV1 = c.router( getDocument: { method: 'GET', - path: `/documents/:id`, + path: '/api/v1/documents/:id', responses: { 200: ZSuccessfulDocumentResponseSchema, 401: ZUnsuccessfulResponseSchema, @@ -42,7 +44,7 @@ export const ApiContractV1 = c.router( createDocument: { method: 'POST', - path: '/documents', + path: '/api/v1/documents', body: ZCreateDocumentMutationSchema, responses: { 200: ZUploadDocumentSuccessfulSchema, @@ -53,8 +55,8 @@ export const ApiContractV1 = c.router( }, sendDocument: { - method: 'PATCH', - path: '/documents/:id/send', + method: 'POST', + path: '/api/v1/documents/:id/send', body: SendDocumentMutationSchema, responses: { 200: ZSuccessfulSigningResponseSchema, @@ -68,7 +70,7 @@ export const ApiContractV1 = c.router( deleteDocument: { method: 'DELETE', - path: `/documents/:id`, + path: '/api/v1/documents/:id', body: ZDeleteDocumentMutationSchema, responses: { 200: ZSuccessfulDocumentResponseSchema, @@ -77,6 +79,20 @@ export const ApiContractV1 = c.router( }, 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', + }, }, { baseHeaders: ZAuthorizationHeadersSchema, diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index b317e95d6..4dd709246 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -1,13 +1,13 @@ 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 { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; +import { DocumentStatus } from '@documenso/prisma/client'; import { ApiContractV1 } from './contract'; import { authenticatedMiddleware } from './middleware/authenticated'; @@ -99,7 +99,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { sendDocument: authenticatedMiddleware(async (args, user) => { const { id } = args.params; - const { body } = args; const document = await getDocumentById({ id: Number(id), userId: user.id }); @@ -122,38 +121,38 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { } try { - await setRecipientsForDocument({ - userId: user.id, - documentId: Number(id), - recipients: [ - { - email: body.signerEmail, - name: body.signerName ?? '', - }, - ], - }); + // 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, - })), - }); + // 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 ?? '', - }); - } + // if (body.emailBody || body.emailSubject) { + // await upsertDocumentMeta({ + // documentId: Number(id), + // subject: body.emailSubject ?? '', + // message: body.emailBody ?? '', + // }); + // } await sendDocument({ documentId: Number(id), @@ -175,4 +174,80 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { }; } }), + + createRecipient: authenticatedMiddleware(async (args, user) => { + const { id: documentId } = args.params; + const { name, email } = 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, + }, + ], + }); + + const newRecipient = newRecipients.find((recipient) => recipient.email === email); + + if (!newRecipient) { + throw new Error('Recipient not found'); + } + + return { + status: 200, + body: newRecipient, + }; + } catch (err) { + return { + status: 500, + body: { + message: 'An error has occured while creating the recipient', + }, + }; + } + }), }); diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index f4c80ca73..91c35ce28 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { FieldType } from '@documenso/prisma/client'; +import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client'; export const ZGetDocumentsQuerySchema = z.object({ page: z.string().optional(), @@ -9,9 +9,9 @@ export const ZGetDocumentsQuerySchema = z.object({ export type TGetDocumentsQuerySchema = z.infer; -export const ZDeleteDocumentMutationSchema = z.string(); +export const ZDeleteDocumentMutationSchema = null; -export type TDeleteDocumentMutationSchema = z.infer; +export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema; export const ZSuccessfulDocumentResponseSchema = z.object({ id: z.number(), @@ -26,26 +26,9 @@ export const ZSuccessfulDocumentResponseSchema = z.object({ 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 const ZSendDocumentForSigningMutationSchema = null; -export type TSendDocumentForSigningMutationSchema = z.infer< - typeof ZSendDocumentForSigningMutationSchema ->; +export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema; export const ZUploadDocumentSuccessfulSchema = z.object({ url: z.string(), @@ -61,6 +44,29 @@ export const ZCreateDocumentMutationSchema = z.object({ export type TCreateDocumentMutationSchema = z.infer; +export const ZCreateRecipientMutationSchema = z.object({ + name: z.string().min(1), + email: z.string().email().min(1), +}); + +export type TCreateRecipientMutationSchema = z.infer; + +export const ZSuccessfulRecipientResponseSchema = z.object({ + id: z.number(), + documentId: z.number(), + email: z.string().email().min(1), + name: z.string(), + 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; + export const ZSuccessfulResponseSchema = z.object({ documents: ZSuccessfulDocumentResponseSchema.array(), totalPages: z.number(), diff --git a/packages/lib/server-only/public-api/get-api-token-by-id.ts b/packages/lib/server-only/public-api/get-api-token-by-id.ts index ae442f05e..8b25717f9 100644 --- a/packages/lib/server-only/public-api/get-api-token-by-id.ts +++ b/packages/lib/server-only/public-api/get-api-token-by-id.ts @@ -6,7 +6,7 @@ export type GetApiTokenByIdOptions = { }; export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => { - return prisma.apiToken.findFirstOrThrow({ + return await prisma.apiToken.findFirstOrThrow({ where: { id, userId, diff --git a/packages/trpc/api-contract/contract.ts b/packages/trpc/api-contract/contract.ts deleted file mode 100644 index 5f28a0c63..000000000 --- a/packages/trpc/api-contract/contract.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { initContract } from '@ts-rest/core'; - -import { - AuthorizationHeadersSchema, - CreateDocumentMutationSchema, - DeleteDocumentMutationSchema, - GetDocumentsQuerySchema, - SendDocumentForSigningMutationSchema, - SuccessfulDocumentResponseSchema, - SuccessfulResponseSchema, - SuccessfulSigningResponseSchema, - UnsuccessfulResponseSchema, - UploadDocumentSuccessfulSchema, -} from './schema'; - -const c = initContract(); - -export const contract = c.router( - { - getDocuments: { - method: 'GET', - path: '/documents', - query: GetDocumentsQuerySchema, - responses: { - 200: SuccessfulResponseSchema, - 401: UnsuccessfulResponseSchema, - 404: UnsuccessfulResponseSchema, - }, - summary: 'Get all documents', - }, - getDocument: { - method: 'GET', - path: `/documents/:id`, - responses: { - 200: SuccessfulDocumentResponseSchema, - 401: UnsuccessfulResponseSchema, - 404: UnsuccessfulResponseSchema, - }, - summary: 'Get a single document', - }, - createDocument: { - method: 'POST', - path: '/documents', - body: CreateDocumentMutationSchema, - responses: { - 200: UploadDocumentSuccessfulSchema, - 401: UnsuccessfulResponseSchema, - 404: UnsuccessfulResponseSchema, - }, - summary: 'Upload a new document and get a presigned URL', - }, - sendDocumentForSigning: { - method: 'PATCH', - path: '/documents/:id/send', - body: SendDocumentForSigningMutationSchema, - responses: { - 200: SuccessfulSigningResponseSchema, - 400: UnsuccessfulResponseSchema, - 401: UnsuccessfulResponseSchema, - 404: UnsuccessfulResponseSchema, - 500: UnsuccessfulResponseSchema, - }, - summary: 'Send a document for signing', - }, - deleteDocument: { - method: 'DELETE', - path: `/documents/:id`, - body: DeleteDocumentMutationSchema, - responses: { - 200: SuccessfulDocumentResponseSchema, - 401: UnsuccessfulResponseSchema, - 404: UnsuccessfulResponseSchema, - }, - summary: 'Delete a document', - }, - }, - { - baseHeaders: AuthorizationHeadersSchema, - }, -); diff --git a/packages/trpc/api-contract/schema.ts b/packages/trpc/api-contract/schema.ts deleted file mode 100644 index d62d50d52..000000000 --- a/packages/trpc/api-contract/schema.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { z } from 'zod'; - -import { FieldType } from '@documenso/prisma/client'; - -export const GetDocumentsQuerySchema = z.object({ - page: z.string().optional(), - perPage: z.string().optional(), -}); - -export const DeleteDocumentMutationSchema = z.string(); - -export const SuccessfulDocumentResponseSchema = 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 SendDocumentForSigningMutationSchema = 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 const UploadDocumentSuccessfulSchema = z.object({ - url: z.string(), - key: z.string(), -}); - -export const CreateDocumentMutationSchema = z.object({ - fileName: z.string(), - contentType: z.string().default('PDF'), -}); - -export const SuccessfulResponseSchema = z.object({ - documents: SuccessfulDocumentResponseSchema.array(), - totalPages: z.number(), -}); - -export const SuccessfulSigningResponseSchema = z.object({ - message: z.string(), -}); - -export const UnsuccessfulResponseSchema = z.object({ - message: z.string(), -}); - -export const AuthorizationHeadersSchema = z.object({ - authorization: z.string(), -}); diff --git a/packages/trpc/server/public-api/ts-rest.ts b/packages/trpc/server/public-api/ts-rest.ts deleted file mode 100644 index 0d66cda1f..000000000 --- a/packages/trpc/server/public-api/ts-rest.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createNextRoute, createNextRouter } from '@ts-rest/next'; - -export { createNextRoute, createNextRouter };