fix: queries not waiting for authentication

This commit is contained in:
Andrew Bastin
2021-10-03 19:12:31 +05:30
committed by liyasthomas
parent 41be5cc4a8
commit e6707c1e4a
4 changed files with 155 additions and 14 deletions

View File

@@ -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) =>

View File

@@ -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)
}

View File

@@ -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",

62
pnpm-lock.yaml generated
View File

@@ -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:
{