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 6b8bc618dc
commit 2761894164
4 changed files with 105 additions and 14 deletions

View File

@@ -11,16 +11,24 @@ import {
createClient, createClient,
TypedDocumentNode, TypedDocumentNode,
OperationResult, OperationResult,
defaultExchanges, dedupExchange,
OperationContext, OperationContext,
cacheExchange,
fetchExchange,
makeOperation,
} from "@urql/core" } from "@urql/core"
import { authExchange } from "@urql/exchange-auth"
import { devtoolsExchange } from "@urql/devtools" import { devtoolsExchange } from "@urql/devtools"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe, constVoid } from "fp-ts/function" import { pipe, constVoid } from "fp-ts/function"
import { subscribe } from "wonka" import { subscribe } from "wonka"
import clone from "lodash/clone" import clone from "lodash/clone"
import { getAuthIDToken } from "~/helpers/fb/auth" import {
getAuthIDToken,
probableUser$,
waitProbableLoginToConfirm,
} from "~/helpers/fb/auth"
const BACKEND_GQL_URL = const BACKEND_GQL_URL =
process.env.CONTEXT === "production" process.env.CONTEXT === "production"
@@ -29,16 +37,47 @@ const BACKEND_GQL_URL =
const client = createClient({ const client = createClient({
url: BACKEND_GQL_URL, url: BACKEND_GQL_URL,
fetchOptions: () => { exchanges: [
const token = getAuthIDToken() 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 { return {
headers: { authToken: getAuthIDToken(),
authorization: token ? `Bearer ${token}` : "",
},
} }
}, },
exchanges: [devtoolsExchange, ...defaultExchanges], }),
fetchExchange,
],
}) })
/** /**
@@ -118,7 +157,7 @@ export function useGQLQuery<
// Take the network error value // Take the network error value
result.error?.networkError, result.error?.networkError,
// If it null, set the left to the generic error name // If it null, set the left to the generic error name
E.fromNullable(result.error?.name), E.fromNullable(result.error?.message),
E.match( E.match(
// The left case (network error was null) // The left case (network error was null)
(gqlErr) => (gqlErr) =>

View File

@@ -33,6 +33,11 @@ import {
Subscription, Subscription,
} from "rxjs" } from "rxjs"
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
import {
setLocalConfig,
getLocalConfig,
removeLocalConfig,
} from "~/newstore/localpersistence"
export type HoppUser = User & { export type HoppUser = User & {
provider?: string provider?: string
@@ -40,9 +45,10 @@ export type HoppUser = User & {
} }
type AuthEvents = type AuthEvents =
| { event: "login"; user: HoppUser } | { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication
| { event: "logout" } | { event: "login"; user: HoppUser } // We are authenticated
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } | { 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) * 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>() 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 * Initializes the firebase authentication related subjects
*/ */
@@ -67,6 +93,17 @@ export function initAuth() {
let extraSnapshotStop: (() => void) | null = null 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) => { onAuthStateChanged(auth, (user) => {
/** Whether the user was logged in before */ /** Whether the user was logged in before */
const wasLoggedIn = currentUser$.value !== null const wasLoggedIn = currentUser$.value !== null
@@ -135,6 +172,8 @@ export function initAuth() {
newToken: authIdToken$.value, newToken: authIdToken$.value,
user: currentUser$.value!!, // Force not-null because user is defined user: currentUser$.value!!, // Force not-null because user is defined
}) })
setLocalConfig("login_state", JSON.stringify(user))
} else { } else {
authIdToken$.next(null) authIdToken$.next(null)
} }

View File

@@ -36,6 +36,7 @@
"@nuxtjs/sitemap": "^2.4.0", "@nuxtjs/sitemap": "^2.4.0",
"@nuxtjs/toast": "^3.3.1", "@nuxtjs/toast": "^3.3.1",
"@urql/core": "^2.3.3", "@urql/core": "^2.3.3",
"@urql/exchange-auth": "^0.1.6",
"acorn": "^8.5.0", "acorn": "^8.5.0",
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"axios": "^0.24.0", "axios": "^0.24.0",

12
pnpm-lock.yaml generated
View File

@@ -49,6 +49,7 @@ importers:
'@types/splitpanes': ^2.2.1 '@types/splitpanes': ^2.2.1
'@urql/core': ^2.3.3 '@urql/core': ^2.3.3
'@urql/devtools': ^2.0.3 '@urql/devtools': ^2.0.3
'@urql/exchange-auth': ^0.1.6
'@vue/runtime-dom': ^3.2.20 '@vue/runtime-dom': ^3.2.20
'@vue/test-utils': ^1.2.2 '@vue/test-utils': ^1.2.2
acorn: ^8.5.0 acorn: ^8.5.0
@@ -123,6 +124,7 @@ importers:
'@nuxtjs/sitemap': 2.4.0 '@nuxtjs/sitemap': 2.4.0
'@nuxtjs/toast': 3.3.1 '@nuxtjs/toast': 3.3.1
'@urql/core': 2.3.3_graphql@15.7.2 '@urql/core': 2.3.3_graphql@15.7.2
'@urql/exchange-auth': 0.1.6_graphql@15.7.2
acorn: 8.5.0 acorn: 8.5.0
acorn-walk: 8.2.0 acorn-walk: 8.2.0
axios: 0.24.0 axios: 0.24.0
@@ -4692,6 +4694,16 @@ packages:
wonka: 4.0.15 wonka: 4.0.15
dev: true 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: /@vue/babel-helper-vue-jsx-merge-props/1.2.1:
resolution: {integrity: sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==} resolution: {integrity: sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==}
dev: false dev: false