From 27618941646b44de63e40b8edad613d63fbaab0b Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Sun, 3 Oct 2021 19:12:31 +0530 Subject: [PATCH] fix: queries not waiting for authentication --- .../helpers/backend/GQLClient.ts | 61 +++++++++++++++---- packages/hoppscotch-app/helpers/fb/auth.ts | 45 +++++++++++++- packages/hoppscotch-app/package.json | 1 + pnpm-lock.yaml | 12 ++++ 4 files changed, 105 insertions(+), 14 deletions(-) diff --git a/packages/hoppscotch-app/helpers/backend/GQLClient.ts b/packages/hoppscotch-app/helpers/backend/GQLClient.ts index 6613d7a6f..9aadfe86e 100644 --- a/packages/hoppscotch-app/helpers/backend/GQLClient.ts +++ b/packages/hoppscotch-app/helpers/backend/GQLClient.ts @@ -11,16 +11,24 @@ import { createClient, TypedDocumentNode, OperationResult, - defaultExchanges, + dedupExchange, OperationContext, + cacheExchange, + fetchExchange, + makeOperation, } from "@urql/core" +import { authExchange } from "@urql/exchange-auth" import { devtoolsExchange } from "@urql/devtools" import * as E from "fp-ts/Either" import * as TE from "fp-ts/TaskEither" import { pipe, constVoid } from "fp-ts/function" import { subscribe } from "wonka" import clone from "lodash/clone" -import { getAuthIDToken } from "~/helpers/fb/auth" +import { + getAuthIDToken, + probableUser$, + waitProbableLoginToConfirm, +} from "~/helpers/fb/auth" const BACKEND_GQL_URL = process.env.CONTEXT === "production" @@ -29,16 +37,47 @@ const BACKEND_GQL_URL = const client = createClient({ url: BACKEND_GQL_URL, - fetchOptions: () => { - const token = getAuthIDToken() + exchanges: [ + devtoolsExchange, + dedupExchange, + cacheExchange, + authExchange({ + addAuthToOperation({ authState, operation }) { + if (!authState || !authState.authToken) { + return operation + } - return { - headers: { - authorization: token ? `Bearer ${token}` : "", + const fetchOptions = + typeof operation.context.fetchOptions === "function" + ? operation.context.fetchOptions() + : operation.context.fetchOptions || {} + + return makeOperation(operation.kind, operation, { + ...operation.context, + fetchOptions: { + ...fetchOptions, + headers: { + ...fetchOptions.headers, + Authorization: `Bearer ${authState.authToken}`, + }, + }, + }) }, - } - }, - exchanges: [devtoolsExchange, ...defaultExchanges], + willAuthError({ authState }) { + return !authState || !authState.authToken + }, + getAuth: async () => { + if (!probableUser$.value) return { authToken: null } + + await waitProbableLoginToConfirm() + + return { + authToken: getAuthIDToken(), + } + }, + }), + fetchExchange, + ], }) /** @@ -118,7 +157,7 @@ export function useGQLQuery< // Take the network error value result.error?.networkError, // If it null, set the left to the generic error name - E.fromNullable(result.error?.name), + E.fromNullable(result.error?.message), E.match( // The left case (network error was null) (gqlErr) => diff --git a/packages/hoppscotch-app/helpers/fb/auth.ts b/packages/hoppscotch-app/helpers/fb/auth.ts index 88767e4c2..d6f24b20c 100644 --- a/packages/hoppscotch-app/helpers/fb/auth.ts +++ b/packages/hoppscotch-app/helpers/fb/auth.ts @@ -33,6 +33,11 @@ import { Subscription, } from "rxjs" import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" +import { + setLocalConfig, + getLocalConfig, + removeLocalConfig, +} from "~/newstore/localpersistence" export type HoppUser = User & { provider?: string @@ -40,9 +45,10 @@ export type HoppUser = User & { } type AuthEvents = - | { event: "login"; user: HoppUser } - | { event: "logout" } - | { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } + | { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication + | { event: "login"; user: HoppUser } // We are authenticated + | { event: "logout" } // No authentication and we have no previous state + | { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Token has been updated /** * A BehaviorSubject emitting the currently logged in user (or null if not logged in) @@ -58,6 +64,26 @@ export const authIdToken$ = new BehaviorSubject(null) */ export const authEvents$ = new Subject() +/** + * Like currentUser$ but also gives probable user value + */ +export const probableUser$ = new BehaviorSubject(null) + +/** + * Resolves when the probable login resolves into proper login + */ +export const waitProbableLoginToConfirm = () => + new Promise((resolve, reject) => { + if (authIdToken$.value) resolve() + + if (!probableUser$.value) reject(new Error("no_probable_user")) + + const sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => { + sub?.unsubscribe() + resolve() + }) + }) + /** * Initializes the firebase authentication related subjects */ @@ -67,6 +93,17 @@ export function initAuth() { let extraSnapshotStop: (() => void) | null = null + probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null")) + + currentUser$.subscribe((user) => { + if (user) { + probableUser$.next(user) + } else { + probableUser$.next(null) + removeLocalConfig("login_state") + } + }) + onAuthStateChanged(auth, (user) => { /** Whether the user was logged in before */ const wasLoggedIn = currentUser$.value !== null @@ -135,6 +172,8 @@ export function initAuth() { newToken: authIdToken$.value, user: currentUser$.value!!, // Force not-null because user is defined }) + + setLocalConfig("login_state", JSON.stringify(user)) } else { authIdToken$.next(null) } diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json index 8cdf18dfd..5259d0c71 100644 --- a/packages/hoppscotch-app/package.json +++ b/packages/hoppscotch-app/package.json @@ -36,6 +36,7 @@ "@nuxtjs/sitemap": "^2.4.0", "@nuxtjs/toast": "^3.3.1", "@urql/core": "^2.3.3", + "@urql/exchange-auth": "^0.1.6", "acorn": "^8.5.0", "acorn-walk": "^8.2.0", "axios": "^0.24.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d07a57526..bbdf55c0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,7 @@ importers: '@types/splitpanes': ^2.2.1 '@urql/core': ^2.3.3 '@urql/devtools': ^2.0.3 + '@urql/exchange-auth': ^0.1.6 '@vue/runtime-dom': ^3.2.20 '@vue/test-utils': ^1.2.2 acorn: ^8.5.0 @@ -123,6 +124,7 @@ importers: '@nuxtjs/sitemap': 2.4.0 '@nuxtjs/toast': 3.3.1 '@urql/core': 2.3.3_graphql@15.7.2 + '@urql/exchange-auth': 0.1.6_graphql@15.7.2 acorn: 8.5.0 acorn-walk: 8.2.0 axios: 0.24.0 @@ -4692,6 +4694,16 @@ packages: wonka: 4.0.15 dev: true + /@urql/exchange-auth/0.1.6_graphql@15.7.2: + resolution: {integrity: sha512-jVyUaV+hHe3p2rIJauh6lgILMAjXOsHQ98xjKhUF3TXYx88TZXuBIl5DPZwnMcGra8YPOSHO/Wsn6NEjO5hQ+Q==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 + dependencies: + '@urql/core': 2.3.3_graphql@15.7.2 + graphql: 15.7.2 + wonka: 4.0.15 + dev: false + /@vue/babel-helper-vue-jsx-merge-props/1.2.1: resolution: {integrity: sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==} dev: false