Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
traik06 opened this issue Sep 3, 2024 · 2 comments
Open

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

traik06 opened this issue Sep 3, 2024 · 2 comments

Comments

@traik06
Copy link

traik06 commented Sep 3, 2024

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

  • When the auth is successful the JWT is generated it is available in my apolloClient.ts file which I will provide how I have it configured below but is not available to my ApolloWrapper.tsx file when trying to run a basic query after the re route to the dashboard.
  • When I am re routed to the protected dashboard route and manually force a refresh the page in my browser then the JWT is present.

Observations / Questions

  • I'm guessing that when the /dashboard page is rendered or compiled that JWT is not accessible by the browser yet so it just sets it to undefined.
  • Would it be acceptable to just force a window reload when the user gets re-routed to the /dashboard route?
  • Am I missing something obvious that I could add to await the rendering of the /dashboard route until the JWT is available and be added to the headers in the ApolloWrapper.tsx file?

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
Copy link
Author

traik06 commented Sep 3, 2024

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

@traik06 traik06 changed the title JWT Issue - ApolloWrapper JWT Doesn't Exist JWT Issue / Question - ApolloWrapper JWT Doesn't Exist Sep 3, 2024
@phryneas
Copy link
Member

phryneas commented Sep 4, 2024

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: #103 (comment)


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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants