From e6707c1e4a43c1cf8f17d9b28d1735a4a476e145 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 | 62 +++++++++++++++++++ 4 files changed, 155 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 bea696052..a6e9ff2c6 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.22.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 937dabf3c..f77b7f741 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,7 @@ importers: packages/hoppscotch-app: specifiers: +<<<<<<< HEAD "@apollo/client": ^3.4.16 "@babel/core": ^7.15.8 "@babel/preset-env": ^7.15.8 @@ -50,6 +51,43 @@ importers: "@urql/devtools": ^2.0.3 "@vue/runtime-dom": ^3.2.20 "@vue/test-utils": ^1.2.2 +======= + '@apollo/client': ^3.4.13 + '@babel/core': ^7.15.5 + '@babel/preset-env': ^7.15.6 + '@commitlint/cli': ^13.1.0 + '@commitlint/config-conventional': ^13.1.0 + '@hoppscotch/js-sandbox': workspace:^1.0.0 + '@nuxt/types': ^2.15.8 + '@nuxt/typescript-build': ^2.1.0 + '@nuxtjs/axios': ^5.13.6 + '@nuxtjs/color-mode': ^2.1.1 + '@nuxtjs/composition-api': ^0.29.2 + '@nuxtjs/dotenv': ^1.4.1 + '@nuxtjs/eslint-config-typescript': ^6.0.1 + '@nuxtjs/eslint-module': ^3.0.2 + '@nuxtjs/google-analytics': ^2.4.0 + '@nuxtjs/google-fonts': ^1.3.0 + '@nuxtjs/gtm': ^2.4.0 + '@nuxtjs/i18n': ^7.0.3 + '@nuxtjs/pwa': ^3.3.5 + '@nuxtjs/robots': ^2.5.0 + '@nuxtjs/sitemap': ^2.4.0 + '@nuxtjs/stylelint-module': ^4.0.0 + '@nuxtjs/svg': ^0.2.0 + '@nuxtjs/toast': ^3.3.1 + '@testing-library/jest-dom': ^5.14.1 + '@types/codemirror': ^5.60.3 + '@types/cookie': ^0.4.1 + '@types/esprima': ^4.0.3 + '@types/lodash': ^4.14.174 + '@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.19 + '@vue/test-utils': ^1.2.2 +>>>>>>> ecdc7919 (fix: queries not waiting for authentication) acorn: ^8.5.0 acorn-walk: ^8.2.0 axios: ^0.22.0 @@ -109,6 +147,7 @@ importers: worker-loader: ^3.0.8 yargs-parser: ^20.2.9 dependencies: +<<<<<<< HEAD "@apollo/client": 3.4.16_graphql@15.6.1 "@hoppscotch/js-sandbox": link:../hoppscotch-js-sandbox "@nuxtjs/axios": 5.13.6 @@ -119,6 +158,19 @@ importers: "@nuxtjs/sitemap": 2.4.0 "@nuxtjs/toast": 3.3.1 "@urql/core": 2.3.3_graphql@15.6.0 +======= + '@apollo/client': 3.4.13_graphql@15.6.0 + '@hoppscotch/js-sandbox': link:../hoppscotch-js-sandbox + '@nuxtjs/axios': 5.13.6 + '@nuxtjs/composition-api': 0.29.2_nuxt@2.15.8 + '@nuxtjs/gtm': 2.4.0 + '@nuxtjs/i18n': 7.0.3 + '@nuxtjs/robots': 2.5.0 + '@nuxtjs/sitemap': 2.4.0 + '@nuxtjs/toast': 3.3.1 + '@urql/core': 2.3.3_graphql@15.6.0 + '@urql/exchange-auth': 0.1.6_graphql@15.6.0 +>>>>>>> ecdc7919 (fix: queries not waiting for authentication) acorn: 8.5.0 acorn-walk: 8.2.0 axios: 0.22.0 @@ -5933,6 +5985,16 @@ packages: wonka: 4.0.15 dev: true + /@urql/exchange-auth/0.1.6_graphql@15.6.0: + 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.6.0 + graphql: 15.6.0 + wonka: 4.0.15 + dev: false + /@vue/babel-helper-vue-jsx-merge-props/1.2.1: resolution: {