Files
web/apps/scandic-web/server/tokenManager.ts
Linus Flood dd3fed9423 Merged in fix/cache-service-token (pull request #1571)
fix(servicetoken): cache it when using unstable_cache

* fix(servicetoken): cache it when using unstable_cache

* Refactor and using cacheOrGet as get/set

* Refactor

* Use expiresAt from cached token


Approved-by: Anton Gunnarsson
2025-03-19 09:16:11 +00:00

125 lines
3.2 KiB
TypeScript

import { metrics, trace, type Tracer } from "@opentelemetry/api"
import { env } from "@/env/server"
import { getCacheClient } from "@/services/dataCache"
import type { ServiceTokenResponse } from "@/types/tokens"
// OpenTelemetry metrics: Service token
const meter = metrics.getMeter("trpc.context.serviceToken")
const fetchServiceTokenCounter = meter.createCounter(
"trpc.context.serviceToken.fetch-new-token"
)
const fetchServiceTokenFailCounter = meter.createCounter(
"trpc.context.serviceToken.fetch-fail"
)
export async function getServiceToken() {
const tracer = trace.getTracer("getServiceToken")
return await tracer.startActiveSpan("getServiceToken", async () => {
const scopes = env.ENABLE_BOOKING_FLOW
? ["profile", "hotel", "booking", "package", "availability"]
: ["profile"]
const cacheKey = getServiceTokenCacheKey(scopes)
const cacheClient = await getCacheClient()
const token = await getOrSetServiceTokenFromCache(cacheKey, scopes, tracer)
if (token.expiresAt < Date.now()) {
await cacheClient.deleteKey(cacheKey)
const newToken = await getOrSetServiceTokenFromCache(
cacheKey,
scopes,
tracer
)
return newToken.jwt
}
return token.jwt
})
}
async function getOrSetServiceTokenFromCache(
cacheKey: string,
scopes: string[],
tracer: Tracer
) {
const cacheClient = await getCacheClient()
const token = await cacheClient.cacheOrGet(
cacheKey,
async () => {
return await tracer.startActiveSpan("fetch new token", async () => {
const newToken = await getJwt(scopes)
return newToken
})
},
"1h"
)
return token
}
async function getJwt(scopes: string[]) {
fetchServiceTokenCounter.add(1)
const jwt = await fetchServiceToken(scopes)
const expiresAt = Date.now() + jwt.expires_in * 1000
return { expiresAt, jwt }
}
async function fetchServiceToken(scopes: string[]) {
fetchServiceTokenCounter.add(1)
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: env.CURITY_CLIENT_ID_SERVICE,
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
scope: scopes.join(" "),
}),
})
if (!response.ok) {
const text = await response.text()
const error = {
status: response.status,
statusText: response.statusText,
text,
}
fetchServiceTokenFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify(error),
})
console.error(
"fetchServiceToken error",
JSON.stringify({
query: {
grant_type: "client_credentials",
client_id: env.CURITY_CLIENT_ID_SERVICE,
scope: scopes.join(" "),
},
error,
})
)
throw new Error(
`[fetchServiceToken] Failed to obtain service token: ${JSON.stringify(error)}`
)
}
return response.json() as Promise<ServiceTokenResponse>
}
function getServiceTokenCacheKey(scopes: string[]): string {
return `serviceToken:${scopes.join(",")}`
}