Browse Source

fix: queries not waiting for authentication

Andrew Bastin 4 years ago
parent
commit
2761894164

+ 50 - 11
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
+        }
+
+        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}`,
+            },
+          },
+        })
+      },
+      willAuthError({ authState }) {
+        return !authState || !authState.authToken
+      },
+      getAuth: async () => {
+        if (!probableUser$.value) return { authToken: null }
+
+        await waitProbableLoginToConfirm()
 
-    return {
-      headers: {
-        authorization: token ? `Bearer ${token}` : "",
+        return {
+          authToken: getAuthIDToken(),
+        }
       },
-    }
-  },
-  exchanges: [devtoolsExchange, ...defaultExchanges],
+    }),
+    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) =>

+ 42 - 3
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<string | null>(null)
  */
 export const authEvents$ = new Subject<AuthEvents>()
 
+/**
+ * Like currentUser$ but also gives probable user value
+ */
+export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
+
+/**
+ * Resolves when the probable login resolves into proper login
+ */
+export const waitProbableLoginToConfirm = () =>
+  new Promise<void>((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)
     }

+ 1 - 0
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",

+ 12 - 0
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': [email protected]
+      '@urql/exchange-auth': [email protected]
       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/[email protected]:
+    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': [email protected]
+      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