mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge branch 'main' into feat/public-profile-1
This commit is contained in:
12
packages/lib/server-only/crypto/sign.ts
Normal file
12
packages/lib/server-only/crypto/sign.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { hashString } from '../auth/hash';
|
||||
import { encryptSecondaryData } from './encrypt';
|
||||
|
||||
export const sign = (data: unknown) => {
|
||||
const stringified = JSON.stringify(data);
|
||||
|
||||
const hashed = hashString(stringified);
|
||||
|
||||
const signature = encryptSecondaryData({ data: hashed });
|
||||
|
||||
return signature;
|
||||
};
|
||||
12
packages/lib/server-only/crypto/verify.ts
Normal file
12
packages/lib/server-only/crypto/verify.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { hashString } from '../auth/hash';
|
||||
import { decryptSecondaryData } from './decrypt';
|
||||
|
||||
export const verify = (data: unknown, signature: string) => {
|
||||
const stringified = JSON.stringify(data);
|
||||
|
||||
const hashed = hashString(stringified);
|
||||
|
||||
const decrypted = decryptSecondaryData(signature);
|
||||
|
||||
return decrypted === hashed;
|
||||
};
|
||||
@ -5,7 +5,9 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sealDocument } from './seal-document';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
|
||||
@ -15,14 +17,8 @@ export type CompleteDocumentWithTokenOptions = {
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
'use server';
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
@ -39,6 +35,16 @@ export const completeDocumentWithToken = async ({
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
'use server';
|
||||
|
||||
const document = await getDocument({ token, documentId });
|
||||
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
throw new Error(`Document ${document.id} has already been completed`);
|
||||
@ -124,4 +130,13 @@ export const completeDocumentWithToken = async ({
|
||||
if (documents.count > 0) {
|
||||
await sealDocument({ documentId: document.id, requestMetadata });
|
||||
}
|
||||
|
||||
const updatedDocument = await getDocument({ token, documentId });
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: updatedDocument,
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@ -5,6 +5,9 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
title: string;
|
||||
@ -63,6 +66,13 @@ export const createDocument = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: document,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return document;
|
||||
});
|
||||
};
|
||||
|
||||
@ -9,12 +9,14 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putFile } from '../../universal/upload/put-file';
|
||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendCompletedEmail } from './send-completed-email';
|
||||
|
||||
export type SealDocumentOptions = {
|
||||
@ -36,6 +38,7 @@ export const sealDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -134,4 +137,11 @@ export const sealDocument = async ({
|
||||
if (sendEmail) {
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: document,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@ -10,12 +10,14 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '../../constants/recipient-roles';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
@ -180,8 +182,18 @@ export const sendDocument = async ({
|
||||
data: {
|
||||
status: DocumentStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||
data: updatedDocument,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
import { ReadStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentAndRecipientByToken } from './get-document-by-token';
|
||||
|
||||
export type ViewedDocumentOptions = {
|
||||
token: string;
|
||||
@ -51,4 +55,13 @@ export const viewedDocument = async ({ token, requestMetadata }: ViewedDocumentO
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const document = await getDocumentAndRecipientByToken({ token });
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_OPENED,
|
||||
data: document,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
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 result = await validateApiToken({ authorization });
|
||||
|
||||
return res.status(200).json({
|
||||
name: result.team?.name ?? result.user.name,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
message: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
};
|
||||
44
packages/lib/server-only/webhooks/create-webhook.ts
Normal file
44
packages/lib/server-only/webhooks/create-webhook.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export interface CreateWebhookOptions {
|
||||
webhookUrl: string;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
secret: string | null;
|
||||
enabled: boolean;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export const createWebhook = async ({
|
||||
webhookUrl,
|
||||
eventTriggers,
|
||||
secret,
|
||||
enabled,
|
||||
userId,
|
||||
teamId,
|
||||
}: CreateWebhookOptions) => {
|
||||
if (teamId) {
|
||||
await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.webhook.create({
|
||||
data: {
|
||||
webhookUrl,
|
||||
eventTriggers,
|
||||
secret,
|
||||
enabled,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
30
packages/lib/server-only/webhooks/delete-webhook-by-id.ts
Normal file
30
packages/lib/server-only/webhooks/delete-webhook-by-id.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type DeleteWebhookByIdOptions = {
|
||||
id: string;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const deleteWebhookById = async ({ id, userId, teamId }: DeleteWebhookByIdOptions) => {
|
||||
return await prisma.webhook.delete({
|
||||
where: {
|
||||
id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
36
packages/lib/server-only/webhooks/edit-webhook.ts
Normal file
36
packages/lib/server-only/webhooks/edit-webhook.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type EditWebhookOptions = {
|
||||
id: string;
|
||||
data: Omit<Prisma.WebhookUpdateInput, 'id' | 'userId' | 'teamId'>;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const editWebhook = async ({ id, data, userId, teamId }: EditWebhookOptions) => {
|
||||
return await prisma.webhook.update({
|
||||
where: {
|
||||
id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export type GetAllWebhooksByEventTriggerOptions = {
|
||||
event: WebhookTriggerEvents;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const getAllWebhooksByEventTrigger = async ({
|
||||
event,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetAllWebhooksByEventTriggerOptions) => {
|
||||
return prisma.webhook.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
eventTriggers: {
|
||||
has: event,
|
||||
},
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
30
packages/lib/server-only/webhooks/get-webhook-by-id.ts
Normal file
30
packages/lib/server-only/webhooks/get-webhook-by-id.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetWebhookByIdOptions = {
|
||||
id: string;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const getWebhookById = async ({ id, userId, teamId }: GetWebhookByIdOptions) => {
|
||||
return await prisma.webhook.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
19
packages/lib/server-only/webhooks/get-webhooks-by-team-id.ts
Normal file
19
packages/lib/server-only/webhooks/get-webhooks-by-team-id.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const getWebhooksByTeamId = async (teamId: number, userId: number) => {
|
||||
return await prisma.webhook.findMany({
|
||||
where: {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
12
packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts
Normal file
12
packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const getWebhooksByUserId = async (userId: number) => {
|
||||
return await prisma.webhook.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
58
packages/lib/server-only/webhooks/trigger/execute-webhook.ts
Normal file
58
packages/lib/server-only/webhooks/trigger/execute-webhook.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
Prisma,
|
||||
type Webhook,
|
||||
WebhookCallStatus,
|
||||
type WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
export type ExecuteWebhookOptions = {
|
||||
event: WebhookTriggerEvents;
|
||||
webhook: Webhook;
|
||||
data: unknown;
|
||||
};
|
||||
|
||||
export const executeWebhook = async ({ event, webhook, data }: ExecuteWebhookOptions) => {
|
||||
const { webhookUrl: url, secret } = webhook;
|
||||
|
||||
console.log('Executing webhook', { event, url });
|
||||
|
||||
const payload = {
|
||||
event,
|
||||
payload: data,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhookEndpoint: url,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Documenso-Secret': secret ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
|
||||
|
||||
try {
|
||||
responseBody = JSON.parse(body);
|
||||
} catch (err) {
|
||||
responseBody = body;
|
||||
}
|
||||
|
||||
await prisma.webhookCall.create({
|
||||
data: {
|
||||
url,
|
||||
event,
|
||||
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
|
||||
requestBody: payload as Prisma.InputJsonValue,
|
||||
responseCode: response.status,
|
||||
responseBody,
|
||||
responseHeaders: Object.fromEntries(response.headers.entries()),
|
||||
webhookId: webhook.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
58
packages/lib/server-only/webhooks/trigger/handler.ts
Normal file
58
packages/lib/server-only/webhooks/trigger/handler.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { verify } from '../../crypto/verify';
|
||||
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
|
||||
import { executeWebhook } from './execute-webhook';
|
||||
import { ZTriggerWebhookBodySchema } from './schema';
|
||||
|
||||
export type HandlerTriggerWebhooksResponse =
|
||||
| {
|
||||
success: true;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export const handlerTriggerWebhooks = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<HandlerTriggerWebhooksResponse>,
|
||||
) => {
|
||||
const signature = req.headers['x-webhook-signature'];
|
||||
|
||||
if (typeof signature !== 'string') {
|
||||
console.log('Missing signature');
|
||||
return res.status(400).json({ success: false, error: 'Missing signature' });
|
||||
}
|
||||
|
||||
const valid = verify(req.body, signature);
|
||||
|
||||
if (!valid) {
|
||||
console.log('Invalid signature');
|
||||
return res.status(400).json({ success: false, error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
const result = ZTriggerWebhookBodySchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Invalid request body');
|
||||
return res.status(400).json({ success: false, error: 'Invalid request body' });
|
||||
}
|
||||
|
||||
const { event, data, userId, teamId } = result.data;
|
||||
|
||||
const allWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
|
||||
|
||||
await Promise.allSettled(
|
||||
allWebhooks.map(async (webhook) =>
|
||||
executeWebhook({
|
||||
event,
|
||||
webhook,
|
||||
data,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return res.status(200).json({ success: true, message: 'Webhooks executed successfully' });
|
||||
};
|
||||
12
packages/lib/server-only/webhooks/trigger/schema.ts
Normal file
12
packages/lib/server-only/webhooks/trigger/schema.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export const ZTriggerWebhookBodySchema = z.object({
|
||||
event: z.nativeEnum(WebhookTriggerEvents),
|
||||
data: z.unknown(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TTriggerWebhookBodySchema = z.infer<typeof ZTriggerWebhookBodySchema>;
|
||||
40
packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
Normal file
40
packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { sign } from '../../crypto/sign';
|
||||
|
||||
export type TriggerWebhookOptions = {
|
||||
event: WebhookTriggerEvents;
|
||||
data: Record<string, unknown>;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => {
|
||||
try {
|
||||
const body = {
|
||||
event,
|
||||
data,
|
||||
userId,
|
||||
teamId,
|
||||
};
|
||||
|
||||
const signature = sign(body);
|
||||
|
||||
await Promise.race([
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-webhook-signature': signature,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Request timeout')), 500);
|
||||
}),
|
||||
]).catch(() => null);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to trigger webhook`);
|
||||
}
|
||||
};
|
||||
67
packages/lib/server-only/webhooks/zapier/list-documents.ts
Normal file
67
packages/lib/server-only/webhooks/zapier/list-documents.ts
Normal file
@ -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 type { Webhook } from '@documenso/prisma/client';
|
||||
|
||||
import { getWebhooksByTeamId } from '../get-webhooks-by-team-id';
|
||||
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, userId, teamId } = await validateApiToken({ authorization });
|
||||
|
||||
let allWebhooks: Webhook[] = [];
|
||||
|
||||
const documents = await findDocuments({
|
||||
userId: userId ?? user.id,
|
||||
teamId: teamId ?? undefined,
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: documents.data[0].id,
|
||||
userId: userId ?? user.id,
|
||||
teamId: teamId ?? undefined,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
allWebhooks = await getWebhooksByUserId(userId);
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
allWebhooks = await getWebhooksByTeamId(teamId, user.id);
|
||||
}
|
||||
|
||||
if (documents && 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: recipients,
|
||||
},
|
||||
};
|
||||
|
||||
return res.status(200).json([testWebhook]);
|
||||
}
|
||||
|
||||
return res.status(200).json([]);
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
message: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
};
|
||||
32
packages/lib/server-only/webhooks/zapier/subscribe.ts
Normal file
32
packages/lib/server-only/webhooks/zapier/subscribe.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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 result = await validateApiToken({ authorization });
|
||||
|
||||
const createdWebhook = await prisma.webhook.create({
|
||||
data: {
|
||||
webhookUrl,
|
||||
eventTriggers: [eventTrigger],
|
||||
secret: null,
|
||||
enabled: true,
|
||||
userId: result.userId ?? result.user.id,
|
||||
teamId: result.teamId ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json(createdWebhook);
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
message: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
};
|
||||
29
packages/lib/server-only/webhooks/zapier/unsubscribe.ts
Normal file
29
packages/lib/server-only/webhooks/zapier/unsubscribe.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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 result = await validateApiToken({ authorization });
|
||||
|
||||
const deletedWebhook = await prisma.webhook.delete({
|
||||
where: {
|
||||
id: webhookId,
|
||||
userId: result.userId ?? result.user.id,
|
||||
teamId: result.teamId ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json(deletedWebhook);
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
message: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
};
|
||||
20
packages/lib/server-only/webhooks/zapier/validateApiToken.ts
Normal file
20
packages/lib/server-only/webhooks/zapier/validateApiToken.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getApiTokenByToken } from '../../public-api/get-api-token-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);
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Missing API token');
|
||||
}
|
||||
|
||||
return await getApiTokenByToken({ token });
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to validate API token`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export const toFriendlyWebhookEventName = (eventName: string) => {
|
||||
return eventName.replace(/_/g, '.').toLowerCase();
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebhookTriggerEvents" AS ENUM ('DOCUMENT_CREATED', 'DOCUMENT_SIGNED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Webhook" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"webhookUrl" TEXT NOT NULL,
|
||||
"eventTriggers" "WebhookTriggerEvents"[],
|
||||
"secret" TEXT,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -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';
|
||||
@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `Webhook` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Webhook" DROP CONSTRAINT "Webhook_pkey",
|
||||
ALTER COLUMN "id" DROP DEFAULT,
|
||||
ALTER COLUMN "id" SET DATA TYPE TEXT,
|
||||
ADD CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id");
|
||||
DROP SEQUENCE "Webhook_id_seq";
|
||||
@ -0,0 +1,20 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebhookCallStatus" AS ENUM ('SUCCESS', 'FAILED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookCall" (
|
||||
"id" TEXT NOT NULL,
|
||||
"status" "WebhookCallStatus" NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"requestBody" JSONB NOT NULL,
|
||||
"responseCode" INTEGER NOT NULL,
|
||||
"responseHeaders" JSONB,
|
||||
"responseBody" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"webhookId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "WebhookCall_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookCall" ADD CONSTRAINT "WebhookCall_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "Webhook"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Webhook" ADD COLUMN "teamId" INTEGER;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `event` to the `WebhookCall` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "WebhookCall" ADD COLUMN "event" "WebhookTriggerEvents" NOT NULL;
|
||||
@ -50,6 +50,7 @@ model User {
|
||||
ApiToken ApiToken[]
|
||||
Template Template[]
|
||||
securityAuditLogs UserSecurityAuditLog[]
|
||||
Webhooks Webhook[]
|
||||
siteSettings SiteSettings[]
|
||||
|
||||
@@index([email])
|
||||
@ -105,6 +106,48 @@ model VerificationToken {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum WebhookTriggerEvents {
|
||||
DOCUMENT_CREATED
|
||||
DOCUMENT_SENT
|
||||
DOCUMENT_OPENED
|
||||
DOCUMENT_SIGNED
|
||||
DOCUMENT_COMPLETED
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
webhookUrl String
|
||||
eventTriggers WebhookTriggerEvents[]
|
||||
secret String?
|
||||
enabled Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
userId Int
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
WebhookCall WebhookCall[]
|
||||
}
|
||||
|
||||
enum WebhookCallStatus {
|
||||
SUCCESS
|
||||
FAILED
|
||||
}
|
||||
|
||||
model WebhookCall {
|
||||
id String @id @default(cuid())
|
||||
status WebhookCallStatus
|
||||
url String
|
||||
event WebhookTriggerEvents
|
||||
requestBody Json
|
||||
responseCode Int
|
||||
responseHeaders Json?
|
||||
responseBody Json?
|
||||
createdAt DateTime @default(now())
|
||||
webhookId String
|
||||
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum ApiTokenAlgorithm {
|
||||
SHA512
|
||||
}
|
||||
@ -386,6 +429,7 @@ model Team {
|
||||
document Document[]
|
||||
templates Template[]
|
||||
ApiToken ApiToken[]
|
||||
Webhook Webhook[]
|
||||
}
|
||||
|
||||
model TeamPending {
|
||||
|
||||
@ -11,6 +11,9 @@ module.exports = {
|
||||
sans: ['var(--font-sans)', ...fontFamily.sans],
|
||||
signature: ['var(--font-signature)'],
|
||||
},
|
||||
zIndex: {
|
||||
9999: '9999',
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
|
||||
@ -12,6 +12,7 @@ import { teamRouter } from './team-router/router';
|
||||
import { templateRouter } from './template-router/router';
|
||||
import { router } from './trpc';
|
||||
import { twoFactorAuthenticationRouter } from './two-factor-authentication-router/router';
|
||||
import { webhookRouter } from './webhook-router/router';
|
||||
|
||||
export const appRouter = router({
|
||||
auth: authRouter,
|
||||
@ -26,6 +27,7 @@ export const appRouter = router({
|
||||
singleplayer: singleplayerRouter,
|
||||
team: teamRouter,
|
||||
template: templateRouter,
|
||||
webhook: webhookRouter,
|
||||
twoFactorAuthentication: twoFactorAuthenticationRouter,
|
||||
});
|
||||
|
||||
|
||||
125
packages/trpc/server/webhook-router/router.ts
Normal file
125
packages/trpc/server/webhook-router/router.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
|
||||
import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
|
||||
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
|
||||
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
|
||||
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
|
||||
import { getWebhooksByUserId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-user-id';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateWebhookMutationSchema,
|
||||
ZDeleteWebhookMutationSchema,
|
||||
ZEditWebhookMutationSchema,
|
||||
ZGetTeamWebhooksQuerySchema,
|
||||
ZGetWebhookByIdQuerySchema,
|
||||
} from './schema';
|
||||
|
||||
export const webhookRouter = router({
|
||||
getWebhooks: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
return await getWebhooksByUserId(ctx.user.id);
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to fetch your webhooks. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getTeamWebhooks: authenticatedProcedure
|
||||
.input(ZGetTeamWebhooksQuerySchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { teamId } = input;
|
||||
|
||||
try {
|
||||
return await getWebhooksByTeamId(teamId, ctx.user.id);
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to fetch your webhooks. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getWebhookById: authenticatedProcedure
|
||||
.input(ZGetWebhookByIdQuerySchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id, teamId } = input;
|
||||
|
||||
return await getWebhookById({
|
||||
id,
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to fetch your webhook. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
createWebhook: authenticatedProcedure
|
||||
.input(ZCreateWebhookMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
|
||||
|
||||
try {
|
||||
return await createWebhook({
|
||||
enabled,
|
||||
secret,
|
||||
webhookUrl,
|
||||
eventTriggers,
|
||||
teamId,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to create this webhook. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
deleteWebhook: authenticatedProcedure
|
||||
.input(ZDeleteWebhookMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id, teamId } = input;
|
||||
|
||||
return await deleteWebhookById({
|
||||
id,
|
||||
teamId,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to create this webhook. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
editWebhook: authenticatedProcedure
|
||||
.input(ZEditWebhookMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id, teamId, ...data } = input;
|
||||
|
||||
return await editWebhook({
|
||||
id,
|
||||
data,
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to create this webhook. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
41
packages/trpc/server/webhook-router/schema.ts
Normal file
41
packages/trpc/server/webhook-router/schema.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export const ZGetTeamWebhooksQuerySchema = z.object({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export type TGetTeamWebhooksQuerySchema = z.infer<typeof ZGetTeamWebhooksQuerySchema>;
|
||||
|
||||
export const ZCreateWebhookMutationSchema = z.object({
|
||||
webhookUrl: z.string().url(),
|
||||
eventTriggers: z
|
||||
.array(z.nativeEnum(WebhookTriggerEvents))
|
||||
.min(1, { message: 'At least one event trigger is required' }),
|
||||
secret: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookMutationSchema>;
|
||||
|
||||
export const ZGetWebhookByIdQuerySchema = z.object({
|
||||
id: z.string(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TGetWebhookByIdQuerySchema = z.infer<typeof ZGetWebhookByIdQuerySchema>;
|
||||
|
||||
export const ZEditWebhookMutationSchema = ZCreateWebhookMutationSchema.extend({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type TEditWebhookMutationSchema = z.infer<typeof ZEditWebhookMutationSchema>;
|
||||
|
||||
export const ZDeleteWebhookMutationSchema = z.object({
|
||||
id: z.string(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TDeleteWebhookMutationSchema = z.infer<typeof ZDeleteWebhookMutationSchema>;
|
||||
@ -6,7 +6,7 @@ import { cva } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md px-2 py-1.5 text-xs font-medium ring-1 ring-inset w-fit',
|
||||
'inline-flex items-center rounded-md text-xs font-medium ring-1 ring-inset w-fit',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@ -21,9 +21,15 @@ const badgeVariants = cva(
|
||||
secondary:
|
||||
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
|
||||
},
|
||||
size: {
|
||||
small: 'px-1.5 py-0.5 text-xs',
|
||||
default: 'px-2 py-1.5 text-xs',
|
||||
large: 'px-3 py-2 text-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -32,8 +38,8 @@ export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
function Badge({ className, variant, size, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
Reference in New Issue
Block a user