diff --git a/apps/web/src/pages/api/v1/me/index.ts b/apps/web/src/pages/api/v1/me/index.ts new file mode 100644 index 000000000..a877c11d0 --- /dev/null +++ b/apps/web/src/pages/api/v1/me/index.ts @@ -0,0 +1,3 @@ +import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/test-credentials'; + +export default testCredentialsHandler; diff --git a/apps/web/src/pages/api/v1/zapier/list-documents/index.ts b/apps/web/src/pages/api/v1/zapier/list-documents/index.ts new file mode 100644 index 000000000..ba2a35b43 --- /dev/null +++ b/apps/web/src/pages/api/v1/zapier/list-documents/index.ts @@ -0,0 +1,3 @@ +import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents'; + +export default listDocumentsHandler; diff --git a/apps/web/src/pages/api/v1/zapier/signed-document/index.ts b/apps/web/src/pages/api/v1/zapier/signed-document/index.ts new file mode 100644 index 000000000..d4a382001 --- /dev/null +++ b/apps/web/src/pages/api/v1/zapier/signed-document/index.ts @@ -0,0 +1,3 @@ +import { signedDocumentHandler } from '@documenso/lib/server-only/webhooks/zapier/signed-document'; + +export default signedDocumentHandler; diff --git a/apps/web/src/pages/api/v1/zapier/subscribe/index.ts b/apps/web/src/pages/api/v1/zapier/subscribe/index.ts new file mode 100644 index 000000000..6bcfe9e74 --- /dev/null +++ b/apps/web/src/pages/api/v1/zapier/subscribe/index.ts @@ -0,0 +1,3 @@ +import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe'; + +export default subscribeHandler; diff --git a/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts b/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts new file mode 100644 index 000000000..f93dd6af7 --- /dev/null +++ b/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts @@ -0,0 +1,3 @@ +import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe'; + +export default unsubscribeHandler; diff --git a/packages/lib/server-only/public-api/test-credentials.ts b/packages/lib/server-only/public-api/test-credentials.ts new file mode 100644 index 000000000..0908873f1 --- /dev/null +++ b/packages/lib/server-only/public-api/test-credentials.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { getUserByApiToken } from './get-user-by-token'; + +export const testCredentialsHandler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { authorization } = 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) { + return res.status(500).json({ + body: { + message: 'API token was not provided', + }, + }); + } + + const user = await getUserByApiToken({ token }); + + return res.status(200).json({ + username: user.name, + email: user.email, + }); + } catch (err) { + return res.status(500).json({ + message: 'Internal Server Error', + }); + } +}; diff --git a/packages/lib/server-only/webhooks/get-all-webhooks.ts b/packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts similarity index 77% rename from packages/lib/server-only/webhooks/get-all-webhooks.ts rename to packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts index a6c88a086..b8070b2a4 100644 --- a/packages/lib/server-only/webhooks/get-all-webhooks.ts +++ b/packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts @@ -5,7 +5,7 @@ export type GetAllWebhooksOptions = { eventTrigger: WebhookTriggerEvents; }; -export const getAllWebhooks = async ({ eventTrigger }: GetAllWebhooksOptions) => { +export const getAllWebhooksByEventTrigger = async ({ eventTrigger }: GetAllWebhooksOptions) => { return prisma.webhook.findMany({ where: { eventTriggers: { diff --git a/packages/lib/server-only/webhooks/zapier/list-documents.ts b/packages/lib/server-only/webhooks/zapier/list-documents.ts new file mode 100644 index 000000000..032d914a9 --- /dev/null +++ b/packages/lib/server-only/webhooks/zapier/list-documents.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; + +import { getWebhooksByUserId } from '../get-webhooks-by-user-id'; +import { validateApiToken } from './validateApiToken'; + +export const listDocumentsHandler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { authorization } = req.headers; + const user = await validateApiToken({ authorization }); + + const documents = await findDocuments({ userId: user.id }); + const allWebhooks = await getWebhooksByUserId(user.id); + + if (documents.data.length > 0 && allWebhooks.length > 0) { + const testWebhook = { + event: allWebhooks[0].eventTriggers.toString(), + createdAt: allWebhooks[0].createdAt, + webhookEndpoint: allWebhooks[0].webhookUrl, + payload: { + id: documents.data[0].id, + userId: documents.data[0].userId, + title: documents.data[0].title, + status: documents.data[0].status, + documentDataId: documents.data[0].documentDataId, + createdAt: documents.data[0].createdAt, + updatedAt: documents.data[0].updatedAt, + completedAt: documents.data[0].completedAt, + deletedAt: documents.data[0].deletedAt, + teamId: documents.data[0].teamId, + }, + }; + + return res.status(200).json([testWebhook]); + } + + return res.status(200).json([]); + } catch (err) { + console.error(err); + return res.status(500).json({ + message: 'Internal Server Error', + }); + } +}; diff --git a/packages/lib/server-only/webhooks/zapier/signed-document.ts b/packages/lib/server-only/webhooks/zapier/signed-document.ts new file mode 100644 index 000000000..452263d20 --- /dev/null +++ b/packages/lib/server-only/webhooks/zapier/signed-document.ts @@ -0,0 +1,67 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; +import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; + +import { getWebhooksByUserId } from '../get-webhooks-by-user-id'; +import { validateApiToken } from './validateApiToken'; + +export const signedDocumentHandler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { authorization } = req.headers; + const user = await validateApiToken({ authorization }); + + const documents = await findDocuments({ userId: user.id }); + + const allWebhooks = await getWebhooksByUserId(user.id); + const recipients = await getRecipientsForDocument({ + documentId: documents.data[0].id, + userId: user.id, + }); + + if (documents.data.length > 0 && allWebhooks.length > 0 && recipients.length > 0) { + const testWebhook = { + event: allWebhooks[0].eventTriggers.toString(), + createdAt: allWebhooks[0].createdAt, + webhookEndpoint: allWebhooks[0].webhookUrl, + payload: { + id: documents.data[0].id, + userId: documents.data[0].userId, + title: documents.data[0].title, + status: documents.data[0].status, + documentDataId: documents.data[0].documentDataId, + createdAt: documents.data[0].createdAt, + updatedAt: documents.data[0].updatedAt, + completedAt: documents.data[0].completedAt, + deletedAt: documents.data[0].deletedAt, + teamId: documents.data[0].teamId, + Recipient: [ + { + id: recipients[0].id, + documentId: recipients[0].documentId, + templateId: recipients[0].templateId, + email: recipients[0].email, + name: recipients[0].name, + token: recipients[0].token, + expired: recipients[0].expired, + signedAt: recipients[0].signedAt, + role: recipients[0].role, + readStatus: recipients[0].readStatus, + signingStatus: recipients[0].signingStatus, + sendStatus: recipients[0].sendStatus, + }, + ], + }, + }; + + return res.status(200).json([testWebhook]); + } + + return res.status(200).json([]); + } catch (err) { + console.error(err); + return res.status(500).json({ + message: 'Internal Server Error', + }); + } +}; diff --git a/packages/lib/server-only/webhooks/zapier/subscribe.ts b/packages/lib/server-only/webhooks/zapier/subscribe.ts new file mode 100644 index 000000000..6fa22ab5f --- /dev/null +++ b/packages/lib/server-only/webhooks/zapier/subscribe.ts @@ -0,0 +1,29 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { prisma } from '@documenso/prisma'; + +import { validateApiToken } from './validateApiToken'; + +export const subscribeHandler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { authorization } = req.headers; + const { webhookUrl, eventTrigger } = req.body; + const user = await validateApiToken({ authorization }); + + const createdWebhook = await prisma.webhook.create({ + data: { + webhookUrl, + eventTriggers: [eventTrigger], + secret: null, + enabled: true, + userId: user.id, + }, + }); + + return res.status(200).json(createdWebhook); + } catch (err) { + return res.status(500).json({ + message: 'Internal Server Error', + }); + } +}; diff --git a/packages/lib/server-only/webhooks/zapier/unsubscribe.ts b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts new file mode 100644 index 000000000..30ee1e25a --- /dev/null +++ b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts @@ -0,0 +1,26 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { prisma } from '@documenso/prisma'; + +import { validateApiToken } from './validateApiToken'; + +export const unsubscribeHandler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { authorization } = req.headers; + const { webhookId } = req.body; + const user = await validateApiToken({ authorization }); + + const deletedWebhook = await prisma.webhook.delete({ + where: { + id: webhookId, + userId: user.id, + }, + }); + + return res.status(200).json(deletedWebhook); + } catch (err) { + return res.status(500).json({ + message: 'Internal Server Error', + }); + } +}; diff --git a/packages/lib/server-only/webhooks/zapier/validateApiToken.ts b/packages/lib/server-only/webhooks/zapier/validateApiToken.ts new file mode 100644 index 000000000..2a8a44777 --- /dev/null +++ b/packages/lib/server-only/webhooks/zapier/validateApiToken.ts @@ -0,0 +1,16 @@ +import { getUserByApiToken } from '../../public-api/get-user-by-token'; + +type ValidateApiTokenOptions = { + authorization: string | undefined; +}; + +export const validateApiToken = async ({ authorization }: ValidateApiTokenOptions) => { + try { + // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx" + const [token] = (authorization || '').split('Bearer ').filter((s) => s.length > 0); + + return await getUserByApiToken({ token }); + } catch (err) { + throw new Error(`Failed to validate API token`); + } +}; diff --git a/packages/lib/universal/trigger-webhook.ts b/packages/lib/universal/trigger-webhook.ts index 025a154bc..72484d6c3 100644 --- a/packages/lib/universal/trigger-webhook.ts +++ b/packages/lib/universal/trigger-webhook.ts @@ -1,6 +1,6 @@ import type { Document, WebhookTriggerEvents } from '@documenso/prisma/client'; -import { getAllWebhooks } from '../server-only/webhooks/get-all-webhooks'; +import { getAllWebhooksByEventTrigger } from '../server-only/webhooks/get-all-webhooks-by-event-trigger'; import { postWebhookPayload } from './post-webhook-payload'; export type TriggerWebhookOptions = { @@ -10,7 +10,7 @@ export type TriggerWebhookOptions = { export const triggerWebhook = async ({ eventTrigger, documentData }: TriggerWebhookOptions) => { try { - const allWebhooks = await getAllWebhooks({ eventTrigger }); + const allWebhooks = await getAllWebhooksByEventTrigger({ eventTrigger }); const webhookPromises = allWebhooks.map((webhook) => { const { webhookUrl, secret } = webhook;