From 3b3e7308cc2eb790f1a699213bd6935468bc24ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Fri, 24 Oct 2025 13:17:02 +0000 Subject: [PATCH] Merged in feat/SW-3549-pass-scandic-token (pull request #2989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feat/SW-3549 pass scandic token * WIP pass scandic token * pass scandic token when booking * Merge branch 'master' of bitbucket.org:scandic-swap/web into feat/SW-3549-pass-scandic-token * pass user token when doing availability search * undo changes * merge * Merged in fix/sw-3551-rsc-bookingflowconfig (pull request #2988) fix(SW-3551): Fix issue with BookingConfigProvider in RSC * wip move config to pages * Move config providing to pages * Merged in fix/update-promo-error-modal-text (pull request #2990) fix: update promo error modal text * fix: update promo error modal text Approved-by: Emma Zettervall * Merged in fix/sw-3514-missing-membership-input-for-multiroom (pull request #2991) fix(SW-3514): Show join Scandic Friends card for SAS multiroom * Show join card for room 2+ Approved-by: Hrishikesh Vaipurkar * Merged in feat/lokalise-rebuild (pull request #2993) Feat/lokalise rebuild * chore(lokalise): update translation ids * chore(lokalise): easier to switch between projects * chore(lokalise): update translation ids * . * . * . * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * chore(lokalise): new translations * merge * switch to errors for missing id's * merge * sync translations Approved-by: Linus Flood * Merged in feat/SW-3552-logout-from-social-session-when- (pull request #2994) feat(SW-3552): Removed scandic session on logout Approved-by: Joakim Jäderberg * merge * replace getRedemptionTokenSafely() with context based instead * Refactor user verification and error handling across multiple components; implement safeTry utility for safer async calls * Refactor user verification and error handling across multiple components; implement safeTry utility for safer async calls * merge * Merge branch 'master' of bitbucket.org:scandic-swap/web into feat/SW-3549-pass-scandic-token * add booking scope remove unused getMembershipNumber() Approved-by: Anton Gunnarsson Approved-by: Hrishikesh Vaipurkar --- apps/partner-sas/auth/scandic/config.ts | 2 +- apps/partner-sas/lib/trpc/index.ts | 32 +++++++ apps/scandic-web/actions/editProfile.ts | 16 ++-- .../sas-x-scandic/otp/OneTimePasswordForm.tsx | 2 +- .../app/[lang]/webview/(views)/layout.tsx | 48 +++++----- .../components/MyPages/Profile/index.tsx | 8 +- .../components/ProtectedLayout.tsx | 26 +----- apps/scandic-web/components/UserExists.tsx | 27 +++--- apps/scandic-web/lib/trpc/server.ts | 55 +++++++++--- packages/common/utils/safeTry.ts | 4 +- packages/trpc/lib/context.ts | 9 ++ packages/trpc/lib/index.ts | 5 +- packages/trpc/lib/procedures.ts | 1 + .../lib/routers/autocomplete/destinations.ts | 45 +++++++--- .../routers/booking/mutation/create/index.ts | 14 +-- .../hotels/availability/enterDetails.ts | 27 +++--- .../hotels/availability/hotelsByCity.ts | 64 ++++++++------ .../hotelsByCityWithBookingCode.ts | 26 +++--- .../hotels/availability/hotelsByHotelIds.ts | 55 ++++++------ .../lib/routers/hotels/availability/myStay.ts | 43 +++++---- .../hotels/availability/selectRate/room.ts | 43 +++++---- .../availability/selectRate/rooms/index.ts | 43 +++++---- .../routers/hotels/services/getCountries.ts | 16 +++- .../services/getHotelsAvailabilityByCity.ts | 30 +++++-- .../getHotelsAvailabilityByHotelIds.ts | 26 ++++-- .../hotels/services/getRoomsAvailability.ts | 19 ++-- .../lib/routers/navigation/mypages/index.ts | 9 +- .../partners/sas/performLevelUpgrade.ts | 9 +- packages/trpc/lib/routers/user/query/index.ts | 66 ++++++-------- .../routers/user/query/userTrackingInfo.ts | 12 +-- packages/trpc/lib/routers/user/utils.ts | 88 ++++++++----------- .../routers/user/utils/getMemberShipNumber.ts | 17 ---- .../lib/routers/user/utils/getVerifiedUser.ts | 37 +++----- .../user/utils/updateStaysBookingUrl.ts | 76 ++++++++-------- .../lib/utils/getRedemptionTokenSafely.ts | 23 ----- .../trpc/lib/utils/getUserPointsBalance.ts | 38 -------- 36 files changed, 558 insertions(+), 503 deletions(-) delete mode 100644 packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts delete mode 100644 packages/trpc/lib/utils/getRedemptionTokenSafely.ts delete mode 100644 packages/trpc/lib/utils/getUserPointsBalance.ts diff --git a/apps/partner-sas/auth/scandic/config.ts b/apps/partner-sas/auth/scandic/config.ts index 5523c4463..671eb2f57 100644 --- a/apps/partner-sas/auth/scandic/config.ts +++ b/apps/partner-sas/auth/scandic/config.ts @@ -8,6 +8,6 @@ export const config = { client_secret: env.CURITY_CLIENT_SECRET_USER, redirect_uri: new URL("/api/web/auth/callback/curity", env.PUBLIC_URL).href, acr_values: "urn:com:scandichotels:sas-eb", - scope: "openid profile availability availability_whitelabel_get", + scope: "openid profile booking availability availability_whitelabel_get", response_type: "code", } as const diff --git a/apps/partner-sas/lib/trpc/index.ts b/apps/partner-sas/lib/trpc/index.ts index 4e99398f0..071a3e560 100644 --- a/apps/partner-sas/lib/trpc/index.ts +++ b/apps/partner-sas/lib/trpc/index.ts @@ -1,12 +1,16 @@ import { headers } from "next/headers" +import { dt } from "@scandic-hotels/common/dt" import { createContext } from "@scandic-hotels/trpc/context" +import { getEuroBonusProfileData } from "@scandic-hotels/trpc/routers/partners/sas/getEuroBonusProfile" +import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils/getVerifiedUser" import { appServerClient, configureServerClient, } from "@scandic-hotels/trpc/serverClient" import { auth } from "@/auth" +import { getSession } from "@/auth/scandic/session" import type { Lang } from "@scandic-hotels/common/constants/language" @@ -24,6 +28,34 @@ export async function createAppContext() { const session = await auth() return session }, + getScandicUserToken: async () => { + const session = await getSession() + return session?.access_token ?? null + }, + getUserPointsBalance: async () => { + const session = await auth() + if (!session) return null + + const euroBonusProfile = await getEuroBonusProfileData({ + accessToken: session.token.access_token, + loginType: session.token.loginType, + }) + + if (!euroBonusProfile) return null + + return euroBonusProfile.points.total + }, + getScandicUser: async () => { + const session = await getSession() + if (!session) return null + + return await getVerifiedUser({ + token: { + expires_at: dt(session.expires_at).unix() * 1000, + access_token: session.access_token, + }, + }) + }, }) return ctx diff --git a/apps/scandic-web/actions/editProfile.ts b/apps/scandic-web/actions/editProfile.ts index 7a276c7da..97e092d1f 100644 --- a/apps/scandic-web/actions/editProfile.ts +++ b/apps/scandic-web/actions/editProfile.ts @@ -105,17 +105,17 @@ export const editProfile = protectedServerActionProcedure if (typedKey === "address") { if ( - (payload.data.address.city === profile.address.city || - (!payload.data.address.city && !profile.address.city)) && - (payload.data.address.countryCode === profile.address.countryCode || + (payload.data.address.city === profile.address?.city || + (!payload.data.address.city && !profile.address?.city)) && + (payload.data.address.countryCode === profile.address?.countryCode || (!payload.data.address.countryCode && - !profile.address.countryCode)) && + !profile.address?.countryCode)) && (payload.data.address.streetAddress === - profile.address.streetAddress || + profile.address?.streetAddress || (!payload.data.address.streetAddress && - !profile.address.streetAddress)) && - (payload.data.address.zipCode === profile.address.zipCode || - (!payload.data.address.zipCode && !profile.address.zipCode)) + !profile.address?.streetAddress)) && + (payload.data.address.zipCode === profile.address?.zipCode || + (!payload.data.address.zipCode && !profile.address?.zipCode)) ) { // untouched - noop } else { diff --git a/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/OneTimePasswordForm.tsx b/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/OneTimePasswordForm.tsx index 6a839466a..659b4d95a 100644 --- a/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/OneTimePasswordForm.tsx +++ b/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/OneTimePasswordForm.tsx @@ -53,7 +53,7 @@ export default function OneTimePasswordForm({ } if (requestOtp.isError) { - const cause = requestOtp.error?.data?.cause as RequestOtpError + const cause = requestOtp.error?.data?.cause as unknown as RequestOtpError const title = intl.formatMessage({ id: "linkEuroBonusAccount.oneTimePasswordGenericError", diff --git a/apps/scandic-web/app/[lang]/webview/(views)/layout.tsx b/apps/scandic-web/app/[lang]/webview/(views)/layout.tsx index 3809e22ec..3c53be0a7 100644 --- a/apps/scandic-web/app/[lang]/webview/(views)/layout.tsx +++ b/apps/scandic-web/app/[lang]/webview/(views)/layout.tsx @@ -1,8 +1,10 @@ import * as Sentry from "@sentry/nextjs" +import { TRPCError } from "@trpc/server" import { headers } from "next/headers" import { redirect } from "next/navigation" import { logger } from "@scandic-hotels/common/logger" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { getProfile } from "@/lib/trpc/memoizedRequests" @@ -17,9 +19,9 @@ export default async function Layout( const { children } = props const intl = await getIntl() - const user = await getProfile() + const [user, error] = await safeTry(getProfile()) - if (!user) { + if (!user && !error) { logger.debug(`[webview:page] unable to load user`) return (

@@ -31,19 +33,21 @@ export default async function Layout( ) } - if ("error" in user) { - switch (user.cause) { - case "unauthorized": // fall through - case "forbidden": // fall through - case "token_expired": - const headersList = await headers() - const returnURL = `/${params.lang}/webview${headersList.get("x-pathname")!}` - const redirectURL = `/${params.lang}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` - logger.debug( - `[webview:page] user error, redirecting to: ${redirectURL}` - ) - redirect(redirectURL) - case "notfound": + const notValidSession = + error instanceof TRPCError && + (error.code === "UNAUTHORIZED" || error.code === "FORBIDDEN") + + if (notValidSession) { + const headersList = await headers() + const returnURL = `/${params.lang}/webview${headersList.get("x-pathname")!}` + const redirectURL = `/${params.lang}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` + logger.debug(`[webview:page] user error, redirecting to: ${redirectURL}`) + redirect(redirectURL) + } + + if (error instanceof TRPCError) { + switch (error.code) { + case "NOT_FOUND": return (

{intl.formatMessage({ @@ -52,7 +56,15 @@ export default async function Layout( })}

) - case "unknown": + default: + logger.error("[webview:page] unexpected error code loading user", error) + Sentry.captureException(error, { + data: { + errorCode: error.code, + message: "[webview:page] unexpected error code loading user", + }, + }) + return (

{intl.formatMessage({ @@ -61,10 +73,6 @@ export default async function Layout( })}

) - default: - const u: never = user - logger.error("[webview:page] unhandled user loading error", u) - Sentry.captureMessage("[webview:page] unhandled user loading error", u) } } diff --git a/apps/scandic-web/components/MyPages/Profile/index.tsx b/apps/scandic-web/components/MyPages/Profile/index.tsx index 4fc387b53..8a7eff23c 100644 --- a/apps/scandic-web/components/MyPages/Profile/index.tsx +++ b/apps/scandic-web/components/MyPages/Profile/index.tsx @@ -30,11 +30,11 @@ export default async function Profile() { const lang = await getLang() const addressParts = [] - if (user.address.streetAddress) { + if (user.address?.streetAddress) { addressParts.push(user.address.streetAddress) } - if (user.address.city) { + if (user.address?.city) { addressParts.push(user.address.city) } @@ -43,8 +43,8 @@ export default async function Profile() { region: new Intl.DisplayNames([lang], { type: "region" }), } - if (user.address.country) { - const countryCode = isValidCountry(user.address.country) + if (user.address?.country) { + const countryCode = isValidCountry(user.address?.country) ? countriesMap[user.address.country] : null const localizedCountry = countryCode diff --git a/apps/scandic-web/components/ProtectedLayout.tsx b/apps/scandic-web/components/ProtectedLayout.tsx index 59e498bb6..030dc8e3f 100644 --- a/apps/scandic-web/components/ProtectedLayout.tsx +++ b/apps/scandic-web/components/ProtectedLayout.tsx @@ -1,8 +1,10 @@ +import { TRPCError } from "@trpc/server" import { headers } from "next/headers" import { redirect } from "next/navigation" import { overview } from "@scandic-hotels/common/constants/routes/myPages" import { logger } from "@scandic-hotels/common/logger" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { isValidSession } from "@scandic-hotels/trpc/utils/session" import { getProfile } from "@/lib/trpc/memoizedRequests" @@ -33,29 +35,9 @@ export async function ProtectedLayout({ children }: React.PropsWithChildren) { redirect(redirectURL) } - const user = await getProfile() + const [user, error] = await safeTry(getProfile()) - if (user && "error" in user) { - // redirect(redirectURL) - logger.error("[layout:protected] error in user", user) - switch (user.cause) { - case "unauthorized": // fall through - case "forbidden": // fall through - case "token_expired": - logger.error( - `[layout:protected] user error, redirecting to: ${redirectURL}` - ) - redirect(redirectURL) - case "notfound": - logger.error(`[layout:protected] notfound user loading error`) - break - case "unknown": - logger.error(`[layout:protected] unknown user loading error`) - break - default: - logger.error(`[layout:protected] unhandled user loading error`) - break - } + if (error instanceof TRPCError && error.code === "INTERNAL_SERVER_ERROR") { return (

{intl.formatMessage({ diff --git a/apps/scandic-web/components/UserExists.tsx b/apps/scandic-web/components/UserExists.tsx index 6abde0846..ba42ff785 100644 --- a/apps/scandic-web/components/UserExists.tsx +++ b/apps/scandic-web/components/UserExists.tsx @@ -16,9 +16,12 @@ export function UserExists() { const isUserLoggedIn = isValidClientSession(session) const lang = useLang() - const { data, isLoading: isLoadingUser } = trpc.user.get.useQuery(undefined, { - enabled: isUserLoggedIn, - }) + const { isLoading: isLoadingUser, error } = trpc.user.get.useQuery( + undefined, + { + enabled: isUserLoggedIn, + } + ) if (!isUserLoggedIn) { return null @@ -28,16 +31,12 @@ export function UserExists() { return null } - if (data && "error" in data && data.error) { - switch (data.cause) { - case "notfound": - redirect( - `${logoutSafely[lang]}?redirectTo=${encodeURIComponent(userNotFound[lang])}` - ) - default: - break - } + switch (error?.data?.code) { + case "NOT_FOUND": + redirect( + `${logoutSafely[lang]}?redirectTo=${encodeURIComponent(userNotFound[lang])}` + ) + default: + return null } - - return null } diff --git a/apps/scandic-web/lib/trpc/server.ts b/apps/scandic-web/lib/trpc/server.ts index c6ad2d137..523638513 100644 --- a/apps/scandic-web/lib/trpc/server.ts +++ b/apps/scandic-web/lib/trpc/server.ts @@ -6,6 +6,7 @@ import { Lang } from "@scandic-hotels/common/constants/language" import { login } from "@scandic-hotels/common/constants/routes/handleAuth" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { createContext } from "@scandic-hotels/trpc/context" +import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils/getVerifiedUser" import { appServerClient, configureServerClient, @@ -23,6 +24,21 @@ export async function createAppContext() { const webviewTokenCookie = cookie.get("webviewToken") const loginType = headersList.get("loginType") + async function getUserSession(): Promise { + const session = await auth() + const webToken = webviewTokenCookie?.value + if (!session?.token && !webToken) { + return null + } + + return ( + session || + ({ + token: { access_token: webToken, loginType }, + } as Session) + ) + } + const ctx = createContext({ app: "scandic-web", lang: headersList.get("x-lang") as Lang, @@ -31,19 +47,38 @@ export async function createAppContext() { url: headersList.get("x-url")!, webToken: webviewTokenCookie?.value, contentType: headersList.get("x-contenttype")!, - auth: async () => { - const session = await auth() - const webToken = webviewTokenCookie?.value - if (!session?.token && !webToken) { + auth: async () => await getUserSession(), + getScandicUserToken: async () => { + const session = await getUserSession() + return session?.token?.access_token ?? null + }, + getUserPointsBalance: async () => { + const session = await getUserSession() + if (!session) return null + + const user = await getVerifiedUser({ + token: { + expires_at: session.token.expires_at ?? 0, + access_token: session.token.access_token, + }, + }) + + if (!user) { return null } - return ( - session || - ({ - token: { access_token: webToken, loginType }, - } as Session) - ) + return user.membership?.currentPoints ?? 0 + }, + getScandicUser: async () => { + const session = await getUserSession() + if (!session) return null + + return await getVerifiedUser({ + token: { + expires_at: session.token.expires_at ?? 0, + access_token: session.token.access_token, + }, + }) }, }) diff --git a/packages/common/utils/safeTry.ts b/packages/common/utils/safeTry.ts index d3bb81596..5581fbe65 100644 --- a/packages/common/utils/safeTry.ts +++ b/packages/common/utils/safeTry.ts @@ -4,8 +4,8 @@ export type SafeTryResult = Promise< export async function safeTry(func: Promise): SafeTryResult { try { - return [await func, undefined] + return [await func, undefined] as const } catch (err) { - return [undefined, err] + return [undefined, err instanceof Error ? err : (err as unknown)] as const } } diff --git a/packages/trpc/lib/context.ts b/packages/trpc/lib/context.ts index 36f38db1e..8760cfe3c 100644 --- a/packages/trpc/lib/context.ts +++ b/packages/trpc/lib/context.ts @@ -2,6 +2,8 @@ import type { Lang } from "@scandic-hotels/common/constants/language" import type { User } from "next-auth" import type { JWT } from "next-auth/jwt" +import type { getVerifiedUser } from "./routers/user/utils/getVerifiedUser" + type Session = { token: JWT expires: string @@ -9,6 +11,7 @@ type Session = { error?: "RefreshAccessTokenError" } +type ScandicUser = Awaited> type CreateContextOptions = { auth: () => Promise lang: Lang @@ -18,6 +21,9 @@ type CreateContextOptions = { webToken?: string contentType?: string app: "scandic-web" | "partner-sas" + getScandicUserToken: () => Promise + getUserPointsBalance: () => Promise + getScandicUser: () => Promise } export function createContext(opts: CreateContextOptions) { @@ -30,6 +36,9 @@ export function createContext(opts: CreateContextOptions) { webToken: opts.webToken, contentType: opts.contentType, app: opts.app, + getScandicUserToken: opts.getScandicUserToken, + getUserPointsBalance: opts.getUserPointsBalance, + getScandicUser: opts.getScandicUser, } } diff --git a/packages/trpc/lib/index.ts b/packages/trpc/lib/index.ts index 9074357e5..95ddfec95 100644 --- a/packages/trpc/lib/index.ts +++ b/packages/trpc/lib/index.ts @@ -19,10 +19,7 @@ const t = initTRPC ...shape, data: { ...shape.data, - cause: - error.cause instanceof ZodError - ? undefined - : JSON.parse(JSON.stringify(error.cause)), + cause: error.cause instanceof ZodError ? undefined : error.cause, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, diff --git a/packages/trpc/lib/procedures.ts b/packages/trpc/lib/procedures.ts index 896afb5e4..5d2816a05 100644 --- a/packages/trpc/lib/procedures.ts +++ b/packages/trpc/lib/procedures.ts @@ -124,6 +124,7 @@ export const serviceProcedure = baseProcedure.use(async (opts) => { if (!access_token) { throw internalServerError(`[serviceProcedure] No service token`) } + return opts.next({ ctx: { serviceToken: access_token, diff --git a/packages/trpc/lib/routers/autocomplete/destinations.ts b/packages/trpc/lib/routers/autocomplete/destinations.ts index 2df6f3cc9..86f5241b5 100644 --- a/packages/trpc/lib/routers/autocomplete/destinations.ts +++ b/packages/trpc/lib/routers/autocomplete/destinations.ts @@ -43,11 +43,18 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure .input(destinationsAutoCompleteInputSchema) .query(async ({ ctx, input }): Promise => { const lang = input.lang || ctx.lang - const locations: AutoCompleteLocation[] = - await getAutoCompleteDestinationsData({ + const [locations, error] = await safeTry( + getAutoCompleteDestinationsData({ lang, serviceToken: ctx.serviceToken, }) + ) + + if (error || !locations) { + throw new Error("Unable to fetch autocomplete destinations data", { + cause: error, + }) + } const hits = filterAndCategorizeAutoComplete({ locations: locations, @@ -115,17 +122,31 @@ export async function getAutoCompleteDestinationsData({ } const countryNames = countries.data.map((country) => country.name) - const citiesByCountry = await getCitiesByCountry({ - countries: countryNames, - serviceToken: serviceToken, - lang, - }) + const [citiesByCountry, citiesByCountryError] = await safeTry( + getCitiesByCountry({ + countries: countryNames, + serviceToken: serviceToken, + lang, + }) + ) - const locations = await getLocations({ - lang: lang, - serviceToken: serviceToken, - citiesByCountry: citiesByCountry, - }) + if (citiesByCountryError || !citiesByCountry) { + autoCompleteLogger.error("Unable to fetch cities by country") + throw new Error("Unable to fetch cities by country") + } + + const [locations, locationsError] = await safeTry( + getLocations({ + lang: lang, + serviceToken: serviceToken, + citiesByCountry: citiesByCountry, + }) + ) + + if (locationsError || !locations) { + autoCompleteLogger.error("Unable to fetch locations") + throw new Error("Unable to fetch locations") + } const activeLocations = locations.filter((location) => { return ( diff --git a/packages/trpc/lib/routers/booking/mutation/create/index.ts b/packages/trpc/lib/routers/booking/mutation/create/index.ts index ec7234d89..77e924365 100644 --- a/packages/trpc/lib/routers/booking/mutation/create/index.ts +++ b/packages/trpc/lib/routers/booking/mutation/create/index.ts @@ -5,18 +5,12 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../../api" import { safeProtectedServiceProcedure } from "../../../../procedures" import { encrypt } from "../../../../utils/encryption" -import { isValidSession } from "../../../../utils/session" -import { getMembershipNumber } from "../../../user/utils" -import { isPartnerLoggedInUser } from "../../utils" import { createBookingInput, createBookingSchema } from "./schema" export const create = safeProtectedServiceProcedure .input(createBookingInput) .use(async ({ ctx, next }) => { - const token = - isValidSession(ctx.session) && !isPartnerLoggedInUser(ctx.session) - ? ctx.session.token.access_token - : ctx.serviceToken + const token = await ctx.getScandicUserToken() return next({ ctx: { @@ -29,8 +23,9 @@ export const create = safeProtectedServiceProcedure const { rooms, ...loggableInput } = inputWithoutLang const createBookingCounter = createCounter("trpc.booking", "create") + const user = await ctx.getScandicUser() const metricsCreateBooking = createBookingCounter.init({ - membershipNumber: await getMembershipNumber(ctx.session), + membershipNumber: user?.membershipNumber, language, ...loggableInput, rooms: inputWithoutLang.rooms.map(({ guest, ...room }) => { @@ -40,9 +35,8 @@ export const create = safeProtectedServiceProcedure }) metricsCreateBooking.start() - const headers = { - Authorization: `Bearer ${ctx.token}`, + Authorization: `Bearer ${ctx.token || ctx.serviceToken}`, } const apiResponse = await api.post( diff --git a/packages/trpc/lib/routers/hotels/availability/enterDetails.ts b/packages/trpc/lib/routers/hotels/availability/enterDetails.ts index 4dc804958..cba490623 100644 --- a/packages/trpc/lib/routers/hotels/availability/enterDetails.ts +++ b/packages/trpc/lib/routers/hotels/availability/enterDetails.ts @@ -9,8 +9,7 @@ import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { AvailabilityEnum } from "../../../enums/selectHotel" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../utils/session" import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input" import { getHotel } from "../services/getHotel" import { getRoomsAvailability } from "../services/getRoomsAvailability" @@ -32,18 +31,23 @@ export const enterDetailsRoomsAvailabilityInputSchema = z.object({ lang: z.nativeEnum(Lang), }) +type Context = { + userToken: string | null + userPoints?: number +} + const logger = createLogger("trpc:availability:enterDetails") export const enterDetails = safeProtectedServiceProcedure .input(enterDetailsRoomsAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ + if (isValidSession(ctx.session)) { + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ ctx: { - token: token, + userToken, userPoints: pointsValue, }, input, @@ -52,19 +56,20 @@ export const enterDetails = safeProtectedServiceProcedure } throw unauthorizedError() } - return next({ + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, }) }) .query(async function ({ ctx, input }) { const availability = await getRoomsAvailability( input, - ctx.token, + ctx.userToken, ctx.serviceToken, ctx.userPoints ) + const hotelData = await getHotel( { hotelId: input.booking.hotelId, diff --git a/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts b/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts index 812d6eb10..abd83e220 100644 --- a/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts +++ b/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts @@ -6,9 +6,7 @@ import { getCacheClient } from "@scandic-hotels/common/dataCache" import { env } from "../../../../env/server" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { toApiLang } from "../../../utils" -import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../utils/session" import { getHotelsAvailabilityByCity } from "../services/getHotelsAvailabilityByCity" export type HotelsAvailabilityInputSchema = z.output< @@ -64,35 +62,40 @@ export const hotelsAvailabilityInputSchema = z } ) +type Context = { + userToken: string | null + userPoints?: number +} + export const hotelsByCity = safeProtectedServiceProcedure .input(hotelsAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() + if (input.redemption) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ - ctx: { - token: token, - userPoints: pointsValue, - }, - input, - }) - } + const hasValidSession = isValidSession(ctx.session) + if (!hasValidSession) { + throw unauthorizedError() + } + + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ + ctx: { + userToken, + userPoints: pointsValue, + }, + }) } - throw unauthorizedError() } - return next({ + + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, - input, }) }) .query(async ({ ctx, input }) => { - const { lang } = ctx - const apiLang = toApiLang(lang) const { cityId, roomStayStartDate, @@ -105,19 +108,26 @@ export const hotelsByCity = safeProtectedServiceProcedure // In case of redemption do not cache result if (redemption) { - return getHotelsAvailabilityByCity( + return getHotelsAvailabilityByCity({ input, - apiLang, - ctx.token, - ctx.userPoints - ) + lang: ctx.lang, + userToken: ctx.userToken, + serviceToken: ctx.serviceToken, + userPoints: ctx.userPoints, + }) } const cacheClient = await getCacheClient() return await cacheClient.cacheOrGet( `${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`, async () => { - return getHotelsAvailabilityByCity(input, apiLang, ctx.token) + return getHotelsAvailabilityByCity({ + input, + lang: ctx.lang, + userToken: undefined, + serviceToken: ctx.serviceToken, + userPoints: undefined, + }) }, env.CACHE_TIME_CITY_SEARCH ) diff --git a/packages/trpc/lib/routers/hotels/availability/hotelsByCityWithBookingCode.ts b/packages/trpc/lib/routers/hotels/availability/hotelsByCityWithBookingCode.ts index 8a86a319f..e342326c0 100644 --- a/packages/trpc/lib/routers/hotels/availability/hotelsByCityWithBookingCode.ts +++ b/packages/trpc/lib/routers/hotels/availability/hotelsByCityWithBookingCode.ts @@ -1,5 +1,4 @@ import { serviceProcedure } from "../../../procedures" -import { toApiLang } from "../../../utils" import { getHotelsAvailabilityByCity } from "../services/getHotelsAvailabilityByCity" import { getHotelsAvailabilityByHotelIds } from "../services/getHotelsAvailabilityByHotelIds" import { hotelsAvailabilityInputSchema } from "./hotelsByCity" @@ -7,14 +6,13 @@ import { hotelsAvailabilityInputSchema } from "./hotelsByCity" export const hotelsByCityWithBookingCode = serviceProcedure .input(hotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { - const { lang } = ctx - const apiLang = toApiLang(lang) - - const bookingCodeAvailabilityResponse = await getHotelsAvailabilityByCity( + const bookingCodeAvailabilityResponse = await getHotelsAvailabilityByCity({ input, - apiLang, - ctx.serviceToken - ) + lang: ctx.lang, + userToken: undefined, + serviceToken: ctx.serviceToken, + userPoints: undefined, + }) // Get regular availability of hotels which don't have availability with booking code. const unavailableHotelIds = bookingCodeAvailabilityResponse.availability @@ -36,11 +34,13 @@ export const hotelsByCityWithBookingCode = serviceProcedure bookingCode: "", hotelIds: unavailableHotelIds, } - const unavailableHotels = await getHotelsAvailabilityByHotelIds( - unavailableHotelsInput, - apiLang, - ctx.serviceToken - ) + const unavailableHotels = await getHotelsAvailabilityByHotelIds({ + input: unavailableHotelsInput, + lang: ctx.lang, + serviceToken: ctx.serviceToken, + userToken: undefined, + userPoints: undefined, + }) // No regular rates available due to network or API failure (no need to filter & merge). if (!unavailableHotels) { diff --git a/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts b/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts index 6b834e425..da3fabc61 100644 --- a/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts +++ b/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts @@ -3,9 +3,7 @@ import { z } from "zod" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { toApiLang } from "../../../utils" -import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../utils/session" import { getHotelsAvailabilityByHotelIds } from "../services/getHotelsAvailabilityByHotelIds" export type HotelsByHotelIdsAvailabilityInputSchema = z.output< @@ -60,40 +58,45 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z message: "FROMDATE_CANNOT_BE_IN_THE_PAST", } ) +type Context = { + userToken: string | null + userPoints?: number +} export const hotelsByHotelIds = safeProtectedServiceProcedure .input(getHotelsByHotelIdsAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() + if (input.redemption) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ - ctx: { - token: token, - userPoints: pointsValue, - }, - input, - }) - } + const hasValidSession = isValidSession(ctx.session) + if (!hasValidSession) { + throw unauthorizedError() + } + + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ + ctx: { + userToken, + userPoints: pointsValue, + }, + }) } - throw unauthorizedError() } - return next({ + + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, - input, }) }) .query(async ({ input, ctx }) => { - const { lang } = ctx - const apiLang = toApiLang(lang) - return getHotelsAvailabilityByHotelIds( + return getHotelsAvailabilityByHotelIds({ input, - apiLang, - ctx.token, - ctx.userPoints - ) + lang: ctx.lang, + userToken: ctx.userToken, + serviceToken: ctx.serviceToken, + userPoints: ctx.userPoints, + }) }) diff --git a/packages/trpc/lib/routers/hotels/availability/myStay.ts b/packages/trpc/lib/routers/hotels/availability/myStay.ts index 0ca779d41..8115aef3c 100644 --- a/packages/trpc/lib/routers/hotels/availability/myStay.ts +++ b/packages/trpc/lib/routers/hotels/availability/myStay.ts @@ -6,8 +6,7 @@ import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../utils/session" import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input" import { getRoomsAvailability } from "../services/getRoomsAvailability" import { getSelectedRoomAvailability } from "../utils" @@ -19,29 +18,37 @@ export const myStayRoomAvailabilityInputSchema = z.object({ lang: z.nativeEnum(Lang), }) +type Context = { + userToken: string | null + userPoints?: number +} + const logger = createLogger("trpc:availability:myStay") export const myStay = safeProtectedServiceProcedure .input(myStayRoomAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() + if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ - ctx: { - token: token, - userPoints: pointsValue, - }, - input, - }) - } + const hasValidSession = isValidSession(ctx.session) + if (!hasValidSession) { + throw unauthorizedError() + } + + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ + ctx: { + userToken, + userPoints: pointsValue, + }, + }) } - throw unauthorizedError() } - return next({ + + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, }) }) @@ -54,7 +61,7 @@ export const myStay = safeProtectedServiceProcedure }, lang: input.lang, }, - ctx.token, + ctx.userToken, ctx.serviceToken, ctx.userPoints ) diff --git a/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts b/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts index c343ee3df..2c2e1c642 100644 --- a/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts +++ b/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts @@ -5,8 +5,7 @@ import { Lang } from "@scandic-hotels/common/constants/language" import { SEARCH_TYPE_REDEMPTION } from "../../../../constants/booking" import { unauthorizedError } from "../../../../errors" import { safeProtectedServiceProcedure } from "../../../../procedures" -import { getRedemptionTokenSafely } from "../../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../../utils/session" import { baseBookingSchema, baseRoomSchema } from "../../input" import { getRoomsAvailability } from "../../services/getRoomsAvailability" import { mergeRoomTypes } from "../../utils" @@ -18,28 +17,36 @@ export const selectRateRoomAvailabilityInputSchema = z.object({ lang: z.nativeEnum(Lang), }) +type Context = { + userToken: string | null + userPoints?: number +} + export const room = safeProtectedServiceProcedure .input(selectRateRoomAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() + if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ - ctx: { - token: token, - userPoints: pointsValue, - }, - input, - }) - } + const hasValidSession = isValidSession(ctx.session) + if (!hasValidSession) { + throw unauthorizedError() + } + + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ + ctx: { + userToken, + userPoints: pointsValue, + }, + }) } - throw unauthorizedError() } - return next({ + + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, }) }) @@ -52,7 +59,7 @@ export const room = safeProtectedServiceProcedure }, lang: input.lang, }, - ctx.token, + ctx.userToken, ctx.serviceToken, ctx.userPoints ) diff --git a/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts b/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts index de70cafd8..d398ce877 100644 --- a/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts +++ b/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts @@ -3,34 +3,41 @@ import "server-only" import { SEARCH_TYPE_REDEMPTION } from "../../../../../constants/booking" import { unauthorizedError } from "../../../../../errors" import { safeProtectedServiceProcedure } from "../../../../../procedures" -import { getRedemptionTokenSafely } from "../../../../../utils/getRedemptionTokenSafely" -import { getUserPointsBalance } from "../../../../../utils/getUserPointsBalance" +import { isValidSession } from "../../../../../utils/session" import { getRoomsAvailability } from "../../../services/getRoomsAvailability" import { mergeRoomTypes } from "../../../utils" import { selectRateRoomsAvailabilityInputSchema } from "./schema" +type Context = { + userToken: string | null + userPoints?: number +} + export const rooms = safeProtectedServiceProcedure .input(selectRateRoomsAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { + const userToken = await ctx.getScandicUserToken() + if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { - if (ctx.session?.token.access_token) { - const pointsValue = await getUserPointsBalance(ctx.session) - const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) - if (pointsValue !== undefined && token) { - return next({ - ctx: { - token: token, - userPoints: pointsValue, - }, - input, - }) - } + const hasValidSession = isValidSession(ctx.session) + if (!hasValidSession) { + throw unauthorizedError() + } + + const pointsValue = await ctx.getUserPointsBalance() + if (pointsValue && userToken) { + return next({ + ctx: { + userToken, + userPoints: pointsValue, + }, + }) } - throw unauthorizedError() } - return next({ + + return next({ ctx: { - token: ctx.serviceToken, + userToken, }, }) }) @@ -42,7 +49,7 @@ export const rooms = safeProtectedServiceProcedure const availability = await getRoomsAvailability( input, - ctx.token, + ctx.userToken, ctx.serviceToken, ctx.userPoints ) diff --git a/packages/trpc/lib/routers/hotels/services/getCountries.ts b/packages/trpc/lib/routers/hotels/services/getCountries.ts index f60a2b3d1..dea57ba20 100644 --- a/packages/trpc/lib/routers/hotels/services/getCountries.ts +++ b/packages/trpc/lib/routers/hotels/services/getCountries.ts @@ -37,14 +37,26 @@ export async function getCountries({ ) if (!countryResponse.ok) { - throw new Error("Unable to fetch countries") + logger.error("Unable to fetch countries", { + status: countryResponse.status, + statusText: countryResponse.statusText, + }) + + throw new Error("Unable to fetch countries", { + cause: { + status: countryResponse.status, + statusText: countryResponse.statusText, + }, + }) } const countriesJson = await countryResponse.json() const countries = countriesSchema.safeParse(countriesJson) if (!countries.success) { logger.error(`Validation for countries failed`, countries.error) - return null + throw new Error("Unable to parse country data", { + cause: countries.error, + }) } return countries.data diff --git a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts index 5e7e98fd5..342ec91e6 100644 --- a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts +++ b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts @@ -2,16 +2,26 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" import { badRequestError } from "../../../errors" +import { toApiLang } from "../../../utils" import { hotelsAvailabilitySchema } from "../output" +import type { Lang } from "@scandic-hotels/common/constants/language" + import type { HotelsAvailabilityInputSchema } from "../availability/hotelsByCity" -export async function getHotelsAvailabilityByCity( - input: HotelsAvailabilityInputSchema, - apiLang: string, - token: string, // Either service token or user access token in case of redemption search - userPoints: number = 0 -) { +export async function getHotelsAvailabilityByCity({ + input, + lang, + userToken, + serviceToken, + userPoints = 0, +}: { + input: HotelsAvailabilityInputSchema + lang: Lang + userToken: string | null | undefined + serviceToken: string + userPoints: number | undefined +}) { const { cityId, roomStayStartDate, @@ -22,7 +32,9 @@ export async function getHotelsAvailabilityByCity( redemption, } = input - const params: Record = { + const apiLang = toApiLang(lang) + + const params = { roomStayStartDate, roomStayEndDate, adults, @@ -30,7 +42,7 @@ export async function getHotelsAvailabilityByCity( ...(bookingCode && { bookingCode }), ...(redemption ? { isRedemption: "true" } : {}), language: apiLang, - } + } satisfies Record const getHotelsAvailabilityByCityCounter = createCounter( "hotel", @@ -54,7 +66,7 @@ export async function getHotelsAvailabilityByCity( api.endpoints.v1.Availability.city(cityId), { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${userToken ?? serviceToken}`, }, }, params diff --git a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByHotelIds.ts b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByHotelIds.ts index 4251370a2..89a48e463 100644 --- a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByHotelIds.ts +++ b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByHotelIds.ts @@ -4,16 +4,26 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { env } from "../../../../env/server" import * as api from "../../../api" import { badRequestError } from "../../../errors" +import { toApiLang } from "../../../utils" import { hotelsAvailabilitySchema } from "../output" +import type { Lang } from "@scandic-hotels/common/constants/language" + import type { HotelsByHotelIdsAvailabilityInputSchema } from "../availability/hotelsByHotelIds" -export async function getHotelsAvailabilityByHotelIds( - input: HotelsByHotelIdsAvailabilityInputSchema, - apiLang: string, - token: string, - userPoints: number = 0 -) { +export async function getHotelsAvailabilityByHotelIds({ + input, + lang, + userToken, + serviceToken, + userPoints = 0, +}: { + input: HotelsByHotelIdsAvailabilityInputSchema + lang: Lang + userToken: string | null | undefined + serviceToken: string + userPoints: number | undefined +}) { const { hotelIds, roomStayStartDate, @@ -24,6 +34,8 @@ export async function getHotelsAvailabilityByHotelIds( redemption, } = input + const apiLang = toApiLang(lang) + const params = new URLSearchParams([ ["roomStayStartDate", roomStayStartDate], ["roomStayEndDate", roomStayEndDate], @@ -72,7 +84,7 @@ export async function getHotelsAvailabilityByHotelIds( api.endpoints.v1.Availability.hotels(), { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${userToken ?? serviceToken}`, }, }, params diff --git a/packages/trpc/lib/routers/hotels/services/getRoomsAvailability.ts b/packages/trpc/lib/routers/hotels/services/getRoomsAvailability.ts index c39bec43b..8d651199d 100644 --- a/packages/trpc/lib/routers/hotels/services/getRoomsAvailability.ts +++ b/packages/trpc/lib/routers/hotels/services/getRoomsAvailability.ts @@ -17,7 +17,7 @@ import type { RoomsAvailabilityOutputSchema } from "../availability/selectRate/r export async function getRoomsAvailability( input: RoomsAvailabilityOutputSchema, - token: string, + userToken: string | null | undefined, serviceToken: string, userPoints: number | undefined ) { @@ -42,12 +42,12 @@ export async function getRoomsAvailability( const apiLang = toApiLang(lang) const baseCacheKey = { - bookingCode, - fromDate, - hotelId, lang, - searchType, + hotelId, + fromDate, toDate, + searchType, + bookingCode, } const cacheClient = await getCacheClient() @@ -57,6 +57,11 @@ export async function getRoomsAvailability( ...baseCacheKey, room, } + + const token = userToken + ? ({ type: "userToken", token: userToken } as const) + : ({ type: "serviceToken", token: serviceToken } as const) + const result = cacheClient.cacheOrGet( stringify(cacheKey), async function () { @@ -78,7 +83,7 @@ export async function getRoomsAvailability( { cache: undefined, // overwrite default headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${token.token}`, }, }, params @@ -181,7 +186,7 @@ export async function getRoomsAvailability( return validateAvailabilityData.data } }, - "1m" + token.type === "userToken" ? "no cache" : "1m" ) return result diff --git a/packages/trpc/lib/routers/navigation/mypages/index.ts b/packages/trpc/lib/routers/navigation/mypages/index.ts index a1f54718c..05f3a4bd4 100644 --- a/packages/trpc/lib/routers/navigation/mypages/index.ts +++ b/packages/trpc/lib/routers/navigation/mypages/index.ts @@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server" import { z } from "zod" import { Lang } from "@scandic-hotels/common/constants/language" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { safeProtectedProcedure } from "../../../procedures" import { isValidSession } from "../../../utils/session" @@ -38,13 +39,15 @@ export const myPagesNavigation = safeProtectedProcedure }) } - const user = await getVerifiedUser({ session: ctx.session }) - if (!user || user.error) { + const [user, error] = await safeTry( + getVerifiedUser({ token: ctx.session.token }) + ) + if (!user || error) { return null } const [primaryLinks, secondaryLinks] = await Promise.all([ - getPrimaryLinks({ lang, userLoyalty: user.data.loyalty }), + getPrimaryLinks({ lang, userLoyalty: user.loyalty }), getSecondaryLinks({ lang }), ]) diff --git a/packages/trpc/lib/routers/partners/sas/performLevelUpgrade.ts b/packages/trpc/lib/routers/partners/sas/performLevelUpgrade.ts index 1ac59703c..cc24ca170 100644 --- a/packages/trpc/lib/routers/partners/sas/performLevelUpgrade.ts +++ b/packages/trpc/lib/routers/partners/sas/performLevelUpgrade.ts @@ -4,6 +4,7 @@ import { z } from "zod" import { FriendsMembershipLevels } from "@scandic-hotels/common/constants/membershipLevels" import { createLogger } from "@scandic-hotels/common/logger/createLogger" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import * as api from "../../../api" import { protectedProcedure } from "../../../procedures" @@ -31,11 +32,13 @@ export const performLevelUpgrade = protectedProcedure return { tierMatchState: "cached" } } - const profile = await getVerifiedUser({ session: ctx.session }) - if (!profile || "error" in profile || !profile.data.membership) { + const [profile, error] = await safeTry( + getVerifiedUser({ token: ctx.session.token }) + ) + if (!profile?.membership || error) { return { tierMatchState: "error" } } - const currentLevel = profile.data.membership.membershipLevel + const currentLevel = profile.membership.membershipLevel sasLogger.debug("tier match started") diff --git a/packages/trpc/lib/routers/user/query/index.ts b/packages/trpc/lib/routers/user/query/index.ts index 19f5e72e6..755a7810c 100644 --- a/packages/trpc/lib/routers/user/query/index.ts +++ b/packages/trpc/lib/routers/user/query/index.ts @@ -1,8 +1,10 @@ import { createCounter } from "@scandic-hotels/common/telemetry" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { router } from "../../.." import * as api from "../../../api" import { Transactions } from "../../../enums/transactions" +import { notFound } from "../../../errors" import { languageProtectedProcedure, protectedProcedure, @@ -42,30 +44,25 @@ export const userQueryRouter = router({ }) }) .query(async function getUser({ ctx }) { - const data = await getVerifiedUser({ session: ctx.session }) - - if (!data) { - return null + const user = await ctx.getScandicUser() + if (!user) { + throw notFound() } - if ("error" in data && data.error) { - return data - } - - return parsedUser(data.data, !ctx.isMFA) + return parsedUser(user, !ctx.isMFA) }), getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) { if (!isValidSession(ctx.session)) { return null } - const data = await getVerifiedUser({ session: ctx.session }) + const user = await ctx.getScandicUser() - if (!data || "error" in data) { + if (!user) { return null } - return parsedUser(data.data, false) + return parsedUser(user, false) }), getWithExtendedPartnerData: safeProtectedProcedure.query( async function getUser({ ctx }) { @@ -73,60 +70,49 @@ export const userQueryRouter = router({ return null } - const data = await getVerifiedUser({ - session: ctx.session, - includeExtendedPartnerData: true, - }) + const user = await ctx.getScandicUser() - if (!data || "error" in data) { + if (!user) { return null } - return parsedUser(data.data, false) + return parsedUser(user, false) } ), name: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } - const verifiedData = await getVerifiedUser({ session: ctx.session }) + const user = await ctx.getScandicUser() - if (!verifiedData || "error" in verifiedData) { + if (!user) { return null } return { - firstName: verifiedData.data.firstName, - lastName: verifiedData.data.lastName, + firstName: user.firstName, + lastName: user.lastName, } }), membershipLevel: protectedProcedure.query(async function ({ ctx }) { - const verifiedData = await getVerifiedUser({ session: ctx.session }) - if ( - !verifiedData || - "error" in verifiedData || - !verifiedData.data.loyalty - ) { + const user = await ctx.getScandicUser() + if (!user?.loyalty) { return null } - const membershipLevel = getFriendsMembership(verifiedData.data.loyalty) + const membershipLevel = getFriendsMembership(user.loyalty) return membershipLevel }), safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } - const verifiedData = await getVerifiedUser({ session: ctx.session }) + const user = await ctx.getScandicUser() - if ( - !verifiedData || - "error" in verifiedData || - !verifiedData.data.loyalty - ) { + if (!user?.loyalty) { return null } - const membershipLevel = getFriendsMembership(verifiedData.data.loyalty) + const membershipLevel = getFriendsMembership(user.loyalty) return membershipLevel }), userTrackingInfo, @@ -327,12 +313,14 @@ export const userQueryRouter = router({ }), membershipCards: protectedProcedure.query(async function ({ ctx }) { - const userData = await getVerifiedUser({ session: ctx.session }) + const [userData, error] = await safeTry( + getVerifiedUser({ token: ctx.session.token }) + ) - if (!userData || "error" in userData || !userData.data.loyalty) { + if (!userData?.loyalty || error) { return null } - return getMembershipCards(userData.data.loyalty) + return getMembershipCards(userData.loyalty) }), }) diff --git a/packages/trpc/lib/routers/user/query/userTrackingInfo.ts b/packages/trpc/lib/routers/user/query/userTrackingInfo.ts index 4c73e2236..f56397d0f 100644 --- a/packages/trpc/lib/routers/user/query/userTrackingInfo.ts +++ b/packages/trpc/lib/routers/user/query/userTrackingInfo.ts @@ -47,13 +47,9 @@ async function getScandicFriendsUserTrackingData(session: Session | null) { } try { - const verifiedUserData = await getVerifiedUser({ session: session }) + const verifiedUserData = await getVerifiedUser({ token: session.token }) - if ( - !verifiedUserData || - "error" in verifiedUserData || - !verifiedUserData.data.loyalty - ) { + if (!verifiedUserData || !verifiedUserData.loyalty) { metricsUserTrackingInfo.success({ reason: "invalid user data", data: notLoggedInUserTrackingData, @@ -61,12 +57,12 @@ async function getScandicFriendsUserTrackingData(session: Session | null) { return notLoggedInUserTrackingData } - const membership = getFriendsMembership(verifiedUserData.data.loyalty) + const membership = getFriendsMembership(verifiedUserData.loyalty) const loggedInUserTrackingData: TrackingUserData = { loginStatus: "logged in", loginType: session.token.loginType as LoginType, - memberId: verifiedUserData.data.profileId, + memberId: verifiedUserData.profileId, membershipNumber: membership?.membershipNumber, memberLevel: membership?.membershipLevel, loginAction: "login success", diff --git a/packages/trpc/lib/routers/user/utils.ts b/packages/trpc/lib/routers/user/utils.ts index 7913e3d21..bfbbbf028 100644 --- a/packages/trpc/lib/routers/user/utils.ts +++ b/packages/trpc/lib/routers/user/utils.ts @@ -1,6 +1,7 @@ import { myStay } from "@scandic-hotels/common/constants/routes/myStay" import { dt } from "@scandic-hotels/common/dt" import { createCounter } from "@scandic-hotels/common/telemetry" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { env } from "../../../env/server" import * as api from "../../api" @@ -8,7 +9,6 @@ import { cache } from "../../DUPLICATED/cache" import { creditCardsSchema } from "../../routers/user/output" import { toApiLang } from "../../utils" import { encrypt } from "../../utils/encryption" -import { isValidSession } from "../../utils/session" import { getVerifiedUser } from "./utils/getVerifiedUser" import { type FriendTransaction, getStaysSchema, type Stay } from "./output" @@ -16,19 +16,6 @@ import type { Lang } from "@scandic-hotels/common/constants/language" import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute" import type { Session } from "next-auth" -export async function getMembershipNumber( - session: Session | null -): Promise { - if (!isValidSession(session)) return undefined - - const verifiedUser = await getVerifiedUser({ session }) - if (!verifiedUser || "error" in verifiedUser) { - return undefined - } - - return verifiedUser.data.membershipNumber -} - export async function getPreviousStays( accessToken: string, limit: number = 10, @@ -202,44 +189,45 @@ export async function updateStaysBookingUrl( session: Session, lang: Lang ) { - const user = await getVerifiedUser({ - session, - }) - - if (user && !("error" in user)) { - return data.map((d) => { - const originalString = - d.attributes.confirmationNumber.toString() + "," + user.data.lastName - const encryptedBookingValue = encrypt(originalString) - - // Get base URL with fallback for ephemeral environments (like deploy previews). - const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" - - // Construct Booking URL. - const bookingUrl = new URL(myStay[lang], baseUrl) - - // Add search parameters. - if (encryptedBookingValue) { - bookingUrl.searchParams.set("RefId", encryptedBookingValue) - } else { - bookingUrl.searchParams.set("lastName", user.data.lastName) - bookingUrl.searchParams.set( - "bookingId", - d.attributes.confirmationNumber.toString() - ) - } - - return { - ...d, - attributes: { - ...d.attributes, - bookingUrl: bookingUrl.toString(), - }, - } + const [user, error] = await safeTry( + getVerifiedUser({ + token: session.token, }) - } + ) - return data + if (!user || error) { + return data + } + return data.map((d) => { + const originalString = + d.attributes.confirmationNumber.toString() + "," + user.lastName + const encryptedBookingValue = encrypt(originalString) + + // Get base URL with fallback for ephemeral environments (like deploy previews). + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + + // Construct Booking URL. + const bookingUrl = new URL(myStay[lang], baseUrl) + + // Add search parameters. + if (encryptedBookingValue) { + bookingUrl.searchParams.set("RefId", encryptedBookingValue) + } else { + bookingUrl.searchParams.set("lastName", user.lastName) + bookingUrl.searchParams.set( + "bookingId", + d.attributes.confirmationNumber.toString() + ) + } + + return { + ...d, + attributes: { + ...d.attributes, + bookingUrl: bookingUrl.toString(), + }, + } + }) } export const myBookingPath: LangRoute = { diff --git a/packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts b/packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts deleted file mode 100644 index 293526ac5..000000000 --- a/packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isValidSession } from "../../../utils/session" -import { getVerifiedUser } from "./getVerifiedUser" - -import type { Session } from "next-auth" - -export async function getMembershipNumber( - session: Session | null -): Promise { - if (!isValidSession(session)) return undefined - - const verifiedUser = await getVerifiedUser({ session }) - if (!verifiedUser || "error" in verifiedUser) { - return undefined - } - - return verifiedUser.data.membershipNumber -} diff --git a/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts b/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts index db28811cd..46f7b343e 100644 --- a/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts +++ b/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts @@ -2,16 +2,19 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" import { cache } from "../../../DUPLICATED/cache" +import { + internalServerError, + serverErrorByStatus, + sessionExpiredError, +} from "../../../errors" import { getUserSchema } from "../output" -import type { Session } from "next-auth" - export const getVerifiedUser = cache( async ({ - session, + token, includeExtendedPartnerData, }: { - session: Session + token: { expires_at?: number; access_token: string } includeExtendedPartnerData?: boolean }) => { const getVerifiedUserCounter = createCounter("user", "getVerifiedUser") @@ -20,16 +23,16 @@ export const getVerifiedUser = cache( metricsGetVerifiedUser.start() const now = Date.now() - if (session.token.expires_at && session.token.expires_at < now) { + if (token.expires_at && token.expires_at < now) { metricsGetVerifiedUser.dataError(`Token expired`) - return { error: true, cause: "token_expired" } as const + throw sessionExpiredError() } const apiResponse = await api.get( api.endpoints.v2.Profile.profile, { headers: { - Authorization: `Bearer ${session.token.access_token}`, + Authorization: `Bearer ${token.access_token}`, }, }, includeExtendedPartnerData @@ -40,19 +43,7 @@ export const getVerifiedUser = cache( if (!apiResponse.ok) { await metricsGetVerifiedUser.httpError(apiResponse) - if (apiResponse.status === 401) { - return { error: true, cause: "unauthorized" } as const - } else if (apiResponse.status === 403) { - return { error: true, cause: "forbidden" } as const - } else if (apiResponse.status === 404) { - return { error: true, cause: "notfound" } as const - } - - return { - error: true, - cause: "unknown", - status: apiResponse.status, - } as const + throw serverErrorByStatus(apiResponse.status, apiResponse) } const apiJson = await apiResponse.json() @@ -63,17 +54,17 @@ export const getVerifiedUser = cache( data: apiJson, } ) - return null + throw internalServerError("Missing data attributes in API response") } const verifiedData = getUserSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetVerifiedUser.validationError(verifiedData.error) - return null + throw verifiedData.error } metricsGetVerifiedUser.success() - return verifiedData + return verifiedData.data } ) diff --git a/packages/trpc/lib/routers/user/utils/updateStaysBookingUrl.ts b/packages/trpc/lib/routers/user/utils/updateStaysBookingUrl.ts index 61047c105..7566539b4 100644 --- a/packages/trpc/lib/routers/user/utils/updateStaysBookingUrl.ts +++ b/packages/trpc/lib/routers/user/utils/updateStaysBookingUrl.ts @@ -1,6 +1,7 @@ import "server-only" import { myStay } from "@scandic-hotels/common/constants/routes/myStay" +import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { env } from "../../../../env/server" import { encrypt } from "../../../utils/encryption" @@ -28,42 +29,47 @@ export async function updateStaysBookingUrl( session: Session, lang: Lang ) { - const user = await getVerifiedUser({ - session, - }) - - if (user && !("error" in user)) { - return data.map((d) => { - const originalString = - d.attributes.confirmationNumber.toString() + "," + user.data.lastName - const encryptedBookingValue = encrypt(originalString) - - // Get base URL with fallback for ephemeral environments (like deploy previews). - const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" - - // Construct Booking URL. - const bookingUrl = new URL(myStay[lang], baseUrl) - - // Add search parameters. - if (encryptedBookingValue) { - bookingUrl.searchParams.set("RefId", encryptedBookingValue) - } else { - bookingUrl.searchParams.set("lastName", user.data.lastName) - bookingUrl.searchParams.set( - "bookingId", - d.attributes.confirmationNumber.toString() - ) - } - - return { - ...d, - attributes: { - ...d.attributes, - bookingUrl: bookingUrl.toString(), - }, - } + const [user, error] = await safeTry( + getVerifiedUser({ + token: { + access_token: session.token.access_token, + expires_at: session.token.expires_at ?? 0, + }, }) + ) + + if (error || !user) { + return data } - return data + return data.map((d) => { + const originalString = + d.attributes.confirmationNumber.toString() + "," + user.lastName + const encryptedBookingValue = encrypt(originalString) + + // Get base URL with fallback for ephemeral environments (like deploy previews). + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + + // Construct Booking URL. + const bookingUrl = new URL(myStay[lang], baseUrl) + + // Add search parameters. + if (encryptedBookingValue) { + bookingUrl.searchParams.set("RefId", encryptedBookingValue) + } else { + bookingUrl.searchParams.set("lastName", user.lastName) + bookingUrl.searchParams.set( + "bookingId", + d.attributes.confirmationNumber.toString() + ) + } + + return { + ...d, + attributes: { + ...d.attributes, + bookingUrl: bookingUrl.toString(), + }, + } + }) } diff --git a/packages/trpc/lib/utils/getRedemptionTokenSafely.ts b/packages/trpc/lib/utils/getRedemptionTokenSafely.ts deleted file mode 100644 index 9f3e9c626..000000000 --- a/packages/trpc/lib/utils/getRedemptionTokenSafely.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isValidSession } from "./session" - -import type { Session } from "next-auth" - -export function getRedemptionTokenSafely( - session: Session, - serviceToken: string -): string | undefined { - if (!isValidSession(session)) return undefined - - // ToDo- Get Curity based token when linked user is logged in - // const token = - // session.token.loginType === "eurobonus" - // ? session.token.curity_access_token ?? serviceToken - // : session.token.access_token - - const token = - session.token.loginType === "eurobonus" - ? serviceToken - : session.token.access_token - - return token -} diff --git a/packages/trpc/lib/utils/getUserPointsBalance.ts b/packages/trpc/lib/utils/getUserPointsBalance.ts deleted file mode 100644 index a01a47f57..000000000 --- a/packages/trpc/lib/utils/getUserPointsBalance.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getEuroBonusProfileData } from "../routers/partners/sas/getEuroBonusProfile" -import { getVerifiedUser } from "../routers/user/utils/getVerifiedUser" -import { isValidSession } from "./session" - -import type { Session } from "next-auth" - -export async function getUserPointsBalance( - session: Session | null -): Promise { - if (!isValidSession(session)) return undefined - - const verifiedUser = - session.token.loginType === "eurobonus" - ? await getEuroBonusProfileSafely(session) - : await getVerifiedUser({ session }) - - if (!verifiedUser || "error" in verifiedUser) { - return undefined - } - - const points = - "points" in verifiedUser - ? verifiedUser.points.total - : verifiedUser.data.membership?.currentPoints - - return points ?? 0 -} - -async function getEuroBonusProfileSafely(session: Session) { - try { - return await getEuroBonusProfileData({ - accessToken: session.token.access_token, - loginType: session.token.loginType, - }) - } catch (_error) { - return undefined - } -}