cal.pub0.org/apps/web/server/routers/viewer/webhook.tsx

245 lines
6.4 KiB
TypeScript

import { Prisma } from "@prisma/client";
import { v4 } from "uuid";
import { z } from "zod";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants";
import sendPayload from "@lib/webhooks/sendPayload";
import { createProtectedRouter } from "@server/createRouter";
import { getTranslation } from "@server/lib/i18n";
import { TRPCError } from "@trpc/server";
// Common data for all endpoints under webhook
const webhookIdAndEventTypeIdSchema = z.object({
// Webhook ID
id: z.string().optional(),
// Event type ID
eventTypeId: z.number().optional(),
});
export const webhookRouter = createProtectedRouter()
.middleware(async ({ ctx, rawInput, next }) => {
// Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
if (!rawInput) {
return next();
}
const webhookIdAndEventTypeId = webhookIdAndEventTypeIdSchema.safeParse(rawInput);
if (!webhookIdAndEventTypeId.success) {
throw new TRPCError({ code: "PARSE_ERROR" });
}
const { eventTypeId, id } = webhookIdAndEventTypeId.data;
// A webhook is either linked to Event Type or to a user.
if (eventTypeId) {
const team = await ctx.prisma.team.findFirst({
where: {
eventTypes: {
some: {
id: eventTypeId,
},
},
},
include: {
members: true,
},
});
// Team should be available and the user should be a member of the team
if (!team?.members.some((membership) => membership.userId === ctx.user.id)) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
} else if (id) {
const authorizedHook = await ctx.prisma.webhook.findFirst({
where: {
id: id,
userId: ctx.user.id,
},
});
if (!authorizedHook) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
}
return next();
})
.query("list", {
input: z
.object({
eventTypeId: z.number().optional(),
})
.optional(),
async resolve({ ctx, input }) {
const where: Prisma.WebhookWhereInput = {
AND: [{ appId: null /* Don't mixup zapier webhooks with normal ones */ }],
};
if (Array.isArray(where.AND)) {
if (input?.eventTypeId) {
where.AND?.push({ eventTypeId: input.eventTypeId });
} else {
where.AND?.push({ userId: ctx.user.id });
}
}
return await ctx.prisma.webhook.findMany({
where,
});
},
})
.mutation("create", {
input: z.object({
subscriberUrl: z.string().url(),
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(),
active: z.boolean(),
payloadTemplate: z.string().nullable(),
eventTypeId: z.number().optional(),
appId: z.string().optional().nullable(),
}),
async resolve({ ctx, input }) {
if (input.eventTypeId) {
return await ctx.prisma.webhook.create({
data: {
id: v4(),
...input,
},
});
}
return await ctx.prisma.webhook.create({
data: {
id: v4(),
userId: ctx.user.id,
...input,
},
});
},
})
.mutation("edit", {
input: z.object({
id: z.string(),
subscriberUrl: z.string().url().optional(),
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
active: z.boolean().optional(),
payloadTemplate: z.string().nullable(),
eventTypeId: z.number().optional(),
appId: z.string().optional().nullable(),
}),
async resolve({ ctx, input }) {
const { id, ...data } = input;
const webhook = input.eventTypeId
? await ctx.prisma.webhook.findFirst({
where: {
eventTypeId: input.eventTypeId,
id,
},
})
: await ctx.prisma.webhook.findFirst({
where: {
userId: ctx.user.id,
id,
},
});
if (!webhook) {
// user does not own this webhook
// team event doesn't own this webhook
return null;
}
return await ctx.prisma.webhook.update({
where: {
id,
},
data,
});
},
})
.mutation("delete", {
input: z.object({
id: z.string(),
eventTypeId: z.number().optional(),
}),
async resolve({ ctx, input }) {
const { id } = input;
input.eventTypeId
? await ctx.prisma.eventType.update({
where: {
id: input.eventTypeId,
},
data: {
webhooks: {
delete: {
id,
},
},
},
})
: await ctx.prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
webhooks: {
delete: {
id,
},
},
},
});
return {
id,
};
},
})
.mutation("testTrigger", {
input: z.object({
url: z.string().url(),
type: z.string(),
payloadTemplate: z.string().optional().nullable(),
}),
async resolve({ input }) {
const { url, type, payloadTemplate = null } = input;
const translation = await getTranslation("en", "common");
const language = {
locale: "en",
translate: translation,
};
const data = {
triggerEvent: "PING",
type: "Test",
title: "Test trigger event",
description: "",
startTime: new Date().toISOString(),
endTime: new Date().toISOString(),
attendees: [
{
email: "jdoe@example.com",
name: "John Doe",
timeZone: "Europe/London",
language,
},
],
organizer: {
name: "Cal",
email: "no-reply@cal.com",
timeZone: "Europe/London",
language,
},
};
try {
const webhook = { subscriberUrl: url, payloadTemplate, appId: null };
return await sendPayload(type, new Date().toISOString(), webhook, data);
} catch (_err) {
const error = getErrorFromUnknown(_err);
return {
ok: false,
status: 500,
message: error.message,
};
}
},
});