从 Gatsby 中的异步 getIdTokenClaims() 使用 JWT 进行 GraphQL 查询

GraphQL query with JWT from async getIdTokenClaims() in Gatsby

已更新

如@shahroon-farooqi 所示,此模式在 urql here including rationale for the wonka library dependency.

中有详细记录

更新了工作演示 on GitHub


我是 React 的新手,仍然无法弄清楚异步等待如何与挂钩相结合。

我有一个最小的 Gatsby/React 启动项目(我在 GitHub here 上的完整代码)是我从其他示例拼凑而成的:

  1. Signs-in 并从 Auth0 获取一个 JWT 并保存在本地存储中;然后
  2. 在 GraphQL 请求中包含该 JWT 以获取和显示组织列表。

当这在浏览器中作为 2 个单独的页面加载进行处理时(即我首先单击 sign-in 然后我被重定向到查看组织列表页面)它按预期工作。但是一旦我已经 signed-in 并单击组织页面列表上的浏览器刷新按钮,我可以看到 GraphQL 提取失败,因为它在客户端有机会将 JWT 加载到 header。 JWT 稍后成功加载,但我需要在尝试 GraphQL 提取之前发生。

身份验证和客户端作为包装器传递:

// gatsby-browser.js    
export const wrapRootElement = ({ element }) => {
  return (
    <Auth0Provider
      domain={process.env.GATSBY_AUTH0_DOMAIN}
      redirectUri={process.env.GATSBY_AUTH0_REDIRECT_URI}
      ...
    >
      <AuthApolloProvider>{element}</AuthApolloProvider>
    </Auth0Provider>
  );
}

客户端是这样设置 JWT 的:

// src/api/AuthApolloProvider.tsx
const setupClient = (_token) => {
  return createClient({
    url: process.env.GATSBY_HASURA_GRAPHQL_URL,
    fetchOptions: () => {
      return {
        headers: {
          authorization: _token ? `Bearer ${_token}` : "",
        },
      };
    },
  });
};
const AuthApolloProvider = ({ children }) => {
  const { getAccessTokenSilently, isAuthenticated, getIdTokenClaims } =
    useAuth0();
  const [token, setToken] = useState("");
  const [client, setClient] = useState(setupClient(token));
  useEffect(() => {
    (async () => {
      if (isAuthenticated) {
        const tokenClaims = await getIdTokenClaims();
        setToken(tokenClaims.__raw);
      }
    })();
  }, [isAuthenticated, getAccessTokenSilently]);
  useEffect(() => {
    setClient(setupClient(token));
  }, [token]);
  return <Provider value={client}>{children}</Provider>;
};

然后我有这个控制器来获取组织列表:

// src/controllers/Organizations.ts
import { useQuery } from "urql"

const GET_ORGANIZATIONS = `
query get_organizations {
  organizations {
    name
    label
  }
}
`
export const useOrganizations = () => {
  const [{ data, fetching }] = useQuery({ query: GET_ORGANIZATIONS })
  return {
    organizations: data?.organizations,
    loading: fetching,
  }
}

最后是我的组织组件列表:

// src/components/Organizations/OrganizationList.tsx
import { useOrganizations } from "../../controllers/Organizations";

const OrganizationList = () => {
  const { organizations, loading } = useOrganizations();
  return (
    <>
      {loading ? (
        <p>Loading...</p>
      ) : (
        organizations.map((organization: OrganizationItemType) => (
          <OrganizationItem
            organization={organization}
            key={organization.name}
          />
        ))
      )}
    </>
  );
};

据我了解,在 AuthApolloProvider 中的异步方法完成并成功将 JWT 加载到客户端之前,我不想在组件中进行 useOrganizations() 调用。

因为我是 React 的新手,而且我是从其他示例中拼凑出来的,所以我不确定如何处理这个问题 - 任何帮助都会很棒。

  useEffect(() => {
    if (token.length === 0) return
    setClient(setupClient(token))
  }, [token])

当令牌为空时,您可能不想setClient/setupClient? useEffect 中的代码至少执行 2 次。

  1. 组件挂载(此时token还是空的)
  2. 令牌值更改时

我修改了你的ApolloProvider。您需要添加 Wonka,因为 urql 使用 Wonka 库。

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { pipe, map, mergeMap, fromPromise, fromValue } from "wonka";
import {
  createClient,
  Provider,
  dedupExchange,
  cacheExchange,
  fetchExchange,
  Exchange,
  Operation,
} from "urql";

interface AuthApolloProviderProps {
  children: React.ReactChildren;
}

const fetchOptionsExchange =
  (fn: any): Exchange =>
  ({ forward }) =>
  (ops$) => {
    return pipe(
      ops$,
      mergeMap((operation: Operation) => {
        const result = fn(operation.context.fetchOptions);
        return pipe(
          (typeof result.then === "function"
            ? fromPromise(result)
            : fromValue(result)) as any,
          map((fetchOptions: RequestInit | (() => RequestInit)) => ({
            ...operation,
            context: { ...operation.context, fetchOptions },
          }))
        );
      }),
      forward
    );
  };

const AuthApolloProvider = ({ children }: AuthApolloProviderProps) => {
  const { getAccessTokenSilently, getIdTokenClaims } = useAuth0();

  const url = process.env.GATSBY_HASURA_GRAPHQL_URL;
  let client = null;

  if (url) {
    client = createClient({
      url: url,
      exchanges: [
        dedupExchange,
        cacheExchange,
        fetchOptionsExchange(async (fetchOptions: any) => {
          await getAccessTokenSilently({
            audience: process.env.GATSBY_AUTH0_AUDIENCE,
            scope: "openid profile email offline_access",
            ignoreCache: true,
          });

          const tokenClaims = await getIdTokenClaims();
          const token = tokenClaims?.__raw;

          return Promise.resolve({
            ...fetchOptions,
            headers: {
              Authorization: token ? `Bearer ${token}` : "",
            },
          });
        }),
        fetchExchange,
      ],
    });
  } else {
    throw new Error("url not define");
  }

  return <Provider value={client}>{children}</Provider>;
};

export default AuthApolloProvider;