00d6c08db2
When getAccessTokenSilently fails with login_required, invalid_grant, missing_refresh_token, consent_required or interaction_required, log the user out (without redirect) so the UI reflects reality instead of appearing logged in while every authenticated query silently fails.
122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
import { ApolloClient, ApolloLink, createHttpLink, from, InMemoryCache, split } from '@apollo/client/core'
|
|
import { setContext } from '@apollo/client/link/context'
|
|
import { WebSocketLink } from '@apollo/client/link/ws'
|
|
import { getMainDefinition } from '@apollo/client/utilities'
|
|
import type { GetTokenSilentlyOptions } from '@auth0/auth0-spa-js'
|
|
import type { Auth0VueClient } from '@auth0/auth0-vue'
|
|
import { SpanKind, TraceFlags } from '@opentelemetry/api'
|
|
import { DefaultApolloClient, provideApolloClient } from '@vue/apollo-composable'
|
|
|
|
import { defineNuxtPlugin, useNuxtApp } from '#app'
|
|
import { envConfig } from '~/utils/environment'
|
|
|
|
const apiUrl = envConfig(window.location.hostname).apiUrl
|
|
const wsUrl = apiUrl.replace(/^http/, 'ws')
|
|
|
|
const cache = new InMemoryCache({
|
|
typePolicies: {
|
|
},
|
|
})
|
|
|
|
const STALE_AUTH_ERRORS = new Set([
|
|
'login_required',
|
|
'consent_required',
|
|
'interaction_required',
|
|
'invalid_grant',
|
|
'missing_refresh_token',
|
|
])
|
|
|
|
const getToken = async (options: GetTokenSilentlyOptions) => {
|
|
const nuxtApp = useNuxtApp()
|
|
const auth0: Auth0VueClient = nuxtApp.$auth0 as Auth0VueClient
|
|
return await auth0.getAccessTokenSilently(options).catch((err) => {
|
|
const code = err && typeof err === 'object' && 'error' in err ? (err as { error?: string }).error : undefined
|
|
if (code && STALE_AUTH_ERRORS.has(code)) {
|
|
auth0.logout({ openUrl: false }).catch(() => {})
|
|
}
|
|
return undefined
|
|
})
|
|
}
|
|
|
|
const httpLink = createHttpLink({
|
|
uri: apiUrl,
|
|
})
|
|
|
|
const wsLink = new WebSocketLink({
|
|
uri: wsUrl,
|
|
options: {
|
|
reconnect: true,
|
|
lazy: true,
|
|
connectionParams: () => {
|
|
return getToken({}).then((token) => ({
|
|
authToken: token,
|
|
}))
|
|
},
|
|
},
|
|
})
|
|
|
|
const authLink = setContext(async (_, { headers }) => {
|
|
return await getToken({}).then((token) => ({
|
|
headers: {
|
|
...headers,
|
|
authorization: token ? `Bearer ${token}` : '',
|
|
},
|
|
}))
|
|
})
|
|
|
|
const createSpanLink = new ApolloLink((operation, forward) => {
|
|
const nuxtApp = useNuxtApp()
|
|
if (nuxtApp.$faro) {
|
|
const { trace } = nuxtApp.$faro.api.getOTEL()
|
|
const span = trace.getTracer('default').startSpan(`gql.${operation.operationName}`, {
|
|
kind: SpanKind.INTERNAL, // 0: Internal, 1: Server, 2: Client, 3: Producer, 4: Consumer
|
|
})
|
|
const spanContext = span.spanContext()
|
|
const traceParent = '00' + '-' + spanContext.traceId + '-' + spanContext.spanId + '-0' + Number(spanContext.traceFlags || TraceFlags.NONE).toString(16)
|
|
operation.setContext({ span, headers: { ...operation.getContext().headers, 'traceparent': traceParent } })
|
|
|
|
return forward(operation).map((data) => {
|
|
span.end()
|
|
return data
|
|
})
|
|
}
|
|
return forward(operation)
|
|
})
|
|
|
|
const link =
|
|
from([
|
|
createSpanLink,
|
|
split(
|
|
({ query }) => {
|
|
const definition = getMainDefinition(query)
|
|
return (
|
|
definition.kind === 'OperationDefinition' &&
|
|
definition.operation === 'subscription'
|
|
)
|
|
},
|
|
authLink.concat(wsLink),
|
|
authLink.concat(httpLink),
|
|
),
|
|
])
|
|
|
|
const instance = new ApolloClient({
|
|
devtools: {
|
|
enabled: true,
|
|
},
|
|
link,
|
|
cache,
|
|
defaultOptions: {
|
|
query: {
|
|
fetchPolicy: 'cache-first',
|
|
},
|
|
watchQuery: {
|
|
fetchPolicy: 'cache-and-network',
|
|
},
|
|
},
|
|
})
|
|
|
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
nuxtApp.provide(DefaultApolloClient[Symbol.toStringTag], instance)
|
|
provideApolloClient(instance)
|
|
})
|