From 8908a66c232747744d57726e0b7b2e114170cb5b Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:23:42 +0530 Subject: [PATCH] fix: allow booking rejection via GET for email clients that do not support forms (#28029) * fix: allow booking rejection via GET for email clients that do not support forms Co-Authored-By: anik@cal.com * Refactor test cases for GET handler in route.test.ts --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../__tests__/route.test.ts | 28 +++++++++++++++++-- .../web/app/api/verify-booking-token/route.ts | 21 ++++---------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/web/app/api/verify-booking-token/__tests__/route.test.ts b/apps/web/app/api/verify-booking-token/__tests__/route.test.ts index 240c9ffb37..f142e2f658 100644 --- a/apps/web/app/api/verify-booking-token/__tests__/route.test.ts +++ b/apps/web/app/api/verify-booking-token/__tests__/route.test.ts @@ -179,7 +179,22 @@ describe("verify-booking-token route", () => { }); describe("GET handler", () => { - it("should redirect to booking page with error when action is reject (requires POST)", async () => { + it("should process rejection via GET (for email clients that don't support POST)", async () => { + createMockBooking({ + id: 1, + uid: "abc123", + oneTimePassword: "test-token", + recurringEventId: null, + }); + createMockUser({ + id: 42, + uuid: "user-uuid", + email: "test@example.com", + username: "testuser", + role: "USER", + destinationCalendar: null, + }); + const baseUrl = "https://app.example.com/api/verify-booking-token?action=reject&token=test-token&bookingUid=abc123&userId=42"; const req = createMockRequest(baseUrl, "GET"); @@ -191,7 +206,16 @@ describe("verify-booking-token route", () => { expect(redirectUrl.origin).toBe("https://app.example.com"); expect(redirectUrl.pathname).toBe("/booking/abc123"); - expect(redirectUrl.searchParams.get("error")).toBe("Rejection requires POST method"); + expect(redirectUrl.searchParams.get("error")).toBeNull(); + expect(mockConfirmHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ + bookingId: 1, + confirmed: false, + actionSource: "MAGIC_LINK", + }), + }) + ); }); it("should redirect with error when query params are invalid (missing action)", async () => { diff --git a/apps/web/app/api/verify-booking-token/route.ts b/apps/web/app/api/verify-booking-token/route.ts index 8a76ada0fb..3ba4f28dfd 100644 --- a/apps/web/app/api/verify-booking-token/route.ts +++ b/apps/web/app/api/verify-booking-token/route.ts @@ -1,15 +1,14 @@ +import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import prisma from "@calcom/prisma"; +import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler"; +import { TRPCError } from "@trpc/server"; import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; -import { distributedTracing } from "@calcom/lib/tracing/factory"; -import prisma from "@calcom/prisma"; -import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler"; -import { TRPCError } from "@trpc/server"; -import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; - enum DirectAction { ACCEPT = "accept", REJECT = "reject", @@ -28,16 +27,6 @@ async function getHandler(request: NextRequest) { try { const { action, token, bookingUid, userId } = querySchema.parse(queryParams); - if (action === DirectAction.REJECT) { - // Rejections should use POST method - return NextResponse.redirect( - new URL( - `/booking/${bookingUid}?error=${encodeURIComponent("Rejection requires POST method")}`, - request.url - ) - ); - } - return await handleBookingAction(action, token, bookingUid, userId, request, undefined); } catch { const bookingUid = queryParams.bookingUid || "";