Merged in feat/SW-2903-tokens (pull request #2508)

feat(SW-2358): Use personal token if logged in

* feat(SW-2903): Use personal token if logged in

* Avoid encoding values in cookie

* Fix tests


Approved-by: Anton Gunnarsson
This commit is contained in:
Linus Flood
2025-07-08 11:24:31 +00:00
committed by Anton Gunnarsson
parent 5d9006bfdc
commit b35ceafc00
9 changed files with 118 additions and 67 deletions

View File

@@ -26,7 +26,7 @@ export default function ManageBooking({ booking }: ManageBookingProps) {
lastName, lastName,
confirmationNumber, confirmationNumber,
}).toString() }).toString()
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict` document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
}, [confirmationNumber, email, firstName, lastName]) }, [confirmationNumber, email, firstName, lastName])
const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}` const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`

View File

@@ -25,7 +25,7 @@ export default function Promos({ booking }: PromosProps) {
lastName, lastName,
confirmationNumber, confirmationNumber,
}).toString() }).toString()
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict` document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
}, [confirmationNumber, email, firstName, lastName]) }, [confirmationNumber, email, firstName, lastName])
const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}` const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`

View File

@@ -18,6 +18,13 @@ import {
import styles from "./findMyBooking.module.css" import styles from "./findMyBooking.module.css"
export type AdditionalInfoCookieValue = {
firstName: string
email: string
confirmationNumber: string
lastName: string
}
export default function AdditionalInfoForm({ export default function AdditionalInfoForm({
confirmationNumber, confirmationNumber,
lastName, lastName,
@@ -36,12 +43,12 @@ export default function AdditionalInfoForm({
function onSubmit() { function onSubmit() {
const values = form.getValues() const values = form.getValues()
const value = new URLSearchParams({ const value: AdditionalInfoCookieValue = {
...values, ...values,
confirmationNumber, confirmationNumber,
lastName, lastName,
}).toString() }
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict` document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
router.refresh() router.refresh()
} }

View File

@@ -24,6 +24,8 @@ import { type FindMyBookingFormSchema, findMyBookingFormSchema } from "./schema"
import styles from "./findMyBooking.module.css" import styles from "./findMyBooking.module.css"
import type { AdditionalInfoCookieValue } from "./AdditionalInfoForm"
export default function FindMyBooking() { export default function FindMyBooking() {
const router = useRouter() const router = useRouter()
const intl = useIntl() const intl = useIntl()
@@ -44,8 +46,10 @@ export default function FindMyBooking() {
const update = trpc.booking.createRefId.useMutation({ const update = trpc.booking.createRefId.useMutation({
onSuccess: (result) => { onSuccess: (result) => {
const values = form.getValues() const values = form.getValues()
const value = new URLSearchParams(values).toString() const value: AdditionalInfoCookieValue = {
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict` ...values,
}
document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
router.push(`${myStay[lang]}?RefId=${encodeURIComponent(result.refId)}`) router.push(`${myStay[lang]}?RefId=${encodeURIComponent(result.refId)}`)
}, },
onError: (error) => { onError: (error) => {

View File

@@ -15,10 +15,12 @@ import {
getProfileSafely, getProfileSafely,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import AdditionalInfoForm, {
type AdditionalInfoCookieValue,
} from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { isLoggedInUser } from "@/utils/isLoggedInUser" import { isLoggedInUser } from "@/utils/isLoggedInUser"
import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm"
import accessBooking, { import accessBooking, {
ACCESS_GRANTED, ACCESS_GRANTED,
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
@@ -48,9 +50,9 @@ export async function Receipt({ refId }: { refId: string }) {
if (isLoggedIn) { if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(refId) bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) { } else if (bv) {
const params = new URLSearchParams(bv) const values = JSON.parse(bv) as AdditionalInfoCookieValue
const firstName = params.get("firstName") const firstName = values.firstName
const email = params.get("email") const email = values.email
if (firstName && email) { if (firstName && email) {
bookingConfirmation = await findBooking( bookingConfirmation = await findBooking(

View File

@@ -11,6 +11,7 @@ import accessBooking, {
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output" import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { AdditionalInfoCookieValue } from "../FindMyBooking/AdditionalInfoForm"
describe("Access booking", () => { describe("Access booking", () => {
describe("for logged in booking", () => { describe("for logged in booking", () => {
@@ -43,90 +44,90 @@ describe("Access booking", () => {
describe("for anonymous booking", () => { describe("for anonymous booking", () => {
it("should enable access if all is provided", () => { it("should enable access if all is provided", () => {
const cookieString = new URLSearchParams({ const cookie: AdditionalInfoCookieValue = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ACCESS_GRANTED accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ACCESS_GRANTED)
}) })
it("should enable access if all is provided and be case-insensitive for first name", () => { it("should enable access if all is provided and be case-insensitive for first name", () => {
const cookieString = new URLSearchParams({ const cookie: AdditionalInfoCookieValue = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "AnOnYmOuS", firstName: "AnOnYmOuS",
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ACCESS_GRANTED accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ACCESS_GRANTED)
}) })
it("should enable access if all is provided and be case-insensitive for last name", () => { it("should enable access if all is provided and be case-insensitive for last name", () => {
const cookieString = new URLSearchParams({ const cookie: AdditionalInfoCookieValue = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }
expect(accessBooking(loggedOutGuest, "BoOkInG", null, cookieString)).toBe( expect(
ACCESS_GRANTED accessBooking(loggedOutGuest, "BoOkInG", null, JSON.stringify(cookie))
) ).toBe(ACCESS_GRANTED)
}) })
it("should enable access if all is provided and be case-insensitive for email", () => { it("should enable access if all is provided and be case-insensitive for email", () => {
const cookieString = new URLSearchParams({ const cookie: AdditionalInfoCookieValue = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
email: "LOGGED+out@scandichotels.com", email: "LOGGED+out@scandichotels.com",
}).toString() }
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ACCESS_GRANTED accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ACCESS_GRANTED)
}) })
it("should prompt logout if user is logged in", () => { it("should prompt logout if user is logged in", () => {
const cookieString = new URLSearchParams({ const cookie: AdditionalInfoCookieValue = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }
expect( expect(
accessBooking( accessBooking(
loggedOutGuest, loggedOutGuest,
"Booking", "Booking",
authenticatedUser, authenticatedUser,
cookieString JSON.stringify(cookie)
) )
).toBe(ERROR_FORBIDDEN) ).toBe(ERROR_FORBIDDEN)
}) })
it("should prompt for more if first name is missing", () => { it("should prompt for more if first name is missing", () => {
const cookieString = new URLSearchParams({ const cookie: Partial<AdditionalInfoCookieValue> = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ERROR_BAD_REQUEST accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ERROR_BAD_REQUEST)
}) })
it("should prompt for more if email is missing", () => { it("should prompt for more if email is missing", () => {
const cookieString = new URLSearchParams({ const cookie: Partial<AdditionalInfoCookieValue> = {
confirmationNumber: "123456789", confirmationNumber: "123456789",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
}).toString() }
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ERROR_BAD_REQUEST accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ERROR_BAD_REQUEST)
}) })
it("should prompt for more if cookie is invalid", () => { it("should prompt for more if cookie is invalid", () => {
const cookieString = new URLSearchParams({}).toString() const cookie = {}
expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe( expect(
ERROR_BAD_REQUEST accessBooking(loggedOutGuest, "Booking", null, JSON.stringify(cookie))
) ).toBe(ERROR_BAD_REQUEST)
}) })
it("should deny access if refId mismatch", () => { it("should deny access if refId mismatch", () => {
expect(accessBooking(loggedOutGuest, "NotBooking", null)).toBe( expect(accessBooking(loggedOutGuest, "NotBooking", null)).toBe(

View File

@@ -1,6 +1,7 @@
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output" import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { AdditionalInfoCookieValue } from "../FindMyBooking/AdditionalInfoForm"
export { export {
ACCESS_GRANTED, ACCESS_GRANTED,
@@ -36,13 +37,21 @@ function accessBooking(
if (guest.lastName?.toLowerCase() === lastName.toLowerCase()) { if (guest.lastName?.toLowerCase() === lastName.toLowerCase()) {
if (user) { if (user) {
return ERROR_FORBIDDEN
} else {
const params = new URLSearchParams(cookie)
if ( if (
params.get("firstName")?.toLowerCase() === user.firstName.toLowerCase() === guest.firstName?.toLowerCase() &&
guest.firstName?.toLowerCase() && user.email.toLowerCase() === guest.email?.toLowerCase()
params.get("email")?.toLowerCase() === guest.email?.toLowerCase() ) {
return ACCESS_GRANTED
} else {
return ERROR_FORBIDDEN
}
} else {
const values =
cookie && (JSON.parse(cookie) as Partial<AdditionalInfoCookieValue>)
if (
values &&
values.firstName?.toLowerCase() === guest.firstName?.toLowerCase() &&
values.email?.toLowerCase() === guest.email?.toLowerCase()
) { ) {
return ACCESS_GRANTED return ACCESS_GRANTED
} else { } else {

View File

@@ -2,6 +2,7 @@ import { cookies } from "next/headers"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { logger } from "@scandic-hotels/common/logger"
import * as maskValue from "@scandic-hotels/common/utils/maskValue" import * as maskValue from "@scandic-hotels/common/utils/maskValue"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
@@ -18,7 +19,9 @@ import {
getSavedPaymentCardsSafely, getSavedPaymentCardsSafely,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" import AdditionalInfoForm, {
type AdditionalInfoCookieValue,
} from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
import accessBooking, { import accessBooking, {
ACCESS_GRANTED, ACCESS_GRANTED,
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
@@ -68,9 +71,10 @@ export default async function MyStay(props: {
if (isLoggedIn) { if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(refId) bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) { } else if (bv) {
const params = new URLSearchParams(bv) logger.info(`MyStay: bv`, bv)
const firstName = params.get("firstName") const values = JSON.parse(bv) as AdditionalInfoCookieValue
const email = params.get("email") const firstName = values.firstName
const email = values.email
if (firstName && email) { if (firstName && email) {
bookingConfirmation = await findBooking( bookingConfirmation = await findBooking(

View File

@@ -11,6 +11,7 @@ import {
import { getHotel } from "../../routers/hotels/utils" import { getHotel } from "../../routers/hotels/utils"
import { toApiLang } from "../../utils" import { toApiLang } from "../../utils"
import { encrypt } from "../../utils/encryption" import { encrypt } from "../../utils/encryption"
import { isValidSession } from "../../utils/session"
import { getBookedHotelRoom } from "./helpers" import { getBookedHotelRoom } from "./helpers"
import { import {
createRefIdInput, createRefIdInput,
@@ -30,22 +31,26 @@ export const bookingQueryRouter = router({
.concat(refIdPlugin.toConfirmationNumber) .concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => { .use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang const lang = input.lang ?? ctx.lang
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({ return next({
ctx: { ctx: {
lang, lang,
token,
}, },
}) })
}) })
.query(async function ({ ctx }) { .query(async function ({ ctx }) {
const { confirmationNumber, lang, serviceToken } = ctx const { confirmationNumber, lang, token, serviceToken } = ctx
const getBookingCounter = createCounter("trpc.booking", "get") const getBookingCounter = createCounter("trpc.booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start() metricsGetBooking.start()
const booking = await getBooking(confirmationNumber, lang, serviceToken) const booking = await getBooking(confirmationNumber, lang, token)
if (!booking) { if (!booking) {
metricsGetBooking.dataError( metricsGetBooking.dataError(
@@ -88,10 +93,24 @@ export const bookingQueryRouter = router({
}), }),
findBooking: safeProtectedServiceProcedure findBooking: safeProtectedServiceProcedure
.input(findBookingInput) .input(findBookingInput)
.use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
lang,
token,
},
})
})
.query(async function ({ .query(async function ({
ctx, ctx,
input: { confirmationNumber, lastName, firstName, email }, input: { confirmationNumber, lastName, firstName, email },
}) { }) {
const { lang, token, serviceToken } = ctx
const findBookingCounter = createCounter("trpc.booking", "findBooking") const findBookingCounter = createCounter("trpc.booking", "findBooking")
const metricsFindBooking = findBookingCounter.init({ confirmationNumber }) const metricsFindBooking = findBookingCounter.init({ confirmationNumber })
@@ -99,8 +118,8 @@ export const bookingQueryRouter = router({
const booking = await findBooking( const booking = await findBooking(
confirmationNumber, confirmationNumber,
ctx.lang, lang,
ctx.serviceToken, token,
lastName, lastName,
firstName, firstName,
email email
@@ -118,9 +137,9 @@ export const bookingQueryRouter = router({
{ {
hotelId: booking.hotelId, hotelId: booking.hotelId,
isCardOnlyPayment: false, isCardOnlyPayment: false,
language: ctx.lang, language: lang,
}, },
ctx.serviceToken serviceToken
) )
if (!hotelData) { if (!hotelData) {
@@ -150,14 +169,19 @@ export const bookingQueryRouter = router({
.concat(refIdPlugin.toConfirmationNumber) .concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => { .use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang const lang = input.lang ?? ctx.lang
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({ return next({
ctx: { ctx: {
lang, lang,
token,
}, },
}) })
}) })
.query(async function ({ ctx }) { .query(async function ({ ctx }) {
const { confirmationNumber, lang, serviceToken } = ctx const { confirmationNumber, lang, token } = ctx
const getLinkedReservationsCounter = createCounter( const getLinkedReservationsCounter = createCounter(
"trpc.booking", "trpc.booking",
@@ -169,7 +193,7 @@ export const bookingQueryRouter = router({
metricsGetLinkedReservations.start() metricsGetLinkedReservations.start()
const booking = await getBooking(confirmationNumber, lang, serviceToken) const booking = await getBooking(confirmationNumber, lang, token)
if (!booking) { if (!booking) {
return [] return []
@@ -177,7 +201,7 @@ export const bookingQueryRouter = router({
const linkedReservationsResults = await Promise.allSettled( const linkedReservationsResults = await Promise.allSettled(
booking.linkedReservations.map((linkedReservation) => booking.linkedReservations.map((linkedReservation) =>
getBooking(linkedReservation.confirmationNumber, lang, serviceToken) getBooking(linkedReservation.confirmationNumber, lang, token)
) )
) )