From 1a60afad7a37ed898a4bc7cd6106cd94bf9c020f Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Sat, 21 Sep 2024 07:55:27 +0530 Subject: [PATCH] chore: booking verification token and booking rejection logic from email (#16324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López --- apps/web/pages/api/verify-booking-token.ts | 97 +++++++++++++++++++ .../components/BookingConfirmationForm.tsx | 90 +++++++++++++++++ .../emails/src/components/CallToAction.tsx | 21 +++- .../src/components/CallToActionTable.tsx | 6 +- packages/emails/src/components/index.ts | 1 + .../src/templates/OrganizerRequestEmailV2.tsx | 43 ++++++++ packages/emails/src/templates/index.ts | 1 + .../templates/organizer-request-email.ts | 19 +++- .../features/bookings/lib/handleNewBooking.ts | 2 + .../lib/handleNewBooking/createBooking.ts | 1 + packages/features/flags/config.ts | 1 + .../flags/features.repository.interface.ts | 5 + .../features/flags/features.repository.ts | 59 +++++++++++ packages/lib/test/builder.ts | 1 + .../migration.sql | 11 +++ .../migration.sql | 9 ++ packages/prisma/schema.prisma | 1 + packages/trpc/server/trpc.ts | 1 + packages/types/Calendar.d.ts | 1 + 19 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 apps/web/pages/api/verify-booking-token.ts create mode 100644 packages/emails/src/components/BookingConfirmationForm.tsx create mode 100644 packages/emails/src/templates/OrganizerRequestEmailV2.tsx create mode 100644 packages/features/flags/features.repository.interface.ts create mode 100644 packages/features/flags/features.repository.ts create mode 100644 packages/prisma/migrations/20240823033245_booking_one_time_password/migration.sql create mode 100644 packages/prisma/migrations/20240920000000_adds_feature_flag_organizer_request_email_v2/migration.sql diff --git a/apps/web/pages/api/verify-booking-token.ts b/apps/web/pages/api/verify-booking-token.ts new file mode 100644 index 0000000000..c220823235 --- /dev/null +++ b/apps/web/pages/api/verify-booking-token.ts @@ -0,0 +1,97 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; +import { UserPermissionRole } from "@calcom/prisma/enums"; +import { TRPCError } from "@calcom/trpc/server"; +import { createContext } from "@calcom/trpc/server/createContext"; +import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router"; +import { createCallerFactory } from "@calcom/trpc/server/trpc"; +import type { UserProfile } from "@calcom/types/UserProfile"; + +enum DirectAction { + ACCEPT = "accept", + REJECT = "reject", +} + +const querySchema = z.object({ + action: z.nativeEnum(DirectAction), + token: z.string(), + bookingUid: z.string(), + userId: z.string(), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { action, token, bookingUid, userId } = querySchema.parse(req.query); + // Rejections runs on a POST request, confirming on a GET request. + const { reason } = z.object({ reason: z.string().optional() }).parse(req.body || {}); + + const booking = await prisma.booking.findUnique({ + where: { oneTimePassword: token }, + }); + + if (!booking) { + res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`); + return; + } + + const user = await prisma.user.findUniqueOrThrow({ + where: { id: Number(userId) }, + }); + + /** We shape the session as required by tRPC router */ + async function sessionGetter() { + return { + user: { + id: Number(userId), + username: "" /* Not used in this context */, + role: UserPermissionRole.USER, + /* Not used in this context */ + profile: { + id: null, + organizationId: null, + organization: null, + username: "", + upId: "", + } satisfies UserProfile, + }, + upId: "", + hasValidLicense: true, + expires: "" /* Not used in this context */, + }; + } + + try { + /** @see https://trpc.io/docs/server-side-calls */ + const createCaller = createCallerFactory(bookingsRouter); + const ctx = await createContext({ req, res }, sessionGetter); + const caller = createCaller({ + ...ctx, + req, + res, + user: { ...user, locale: user?.locale ?? "en" }, + }); + await caller.confirm({ + bookingId: booking.id, + recurringEventId: booking.recurringEventId || undefined, + confirmed: action === DirectAction.ACCEPT, + /** Ignored reason input unless we're rejecting */ + reason: action === DirectAction.REJECT ? reason : undefined, + }); + } catch (e) { + let message = "Error confirming booking"; + if (e instanceof TRPCError) message = (e as TRPCError).message; + res.redirect(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`); + return; + } + + await prisma.booking.update({ + where: { id: booking.id }, + data: { oneTimePassword: null }, + }); + + res.redirect(`/booking/${booking.uid}`); +} + +export default defaultResponder(handler); diff --git a/packages/emails/src/components/BookingConfirmationForm.tsx b/packages/emails/src/components/BookingConfirmationForm.tsx new file mode 100644 index 0000000000..13f3a9f016 --- /dev/null +++ b/packages/emails/src/components/BookingConfirmationForm.tsx @@ -0,0 +1,90 @@ +export const BookingConfirmationForm = (props: { action: string; children: React.ReactNode }) => { + return ( +
+ {props.children} +

+ +