2

I've been developing a project with a simple authorization using cookies and apollo-client. The challenge is, sometimes when I try to useQUery(isAuthenticatedQuery) they retrieve correct data and sometimes not. This query is used to check if my users is loggedIn, I sent in my request Header the token returned after my LoginMutation. I've already checked my request in the network tab and when I got error is when the header is sending "bearer undefined" instead of "bearer ${token}".

It's my first app using apollo so probably it's a dummy question, I was thinking there was some issue with the asynchronous request, but all the requests in useQuery are already async, right?

login.tsx

import React, { useState } from 'react'
import Layout from '../components/Layout'
import Router from 'next/router'
import { withApollo } from '../apollo/client'
import gql from 'graphql-tag'
import { useMutation, useQuery, useApolloClient } from '@apollo/react-hooks'


const LoginMutation = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`


function Login(props) {
  const client = useApolloClient()
  const [password, setPassword] = useState('')
  const [email, setEmail] = useState('')

  const [login] = useMutation(LoginMutation, {
    onCompleted(data) {
      document.cookie = `token=${data.login.token}; path=/`
    }
  })

  return (
    <Layout>
      <div>
        <form
          onSubmit={async e => {
            e.preventDefault();

            await login({
              variables: {
                email: email,
                password: password,
              }
            })

            Router.push('/')
          }}>
          <h1>Login user</h1>
          <input
            autoFocus
            onChange={e => setEmail(e.target.value)}
            placeholder="Email"
            type="text"
            value={email}
          />
          <input
            onChange={e => setPassword(e.target.value)}
            placeholder="Password"
            type="password"
            value={password}
          />
          <input disabled={!password || !email} type="submit" value="Login" />
          <a className="back" href="#" onClick={() => Router.push('/')}>
            or Cancel
          </a>
        </form>
      </div>
    </Layout>
  )
}

export default withApollo(Login)

index.tsx

import { useEffect } from 'react'
import Layout from '../components/Layout'
import Link from 'next/link'
import { withApollo } from '../apollo/client'
import { useQuery } from '@apollo/react-hooks'
import { FeedQuery, isAuthenticatedQuery } from '../queries';


export interface Item {
  content: string
  author: string
  title: string
  name: string
}

export interface Post {
  post: {
    [key: string]: Item
  }
}
const Post = ({ post }: Post) => (
  <Link href="/p/[id]" as={`/p/${post.id}`}>
    <a>
      <h2>{post.title}</h2>
      <small>By {post.author.name}</small>
      <p>{post.content}</p>
      <style jsx>{`
        a {
          text-decoration: none;
          color: inherit;
          padding: 2rem;
          display: block;
        }
      `}</style>
    </a>
  </Link>
)

const Blog = () => {
  const { loading, error, data } = useQuery(FeedQuery)

  const { loading: loadingAuth, data: dataAuth, error: errorAuth } = useQuery(isAuthenticatedQuery)

  console.log("data auth", dataAuth, loadingAuth, errorAuth);


  if (loading) {
    return <div>Loading ...</div>
  }
  if (error) {
    return <div>Error: {error.message}</div>
  }

  return (
    <Layout>
      <div className="page">
        {!!dataAuth && !loadingAuth ? (
          <h1> Welcome back {dataAuth.me.name} </h1>
        ) : (
            <h1>My Blog</h1>
          )}
        <main>
          {data.feed.map(post => (
            <div className="post">
              <Post key={post.id} post={post} />
            </div>
          ))}
        </main>
      </div>
      <style jsx>{`

        h1 {
          text-transform: capitalize;
        }
        .post {
          background: white;
          transition: box-shadow 0.1s ease-in;
        }

        .post:hover {
          box-shadow: 1px 1px 3px #aaa;
        }

        .post + .post {
          margin-top: 2rem;
        }
      `}</style>
    </Layout>
  )
}

export default withApollo(Blog)

client.js(my configuring apollo hoc file)

import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'isomorphic-unfetch'
import cookies from 'next-cookies'

let apolloClient = null
let token = undefined
/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState)
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    )
  }

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName =
      PageComponent.displayName || PageComponent.name || 'Component'

    if (displayName === 'App') {
      console.warn('This withApollo HOC only works with PageComponents.')
    }

    WithApollo.displayName = `withApollo(${displayName})`
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async ctx => {
      const { AppTree } = ctx
      token = cookies(ctx).token || ''
      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient())

      // Run wrapped getInitialProps methods
      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      // Only on the server:
      if (typeof window === 'undefined') {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient,
                }}
              />,
            )
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error)
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind()
        }
      }

      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract()
      return {
        ...pageProps,
        apolloState,
      }
    }
  }

  return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState) {
  // 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 (!apolloClient) {
    apolloClient = createApolloClient(initialState)
  }

  return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient(initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache,
  })
}

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    headers: { Authorization: `Bearer ${token}` },
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

TLDR; check the client.js file inside my HttpLink how I define headers, and index.tsx > Blog how I'm using the useQuery(isAuthenticatedQuery) to check if the user is signed in.

obs.: If I refresh the page the token is always set and the query works as expected.

Yuri Pereira
  • 1,945
  • 17
  • 24
  • `token` not read (from `document.cookie`) in link ... await query ? ... just move `Router.push` into `onCompleted` ? – xadm Apr 04 '20 at 20:46
  • I moved Router.push('/') to inside `onCompleted` but without success – Yuri Pereira Apr 05 '20 at 19:41
  • "`token` not read (from document.cookie) in link" – xadm Apr 05 '20 at 19:42
  • The cookie is set when I check in dev-tools the problem is I'm using SSR(nextjs) and the cookie is only read when refreshing the page. I need to find a way after the cookie is set to read then in `client.js` (my ssr config file) – Yuri Pereira Apr 06 '20 at 00:11
  • cookie is set, ok ... it's not used later in link – xadm Apr 06 '20 at 00:20
  • I'm not sure if inserting the cookie in the right place but I'm inserting inside this if `ssr || PageComponent.getInitialProps)` – Yuri Pereira Apr 06 '20 at 00:23
  • in other words: `createIsomorphLink` ... `token` value is taken from ...? – xadm Apr 06 '20 at 00:25
  • `token = cookies(ctx).token || ''` – Yuri Pereira Apr 06 '20 at 00:26
  • check ... console.log it in `createIsomorphLink` – xadm Apr 06 '20 at 00:28
  • I've already checked, its undefined, get the value only after I refresh the page... I'm looking for a solution to after set cookies don't need to refresh the page to get the cookie inside link. – Yuri Pereira Apr 06 '20 at 00:34
  • check if it can be available from `document.cookie` ... `console.log(getCookie('token'))` `const headerToken = token ? token : getCookie('token') | ''` in `createIsomorphLink` https://stackoverflow.com/a/51313011/6124657 – xadm Apr 06 '20 at 02:28
  • ... or read https://github.com/apollographql/apollo-client/issues/5089 – xadm Apr 06 '20 at 02:37
  • When it comes to Apollo and cookies, it's only available when you make the first request after the cookie is stored. It shouldn't affect your API calls except you need the cookie for something different, it should suffice – Rex Raphael Apr 06 '20 at 15:16

1 Answers1

1

First, you are not passing the token to the apollo HTTP client here. You can see token is resolved to undefined.

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

Here is what you should do

import { setContext } from 'apollo-link-context';
import localForage from 'localforage';

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

const authLink = setContext((_, { headers }) => {
  // I recommend using localforage since it's ssr
  const token = localForage.getItem('token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
});

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient(initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  return new ApolloClient({
    ssrMode,
    link: authLink.concat(createIsomorphLink()),
    cache,
  })
}

Now in your login component

import localForage from 'localforage';

const LoginMutation = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`


function Login(props) {
  const client = useApolloClient()
  const [password, setPassword] = useState('')
  const [email, setEmail] = useState('')

  const [login] = useMutation(LoginMutation, {
    onCompleted(data) {
      // document.cookie = `token=${data.login.token}; path=/`
      localForage. setItem('token', data.login.token)
    }
  })

  return (
    <Layout>
      <div>
        <form
          onSubmit={async e => {
            e.preventDefault();

            await login({
              variables: {
                email: email,
                password: password,
              }
            })

            Router.push('/')
          }}>
          <h1>Login user</h1>
          <input
            autoFocus
            onChange={e => setEmail(e.target.value)}
            placeholder="Email"
            type="text"
            value={email}
          />
          <input
            onChange={e => setPassword(e.target.value)}
            placeholder="Password"
            type="password"
            value={password}
          />
          <input disabled={!password || !email} type="submit" value="Login" />
          <a className="back" href="#" onClick={() => Router.push('/')}>
            or Cancel
          </a>
        </form>
      </div>
    </Layout>
  )
}

export default withApollo(Login)

As long as your authentication strategy is Bearer token, that should work. If you are using Cookie or session cookie, you should just pass in a custom fetch with credential include if your frontend and backend have different domain names, else just leave it as same-site and make cors is enabled in the backend and your localhost if on development is whitelisted in the cors option.

Rex Raphael
  • 200
  • 2
  • 7
  • Thanks for your answer but I'm facing the same issue when I try to logout I just can see the data updated after refreshing the page. The behavior of the logout is sent a query to me and check if I got response. and inside this query, there's my authentication header. IDK why but the query still saving the local storage value like they was doing with cookie. – Yuri Pereira Apr 07 '20 at 11:06