apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
449 stars 35 forks source link

JWT Issue / Question - ApolloWrapper JWT Doesn't Exist #356

Open traik06 opened 2 months ago

traik06 commented 2 months ago

Description

Hello all I'm relatively new to this so if this is a basic question please bear with me here. I am creating a nextjs14 app and I'm trying to implement apollo client into the application. the main part of the application is protected by a login that calls to my express backend to get a JWT token. I want to then pass the JWT token in as a header for the graphql queries.

Issue

Observations / Questions

I have some hardcoded urls and such just for testing and some logs in there that would be removed once I figure this out

Project Versions

"next": "^14.2.6"
"react": "^18"
"react-dom": "^18"
"@apollo/client": "^3.11.4"
"@apollo/experimental-nextjs-app-support": "^0.11.2"
"graphql": "^16.9.0"

Folder Layout

|-- app/
|    |-- auth
|    |     |-- login (login form)
|    |           |-- page.tsx (apolloClient.ts used here but no headers needed for anon login till user is verified on backend)
|    |     |-- reset
|    |           |-- page.tsx
|    |     |-- layout.tsx
|    |-- (root)
|          |-- dashboard (folder containing page.tsx)
|          |-- admin (folder containing page.tsx)
|          |-- profile (folder containing page.tsx)
|          |-- settings (folder containing page.tsx)
|          |-- layout.tsx (ApolloWrapper.tsx used here - when doing a mutation no JWT exists for logged in user)
|    |-- layout.tsx

apolloClient.ts

import { HttpLink, split } from "@apollo/client";
import { cookies } from "next/headers";
import {
  registerApolloClient,
  ApolloClient,
  InMemoryCache,
} from "@apollo/experimental-nextjs-app-support";
import { createClient } from "graphql-ws";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { setContext } from "@apollo/client/link/context";

export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
  // check if a token exists for the user
  const token = cookies().get("jwt")?.value;

  // Conditionally add headers
  const httpLink = new HttpLink({
    uri: "http://localhost:8080/v1/graphql",
    fetchOptions: { cache: "no-store" },
    headers: token ? { Authorization: `Bearer ${token}` } : undefined,
  });

  const logHeadersLink = setContext((_, { headers }) => {
    const authorizationHeader = token
      ? { Authorization: `Bearer ${token}` }
      : undefined;
    const mergedHeaders = {
      ...headers,
      ...authorizationHeader,
    };

    console.log("Request Headers (apolloClient):", mergedHeaders);

    return {
      headers: mergedHeaders,
    };
  });
  const finalHttpLink = logHeadersLink.concat(httpLink);

  // Conditionally add connectionParams
  const wsClient = createClient({
    url: "ws://localhost:8080/v1/graphql",
    connectionParams: token ? { Authorization: `Bearer ${token}` } : undefined,
    on: {
      connected: () => console.log("WebSocket connected"),
      closed: (event) => console.log("WebSocket closed", event),
      error: (error) => console.log("WebSocket error", error),
    },
  });

  const wsLink = new GraphQLWsLink(wsClient);

  const splitLink = split(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return (
        definition.kind === "OperationDefinition" &&
        definition.operation === "subscription"
      );
    },
    wsLink,
    // httpLink
    finalHttpLink
  );

  return new ApolloClient({
    cache: new InMemoryCache({ addTypename: false }),
    link: splitLink,
  });
});

ApolloWrapper.tsx

"use client";
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { split, HttpLink } from "@apollo/client";
import { setVerbosity } from "ts-invariant";
import { createClient } from "graphql-ws";
import {
  ApolloClient,
  InMemoryCache,
  ApolloNextAppProvider,
} from "@apollo/experimental-nextjs-app-support";
import { setContext } from "@apollo/client/link/context";

// set debugging mode to true when in development mode
if (process.env.NODE_ENV === "development") {
  setVerbosity("debug");
  loadDevMessages();
  loadErrorMessages();
}

const makeClient = (token: string | undefined) => {
  const httpLink = new HttpLink({
    uri: "http://localhost:8080/v1/graphql",
    fetchOptions: { cache: "no-store" },
    headers: {
      Authorization: token ? `Bearer ${token}` : "",
    },
  });

  const logHeadersLink = setContext((_, { headers }) => {
    const authorizationHeader = token
      ? { Authorization: `Bearer ${token}` }
      : undefined;
    const mergedHeaders = {
      ...headers,
      ...authorizationHeader,
    };

    console.log("mergedHeaders :", mergedHeaders);

    return {
      headers: mergedHeaders,
    };
  });

  const finalHttpLink = logHeadersLink.concat(httpLink);

  const wsClient = createClient({
    url: "ws://localhost:8080/v1/graphql",
    connectionParams: {
      Authorization: token ? `Bearer ${token}` : "",
    },
    on: {
      connected: () => console.log("WebSocket connected"),
      closed: (event) => console.log("WebSocket closed", event),
      error: (error) => console.log("WebSocket error", error),
    },
  });

  const wsLink = new GraphQLWsLink(wsClient);

  const splitLink = split(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return (
        definition.kind === "OperationDefinition" &&
        definition.operation === "subscription"
      );
    },
    wsLink,
    // httpLink
    finalHttpLink
  );

  return new ApolloClient({
    ssrMode: true,
    cache: new InMemoryCache(),
    link: splitLink,
  });
};

// you need to create a component to wrap your app in
export function ApolloWrapper({
  children,
  jwt,
}: React.PropsWithChildren<{ jwt?: string }>) {
  return (
    <ApolloNextAppProvider makeClient={() => makeClient(jwt)}>
      {children}
    </ApolloNextAppProvider>
  );
}

Layout.tsx : app/(root)/layout.tsx

import { getUser } from "@/lib/actions/user.actions";
import MobileNav from "@/components/MobileNav";
import SideNav from "@/components/SideNav";
import Image from "next/image";
import { ApolloWrapper } from "@/components/ApolloWrapper";
import { cookies } from "next/headers";

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const user = await getUser();

  if (user) {
    const jwt = cookies().get("jwt")?.value;
    return (
      <ApolloWrapper jwt={jwt}>
        <main className="flex h-screen w-full font-inter">
          <SideNav {...user} />

          <div className="flex size-full flex-col">
            <div className="root-layout bg-blue-800">
              <Image
                src="/app-test-logo.png"
                width={200}
                height={30}
                alt="menu icon"
              />
              <div>
                <MobileNav {...user} />
              </div>
            </div>
            {children}
          </div>
        </main>
      </ApolloWrapper>
    );
  }
}
traik06 commented 2 months ago

If there is anything else I can add to help with my question please let me know!

phryneas commented 2 months ago

I think the core problem that you have here is that makeClient is only called once for the whole lifetime of your application - anything else would throw away your cache and require a lot of requests to be made again.

Instead of passing a token into your makeClient function, I would recommend that you work with defaultContext as shown in this comment: https://github.com/apollographql/apollo-client-nextjs/issues/103#issuecomment-1790941212


One random observation:

Please don't use ssrMode with this package. It's something that's sometimes made up by ChatGPT because it exists on the normal ApolloClient, but we don't show it anywhere in the docs for this package - you don't need it for streaming SSR and it might even be counterproductive.