diff --git a/packages/frontend-2/lib/auth/composables/auth.ts b/packages/frontend-2/lib/auth/composables/auth.ts index f22fa5074..0da194777 100644 --- a/packages/frontend-2/lib/auth/composables/auth.ts +++ b/packages/frontend-2/lib/auth/composables/auth.ts @@ -27,6 +27,7 @@ import type { ActiveUserMainMetadataQuery } from '~~/lib/common/generated/gql/gr import { useScopedState } from '~/lib/common/composables/scopedState' import type { ApolloClient } from '@apollo/client/core' import { AuthFailedError } from '~/lib/auth/errors/errors' +import { useAppErrorState } from '~/lib/core/composables/error' type UseOnAuthStateChangeCallback = ( user: MaybeNullOrUndefined, @@ -220,6 +221,7 @@ export const useAuthManager = ( ) => { const { deferredApollo } = options || {} + const ssrEvent = useRequestEvent() const apiOrigin = useApiOrigin() const resetAuthState = useResetAuthState({ deferredApollo }) const route = useRoute() @@ -230,6 +232,7 @@ export const useAuthManager = ( const postAuthRedirect = usePostAuthRedirect() const { markLoggedOut } = useJustLoggedOutTracking() const { logger } = useSafeLogger() + const { isFullRedirectState } = useAppErrorState() /** * Invite token, if any @@ -259,6 +262,24 @@ export const useAuthManager = ( () => dashboardToken.value || embedToken.value || authToken.value ) + /** + * Trigger full redirect that causes a full reload, instead of an in-session navigation + */ + const sendFullRedirect = async (relativeUrl: string) => { + if (isFullRedirectState.value) return + + isFullRedirectState.value = true + + if (import.meta.client) { + window.location.href = relativeUrl + } else if (ssrEvent) { + const { sendRedirect } = await import('h3') + await sendRedirect(ssrEvent, relativeUrl) + } else { + logger().fatal('Failed to send full redirect') + } + } + /** * Set/clear new token value and redirect to home */ @@ -513,10 +534,10 @@ export const useAuthManager = ( } if (!options?.skipRedirect) { - if (options?.forceFullReload && import.meta.client) { - window.location.href = loginRoute - } else { + if (!options?.forceFullReload) { await goToLogin() + } else { + await sendFullRedirect(loginRoute) } } } diff --git a/packages/frontend-2/lib/core/composables/error.ts b/packages/frontend-2/lib/core/composables/error.ts index a19084aef..fe0a5c952 100644 --- a/packages/frontend-2/lib/core/composables/error.ts +++ b/packages/frontend-2/lib/core/composables/error.ts @@ -15,7 +15,8 @@ const ENTER_STATE_AT_ERRORS_PER_MIN = 100 export function useAppErrorState() { const state = useScopedState('appErrorState', () => ({ inErrorState: ref(false), - errorRpm: Observability.simpleRpmCounter() + errorRpm: Observability.simpleRpmCounter(), + isFullRedirectState: ref(false) })) const nuxtApp = useNuxtApp() @@ -32,7 +33,22 @@ export function useAppErrorState() { ) state.inErrorState.value = true } - } + }, + /** + * Similar to error state, except we don't show any UI elements to the user, we just stop processing + * API calls etc. because we're redirecting fully away + */ + isFullRedirectState: state.isFullRedirectState, + /** + * Whether to prevent HTTP API calls + */ + preventHttpCalls: computed(() => state.isFullRedirectState.value), + /** + * Whether to prevent websocket messaging + */ + preventWebsocketMessaging: computed( + () => state.isFullRedirectState.value || state.inErrorState.value + ) } } diff --git a/packages/frontend-2/lib/core/configs/apollo.ts b/packages/frontend-2/lib/core/configs/apollo.ts index aa5c718c9..063265ec6 100644 --- a/packages/frontend-2/lib/core/configs/apollo.ts +++ b/packages/frontend-2/lib/core/configs/apollo.ts @@ -4,7 +4,7 @@ import { SubscriptionClient } from 'subscriptions-transport-ws' import type { ApolloConfigResolver } from '~~/lib/core/nuxt-modules/apollo/module' import createUploadLink from 'apollo-upload-client/createUploadLink.mjs' import { WebSocketLink } from '@apollo/client/link/ws' -import { getMainDefinition } from '@apollo/client/utilities' +import { getMainDefinition, Observable } from '@apollo/client/utilities' import { Kind } from 'graphql' import type { GraphQLError, OperationDefinitionNode } from 'graphql' import type { CookieRef, NuxtApp } from '#app' @@ -441,7 +441,23 @@ function createLink(params: { logout: ReturnType['logout'] }): ApolloLink { const { httpEndpoint, wsClient, authToken, nuxtApp, reqId, logout } = params - const { registerError, isErrorState } = useAppErrorState() + const { + registerError, + preventHttpCalls, + preventWebsocketMessaging, + isFullRedirectState + } = useAppErrorState() + + const stopLink = new ApolloLink((operation, forward) => { + if (preventHttpCalls.value) { + // swallow the req, we're blocking them all + return new Observable(() => { + return () => {} + }) + } + + return forward(operation) + }) const errorLink = onError((res) => { const logger = nuxtApp.$logger @@ -487,7 +503,7 @@ function createLink(params: { } const { networkError } = res - if (networkError && isInvalidAuth(networkError)) { + if (networkError && isInvalidAuth(networkError) && !isFullRedirectState.value) { // Reset auth // since this may happen mid-routing, a standard router.push call may not work - do full reload void logout({ skipToast: true, forceFullReload: true }) @@ -577,7 +593,7 @@ function createLink(params: { wsClient.use([ { applyMiddleware: (_opt, next) => { - if (isErrorState.value) { + if (preventWebsocketMessaging.value) { return // never invokes next() - essentially stuck } @@ -614,7 +630,7 @@ function createLink(params: { }) }) - return from([...(import.meta.server ? [loggerLink] : []), errorLink, link]) + return from([stopLink, ...(import.meta.server ? [loggerLink] : []), errorLink, link]) } const defaultConfigResolver: ApolloConfigResolver = () => {