diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index db4e31a3c6..8f95076572 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -8,8 +8,13 @@ import { eventTypeMetaDataSchemaWithTypedApps, eventTypeAppMetadataOptionalSchema, } from "@calcom/app-store/zod-utils"; -import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager"; -import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import { + sendAttendeeRequestEmailAndSMS, + sendOrganizerRequestEmail, +} from "@calcom/emails/email-manager"; +import EventManager, { + placeholderCreatedEvent, +} from "@calcom/features/bookings/lib/EventManager"; import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation"; import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; @@ -36,7 +41,10 @@ export const config = { }, }; -export async function handleStripePaymentSuccess(event: Stripe.Event, traceContext: TraceContext) { +export async function handleStripePaymentSuccess( + event: Stripe.Event, + traceContext: TraceContext +) { const paymentIntent = event.data.object as Stripe.PaymentIntent; const payment = await prisma.payment.findFirst({ where: { @@ -49,10 +57,15 @@ export async function handleStripePaymentSuccess(event: Stripe.Event, traceConte }); if (!payment?.bookingId) { - log.error("Stripe: Payment Not Found", safeStringify(paymentIntent), safeStringify(payment)); + log.error( + "Stripe: Payment Not Found", + safeStringify(paymentIntent), + safeStringify(payment) + ); throw new HttpCode({ statusCode: 204, message: "Payment not found" }); } - if (!payment?.bookingId) throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + if (!payment?.bookingId) + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); await handlePaymentSuccess({ paymentId: payment.id, @@ -62,7 +75,10 @@ export async function handleStripePaymentSuccess(event: Stripe.Event, traceConte }); } -const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContext) => { +const handleSetupSuccess = async ( + event: Stripe.Event, + traceContext: TraceContext +) => { const setupIntent = event.data.object as Stripe.SetupIntent; const payment = await prisma.payment.findFirst({ where: { @@ -70,7 +86,8 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex }, }); - if (!payment?.data || !payment?.id) throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + if (!payment?.data || !payment?.id) + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); const { booking, user, evt, eventType } = await getBooking(payment.bookingId); @@ -91,7 +108,9 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex }, }); - const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType?.metadata); + const metadata = eventTypeMetaDataSchemaWithTypedApps.parse( + eventType?.metadata + ); const allCredentials = await getAllCredentialsIncludeServiceAccountKey(user, { ...booking.eventType, metadata, @@ -101,11 +120,15 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex const platformOAuthClient = user.isPlatformManaged ? await platformOAuthClientRepository.getByUserId(user.id) : null; - const areCalendarEventsEnabled = platformOAuthClient?.areCalendarEventsEnabled ?? true; + const areCalendarEventsEnabled = + platformOAuthClient?.areCalendarEventsEnabled ?? true; const areEmailsEnabled = platformOAuthClient?.areEmailsEnabled ?? true; if (!requiresConfirmation) { - const eventManager = new EventManager({ ...user, credentials: allCredentials }, metadata?.apps); + const eventManager = new EventManager( + { ...user, credentials: allCredentials }, + metadata?.apps + ); const scheduleResult = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent; @@ -142,18 +165,27 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex bookingId: booking.id, booking, paid: true, - platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined, + platformClientParams: platformOAuthClient + ? getPlatformParams(platformOAuthClient) + : undefined, traceContext: updatedTraceContext, actionSource: "WEBHOOK", actor, }); } else if (areEmailsEnabled) { await sendOrganizerRequestEmail({ ...evt }, eventType.metadata); - await sendAttendeeRequestEmailAndSMS({ ...evt }, evt.attendees[0], eventType.metadata); + await sendAttendeeRequestEmailAndSMS( + { ...evt }, + evt.attendees[0], + eventType.metadata + ); } }; -type WebhookHandler = (event: Stripe.Event, traceContext: TraceContext) => Promise; +type WebhookHandler = ( + event: Stripe.Event, + traceContext: TraceContext +) => Promise; const webhookHandlers: Record = { "payment_intent.succeeded": handleStripePaymentSuccess, @@ -165,23 +197,54 @@ const webhookHandlers: Record = { * We need to create a PaymentManager in `@calcom/lib` * to prevent circular dependencies on App Store migration */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { try { if (req.method !== "POST") { throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); } const sig = req.headers["stripe-signature"]; if (!sig) { - throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" }); + throw new HttpCode({ + statusCode: 400, + message: "Missing stripe-signature", + }); } if (!process.env.STRIPE_WEBHOOK_SECRET) { - throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" }); + throw new HttpCode({ + statusCode: 500, + message: "Missing process.env.STRIPE_WEBHOOK_SECRET", + }); } const requestBuffer = await buffer(req); const payload = requestBuffer.toString(); - const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); + let stripeEventId: string | undefined; + try { + const parsedPayload = JSON.parse(payload); + stripeEventId = parsedPayload?.id; + log.debug("Stripe webhook eventId", { stripeEventId }); + } catch { + // Ignore parse errors, this is just for debugging + } + + let event: Stripe.Event; + try { + event = stripe.webhooks.constructEvent( + payload, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + log.error("Stripe webhook signature verification failed", { + stripeEventId, + error: err instanceof Error ? err.message : "Unknown error", + }); + throw err; + } const traceContext = distributedTracing.createTrace(`stripe_webhook`, { meta: { @@ -193,7 +256,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // bypassing this validation for e2e tests // in order to successfully confirm the payment if (!event.account && !process.env.NEXT_PUBLIC_IS_E2E) { - throw new HttpCode({ statusCode: 202, message: "Incoming connected account" }); + throw new HttpCode({ + statusCode: 202, + message: "Incoming connected account", + }); } const handler = webhookHandlers[event.type];