在 NextJS 上修改 HOC 内的值

Modify value inside HOC on NextJS

我一直在努力为我的 NextJS 应用程序设置身份验证和授权,到目前为止它非常简单,但我遇到了困难。

我有一个在上下文中存在并被监视的值,我有一个 HOC,我需要它让我的 NextJS 应用程序能够使用 GraphQl 的钩子,问题是我认为我做不到调用上下文并使用 HOC 中的值,因为根本不允许这样做。

有没有一种方法可以动态更改 HOC 上的值,以便当用户登录时,我可以更新 HOC 以获得正确的访问令牌?

一些上下文:用户首先是匿名的,每当 he/she 登录时,我都会从 Firebase 获得身份验证状态更改,我可以从中提取访问令牌并将其添加到任何未来的请求中。但是 hoc 的目的是为 next 提供完整的 Graphql 功能,问题是我需要 hoc 去监听上下文状态的变化。

这是 Connection Builder:

import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  NormalizedCacheObject,
} from "@apollo/client";

import { WebSocketLink } from "@apollo/client/link/ws";
import { SubscriptionClient } from "subscriptions-transport-ws";

const connectionString = process.env.HASURA_GRAPHQL_API_URL || "";

const createHttpLink = (authState: string, authToken: string) => {
  const isIn = authState === "in";
  const httpLink = new HttpLink({
    uri: `https${connectionString}`,
    headers: {
      // "X-hasura-admin-secret": `https${connectionString}`,
      lang: "en",
      "content-type": "application/json",
      Authorization: isIn && `Bearer ${authToken}`,
    },
  });
  return httpLink;
};

const createWSLink = (authState: string, authToken: string) => {
  const isIn = authState === "in";
  return new WebSocketLink(
    new SubscriptionClient(`wss${connectionString}`, {
      lazy: true,
      reconnect: true,
      connectionParams: async () => {
        return {
          headers: {
            // "X-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET,
            lang: "en",
            "content-type": "application/json",
            Authorization: isIn && `Bearer ${authToken}`,
          },
        };
      },
    })
  );
};

export default function createApolloClient(
  initialState: NormalizedCacheObject,
  authState: string,
  authToken: string
) {
  const ssrMode = typeof window === "undefined";
  let link;
  if (ssrMode) {
    link = createHttpLink(authState, authToken);
  } else {
    link = createWSLink(authState, authToken);
  }
  return new ApolloClient({
    ssrMode,
    link,
    cache: new InMemoryCache().restore(initialState),
  });
}

这是上下文:

import { useState, useEffect, createContext, useContext } from "react";
import { getDatabase, ref, set, onValue } from "firebase/database";
import { useFirebase } from "./use-firebase";
import { useGetUser } from "../hooks/use-get-user";
import { getUser_Users_by_pk } from "../types/generated/getUser";
import { getApp } from "firebase/app";

const FirebaseAuthContext = createContext<FirebaseAuthContextProps>({
  authUser: null,
  authState: "",
  authToken: null,
  currentUser: undefined,
  loading: true,
  login: () => Promise.resolve(undefined),
  registerUser: () => Promise.resolve(undefined),
  loginWithGoogle: () => Promise.resolve(undefined),
  loginWithMicrosoft: () => Promise.resolve(undefined),
});

export const FirebaseAuthContextProvider: React.FC = ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const [authUser, setAuthUser] = useState<User | null>(null);
  const { data } = useGetUser(authUser?.uid || "");
  const [authState, setAuthState] = useState("loading");
  const [authToken, setAuthToken] = useState<string | null>(null);

  const currentUser = data?.Users_by_pk;

 // ...

  const authStateChanged = async (user: User | null) => {
    if (!user) {
      setAuthUser(null);
      setLoading(false);
      setAuthState("out");
      return;
    }

    const token = await user.getIdToken();
    const idTokenResult = await user.getIdTokenResult();
    const hasuraClaim = idTokenResult.claims["https://hasura.io/jwt/claims"];

    if (hasuraClaim) {
      setAuthState("in");
      setAuthToken(token);
      setAuthUser(user);
    } else {
      // Check if refresh is required.
      const metadataRef = ref(
        getDatabase(getApp()),
        "metadata/" + user.uid + "/refreshTime"
      );
      onValue(metadataRef, async (data) => {
        if (!data.exists) return;

        const token = await user.getIdToken(true);
        setAuthState("in");
        setAuthUser(user);
        setAuthToken(token);
      });
    }
  };

  useEffect(() => {
    const unsubscribe = getAuth().onAuthStateChanged(authStateChanged);
    return () => unsubscribe();
  }, []);

  const contextValue: FirebaseAuthContextProps = {
    authUser,
    authState,
    authToken,
    currentUser,
    loading,
    login,
    registerUser,
    loginWithGoogle,
    loginWithMicrosoft,
  };

  return (
    <FirebaseAuthContext.Provider value={contextValue}>
      {children}
    </FirebaseAuthContext.Provider>
  );
};

export const useFirebaseAuth = () =>
  useContext<FirebaseAuthContextProps>(FirebaseAuthContext);

这是 HOC:

export const withApollo =
  ({ ssr = true } = {}) =>
  (PageComponent: NextComponentType<NextPageContext, any, {}>) => {
    const WithApollo = ({
      apolloClient,
      apolloState,
      ...pageProps
    }: {
      apolloClient: ApolloClient<NormalizedCacheObject>;
      apolloState: NormalizedCacheObject;
    }) => {
      let client;
      if (apolloClient) {
        // Happens on: getDataFromTree & next.js ssr
        client = apolloClient;
      } else {
        // Happens on: next.js csr
        // client = initApolloClient(apolloState, undefined);
        client = initApolloClient(apolloState);
      }

      return (
        <ApolloProvider client={client}>
          <PageComponent {...pageProps} />
        </ApolloProvider>
      );
    };

const initApolloClient = (initialState: NormalizedCacheObject) => {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === "undefined") {
    return createApolloClient(initialState, "", "");
  }

  // Reuse client on the client-side
  if (!globalApolloClient) {
    globalApolloClient = createApolloClient(initialState, "", "");
  }

  return globalApolloClient;
};

每当我对令牌进行更新时,我都会使用它来修复它:

 import { setContext } from "@apollo/client/link/context";

 const authStateChanged = async (user: User | null) => {
    if (!user) {
      setAuthUser(null);
      setLoading(false);
      setAuthState("out");
      return;
    }
    setAuthUser(user);

    const token = await user.getIdToken();
    const idTokenResult = await user.getIdTokenResult();
    const hasuraClaim = idTokenResult.claims["hasura"];

    if (hasuraClaim) {
      setAuthState("in");
      setAuthToken(token);
       // THIS IS THE FIX
      setContext(() => ({
        headers: { Authorization: `Bearer ${token}` },
      }));
    } else {
      // Check if refresh is required.
      const metadataRef = ref(
        getDatabase(getApp()),
        "metadata/" + user.uid + "/refreshTime"
      );
      onValue(metadataRef, async (data) => {
        if (!data.exists) return;

        const token = await user.getIdToken(true);
        setAuthState("in");

        setAuthToken(token);
        // THIS IS THE FIX
        setContext(() => ({
          headers: { Authorization: `Bearer ${token}` },
        }));
      });
    }
  };