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,
confirmationNumber,
}).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])
const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`

View File

@@ -25,7 +25,7 @@ export default function Promos({ booking }: PromosProps) {
lastName,
confirmationNumber,
}).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])
const myStayURL = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`

View File

@@ -18,6 +18,13 @@ import {
import styles from "./findMyBooking.module.css"
export type AdditionalInfoCookieValue = {
firstName: string
email: string
confirmationNumber: string
lastName: string
}
export default function AdditionalInfoForm({
confirmationNumber,
lastName,
@@ -36,12 +43,12 @@ export default function AdditionalInfoForm({
function onSubmit() {
const values = form.getValues()
const value = new URLSearchParams({
const value: AdditionalInfoCookieValue = {
...values,
confirmationNumber,
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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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