mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: add zapier support
This commit is contained in:
3
apps/web/src/pages/api/v1/me/index.ts
Normal file
3
apps/web/src/pages/api/v1/me/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/test-credentials';
|
||||||
|
|
||||||
|
export default testCredentialsHandler;
|
||||||
3
apps/web/src/pages/api/v1/zapier/list-documents/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/list-documents/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents';
|
||||||
|
|
||||||
|
export default listDocumentsHandler;
|
||||||
3
apps/web/src/pages/api/v1/zapier/subscribe/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/subscribe/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe';
|
||||||
|
|
||||||
|
export default subscribeHandler;
|
||||||
3
apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
|
||||||
|
|
||||||
|
export default unsubscribeHandler;
|
||||||
@ -9,9 +9,11 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
|||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
import { signPdf } from '@documenso/signing';
|
import { signPdf } from '@documenso/signing';
|
||||||
|
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
|
import { triggerWebhook } from '../../universal/trigger-webhook';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putFile } from '../../universal/upload/put-file';
|
import { putFile } from '../../universal/upload/put-file';
|
||||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||||
@ -134,4 +136,9 @@ export const sealDocument = async ({
|
|||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
await sendCompletedEmail({ documentId, requestMetadata });
|
await sendCompletedEmail({ documentId, requestMetadata });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
eventTrigger: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||||
|
documentData: document,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,12 +10,14 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
|
|||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||||
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import {
|
import {
|
||||||
RECIPIENT_ROLES_DESCRIPTION,
|
RECIPIENT_ROLES_DESCRIPTION,
|
||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
} from '../../constants/recipient-roles';
|
} from '../../constants/recipient-roles';
|
||||||
|
import { triggerWebhook } from '../../universal/trigger-webhook';
|
||||||
|
|
||||||
export type SendDocumentOptions = {
|
export type SendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -163,5 +165,10 @@ export const sendDocument = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
eventTrigger: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||||
|
documentData: updatedDocument,
|
||||||
|
});
|
||||||
|
|
||||||
return updatedDocument;
|
return updatedDocument;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
|
|||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { ReadStatus } from '@documenso/prisma/client';
|
import { ReadStatus } from '@documenso/prisma/client';
|
||||||
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { triggerWebhook } from '../../universal/trigger-webhook';
|
||||||
|
import { getDocumentAndSenderByToken } from './get-document-by-token';
|
||||||
|
|
||||||
export type ViewedDocumentOptions = {
|
export type ViewedDocumentOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
@ -51,4 +55,22 @@ export const viewedDocument = async ({ token, requestMetadata }: ViewedDocumentO
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const document = await getDocumentAndSenderByToken({ token });
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
eventTrigger: WebhookTriggerEvents.DOCUMENT_OPENED,
|
||||||
|
documentData: {
|
||||||
|
id: document.id,
|
||||||
|
userId: document.userId,
|
||||||
|
title: document.title,
|
||||||
|
status: document.status,
|
||||||
|
documentDataId: document.documentDataId,
|
||||||
|
createdAt: document.createdAt,
|
||||||
|
updatedAt: document.updatedAt,
|
||||||
|
completedAt: document.completedAt,
|
||||||
|
deletedAt: document.deletedAt,
|
||||||
|
teamId: document.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
19
packages/lib/server-only/public-api/test-credentials.ts
Normal file
19
packages/lib/server-only/public-api/test-credentials.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
import { validateApiToken } from '@documenso/lib/server-only/webhooks/zapier/validateApiToken';
|
||||||
|
|
||||||
|
export const testCredentialsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
try {
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
const user = await validateApiToken({ authorization });
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
username: user.name,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Internal Server Error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,11 +1,13 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type GetAllWebhooksOptions = {
|
export type GetAllWebhooksByEventTriggerOptions = {
|
||||||
eventTrigger: WebhookTriggerEvents;
|
eventTrigger: WebhookTriggerEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllWebhooks = async ({ eventTrigger }: GetAllWebhooksOptions) => {
|
export const getAllWebhooksByEventTrigger = async ({
|
||||||
|
eventTrigger,
|
||||||
|
}: GetAllWebhooksByEventTriggerOptions) => {
|
||||||
return prisma.webhook.findMany({
|
return prisma.webhook.findMany({
|
||||||
where: {
|
where: {
|
||||||
eventTriggers: {
|
eventTriggers: {
|
||||||
@ -5,5 +5,8 @@ export const getWebhooksByUserId = async (userId: number) => {
|
|||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
51
packages/lib/server-only/webhooks/zapier/list-documents.ts
Normal file
51
packages/lib/server-only/webhooks/zapier/list-documents.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 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);
|
||||||
|
const recipients = await getRecipientsForDocument({
|
||||||
|
documentId: documents.data[0].id,
|
||||||
|
userId: 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,
|
||||||
|
Recipient: recipients,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
29
packages/lib/server-only/webhooks/zapier/subscribe.ts
Normal file
29
packages/lib/server-only/webhooks/zapier/subscribe.ts
Normal file
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
26
packages/lib/server-only/webhooks/zapier/unsubscribe.ts
Normal file
26
packages/lib/server-only/webhooks/zapier/unsubscribe.ts
Normal file
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
16
packages/lib/server-only/webhooks/zapier/validateApiToken.ts
Normal file
16
packages/lib/server-only/webhooks/zapier/validateApiToken.ts
Normal file
@ -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`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Document, WebhookTriggerEvents } from '@documenso/prisma/client';
|
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';
|
import { postWebhookPayload } from './post-webhook-payload';
|
||||||
|
|
||||||
export type TriggerWebhookOptions = {
|
export type TriggerWebhookOptions = {
|
||||||
@ -10,7 +10,7 @@ export type TriggerWebhookOptions = {
|
|||||||
|
|
||||||
export const triggerWebhook = async ({ eventTrigger, documentData }: TriggerWebhookOptions) => {
|
export const triggerWebhook = async ({ eventTrigger, documentData }: TriggerWebhookOptions) => {
|
||||||
try {
|
try {
|
||||||
const allWebhooks = await getAllWebhooks({ eventTrigger });
|
const allWebhooks = await getAllWebhooksByEventTrigger({ eventTrigger });
|
||||||
|
|
||||||
const webhookPromises = allWebhooks.map((webhook) => {
|
const webhookPromises = allWebhooks.map((webhook) => {
|
||||||
const { webhookUrl, secret } = webhook;
|
const { webhookUrl, secret } = webhook;
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
-- This migration adds more than one value to an enum.
|
||||||
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||||
|
-- in a single migration. This can be worked around by creating
|
||||||
|
-- multiple migrations, each migration adding only one value to
|
||||||
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_SENT';
|
||||||
|
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_OPENED';
|
||||||
|
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_COMPLETED';
|
||||||
@ -97,7 +97,10 @@ model VerificationToken {
|
|||||||
|
|
||||||
enum WebhookTriggerEvents {
|
enum WebhookTriggerEvents {
|
||||||
DOCUMENT_CREATED
|
DOCUMENT_CREATED
|
||||||
|
DOCUMENT_SENT
|
||||||
|
DOCUMENT_OPENED
|
||||||
DOCUMENT_SIGNED
|
DOCUMENT_SIGNED
|
||||||
|
DOCUMENT_COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
model Webhook {
|
model Webhook {
|
||||||
|
|||||||
Reference in New Issue
Block a user