diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index 0a38ba25a..743c398f6 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -1,21 +1,41 @@ -import { notFound } from "next/navigation" +import { cookies } from "next/headers" +import { notFound, redirect } from "next/navigation" import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking" import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" +import { decrypt } from "@/utils/encryption" import type { LangParams, PageArgs } from "@/types/params" export default async function BookingConfirmationPage({ + params, searchParams, -}: PageArgs) { +}: PageArgs< + LangParams, + { + RefId?: string + } +>) { const refId = searchParams.RefId if (!refId) { notFound() } + const sig = cookies().get("bcsig")?.value + + if (!sig) { + redirect(`/${params.lang}`) + } + + const expire = Number(decrypt(sig)) + const now = Math.floor(Date.now() / 1000) + if (typeof expire === "number" && !isNaN(expire) && now > expire) { + redirect(`/${params.lang}`) + } + void getBookingConfirmation(refId) const membershipFailedError = diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx index 2fa2619c8..12e48a763 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx @@ -61,7 +61,9 @@ export default async function GuaranteePaymentCallbackPage({ refId, }) - const error = bookingStatus.errors.find((e) => e.errorCode) + const { booking } = bookingStatus + + const error = booking.errors.find((e) => e.errorCode) errorMessage = error?.description ?? `No error message found for booking ${confirmationNumber}, status: ${status}` diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx index d2be84449..459bed33a 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -15,6 +15,7 @@ import { getServiceToken } from "@/server/tokenManager" import { auth } from "@/auth" import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback" import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback" +import { encrypt } from "@/utils/encryption" import type { LangParams, PageArgs } from "@/types/params" @@ -62,6 +63,8 @@ export default async function PaymentCallbackPage({ const { refId } = booking if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) { + const expire = Math.floor(Date.now() / 1000) + 60 + const sig = encrypt(expire.toString()) const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}` console.log( `[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}` @@ -70,6 +73,7 @@ export default async function PaymentCallbackPage({ return ( ) @@ -86,8 +90,10 @@ export default async function PaymentCallbackPage({ refId, }) + const { booking } = bookingStatus + // TODO: how to handle errors for multiple rooms? - const error = bookingStatus.errors.find((e) => e.errorCode) + const error = booking.errors.find((e) => e.errorCode) errorMessage = error?.description ?? diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback.tsx index e7407e860..f563f1856 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback.tsx @@ -19,15 +19,22 @@ const validBookingStatuses = [ interface HandleStatusPollingProps { refId: string + sig: string successRedirectUrl: string } export default function HandleSuccessCallback({ refId, + sig, successRedirectUrl, }: HandleStatusPollingProps) { const router = useRouter() + useEffect(() => { + // Cookie is used by Booking Confirmation page to validate that the user came from payment callback + document.cookie = `bcsig=${sig}; Path=/; Max-Age=60; Secure; SameSite=Strict` + }, [sig]) + const { data: bookingStatus, error, @@ -41,13 +48,13 @@ export default function HandleSuccessCallback({ }) useEffect(() => { - if (!bookingStatus?.reservationStatus) { + if (!bookingStatus?.booking.reservationStatus) { return } if ( validBookingStatuses.includes( - bookingStatus.reservationStatus as BookingStatusEnum + bookingStatus.booking.reservationStatus as BookingStatusEnum ) ) { const glaSessionData = readGlaFromSessionStorage() @@ -63,7 +70,7 @@ export default function HandleSuccessCallback({ clearGlaSessionStorage() } // a successful booking can still have membership errors - const membershipFailedError = bookingStatus.errors.find( + const membershipFailedError = bookingStatus.booking.errors.find( (e) => e.errorCode === MEMBERSHIP_FAILED_ERROR ) const errorParam = membershipFailedError diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 031f1e51b..8a0f30354 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -155,9 +155,12 @@ export default function PaymentClient({ return } - const mainRoom = result.rooms[0] + const { booking } = result + const mainRoom = booking.rooms[0] - if (result.reservationStatus == BookingStatusEnum.BookingCompleted) { + if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) { + // Cookie is used by Booking Confirmation page to validate that the user came from payment callback + document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict` const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}` router.push(confirmationUrl) return @@ -165,9 +168,9 @@ export default function PaymentClient({ setRefId(mainRoom.refId) - const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata) + const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata) if (hasPriceChange) { - const priceChangeData = result.rooms.map( + const priceChangeData = booking.rooms.map( (room) => room.priceChangedMetadata || null ) setPriceChangeData(priceChangeData) @@ -258,13 +261,15 @@ export default function PaymentClient({ ) useEffect(() => { - if (bookingStatus?.data?.paymentUrl) { - router.push(bookingStatus.data.paymentUrl) + if (bookingStatus?.data?.booking.paymentUrl) { + router.push(bookingStatus.data.booking.paymentUrl) } else if ( - bookingStatus?.data?.reservationStatus === + bookingStatus?.data?.booking.reservationStatus === BookingStatusEnum.BookingCompleted ) { - const mainRoom = bookingStatus.data.rooms[0] + const mainRoom = bookingStatus.data.booking.rooms[0] + // Cookie is used by Booking Confirmation page to validate that the user came from payment callback + document.cookie = `bcsig=${bookingStatus.data.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict` const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}` router.push(confirmationUrl) } else if (bookingStatus.isTimeout) { diff --git a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts index ba0a57fe7..37f0aa1e6 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts @@ -75,8 +75,8 @@ export function useGuaranteeBooking( }) useEffect(() => { - if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) { - router.push(bookingStatus.data.paymentUrl) + if (bookingStatus?.data?.booking.paymentUrl && isPollingForBookingStatus) { + router.push(bookingStatus.data.booking.paymentUrl) utils.booking.get.invalidate({ refId }) setIsPollingForBookingStatus(false) } else if (bookingStatus.isTimeout) { @@ -95,7 +95,7 @@ export function useGuaranteeBooking( const isLoading = guaranteeBooking.isPending || (isPollingForBookingStatus && - !bookingStatus.data?.paymentUrl && + !bookingStatus.data?.booking.paymentUrl && !bookingStatus.isTimeout) return { diff --git a/apps/scandic-web/hooks/booking/useHandleBookingStatus.ts b/apps/scandic-web/hooks/booking/useHandleBookingStatus.ts index 4b0d7b4a6..82de8119c 100644 --- a/apps/scandic-web/hooks/booking/useHandleBookingStatus.ts +++ b/apps/scandic-web/hooks/booking/useHandleBookingStatus.ts @@ -34,7 +34,7 @@ export function useHandleBookingStatus({ if ( expectedStatuses.includes( - query.state.data?.reservationStatus as BookingStatusEnum + query.state.data?.booking.reservationStatus as BookingStatusEnum ) ) { return false diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index a9d1c6686..0c908ffe2 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -4,6 +4,8 @@ import { getMembershipNumber } from "@/server/routers/user/utils" import { createCounter } from "@/server/telemetry" import { router, safeProtectedServiceProcedure } from "@/server/trpc" +import { encrypt } from "@/utils/encryption" + import { addPackageInput, cancelBookingsInput, @@ -71,7 +73,12 @@ export const bookingMutationRouter = router({ metricsCreateBooking.success() - return verifiedData.data + const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry + + return { + booking: verifiedData.data, + sig: encrypt(expire.toString()), + } }), priceChange: safeProtectedServiceProcedure .concat(refIdPlugin.toConfirmationNumber) diff --git a/apps/scandic-web/server/routers/booking/query.ts b/apps/scandic-web/server/routers/booking/query.ts index b24fe05a2..0a84eb73a 100644 --- a/apps/scandic-web/server/routers/booking/query.ts +++ b/apps/scandic-web/server/routers/booking/query.ts @@ -244,7 +244,12 @@ export const bookingQueryRouter = router({ metricsGetBookingStatus.success() - return verifiedData.data + const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry + + return { + booking: verifiedData.data, + sig: encrypt(expire.toString()), + } }), createRefId: serviceProcedure .input(createRefIdInput)