fix: refactor scopes for service token

This commit is contained in:
Christel Westerberg
2024-10-07 16:48:23 +02:00
committed by Pontus Dreij
parent 3c548923ff
commit ea8fdc940d
8 changed files with 128 additions and 110 deletions

View File

@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { getVerifiedUser } from "@/server/routers/user/query" import { getVerifiedUser } from "@/server/routers/user/query"
import { bookingServiceProcedure, router } from "@/server/trpc" import { router, serviceProcedure } from "@/server/trpc"
import { getMembership } from "@/utils/user" import { getMembership } from "@/utils/user"
@@ -36,7 +36,7 @@ async function getMembershipNumber(
export const bookingMutationRouter = router({ export const bookingMutationRouter = router({
booking: router({ booking: router({
create: bookingServiceProcedure create: serviceProcedure
.input(createBookingInput) .input(createBookingInput)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const { checkInDate, checkOutDate, hotelId } = input const { checkInDate, checkOutDate, hotelId } = input

View File

@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { bookingServiceProcedure, router } from "@/server/trpc" import { router, serviceProcedure } from "@/server/trpc"
import { getBookingStatusInput } from "./input" import { getBookingStatusInput } from "./input"
import { createBookingSchema } from "./output" import { createBookingSchema } from "./output"
@@ -17,69 +17,70 @@ const getBookingStatusFailCounter = meter.createCounter(
) )
export const bookingQueryRouter = router({ export const bookingQueryRouter = router({
status: bookingServiceProcedure status: serviceProcedure.input(getBookingStatusInput).query(async function ({
.input(getBookingStatusInput) ctx,
.query(async function ({ ctx, input }) { input,
const { confirmationNumber } = input }) {
getBookingStatusCounter.add(1, { confirmationNumber }) const { confirmationNumber } = input
getBookingStatusCounter.add(1, { confirmationNumber })
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}/status`, `${api.endpoints.v1.booking}/${confirmationNumber}/status`,
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, 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 apiJson = await apiResponse.json() if (!apiResponse.ok) {
const verifiedData = createBookingSchema.safeParse(apiJson) const responseMessage = await apiResponse.text()
if (!verifiedData.success) { getBookingStatusFailCounter.add(1, {
getBookingStatusFailCounter.add(1, { confirmationNumber,
confirmationNumber, error_type: "http_error",
error_type: "validation_error", error: responseMessage,
error: JSON.stringify(verifiedData.error), })
}) console.error(
console.error( "api.booking.status 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({ JSON.stringify({
query: { confirmationNumber }, 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
}),
}) })

View File

@@ -7,8 +7,8 @@ import { request } from "@/lib/graphql/request"
import { Context } from "@/server/context" import { Context } from "@/server/context"
import { notFound } from "@/server/errors/trpc" import { notFound } from "@/server/errors/trpc"
import { import {
contentStackBaseWithProfileServiceProcedure,
contentStackBaseWithProtectedProcedure, contentStackBaseWithProtectedProcedure,
contentStackBaseWithServiceProcedure,
router, router,
} from "@/server/trpc" } from "@/server/trpc"
@@ -260,7 +260,7 @@ export const rewardQueryRouter = router({
nextCursor, nextCursor,
} }
}), }),
byLevel: contentStackBaseWithProfileServiceProcedure byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput) .input(rewardsByLevelInput)
.query(async function ({ input, ctx }) { .query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1) getByLevelRewardCounter.add(1)
@@ -310,7 +310,7 @@ export const rewardQueryRouter = router({
getByLevelRewardSuccessCounter.add(1) getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}), }),
all: contentStackBaseWithProfileServiceProcedure all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput) .input(rewardsAllInput)
.query(async function ({ input, ctx }) { .query(async function ({ input, ctx }) {
getAllRewardCounter.add(1) getAllRewardCounter.add(1)

View File

@@ -11,10 +11,10 @@ import {
} from "@/server/errors/trpc" } from "@/server/errors/trpc"
import { extractHotelImages } from "@/server/routers/utils/hotels" import { extractHotelImages } from "@/server/routers/utils/hotels"
import { import {
contentStackUidWithHotelServiceProcedure, contentStackUidWithServiceProcedure,
hotelServiceProcedure,
publicProcedure, publicProcedure,
router, router,
serviceProcedure,
} from "@/server/trpc" } from "@/server/trpc"
import { toApiLang } from "@/server/utils" import { toApiLang } from "@/server/utils"
@@ -99,7 +99,7 @@ async function getContentstackData(
} }
export const hotelQueryRouter = router({ export const hotelQueryRouter = router({
get: contentStackUidWithHotelServiceProcedure get: contentStackUidWithServiceProcedure
.input(getHotelInputSchema) .input(getHotelInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { lang, uid } = ctx const { lang, uid } = ctx
@@ -264,7 +264,7 @@ export const hotelQueryRouter = router({
} }
}), }),
availability: router({ availability: router({
hotels: hotelServiceProcedure hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema) .input(getHotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const { const {
@@ -388,7 +388,7 @@ export const hotelQueryRouter = router({
.flatMap((hotels) => hotels.attributes), .flatMap((hotels) => hotels.attributes),
} }
}), }),
rooms: hotelServiceProcedure rooms: serviceProcedure
.input(getRoomsAvailabilityInputSchema) .input(getRoomsAvailabilityInputSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const { const {
@@ -543,7 +543,7 @@ export const hotelQueryRouter = router({
}), }),
}), }),
hotelData: router({ hotelData: router({
get: hotelServiceProcedure get: serviceProcedure
.input(getlHotelDataInputSchema) .input(getlHotelDataInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { hotelId, language, include } = input const { hotelId, language, include } = input
@@ -641,7 +641,7 @@ export const hotelQueryRouter = router({
}), }),
}), }),
locations: router({ locations: router({
get: hotelServiceProcedure.query(async function ({ ctx }) { get: serviceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams() const searchParams = new URLSearchParams()
searchParams.set("language", toApiLang(ctx.lang)) searchParams.set("language", toApiLang(ctx.lang))

View File

@@ -1,5 +1,4 @@
import { metrics } from "@opentelemetry/api" import { metrics } from "@opentelemetry/api"
import { SafeParseSuccess } from "zod"
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { import {

View File

@@ -1,13 +1,28 @@
import { metrics } from "@opentelemetry/api"
import { revalidateTag, unstable_cache } from "next/cache" import { revalidateTag, unstable_cache } from "next/cache"
import { env } from "@/env/server" import { env } from "@/env/server"
import { generateServiceTokenTag } from "@/utils/generateTag" import { generateServiceTokenTag } from "@/utils/generateTag"
import { ServiceTokenScope } from "@/types/enums/serviceToken" import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken"
import { ServiceTokenResponse } from "@/types/tokens" 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`, { const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -23,32 +38,45 @@ async function getServiceToken(scopes: ServiceTokenScope[]) {
}) })
if (!response.ok) { 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") throw new Error("Failed to obtain service token")
} }
return response.json() return response.json()
} }
export async function fetchServiceToken( export async function fetchServiceToken(): Promise<ServiceTokenResponse> {
scopes: ServiceTokenScope[]
): Promise<ServiceTokenResponse> {
try { try {
const tag = generateServiceTokenTag(scopes) const tag = generateServiceTokenTag()
const getCachedJwt = unstable_cache( const getCachedJwt = unstable_cache(
async (scopes) => { async () => {
const jwt = await getServiceToken(scopes) const jwt = await getServiceToken()
const expiresAt = Date.now() + jwt.expires_in * 1000 const expiresAt = Date.now() + jwt.expires_in * 1000
return { expiresAt, jwt } return { expiresAt, jwt }
}, },
scopes, [],
{ tags: [tag] } { tags: [tag] }
) )
const cachedJwt = await getCachedJwt(scopes) const cachedJwt = await getCachedJwt()
if (cachedJwt.expiresAt < Date.now()) { if (cachedJwt.expiresAt < Date.now()) {
console.log(
"trpc.context.serviceToken: Service token expired, revalidating tag"
)
revalidateTag(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 return newToken
} }

View File

@@ -125,29 +125,17 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
}) })
}) })
function createServiceProcedure(serviceName: ServiceTokenScope) { export const serviceProcedure = t.procedure.use(async (opts) => {
return t.procedure.use(async (opts) => { const { access_token } = await fetchServiceToken()
const { access_token } = await fetchServiceToken([serviceName]) if (!access_token) {
if (!access_token) { throw internalServerError(`Failed to obtain service token`)
throw internalServerError(`Failed to obtain ${serviceName} service token`) }
} return opts.next({
return opts.next({ ctx: {
ctx: { serviceToken: access_token,
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( export const serverActionProcedure = t.procedure.experimental_caller(
experimental_nextAppDirCaller({ experimental_nextAppDirCaller({
@@ -178,11 +166,11 @@ export const protectedServerActionProcedure = serverActionProcedure.use(
// NOTE: This is actually save to use, just the implementation could change // NOTE: This is actually save to use, just the implementation could change
// in minor version bumps. Please read: https://trpc.io/docs/faq#unstable // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable
export const contentStackUidWithHotelServiceProcedure = export const contentStackUidWithServiceProcedure =
contentstackExtendedProcedureUID.unstable_concat(hotelServiceProcedure) contentstackExtendedProcedureUID.unstable_concat(serviceProcedure)
export const contentStackBaseWithProfileServiceProcedure = export const contentStackBaseWithServiceProcedure =
contentstackBaseProcedure.unstable_concat(profileServiceProcedure) contentstackBaseProcedure.unstable_concat(serviceProcedure)
export const contentStackBaseWithProtectedProcedure = export const contentStackBaseWithProtectedProcedure =
contentstackBaseProcedure.unstable_concat(protectedProcedure) contentstackBaseProcedure.unstable_concat(protectedProcedure)

View File

@@ -1,3 +1,4 @@
import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken"
import { System } from "@/types/requests/system" import { System } from "@/types/requests/system"
import type { Edges } from "@/types/requests/utils/edges" import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs" import type { NodeRefs } from "@/types/requests/utils/refs"
@@ -106,6 +107,7 @@ export function generateLoyaltyConfigTag(
* @param serviceTokenScope scope of service token * @param serviceTokenScope scope of service token
* @returns string * @returns string
*/ */
export function generateServiceTokenTag(serviceTokenScopes: string[]) { export function generateServiceTokenTag() {
return `service_token:${serviceTokenScopes.join("-")}` const scopes = Object.keys(ServiceTokenScopeEnum).join("-")
return `service_token:${scopes}`
} }