This reverts commit 4c07faefe7
.
This commit is contained in:
parent
4c07faefe7
commit
6868474c92
19 changed files with 66 additions and 960 deletions
|
@ -15,7 +15,7 @@ export default function SettingsShell({ children }: { children: React.ReactNode
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: KeyIcon,
|
icon: KeyIcon,
|
||||||
},
|
},
|
||||||
{ name: "Embed & Webhooks", href: "/settings/embed", icon: CodeIcon },
|
{ name: "Embed", href: "/settings/embed", icon: CodeIcon },
|
||||||
{
|
{
|
||||||
name: "Teams",
|
name: "Teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
|
|
|
@ -1,176 +0,0 @@
|
||||||
import { ArrowLeftIcon } from "@heroicons/react/solid";
|
|
||||||
import { EventType } from "@prisma/client";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import showToast from "@lib/notification";
|
|
||||||
import { Webhook } from "@lib/webhook";
|
|
||||||
|
|
||||||
import Button from "@components/ui/Button";
|
|
||||||
import Switch from "@components/ui/Switch";
|
|
||||||
|
|
||||||
export default function EditTeam(props: {
|
|
||||||
webhook: Webhook;
|
|
||||||
eventTypes: EventType[];
|
|
||||||
onCloseEdit: () => void;
|
|
||||||
}) {
|
|
||||||
const [bookingCreated, setBookingCreated] = useState(
|
|
||||||
props.webhook.eventTriggers.includes("booking_created")
|
|
||||||
);
|
|
||||||
const [bookingRescheduled, setBookingRescheduled] = useState(
|
|
||||||
props.webhook.eventTriggers.includes("booking_rescheduled")
|
|
||||||
);
|
|
||||||
const [bookingCancelled, setBookingCancelled] = useState(
|
|
||||||
props.webhook.eventTriggers.includes("booking_cancelled")
|
|
||||||
);
|
|
||||||
const [webhookEnabled, setWebhookEnabled] = useState(props.webhook.active);
|
|
||||||
const [webhookEventTrigger, setWebhookEventTriggers] = useState([
|
|
||||||
"BOOKING_CREATED",
|
|
||||||
"BOOKING_RESCHEDULED",
|
|
||||||
"BOOKING_CANCELLED",
|
|
||||||
]);
|
|
||||||
const [btnLoading, setBtnLoading] = useState(false);
|
|
||||||
const subUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const arr = [];
|
|
||||||
bookingCreated && arr.push("BOOKING_CREATED");
|
|
||||||
bookingRescheduled && arr.push("BOOKING_RESCHEDULED");
|
|
||||||
bookingCancelled && arr.push("BOOKING_CANCELLED");
|
|
||||||
setWebhookEventTriggers(arr);
|
|
||||||
}, [bookingCreated, bookingRescheduled, bookingCancelled, webhookEnabled]);
|
|
||||||
|
|
||||||
const handleErrors = async (resp: Response) => {
|
|
||||||
if (!resp.ok) {
|
|
||||||
const err = await resp.json();
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateWebhookHandler = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setBtnLoading(true);
|
|
||||||
return fetch("/api/webhooks/" + props.webhook.id, {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({
|
|
||||||
subscriberUrl: subUrlRef.current.value,
|
|
||||||
eventTriggers: webhookEventTrigger,
|
|
||||||
enabled: webhookEnabled,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(handleErrors)
|
|
||||||
.then(() => {
|
|
||||||
showToast("Webhook updated successfully!", "success");
|
|
||||||
setBtnLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
|
||||||
<div className="py-6 lg:pb-8">
|
|
||||||
<div className="mb-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="secondary"
|
|
||||||
size="sm"
|
|
||||||
loading={btnLoading}
|
|
||||||
StartIcon={ArrowLeftIcon}
|
|
||||||
onClick={() => props.onCloseEdit()}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="pb-5 pr-4 sm:pb-6">
|
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900">Manage your webhook</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className="mt-2" />
|
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateWebhookHandler}>
|
|
||||||
<div className="my-4">
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="subUrl" className="block text-sm font-medium text-gray-700">
|
|
||||||
Subscriber Url
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={subUrlRef}
|
|
||||||
type="text"
|
|
||||||
name="subUrl"
|
|
||||||
id="subUrl"
|
|
||||||
defaultValue={props.webhook.subscriberUrl || ""}
|
|
||||||
placeholder="https://example.com/sub"
|
|
||||||
required
|
|
||||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
<legend className="block pt-4 mb-2 text-sm font-medium text-gray-700"> Event Triggers </legend>
|
|
||||||
<div className="p-2 bg-white border border-gray-300 rounded-sm">
|
|
||||||
<div className="flex p-2">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="text-sm text-gray-800">Booking Created</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={bookingCreated}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingCreated(!bookingCreated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex px-2 py-1">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="text-sm text-gray-800">Booking Rescheduled</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={bookingRescheduled}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingRescheduled(!bookingRescheduled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex p-2">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="text-sm text-gray-800">Booking Cancelled</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={bookingCancelled}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingCancelled(!bookingCancelled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<legend className="block pt-4 mb-2 text-sm font-medium text-gray-700"> Webhook Status </legend>
|
|
||||||
<div className="p-2 bg-white border border-gray-300 rounded-sm">
|
|
||||||
<div className="flex p-2">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="text-sm text-gray-800">Webhook Enabled</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={webhookEnabled}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setWebhookEnabled(!webhookEnabled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<Button type="submit" color="primary" className="ml-2" loading={btnLoading}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Webhook } from "@lib/webhook";
|
|
||||||
|
|
||||||
import WebhookListItem from "./WebhookListItem";
|
|
||||||
|
|
||||||
export default function WebhookList(props: {
|
|
||||||
webhooks: Webhook[];
|
|
||||||
onChange: () => void;
|
|
||||||
onEditWebhook: (webhook: Webhook) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
|
||||||
{props.webhooks.map((webhook: Webhook) => (
|
|
||||||
<WebhookListItem
|
|
||||||
onChange={props.onChange}
|
|
||||||
key={webhook.id}
|
|
||||||
webhook={webhook}
|
|
||||||
onEditWebhook={() => props.onEditWebhook(webhook)}></WebhookListItem>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
import { TrashIcon, PencilAltIcon } from "@heroicons/react/outline";
|
|
||||||
|
|
||||||
import showToast from "@lib/notification";
|
|
||||||
import { Webhook } from "@lib/webhook";
|
|
||||||
|
|
||||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
|
||||||
import { Tooltip } from "@components/Tooltip";
|
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
|
||||||
import Button from "@components/ui/Button";
|
|
||||||
|
|
||||||
export default function WebhookListItem(props: {
|
|
||||||
onChange: () => void;
|
|
||||||
key: number;
|
|
||||||
webhook: Webhook;
|
|
||||||
onEditWebhook: () => void;
|
|
||||||
}) {
|
|
||||||
const handleErrors = async (resp: Response) => {
|
|
||||||
if (!resp.ok) {
|
|
||||||
const err = await resp.json();
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteWebhook = (webhookId: string) => {
|
|
||||||
fetch("/api/webhooks/" + webhookId, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(handleErrors)
|
|
||||||
.then(() => {
|
|
||||||
showToast("Webhook removed successfully!", "success");
|
|
||||||
props.onChange();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="divide-y">
|
|
||||||
<div className="flex justify-between my-4">
|
|
||||||
<div className="flex pr-2 border-r border-gray-100">
|
|
||||||
<span className="flex flex-col space-y-2 text-xs">
|
|
||||||
{props.webhook.eventTriggers.map((eventTrigger, ind) => (
|
|
||||||
<span key={ind} className="px-1 text-xs text-blue-700 rounded-md w-max bg-blue-50">
|
|
||||||
{eventTrigger}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full">
|
|
||||||
<div className="self-center inline-block ml-3 space-y-1">
|
|
||||||
<span className="flex text-sm text-neutral-700">{props.webhook.subscriberUrl}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
{!props.webhook.active && (
|
|
||||||
<span className="self-center h-6 px-3 py-1 text-xs text-red-700 capitalize rounded-md bg-red-50">
|
|
||||||
Disabled
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!!props.webhook.active && (
|
|
||||||
<span className="self-center h-6 px-3 py-1 text-xs text-green-700 capitalize rounded-md bg-green-50">
|
|
||||||
Enabled
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tooltip content="Edit Webhook">
|
|
||||||
<Button
|
|
||||||
onClick={() => props.onEditWebhook()}
|
|
||||||
color="minimal"
|
|
||||||
size="icon"
|
|
||||||
StartIcon={PencilAltIcon}
|
|
||||||
className="self-center w-full p-2 ml-4"></Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog>
|
|
||||||
<Tooltip content="Delete Webhook">
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
color="minimal"
|
|
||||||
size="icon"
|
|
||||||
StartIcon={TrashIcon}
|
|
||||||
className="self-center w-full p-2 ml-2"></Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title="Delete Webhook"
|
|
||||||
confirmBtnText="Yes, delete webhook"
|
|
||||||
cancelBtnText="Cancel"
|
|
||||||
onConfirm={() => {
|
|
||||||
deleteWebhook(props.webhook.id);
|
|
||||||
}}>
|
|
||||||
Are you sure you want to delete this webhook? You will no longer receive Cal.com meeting data at
|
|
||||||
a specified URL, in real-time, when an event is scheduled or canceled .
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Credential } from "@prisma/client";
|
import { Credential } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
import { createEvent, DurationObject, Attendee, Person } from "ics";
|
import { createEvent, DurationObject, Attendee, Person } from "ics";
|
||||||
import {
|
import {
|
||||||
|
@ -20,8 +19,6 @@ import logger from "@lib/logger";
|
||||||
import { IntegrationCalendar, CalendarApiAdapter, CalendarEvent } from "../../calendarClient";
|
import { IntegrationCalendar, CalendarApiAdapter, CalendarEvent } from "../../calendarClient";
|
||||||
import { stripHtml } from "../../emails/helpers";
|
import { stripHtml } from "../../emails/helpers";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
|
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
|
||||||
|
|
||||||
type EventBusyDate = Record<"start" | "end", Date>;
|
type EventBusyDate = Record<"start" | "end", Date>;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Credential } from "@prisma/client";
|
import { Credential } from "@prisma/client";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
import { Attendee, createEvent, DurationObject, Person } from "ics";
|
import { Attendee, createEvent, DurationObject, Person } from "ics";
|
||||||
import {
|
import {
|
||||||
|
@ -20,8 +19,6 @@ import logger from "@lib/logger";
|
||||||
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "../../calendarClient";
|
import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "../../calendarClient";
|
||||||
import { stripHtml } from "../../emails/helpers";
|
import { stripHtml } from "../../emails/helpers";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[lib] caldav"] });
|
const log = logger.getChildLogger({ prefix: ["[lib] caldav"] });
|
||||||
|
|
||||||
type EventBusyDate = Record<"start" | "end", Date>;
|
type EventBusyDate = Record<"start" | "end", Date>;
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { Webhook as PrismaWebhook } from "@prisma/client";
|
|
||||||
|
|
||||||
export type Webhook = PrismaWebhook & { prevState: null };
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { CalendarEvent } from "@lib/calendarClient";
|
|
||||||
|
|
||||||
const sendPayload = (
|
|
||||||
triggerEvent: string,
|
|
||||||
createdAt: string,
|
|
||||||
subscriberUrl: string,
|
|
||||||
payload: CalendarEvent
|
|
||||||
): Promise<string | Response> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
if (!subscriberUrl || !payload) {
|
|
||||||
return reject("Missing required elements to send webhook payload.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(subscriberUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
triggerEvent: triggerEvent,
|
|
||||||
createdAt: createdAt,
|
|
||||||
payload: payload,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
resolve(response);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default sendPayload;
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { WebhookTriggerEvents } from "@prisma/client";
|
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
|
||||||
|
|
||||||
const getSubscriberUrls = async (userId: number, triggerEvent: WebhookTriggerEvents): Promise<string[]> => {
|
|
||||||
const allWebhooks = await prisma.webhook.findMany({
|
|
||||||
where: {
|
|
||||||
userId: userId,
|
|
||||||
AND: {
|
|
||||||
eventTriggers: {
|
|
||||||
has: triggerEvent,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
equals: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
subscriberUrl: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const subscriberUrls = allWebhooks.map(({ subscriberUrl }) => subscriberUrl);
|
|
||||||
|
|
||||||
return subscriberUrls;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getSubscriberUrls;
|
|
|
@ -136,6 +136,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const openingHours = req.body.availability.openingHours || [];
|
const openingHours = req.body.availability.openingHours || [];
|
||||||
// const overrides = req.body.availability.dateOverrides || [];
|
// const overrides = req.body.availability.dateOverrides || [];
|
||||||
|
|
||||||
|
await prisma.availability.deleteMany({
|
||||||
|
where: {
|
||||||
|
eventTypeId: +req.body.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
Promise.all(
|
Promise.all(
|
||||||
openingHours.map((schedule) =>
|
openingHours.map((schedule) =>
|
||||||
prisma.availability.create({
|
prisma.availability.create({
|
||||||
|
@ -169,12 +174,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.webhookEventTypes.deleteMany({
|
|
||||||
where: {
|
|
||||||
eventTypeId: req.body.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.eventType.delete({
|
await prisma.eventType.delete({
|
||||||
where: {
|
where: {
|
||||||
id: req.body.id,
|
id: req.body.id,
|
||||||
|
|
|
@ -19,8 +19,6 @@ import logger from "@lib/logger";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { BookingCreateBody } from "@lib/types/booking";
|
import { BookingCreateBody } from "@lib/types/booking";
|
||||||
import { getBusyVideoTimes } from "@lib/videoClient";
|
import { getBusyVideoTimes } from "@lib/videoClient";
|
||||||
import sendPayload from "@lib/webhooks/sendPayload";
|
|
||||||
import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
|
|
||||||
|
|
||||||
dayjs.extend(dayjsBusinessDays);
|
dayjs.extend(dayjsBusinessDays);
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
@ -466,16 +464,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
log.debug(`Booking ${user.username} completed`);
|
log.debug(`Booking ${user.username} completed`);
|
||||||
|
|
||||||
const eventTrigger = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED";
|
|
||||||
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
|
||||||
const subscriberUrls = await getSubscriberUrls(user.id, eventTrigger);
|
|
||||||
const promises = subscriberUrls.map((url) =>
|
|
||||||
sendPayload(eventTrigger, new Date().toISOString(), url, evt).catch((e) => {
|
|
||||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${url}`, e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
await prisma.booking.update({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
|
|
|
@ -8,12 +8,10 @@ import { getSession } from "@lib/auth";
|
||||||
import { CalendarEvent, deleteEvent } from "@lib/calendarClient";
|
import { CalendarEvent, deleteEvent } from "@lib/calendarClient";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { deleteMeeting } from "@lib/videoClient";
|
import { deleteMeeting } from "@lib/videoClient";
|
||||||
import sendPayload from "@lib/webhooks/sendPayload";
|
|
||||||
import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
// just bail if it not a DELETE
|
// just bail if it not a DELETE
|
||||||
if (req.method !== "DELETE" && req.method !== "POST") {
|
if (req.method !== "DELETE") {
|
||||||
return res.status(405).end();
|
return res.status(405).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +24,6 @@ export default async function handler(req, res) {
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
userId: true,
|
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -51,7 +48,6 @@ export default async function handler(req, res) {
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
uid: true,
|
uid: true,
|
||||||
eventTypeId: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,41 +59,6 @@ export default async function handler(req, res) {
|
||||||
return res.status(403).json({ message: "Cannot cancel past events" });
|
return res.status(403).json({ message: "Cannot cancel past events" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizer = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: bookingToDelete.userId as number,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
type: bookingToDelete?.title,
|
|
||||||
title: bookingToDelete?.title,
|
|
||||||
description: bookingToDelete?.description || "",
|
|
||||||
startTime: bookingToDelete?.startTime.toString(),
|
|
||||||
endTime: bookingToDelete?.endTime.toString(),
|
|
||||||
organizer: organizer,
|
|
||||||
attendees: bookingToDelete?.attendees.map((attendee) => {
|
|
||||||
const retObj = { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
|
||||||
return retObj;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hook up the webhook logic here
|
|
||||||
const eventTrigger = "BOOKING_CANCELLED";
|
|
||||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
|
||||||
const subscriberUrls = await getSubscriberUrls(bookingToDelete.userId, eventTrigger);
|
|
||||||
const promises = subscriberUrls.map((url) =>
|
|
||||||
sendPayload(eventTrigger, new Date().toISOString(), url, evt).catch((e) => {
|
|
||||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${url}`, e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
|
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
|
||||||
// action always succeeds even if subsequent integrations fail cancellation.
|
// action always succeeds even if subsequent integrations fail cancellation.
|
||||||
await prisma.booking.update({
|
await prisma.booking.update({
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getSession } from "next-auth/client";
|
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
length: true,
|
|
||||||
schedulingType: true,
|
|
||||||
slug: true,
|
|
||||||
hidden: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
avatar: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
bufferTime: true,
|
|
||||||
avatar: true,
|
|
||||||
completedOnboarding: true,
|
|
||||||
createdDate: true,
|
|
||||||
plan: true,
|
|
||||||
teams: {
|
|
||||||
where: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
logo: true,
|
|
||||||
members: {
|
|
||||||
select: {
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eventTypes: {
|
|
||||||
select: eventTypeSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eventTypes: {
|
|
||||||
where: {
|
|
||||||
team: null,
|
|
||||||
},
|
|
||||||
select: eventTypeSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// backwards compatibility, TMP:
|
|
||||||
const typesRaw = await prisma.eventType.findMany({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
select: eventTypeSelect,
|
|
||||||
});
|
|
||||||
|
|
||||||
type EventTypeGroup = {
|
|
||||||
teamId?: number | null;
|
|
||||||
profile?: {
|
|
||||||
slug: typeof user["username"];
|
|
||||||
name: typeof user["name"];
|
|
||||||
image: typeof user["avatar"];
|
|
||||||
};
|
|
||||||
metadata: {
|
|
||||||
membershipCount: number;
|
|
||||||
readOnly: boolean;
|
|
||||||
};
|
|
||||||
eventTypes: (typeof user.eventTypes[number] & { $disabled?: boolean })[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventTypesHashMap = user.eventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
|
|
||||||
const oldItem = hashMap[newItem.id] || {};
|
|
||||||
hashMap[newItem.id] = { ...oldItem, ...newItem };
|
|
||||||
return hashMap;
|
|
||||||
}, {} as Record<number, EventTypeGroup["eventTypes"][number]>);
|
|
||||||
const mergedEventTypes = Object.values(eventTypesHashMap).map((et, index) => ({
|
|
||||||
...et,
|
|
||||||
$disabled: user.plan === "FREE" && index > 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return res.status(200).json({ eventTypes: mergedEventTypes });
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import short from "short-uuid";
|
|
||||||
import { v5 as uuidv5 } from "uuid";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
import prisma from "@lib/prisma";
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req });
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// List webhooks
|
|
||||||
if (req.method === "GET") {
|
|
||||||
const webhooks = await prisma.webhook.findMany({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({ webhooks: webhooks });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
|
||||||
const translator = short();
|
|
||||||
const seed = `${req.body.subscriberUrl}:${dayjs(new Date()).utc().format()}`;
|
|
||||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
|
||||||
|
|
||||||
await prisma.webhook.create({
|
|
||||||
data: {
|
|
||||||
id: uid,
|
|
||||||
userId: session.user.id,
|
|
||||||
subscriberUrl: req.body.subscriberUrl,
|
|
||||||
eventTriggers: req.body.eventTriggers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(201).json({ message: "Webhook created" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(404).json({ message: "Webhook not found" });
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getSession } from "next-auth/client";
|
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
if (!session) {
|
|
||||||
return res.status(401).json({ message: "Not authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/webhook/{hook}
|
|
||||||
const webhooks = await prisma.webhook.findFirst({
|
|
||||||
where: {
|
|
||||||
id: String(req.query.hook),
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (req.method === "GET") {
|
|
||||||
return res.status(200).json({ webhooks: webhooks });
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/webhook/{hook}
|
|
||||||
if (req.method === "DELETE") {
|
|
||||||
await prisma.webhook.delete({
|
|
||||||
where: {
|
|
||||||
id: String(req.query.hook),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return res.status(200).json({});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "PATCH") {
|
|
||||||
const webhook = await prisma.webhook.findUnique({
|
|
||||||
where: {
|
|
||||||
id: req.query.hook as string,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!webhook) {
|
|
||||||
return res.status(404).json({ message: "Invalid Webhook" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.webhook.update({
|
|
||||||
where: {
|
|
||||||
id: req.query.hook as string,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
subscriberUrl: req.body.subscriberUrl,
|
|
||||||
eventTriggers: req.body.eventTriggers,
|
|
||||||
active: req.body.enabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({ message: "Webhook updated successfully" });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -63,24 +63,24 @@ export default function Type(props) {
|
||||||
description={`Cancel ${props.booking && props.booking.title} | ${props.profile.name}`}
|
description={`Cancel ${props.booking && props.booking.title} | ${props.profile.name}`}
|
||||||
/>
|
/>
|
||||||
<main className="max-w-3xl mx-auto my-24">
|
<main className="max-w-3xl mx-auto my-24">
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
<div className="fixed inset-0 my-4 sm:my-0 transition-opacity" aria-hidden="true">
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
|
className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-headline">
|
aria-labelledby="modal-headline">
|
||||||
{error && (
|
{error && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||||
<XIcon className="w-6 h-6 text-red-600" />
|
<XIcon className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
{error}
|
{error}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,11 +89,11 @@ export default function Type(props) {
|
||||||
{!error && (
|
{!error && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||||
<XIcon className="w-6 h-6 text-red-600" />
|
<XIcon className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||||
{props.cancellationAllowed
|
{props.cancellationAllowed
|
||||||
? "Really cancel your booking?"
|
? "Really cancel your booking?"
|
||||||
: "You cannot cancel this booking"}
|
: "You cannot cancel this booking"}
|
||||||
|
@ -105,8 +105,8 @@ export default function Type(props) {
|
||||||
: "The event is in the past"}
|
: "The event is in the past"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4 mt-4 border-t border-b">
|
<div className="mt-4 border-t border-b py-4">
|
||||||
<h2 className="mb-2 text-lg font-medium text-gray-600 font-cal">
|
<h2 className="font-cal text-lg font-medium text-gray-600 mb-2">
|
||||||
{props.booking.title}
|
{props.booking.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
|
@ -119,7 +119,7 @@ export default function Type(props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.cancellationAllowed && (
|
{props.cancellationAllowed && (
|
||||||
<div className="mt-5 space-x-2 text-center sm:mt-6">
|
<div className="mt-5 sm:mt-6 text-centerspace-x-2">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
|
@ -156,7 +156,6 @@ export async function getServerSideProps(context) {
|
||||||
attendees: true,
|
attendees: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
|
||||||
username: true,
|
username: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,287 +1,72 @@
|
||||||
import { PlusIcon } from "@heroicons/react/outline";
|
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useSession } from "next-auth/client";
|
import { useSession } from "next-auth/client";
|
||||||
import { useEffect, useState, useRef } from "react";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { Webhook } from "@lib/webhook";
|
|
||||||
|
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTrigger } from "@components/Dialog";
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import Button from "@components/ui/Button";
|
|
||||||
import Switch from "@components/ui/Switch";
|
|
||||||
import EditWebhook from "@components/webhook/EditWebhook";
|
|
||||||
import WebhookList from "@components/webhook/WebhookList";
|
|
||||||
|
|
||||||
export default function Embed(props: inferSSRProps<typeof getServerSideProps>) {
|
export default function Embed(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const [, loading] = useSession();
|
const [, loading] = useSession();
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
const [bookingCreated, setBookingCreated] = useState(true);
|
|
||||||
const [bookingRescheduled, setBookingRescheduled] = useState(true);
|
|
||||||
const [bookingCancelled, setBookingCancelled] = useState(true);
|
|
||||||
const [editWebhookEnabled, setEditWebhookEnabled] = useState(false);
|
|
||||||
const [webhooks, setWebhooks] = useState([]);
|
|
||||||
const [webhookToEdit, setWebhookToEdit] = useState<Webhook | null>();
|
|
||||||
const [webhookEventTrigger, setWebhookEventTriggers] = useState([
|
|
||||||
"BOOKING_CREATED",
|
|
||||||
"BOOKING_RESCHEDULED",
|
|
||||||
"BOOKING_CANCELLED",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const subUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const arr = [];
|
|
||||||
bookingCreated && arr.push("BOOKING_CREATED");
|
|
||||||
bookingRescheduled && arr.push("BOOKING_RESCHEDULED");
|
|
||||||
bookingCancelled && arr.push("BOOKING_CANCELLED");
|
|
||||||
setWebhookEventTriggers(arr);
|
|
||||||
}, [bookingCreated, bookingRescheduled, bookingCancelled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getWebhooks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_APP_URL}/${props.user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_APP_URL}/${props.user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
||||||
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
|
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
|
||||||
const handleErrors = async (resp: Response) => {
|
|
||||||
if (!resp.ok) {
|
|
||||||
const err = await resp.json();
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWebhooks = () => {
|
|
||||||
fetch("/api/webhook")
|
|
||||||
.then(handleErrors)
|
|
||||||
.then((data) => {
|
|
||||||
setWebhooks(
|
|
||||||
data.webhooks.map((webhook: Webhook) => {
|
|
||||||
return {
|
|
||||||
...webhook,
|
|
||||||
eventTriggers: webhook.eventTriggers.map((eventTrigger: string) => eventTrigger.toLowerCase()),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log(data.webhooks);
|
|
||||||
})
|
|
||||||
.catch(console.log);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createWebhook = () => {
|
|
||||||
setLoading(true);
|
|
||||||
fetch("/api/webhook", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
subscriberUrl: subUrlRef.current.value,
|
|
||||||
eventTriggers: webhookEventTrigger,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(getWebhooks)
|
|
||||||
.catch(console.log);
|
|
||||||
};
|
|
||||||
|
|
||||||
const editWebhook = (webhook: Webhook) => {
|
|
||||||
setEditWebhookEnabled(true);
|
|
||||||
setWebhookToEdit(webhook);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCloseEdit = () => {
|
|
||||||
getWebhooks();
|
|
||||||
setEditWebhookEnabled(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell heading="Embed" subtitle="Integrate with your website using our embed options.">
|
||||||
heading="Embed & Webhooks"
|
|
||||||
subtitle="Integrate with your website using our embed options, or get real-time booking information using custom webhooks.">
|
|
||||||
<SettingsShell>
|
<SettingsShell>
|
||||||
{!editWebhookEnabled && (
|
<div className="py-6 lg:pb-8 lg:col-span-9">
|
||||||
<div className="py-6 lg:pb-8 lg:col-span-9">
|
<div className="mb-6">
|
||||||
<div className="mb-6">
|
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">iframe Embed</h2>
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-900 font-cal">iframe Embed</h2>
|
<p className="mt-1 text-sm text-gray-500">The easiest way to embed Cal.com on your website.</p>
|
||||||
<p className="mt-1 text-sm text-gray-500">The easiest way to embed Cal.com on your website.</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 space-x-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="iframe" className="block text-sm font-medium text-gray-700">
|
|
||||||
Standard iframe
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<textarea
|
|
||||||
id="iframe"
|
|
||||||
className="block w-full h-32 border-gray-300 rounded-sm shadow-sm focus:ring-black focus:border-black sm:text-sm"
|
|
||||||
placeholder="Loading..."
|
|
||||||
defaultValue={iframeTemplate}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700">
|
|
||||||
Responsive full screen iframe
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<textarea
|
|
||||||
id="fullscreen"
|
|
||||||
className="block w-full h-32 border-gray-300 rounded-sm shadow-sm focus:ring-black focus:border-black sm:text-sm"
|
|
||||||
placeholder="Loading..."
|
|
||||||
defaultValue={htmlTemplate}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className="mt-8" />
|
|
||||||
<div className="flex justify-between my-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-900 font-cal">Webhooks</h2>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Receive Cal meeting data at a specified URL, in real-time, when an event is scheduled or
|
|
||||||
cancelled.{" "}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger className="px-4 py-2 my-6 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
|
|
||||||
<PlusIcon className="inline w-5 h-5 mr-1" />
|
|
||||||
New Webhook
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader
|
|
||||||
title="Create a new webhook"
|
|
||||||
subtitle="Create a new webhook to your account"
|
|
||||||
/>
|
|
||||||
<div className="my-4">
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="subUrl" className="block text-sm font-medium text-gray-700">
|
|
||||||
Subscriber Url
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={subUrlRef}
|
|
||||||
type="text"
|
|
||||||
name="subUrl"
|
|
||||||
id="subUrl"
|
|
||||||
placeholder="https://example.com/sub"
|
|
||||||
required
|
|
||||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
<legend className="block pt-4 mb-2 text-sm font-medium text-gray-700">
|
|
||||||
{" "}
|
|
||||||
Event Triggers{" "}
|
|
||||||
</legend>
|
|
||||||
<div className="p-2 border border-gray-300 rounded-sm">
|
|
||||||
<div className="flex pb-4">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="font-medium text-gray-800">Booking Created</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={true}
|
|
||||||
id="booking-created"
|
|
||||||
value={bookingCreated}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingCreated(!bookingCreated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex py-1">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="font-medium text-gray-800">Booking Rescheduled</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={true}
|
|
||||||
id="booking-rescheduled"
|
|
||||||
value={bookingRescheduled}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingRescheduled(!bookingRescheduled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex pt-4">
|
|
||||||
<div className="w-10/12">
|
|
||||||
<h2 className="font-medium text-gray-800">Booking Cancelled</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center w-2/12 text-right">
|
|
||||||
<Switch
|
|
||||||
defaultChecked={true}
|
|
||||||
id="booking-cancelled"
|
|
||||||
value={bookingCancelled}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
setBookingCancelled(!bookingCancelled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
loading={isLoading}
|
|
||||||
onClick={createWebhook}
|
|
||||||
color="primary"
|
|
||||||
className="ml-2">
|
|
||||||
Create Webhook
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
|
||||||
<div className="py-6 lg:pb-8">
|
|
||||||
<div className="flex flex-col justify-between md:flex-row">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{!!webhooks.length && (
|
|
||||||
<WebhookList
|
|
||||||
webhooks={webhooks}
|
|
||||||
onChange={getWebhooks}
|
|
||||||
onEditWebhook={editWebhook}></WebhookList>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="mt-8" />
|
|
||||||
<div className="my-6">
|
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-900 font-cal">Cal.com API</h2>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Leverage our API for full control and customizability.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<a href="https://developer.cal.com/api" className="btn btn-primary">
|
|
||||||
Browse our API documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="grid grid-cols-2 space-x-4">
|
||||||
{!!editWebhookEnabled && <EditWebhook webhook={webhookToEdit} onCloseEdit={onCloseEdit} />}
|
<div>
|
||||||
|
<label htmlFor="iframe" className="block text-sm font-medium text-gray-700">
|
||||||
|
Standard iframe
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="iframe"
|
||||||
|
className="h-32 shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||||
|
placeholder="Loading..."
|
||||||
|
defaultValue={iframeTemplate}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700">
|
||||||
|
Responsive full screen iframe
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="fullscreen"
|
||||||
|
className="h-32 shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||||
|
placeholder="Loading..."
|
||||||
|
defaultValue={htmlTemplate}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="my-6">
|
||||||
|
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">Cal.com API</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Leverage our API for full control and customizability.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a href="https://developer.cal.com/api" className="btn btn-primary">
|
||||||
|
Browse our API documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
|
@ -295,7 +80,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: session?.user?.email,
|
email: session.user.email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
-- CreateEnum
|
|
||||||
CREATE TYPE "WebhookTriggerEvents" AS ENUM ('BOOKING_CREATED', 'BOOKING_RESCHEDULED', 'BOOKING_CANCELLED');
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Webhook" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"userId" INTEGER NOT NULL,
|
|
||||||
"subscriberUrl" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
"eventTriggers" "WebhookTriggerEvents"[],
|
|
||||||
|
|
||||||
PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Webhook.id_unique" ON "Webhook"("id");
|
|
|
@ -275,18 +275,3 @@ model Payment {
|
||||||
data Json
|
data Json
|
||||||
externalId String @unique
|
externalId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WebhookTriggerEvents {
|
|
||||||
BOOKING_CREATED
|
|
||||||
BOOKING_RESCHEDULED
|
|
||||||
BOOKING_CANCELLED
|
|
||||||
}
|
|
||||||
|
|
||||||
model Webhook {
|
|
||||||
id String @unique @id
|
|
||||||
userId Int
|
|
||||||
subscriberUrl String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
active Boolean @default(true)
|
|
||||||
eventTriggers WebhookTriggerEvents[]
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue