From b0df70e5523ab203707158ce6e77473b434ccec6 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 5 Mar 2025 13:46:09 +0100 Subject: [PATCH] feat(SW-1710): add access checks to my stay page for viewing booking --- .../hotelreservation/my-stay/[refId]/page.tsx | 2 +- .../FindMyBooking/AdditionalInfoForm.tsx | 89 +++++++++ .../FindMyBooking/findMyBooking.module.css | 22 ++- .../HotelReservation/FindMyBooking/index.tsx | 18 +- .../HotelReservation/FindMyBooking/schema.ts | 27 ++- .../MyStay/accessBooking.test.ts | 114 +++++++++++ .../HotelReservation/MyStay/accessBooking.ts | 64 ++++++ .../HotelReservation/MyStay/index.tsx | 184 +++++++++++------- .../HotelReservation/MyStay/myStay.module.css | 15 +- apps/scandic-web/i18n/dictionaries/da.json | 3 + apps/scandic-web/i18n/dictionaries/de.json | 3 + apps/scandic-web/i18n/dictionaries/en.json | 3 + apps/scandic-web/i18n/dictionaries/fi.json | 3 + apps/scandic-web/i18n/dictionaries/no.json | 3 + apps/scandic-web/i18n/dictionaries/sv.json | 3 + .../server/routers/booking/input.ts | 2 +- .../server/routers/booking/output.ts | 2 + .../server/routers/booking/query.ts | 6 +- apps/scandic-web/server/routers/user/utils.ts | 4 +- .../server/routers/utils/encryptValue.ts | 27 --- .../server/routers/utils/encryption.ts | 54 +++++ 21 files changed, 515 insertions(+), 133 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts delete mode 100644 apps/scandic-web/server/routers/utils/encryptValue.ts create mode 100644 apps/scandic-web/server/routers/utils/encryption.ts diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/[refId]/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/[refId]/page.tsx index 509aae1de..3ce7c34f5 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/[refId]/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/[refId]/page.tsx @@ -10,7 +10,7 @@ export default async function MyStayPage({ }: PageArgs) { return ( }> - + ) } diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx b/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx new file mode 100644 index 000000000..28239bc5e --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx @@ -0,0 +1,89 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import Button from "@/components/TempDesignSystem/Button" +import Input from "@/components/TempDesignSystem/Form/Input" +import Body from "@/components/TempDesignSystem/Text/Body" +import Title from "@/components/TempDesignSystem/Text/Title" + +import { + type AdditionalInfoFormSchema, + additionalInfoFormSchema, +} from "./schema" + +import styles from "./findMyBooking.module.css" + +export default function AdditionalInfoForm({ + confirmationNumber, + lastName, +}: { + confirmationNumber: string + lastName: string +}) { + const router = useRouter() + const intl = useIntl() + const form = useForm({ + resolver: zodResolver(additionalInfoFormSchema), + mode: "all", + criteriaMode: "all", + reValidateMode: "onChange", + }) + + function onSubmit() { + const values = form.getValues() + const value = new URLSearchParams({ + ...values, + confirmationNumber, + lastName, + }).toString() + document.cookie = `bv=${value}; Path=/; Max-Age=30; Secure; SameSite=Strict` + router.refresh() + } + + return ( + +
+
+ + {intl.formatMessage({ + id: "One last step", + })} + + + {intl.formatMessage({ + id: "We need some more details to confirm your identity.", + })} + +
+
+ + +
+
+ +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css b/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css index b4051857a..d0f4af76f 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css @@ -1,9 +1,12 @@ .form { box-shadow: var(--popup-box-shadow); + padding: var(--Spacing-x3) 0; + display: grid; + gap: var(--Spacing-x3); } -.form > div { - padding: var(--Spacing-x3); +.form > div:not(.buttons) { + padding: 0 var(--Spacing-x3); } .inputs { @@ -12,23 +15,23 @@ } @media screen and (min-width: 768px) { - .inputs { + .grid { grid-template-areas: "a a" "b c" "d d"; } - .inputs > div:nth-child(1) { + .grid > div:nth-child(1) { grid-area: a; } - .inputs > div:nth-child(2) { + .grid > div:nth-child(2) { grid-area: b; } - .inputs > div:nth-child(3) { + .grid > div:nth-child(3) { grid-area: c; } - .inputs > div:nth-child(4) { + .grid > div:nth-child(4) { grid-area: d; } } @@ -38,9 +41,14 @@ justify-content: space-between; align-items: center; border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x3) var(--Spacing-x3) 0; gap: var(--Spacing-x2); } +.buttons > button:only-child { + margin-left: auto; +} + .buttons > button { min-width: 140px; } diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx b/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx index 6324ba31e..7d45914f5 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx @@ -21,7 +21,7 @@ import { type FindMyBookingFormSchema, findMyBookingFormSchema } from "./schema" import styles from "./findMyBooking.module.css" -export default function Form() { +export default function FindMyBooking() { const router = useRouter() const intl = useIntl() const lang = useLang() @@ -53,7 +53,7 @@ export default function Form() { async function onSubmit(data: FindMyBookingFormSchema) { update.mutate({ - bookingNumber: data.bookingNumber, + confirmationNumber: data.confirmationNumber, lastName: data.lastName, }) } @@ -62,7 +62,7 @@ export default function Form() {
- + <Title level="h2" as="h3"> {intl.formatMessage({ id: "Find your stay" })} @@ -71,27 +71,27 @@ export default function Form() { })}
-
+
{} +type AdditionalInfoFormSchema = z.output + +type FindMyBookingFormSchema = z.output diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts new file mode 100644 index 000000000..a1ba066d5 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "@jest/globals" + +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_NOT_FOUND, + ERROR_UNAUTHORIZED, +} from "./accessBooking" + +import type { SafeUser } from "@/types/user" +import type { Guest } from "@/server/routers/booking/output" + +describe("Access booking", () => { + describe("for logged in booking", () => { + it("should enable access if all is provided", () => { + expect(accessBooking(loggedIn, "Booking", user)).toBe(ACCESS_GRANTED) + }) + it("should prompt to login", () => { + expect(accessBooking(loggedIn, "Booking", null)).toBe(ERROR_UNAUTHORIZED) + }) + it("should deny access", () => { + expect(accessBooking(loggedIn, "NotBooking", user)).toBe(ERROR_NOT_FOUND) + }) + }) + describe("for anonymous booking", () => { + it("should enable access if all is provided", () => { + const cookieString = new URLSearchParams({ + confirmationNumber: "123456789", + firstName: "Anonymous", + lastName: "Booking", + email: "logged-out@scandichotels.com", + }).toString() + expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( + ACCESS_GRANTED + ) + }) + it("should prompt logout if user is logged in", () => { + const cookieString = new URLSearchParams({ + confirmationNumber: "123456789", + firstName: "Anonymous", + lastName: "Booking", + email: "logged-out@scandichotels.com", + }).toString() + expect(accessBooking(loggedOut, "Booking", user, cookieString)).toBe( + ACCESS_GRANTED + ) + }) + it("should prompt for more if first name is missing", () => { + const cookieString = new URLSearchParams({ + confirmationNumber: "123456789", + lastName: "Booking", + email: "logged-out@scandichotels.com", + }).toString() + expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( + ERROR_BAD_REQUEST + ) + }) + it("should prompt for more if email is missing", () => { + const cookieString = new URLSearchParams({ + confirmationNumber: "123456789", + firstName: "Anonymous", + lastName: "Booking", + }).toString() + expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( + ERROR_BAD_REQUEST + ) + }) + it("should prompt for more if cookie is invalid", () => { + const cookieString = new URLSearchParams({}).toString() + expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( + ERROR_BAD_REQUEST + ) + }) + it("should deny access", () => { + expect(accessBooking(loggedOut, "NotBooking", null)).toBe(ERROR_NOT_FOUND) + }) + }) +}) + +const user: SafeUser = { + address: { + city: undefined, + country: "Sweden", + countryCode: "SE", + streetAddress: undefined, + zipCode: undefined, + }, + dateOfBirth: "", + email: "", + firstName: "", + language: undefined, + lastName: "", + membership: undefined, + memberships: [], + name: "", + phoneNumber: undefined, + profileId: "", +} + +const loggedOut: Guest = { + email: "logged-out@scandichotels.com", + firstName: "Anonymous", + lastName: "Booking", + membershipNumber: null, + phoneNumber: "+46701234567", +} + +const loggedIn: Guest = { + email: "logged-in@scandichotels.com", + firstName: "Authenticated", + lastName: "Booking", + membershipNumber: "01234567890123", + phoneNumber: "+46701234567", +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts new file mode 100644 index 000000000..8512fb771 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts @@ -0,0 +1,64 @@ +import type { SafeUser } from "@/types/user" +import type { Guest } from "@/server/routers/booking/output" + +export { + ACCESS_GRANTED, + accessBooking as default, + ERROR_BAD_REQUEST, + ERROR_NOT_FOUND, + ERROR_UNAUTHORIZED, +} + +/** + * Whether a request can access a confirmed booking or not. + */ +function accessBooking( + guest: Guest, + lastName: string, + user: SafeUser | null, + cookie: string = "" +) { + if (guest.membershipNumber) { + if (user) { + if (lastName === guest.lastName) { + return ACCESS_GRANTED + } + } else { + return ERROR_UNAUTHORIZED + } + } + + if (guest.lastName === lastName) { + const params = new URLSearchParams(cookie) + if ( + params.get("firstName") === guest.firstName && + params.get("email") === guest.email + ) { + return ACCESS_GRANTED + } else { + return ERROR_BAD_REQUEST + } + } + + return ERROR_NOT_FOUND +} + +const ERROR_BAD_REQUEST = { + code: "BAD_REQUEST", + status: 400, +} as const + +const ERROR_UNAUTHORIZED = { + code: "UNAUTHORIZED", + status: 401, +} as const + +const ERROR_NOT_FOUND = { + code: "NOT_FOUND", + status: 404, +} as const + +const ACCESS_GRANTED = { + code: "ACCESS_GRANTED", + status: 200, +} as const diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx index 8bb095792..42a56568a 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx @@ -1,3 +1,4 @@ +import { cookies } from "next/headers" import { notFound } from "next/navigation" import { Suspense } from "react" @@ -9,12 +10,20 @@ import { getBookingConfirmation, getProfileSafely, } from "@/lib/trpc/memoizedRequests" +import { decrypt } from "@/server/routers/utils/encryption" import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import AdditionalInfoForm from "../FindMyBooking/AdditionalInfoForm" import LinkedReservationSkeleton from "./LinkedReservation/LinkedReservationSkeleton" +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_UNAUTHORIZED, +} from "./accessBooking" import { Ancillaries } from "./Ancillaries" import BookingSummary from "./BookingSummary" import { Header } from "./Header" @@ -25,83 +34,120 @@ import { Room } from "./Room" import styles from "./myStay.module.css" -export async function MyStay({ reservationId }: { reservationId: string }) { - const bookingConfirmation = await getBookingConfirmation(reservationId) +export async function MyStay({ refId }: { refId: string }) { + const value = decrypt(refId) + if (!value) { + return notFound() + } + const [confirmationNumber, lastName] = value.split(",") + const bookingConfirmation = await getBookingConfirmation(confirmationNumber) if (!bookingConfirmation) { return notFound() } const { booking, hotel, room } = bookingConfirmation - - const linkedBookingPromises = booking.linkedReservations - ? booking.linkedReservations.map((linkedBooking) => { - return getBookingConfirmation(linkedBooking.confirmationNumber) - }) - : [] - - const userResponse = await getProfileSafely() - const user = userResponse && !("error" in userResponse) ? userResponse : null + const user = await getProfileSafely() + const cookie = cookies() + const bv = cookie.get("bv")?.value const intl = await getIntl() - const lang = getLang() - const homeUrl = homeHrefs[env.NODE_ENV][lang] - const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") - const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") - const hotelId = hotel.operaId - const ancillaryInput = { fromDate, hotelId, toDate } - const ancillaryPackages = await getAncillaryPackages(ancillaryInput) - return ( -
-
-
+ const access = accessBooking(booking.guest, lastName, user, bv) + if (access.status === ACCESS_GRANTED.status) { + const linkedBookingPromises = booking.linkedReservations + ? booking.linkedReservations.map((linkedBooking) => { + return getBookingConfirmation(linkedBooking.confirmationNumber) + }) + : [] - {hotel.name} -
-
-
-
- -
- {booking.showAncillaries && ( - +
+
+ + {hotel.name} - )} -
- - {booking.linkedReservations.map((linkedRes, index) => ( - } - > - - - ))}
- - -
-
- ) +
+
+
+ +
+ {booking.showAncillaries && ( + + )} +
+ + {booking.linkedReservations.map((linkedRes, index) => ( + } + > + + + ))} +
+ + +
+ + ) + } + + if (access.status === ERROR_BAD_REQUEST.status) { + return ( +
+
+ +
+
+ ) + } + + if (access.status === ERROR_UNAUTHORIZED.status) { + return ( +
+
+ + {intl.formatMessage({ + id: "In order to view your booking, please log in.", + })} + +
+
+ ) + } + + return notFound() } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css index 6570f091e..b9357768c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css @@ -1,6 +1,5 @@ .main { background-color: var(--Base-Surface-Primary-light-Normal); - min-height: 100dvh; } .imageContainer { @@ -52,11 +51,11 @@ } } -@media (min-width: 768px) { - .content { - width: var(--max-width-content); - padding-bottom: 160px; - } +.form { + max-width: 640px; + margin-left: auto; + margin-right: auto; + padding: var(--Spacing-x5) 0; } .headerSkeleton { @@ -103,3 +102,7 @@ flex-direction: column; gap: var(--Spacing-x2); } + +.logIn { + padding: var(--Spacing-x5) var(--Spacing-x2); +} diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index 8ac929ca1..8d8cb3067 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -324,6 +324,7 @@ "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "Log ind for at se din reservation.", "Included": "Inkluderet", "Indoor pool": "Indendørs pool", "Indoor windows and excellent lighting": "Indoor windows and excellent lighting", @@ -462,6 +463,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din rejse", + "One last step": "Et sidste skridt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du kontakte supporten.", "Open": "Åben", "Open for application": "Åben for ansøgning", @@ -709,6 +711,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "Vi havde et problem med at behandle din booking. Prøv venligst igen. Ingen gebyrer er blevet opkrævet.", "We have a special gift waiting for you!": "Vi har en speciel gave, der venter på dig!", "We look forward to your visit!": "Vi ser frem til dit besøg!", + "We need some more details to confirm your identity.": "Vi har brug for nogle flere detaljer for at bekræfte din identitet.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "Vi beklager", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index 3a13840a0..19e7de919 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -325,6 +325,7 @@ "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "Um Ihre Buchung einzusehen, loggen Sie sich bitte ein.", "Included": "Iinklusive", "Indoor pool": "Innenpool", "Indoor windows and excellent lighting": "Indoor windows and excellent lighting", @@ -463,6 +464,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE", "On your journey": "Auf deiner Reise", + "One last step": "Ein letzter Schritt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Beim Anzeigen Ihrer Überraschung ist ein Fehler aufgetreten. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support.", "Open": "Offen", "Open for application": "Offen für Bewerbungen", @@ -707,6 +709,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "Wir hatten ein Problem beim Verarbeiten Ihrer Buchung. Bitte versuchen Sie es erneut. Es wurden keine Gebühren erhoben.", "We have a special gift waiting for you!": "Wir haben ein besonderes Geschenk für Sie!", "We look forward to your visit!": "Wir freuen uns auf Ihren Besuch!", + "We need some more details to confirm your identity.": "Zur Bestätigung Ihrer Identität benötigen wir noch einige weitere Angaben.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "Es tut uns leid", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index 4fdbae580..dbca2c876 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -330,6 +330,7 @@ "In crib": "In crib", "In extra bed": "In extra bed", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "In order to view your booking, please log in.", "Included": "Included", "Indoor pool": "Indoor pool", "Indoor windows and excellent lighting": "Indoor windows and excellent lighting", @@ -469,6 +470,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", + "One last step": "One last step", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.", "Open": "Open", "Open for application": "Open for application", @@ -715,6 +717,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "We had an issue processing your booking. Please try again. No charges have been made.", "We have a special gift waiting for you!": "We have a special gift waiting for you!", "We look forward to your visit!": "We look forward to your visit!", + "We need some more details to confirm your identity.": "We need some more details to confirm your identity.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "We're sorry", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index eb75d5537..64a475732 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -324,6 +324,7 @@ "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "Nähdäksesi varauksesi, ole hyvä ja kirjaudu sisään.", "Included": "Sisälly hintaan", "Indoor pool": "Sisäuima-allas", "Indoor windows and excellent lighting": "Indoor windows and excellent lighting", @@ -462,6 +463,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "MUISE KORT", "On your journey": "Matkallasi", + "One last step": "Viimeinen askel", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, ota yhteyttä tukeen.", "Open": "Avata", "Open for application": "Avoinna hakemuksille", @@ -707,6 +709,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "Meillä oli ongelma varauksen käsittelyssä. Yritä uudelleen. Ei maksuja on tehty.", "We have a special gift waiting for you!": "Meillä on erityinen lahja odottamassa sinua!", "We look forward to your visit!": "Odotamme innolla vierailuasi!", + "We need some more details to confirm your identity.": "Tarvitsemme lisätietoja henkilöllisyytesi vahvistamiseksi.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "Olemme pahoillamme", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index ea059c61b..34f1d6f18 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -323,6 +323,7 @@ "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "For å se bestillingen din, vennligst logg inn.", "Included": "Inkludert", "Indoor pool": "Innendørs basseng", "Indoor windows and excellent lighting": "Indoor windows and excellent lighting", @@ -461,6 +462,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På reisen din", + "One last step": "Et siste skritt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, kontakt brukerstøtten.", "Open": "Åpen", "Open for application": "Åpen for søknad", @@ -705,6 +707,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "Vi hadde et problem med å behandle din bestilling. Vær så snill å prøv igjen. Ingen gebyrer er blevet belastet.", "We have a special gift waiting for you!": "Vi har en spesiell gave som venter på deg!", "We look forward to your visit!": "Vi ser frem til ditt besøk!", + "We need some more details to confirm your identity.": "Vi trenger noen flere detaljer for å bekrefte identiteten din.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "Vi beklager", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index 8b0a02c3b..73c177182 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -323,6 +323,7 @@ "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.": "In order to verify your account linking we will ask you to sign in to your SAS EuroBonus account.", + "In order to view your booking, please log in.": "För att se din bokning, vänligen logga in.", "Included": "Inkluderad", "Indoor pool": "Inomhuspool", "Indoor windows and excellent lighting": "Fönster inomhus och utmärkt belysning", @@ -461,6 +462,7 @@ "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din resa", + "One last step": "Ett sista steg", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, kontakta supporten.", "Open": "Öppna", "Open for application": "Öppen för ansökan", @@ -705,6 +707,7 @@ "We had an issue processing your booking. Please try again. No charges have been made.": "Vi hade ett problem med att bearbeta din bokning. Vänligen försök igen. Inga avgifter har debiterats.", "We have a special gift waiting for you!": "Vi har en speciell present som väntar på dig!", "We look forward to your visit!": "Vi ser fram emot ditt besök!", + "We need some more details to confirm your identity.": "Vi behöver lite mer information för att bekräfta din identitet.", "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.": "We require your birth date in order to link your Scandic account with your SAS EuroBonus account. Please check that it is correct.", "We successfully connected your accounts!": "We successfully connected your accounts!", "We're sorry": "Vi beklagar", diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 9944efc5d..e91036183 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -112,7 +112,7 @@ export const cancelBookingInput = z.object({ }) export const createRefIdInput = z.object({ - bookingNumber: z + confirmationNumber: z .string() .trim() .regex(/^\s*[0-9]+(-[0-9])?\s*$/) diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index 1923688d4..cc4460fde 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -80,6 +80,8 @@ const guestSchema = z.object({ countryCode: z.string().nullable().default(""), }) +export type Guest = z.output + export const packageSchema = z .object({ type: z.string().nullable(), diff --git a/apps/scandic-web/server/routers/booking/query.ts b/apps/scandic-web/server/routers/booking/query.ts index e162be478..5d7b0cb86 100644 --- a/apps/scandic-web/server/routers/booking/query.ts +++ b/apps/scandic-web/server/routers/booking/query.ts @@ -10,7 +10,7 @@ import { } from "@/server/trpc" import { getHotel } from "../hotels/query" -import encryptValue from "../utils/encryptValue" +import { encrypt } from "../utils/encryption" import { bookingConfirmationInput, createRefIdInput, @@ -241,8 +241,8 @@ export const bookingQueryRouter = router({ createRefId: serviceProcedure .input(createRefIdInput) .mutation(async function ({ input }) { - const { bookingNumber, lastName } = input - const encryptedRefId = encryptValue(`${bookingNumber},${lastName}`) + const { confirmationNumber, lastName } = input + const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`) if (!encryptedRefId) { throw serverErrorByStatus(422, "Was not able to encrypt ref id") diff --git a/apps/scandic-web/server/routers/user/utils.ts b/apps/scandic-web/server/routers/user/utils.ts index c720195b1..5b903064e 100644 --- a/apps/scandic-web/server/routers/user/utils.ts +++ b/apps/scandic-web/server/routers/user/utils.ts @@ -4,7 +4,7 @@ import { Lang } from "@/constants/languages" import { env } from "@/env/server" import * as api from "@/lib/api" -import encryptValue from "../utils/encryptValue" +import { encrypt } from "../utils/encryption" import type { FriendTransaction, Stay } from "./output" @@ -93,7 +93,7 @@ async function updateStaysBookingUrl( d.attributes.confirmationNumber.toString() + "," + apiJson.data.attributes.lastName - const encryptedBookingValue = encryptValue(originalString) + const encryptedBookingValue = encrypt(originalString) if (!!encryptedBookingValue) { bookingUrl.searchParams.set("RefId", encryptedBookingValue) } else { diff --git a/apps/scandic-web/server/routers/utils/encryptValue.ts b/apps/scandic-web/server/routers/utils/encryptValue.ts deleted file mode 100644 index db888510d..000000000 --- a/apps/scandic-web/server/routers/utils/encryptValue.ts +++ /dev/null @@ -1,27 +0,0 @@ -import crypto from "crypto" - -import { env } from "@/env/server" - -export default function encryptValue(originalString: string) { - try { - const encryptionKey = env.BOOKING_ENCRYPTION_KEY - const bufferKey = Buffer.from(encryptionKey, "utf8") - const cipher = crypto.createCipheriv("DES-ECB", bufferKey, null) - cipher.setAutoPadding(false) - const bufferString = Buffer.from(originalString, "utf8") - const paddingSize = - bufferKey.length - (bufferString.length % bufferKey.length) - const paddedStr = Buffer.concat([ - bufferString, - Buffer.alloc(paddingSize, 0), - ]) - const buffers: Buffer[] = [] - buffers.push(cipher.update(paddedStr)) - buffers.push(cipher.final()) - const result = Buffer.concat(buffers).toString("base64").replace(/\+/g, "-") - return result - } catch (e) { - console.log(e) - return "" - } -} diff --git a/apps/scandic-web/server/routers/utils/encryption.ts b/apps/scandic-web/server/routers/utils/encryption.ts new file mode 100644 index 000000000..09341158e --- /dev/null +++ b/apps/scandic-web/server/routers/utils/encryption.ts @@ -0,0 +1,54 @@ +import "server-only" + +import crypto from "crypto" + +import { env } from "@/env/server" + +export { decrypt, encrypt } + +const algorithm = "DES-ECB" +const encryptionKey = env.BOOKING_ENCRYPTION_KEY +const bufferKey = Buffer.from(encryptionKey, "utf8") + +function encrypt(originalString: string) { + try { + const cipher = crypto.createCipheriv(algorithm, bufferKey, null) + cipher.setAutoPadding(false) + const bufferString = Buffer.from(originalString, "utf8") + const paddingSize = + bufferKey.length - (bufferString.length % bufferKey.length) + const paddedStr = Buffer.concat([ + bufferString, + Buffer.alloc(paddingSize, 0), + ]) + const buffers: Buffer[] = [] + buffers.push(cipher.update(paddedStr)) + buffers.push(cipher.final()) + const result = Buffer.concat(buffers).toString("base64").replace(/\+/g, "-") + return result + } catch (e) { + console.log(e) + return "" + } +} + +function decrypt(encryptedString: string) { + try { + const decipher = crypto.createDecipheriv(algorithm, bufferKey, null) + decipher.setAutoPadding(false) + const buffers: Buffer[] = [] + buffers.push(decipher.update(encryptedString, "base64")) + buffers.push(decipher.final()) + const result = Buffer.concat(buffers) + .toString("utf8") + /* + * Hexadecimal byte (null byte) replace. These occur when decrypting because + * we're disabling the auto padding for historical/compatibility reasons. + */ + .replace(/(\x00)*/g, "") + return result + } catch (e) { + console.log(e) + return "" + } +}