diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 53595b2d0..1ff3422af 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" import { getVerifiedUser } from "@/server/routers/user/query" -import { bookingServiceProcedure, router } from "@/server/trpc" +import { router, serviceProcedure } from "@/server/trpc" import { getMembership } from "@/utils/user" @@ -36,7 +36,7 @@ async function getMembershipNumber( export const bookingMutationRouter = router({ booking: router({ - create: bookingServiceProcedure + create: serviceProcedure .input(createBookingInput) .mutation(async function ({ ctx, input }) { const { checkInDate, checkOutDate, hotelId } = input diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index f7f439b90..d053782bd 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" -import { bookingServiceProcedure, router } from "@/server/trpc" +import { router, serviceProcedure } from "@/server/trpc" import { getBookingStatusInput } from "./input" import { createBookingSchema } from "./output" @@ -17,69 +17,70 @@ const getBookingStatusFailCounter = meter.createCounter( ) export const bookingQueryRouter = router({ - status: bookingServiceProcedure - .input(getBookingStatusInput) - .query(async function ({ ctx, input }) { - const { confirmationNumber } = input - getBookingStatusCounter.add(1, { confirmationNumber }) + status: serviceProcedure.input(getBookingStatusInput).query(async function ({ + ctx, + input, + }) { + const { confirmationNumber } = input + getBookingStatusCounter.add(1, { confirmationNumber }) - const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}/status`, - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - } - ) - - if (!apiResponse.ok) { - const responseMessage = await apiResponse.text() - getBookingStatusFailCounter.add(1, { - confirmationNumber, - error_type: "http_error", - error: responseMessage, - }) - console.error( - "api.booking.status error", - JSON.stringify({ - query: { confirmationNumber }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text: responseMessage, - }, - }) - ) - - throw serverErrorByStatus(apiResponse.status, apiResponse) + const apiResponse = await api.get( + `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, } + ) - const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) - if (!verifiedData.success) { - getBookingStatusFailCounter.add(1, { - confirmationNumber, - error_type: "validation_error", - error: JSON.stringify(verifiedData.error), - }) - console.error( - "api.booking.status validation error", - JSON.stringify({ - query: { confirmationNumber }, - error: verifiedData.error, - }) - ) - throw badRequestError() - } - - getBookingStatusSuccessCounter.add(1, { confirmationNumber }) - console.info( - "api.booking.status success", + if (!apiResponse.ok) { + const responseMessage = await apiResponse.text() + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: responseMessage, + }) + console.error( + "api.booking.status error", JSON.stringify({ query: { confirmationNumber }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text: responseMessage, + }, }) ) - return verifiedData.data - }), + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + error: JSON.stringify(verifiedData.error), + }) + console.error( + "api.booking.status validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: verifiedData.error, + }) + ) + throw badRequestError() + } + + getBookingStatusSuccessCounter.add(1, { confirmationNumber }) + console.info( + "api.booking.status success", + JSON.stringify({ + query: { confirmationNumber }, + }) + ) + + return verifiedData.data + }), }) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 543cd46cc..ea6023fee 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -7,8 +7,8 @@ import { request } from "@/lib/graphql/request" import { Context } from "@/server/context" import { notFound } from "@/server/errors/trpc" import { - contentStackBaseWithProfileServiceProcedure, contentStackBaseWithProtectedProcedure, + contentStackBaseWithServiceProcedure, router, } from "@/server/trpc" @@ -260,7 +260,7 @@ export const rewardQueryRouter = router({ nextCursor, } }), - byLevel: contentStackBaseWithProfileServiceProcedure + byLevel: contentStackBaseWithServiceProcedure .input(rewardsByLevelInput) .query(async function ({ input, ctx }) { getByLevelRewardCounter.add(1) @@ -310,7 +310,7 @@ export const rewardQueryRouter = router({ getByLevelRewardSuccessCounter.add(1) return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } }), - all: contentStackBaseWithProfileServiceProcedure + all: contentStackBaseWithServiceProcedure .input(rewardsAllInput) .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 859e5662c..8ce752577 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -11,10 +11,10 @@ import { } from "@/server/errors/trpc" import { extractHotelImages } from "@/server/routers/utils/hotels" import { - contentStackUidWithHotelServiceProcedure, - hotelServiceProcedure, + contentStackUidWithServiceProcedure, publicProcedure, router, + serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -99,7 +99,7 @@ async function getContentstackData( } export const hotelQueryRouter = router({ - get: contentStackUidWithHotelServiceProcedure + get: contentStackUidWithServiceProcedure .input(getHotelInputSchema) .query(async ({ ctx, input }) => { const { lang, uid } = ctx @@ -264,7 +264,7 @@ export const hotelQueryRouter = router({ } }), availability: router({ - hotels: hotelServiceProcedure + hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { @@ -388,7 +388,7 @@ export const hotelQueryRouter = router({ .flatMap((hotels) => hotels.attributes), } }), - rooms: hotelServiceProcedure + rooms: serviceProcedure .input(getRoomsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { @@ -543,7 +543,7 @@ export const hotelQueryRouter = router({ }), }), hotelData: router({ - get: hotelServiceProcedure + get: serviceProcedure .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { const { hotelId, language, include } = input @@ -641,7 +641,7 @@ export const hotelQueryRouter = router({ }), }), locations: router({ - get: hotelServiceProcedure.query(async function ({ ctx }) { + get: serviceProcedure.query(async function ({ ctx }) { const searchParams = new URLSearchParams() searchParams.set("language", toApiLang(ctx.lang)) diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 28dee9cd2..df900953c 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,5 +1,4 @@ import { metrics } from "@opentelemetry/api" -import { SafeParseSuccess } from "zod" import * as api from "@/lib/api" import { diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 4681721e3..6df7def66 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -1,13 +1,28 @@ +import { metrics } from "@opentelemetry/api" import { revalidateTag, unstable_cache } from "next/cache" import { env } from "@/env/server" import { generateServiceTokenTag } from "@/utils/generateTag" -import { ServiceTokenScope } from "@/types/enums/serviceToken" +import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { ServiceTokenResponse } from "@/types/tokens" -async function getServiceToken(scopes: ServiceTokenScope[]) { +// OpenTelemetry metrics: Service token +const meter = metrics.getMeter("trpc.context.serviceToken") +const getServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.get-new-token" +) +const getTempServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.get-temporary" +) +const getServiceTokenFailCounter = meter.createCounter( + "trpc.context.serviceToken.get-fail" +) + +async function getServiceToken() { + getServiceTokenCounter.add(1) + const scopes = Object.keys(ServiceTokenScopeEnum) const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { @@ -23,32 +38,45 @@ async function getServiceToken(scopes: ServiceTokenScope[]) { }) if (!response.ok) { + getServiceTokenFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: response.status, + statusText: response.statusText, + }), + }) throw new Error("Failed to obtain service token") } return response.json() } -export async function fetchServiceToken( - scopes: ServiceTokenScope[] -): Promise { +export async function fetchServiceToken(): Promise { try { - const tag = generateServiceTokenTag(scopes) + const tag = generateServiceTokenTag() const getCachedJwt = unstable_cache( - async (scopes) => { - const jwt = await getServiceToken(scopes) + async () => { + const jwt = await getServiceToken() const expiresAt = Date.now() + jwt.expires_in * 1000 return { expiresAt, jwt } }, - scopes, + [], { tags: [tag] } ) - const cachedJwt = await getCachedJwt(scopes) + const cachedJwt = await getCachedJwt() if (cachedJwt.expiresAt < Date.now()) { + console.log( + "trpc.context.serviceToken: Service token expired, revalidating tag" + ) revalidateTag(tag) - const newToken = await getServiceToken(scopes) + + console.log( + "trpc.context.serviceToken: Fetching new temporary service token." + ) + getTempServiceTokenCounter.add(1) + const newToken = await getServiceToken() return newToken } diff --git a/server/trpc.ts b/server/trpc.ts index 2ea6a871e..94d4e47b5 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -125,29 +125,17 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -function createServiceProcedure(serviceName: ServiceTokenScope) { - return t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken([serviceName]) - if (!access_token) { - throw internalServerError(`Failed to obtain ${serviceName} service token`) - } - return opts.next({ - ctx: { - serviceToken: access_token, - }, - }) +export const serviceProcedure = t.procedure.use(async (opts) => { + const { access_token } = await fetchServiceToken() + if (!access_token) { + throw internalServerError(`Failed to obtain service token`) + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, }) -} - -export const bookingServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.booking -) -export const hotelServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.hotel -) -export const profileServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.profile -) +}) export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ @@ -178,11 +166,11 @@ export const protectedServerActionProcedure = serverActionProcedure.use( // NOTE: This is actually save to use, just the implementation could change // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable -export const contentStackUidWithHotelServiceProcedure = - contentstackExtendedProcedureUID.unstable_concat(hotelServiceProcedure) +export const contentStackUidWithServiceProcedure = + contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) -export const contentStackBaseWithProfileServiceProcedure = - contentstackBaseProcedure.unstable_concat(profileServiceProcedure) +export const contentStackBaseWithServiceProcedure = + contentstackBaseProcedure.unstable_concat(serviceProcedure) export const contentStackBaseWithProtectedProcedure = contentstackBaseProcedure.unstable_concat(protectedProcedure) diff --git a/utils/generateTag.ts b/utils/generateTag.ts index 41251245c..0dbf95d33 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -1,3 +1,4 @@ +import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { System } from "@/types/requests/system" import type { Edges } from "@/types/requests/utils/edges" import type { NodeRefs } from "@/types/requests/utils/refs" @@ -106,6 +107,7 @@ export function generateLoyaltyConfigTag( * @param serviceTokenScope scope of service token * @returns string */ -export function generateServiceTokenTag(serviceTokenScopes: string[]) { - return `service_token:${serviceTokenScopes.join("-")}` +export function generateServiceTokenTag() { + const scopes = Object.keys(ServiceTokenScopeEnum).join("-") + return `service_token:${scopes}` }