Skip to content

Instantly share code, notes, and snippets.

@neil-gebbie-smarterley
Last active December 19, 2020 17:02
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save neil-gebbie-smarterley/cd8356df4c786c4c9dacfc9d46e890ac to your computer and use it in GitHub Desktop.
Save neil-gebbie-smarterley/cd8356df4c786c4c9dacfc9d46e890ac to your computer and use it in GitHub Desktop.
Apollo client, apollo server, next js with cookies
const typeDefs = require('./schema/schema')
const someRestAPI = require('./someRestAPI')
const resolvers = require('./resolvers')
const apolloServer = {
typeDefs,
resolvers,
dataSources: () => ({
someRestAPI: new someRestAPI(),
}),
context: ({ req }) => {
return { req }
}
}
module.exports = apolloServer
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'
import fetch from 'isomorphic-unfetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
const graphqlEndpoint =
process.env.NODE_ENV !== 'production'
? 'http://localhost:3000'
: process.env.BASE_URL
const create = (initialState, cookie) => {
const httpLink = createHttpLink({
uri: `${graphqlEndpoint}/graphql`,
credentials: 'include'
})
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
Cookie: cookie ? cookie : '',
}
}
})
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: authLink.concat(httpLink),
cache: new InMemoryCache().restore(initialState || {})
})
}
const initApollo = (initialState, cookie) => {
// 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 (!process.browser) {
return create(initialState, cookie)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState, document.cookie)
}
return apolloClient
}
export default initApollo
const express = require('express')
const next = require('next')
const cors = require('cors')
const cookieParser = require('cookie-parser')
const connect = require('connect')
const { ApolloServer } = require('apollo-server-express')
const apolloServer = require('./apolloServer')
const dev = process.env.NODE_ENV !== 'production'
const port = process.env.PORT || 3000
const app = next({ dev })
const handle = app.getRequestHandler()
var corsOptions = {
origin:
process.env.NODE_ENV !== 'production'
? 'http://localhost:3000'
: process.env.BASE_URL,
credentials: true
}
app
.prepare()
.then(() => {
const server = express()
.use(connect())
.use(cookieParser())
.use(cors(corsOptions))
new ApolloServer({ ...apolloServer }).applyMiddleware({
app: server,
cors: corsOptions
})
// your next config
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(port, err => {
if (err) throw err
// eslint-disable-next-line no-console
console.log(`> Ready on http://localhost:${port} at ${Date.now()}`)
})
})
.catch(ex => {
// eslint-disable-next-line no-console
console.error(ex.stack)
process.exit(1)
})
class someRestAPI extends RESTDataSource {
constructor() {
super()
this.baseURL = getConfig.env.API_ENDPOINT
}
willSendRequest(request) {
request.headers.set('cookie', this.context.req.headers.cookie)
}
// your functions
}
/* eslint-disable no-console */
import React from 'react'
import PropTypes from 'prop-types'
import initApollo from './init-apollo'
import Head from 'next/head'
import idx from 'idx'
import { getDataFromTree } from 'react-apollo'
export default App => {
return class Apollo extends React.Component {
static displayName = 'withApollo(App)'
static async getInitialProps(ctx) {
const { Component, router } = ctx
// get the cookies sent on the initial request
const cookie = idx(ctx, _ => _.ctx.req.headers.cookie)
// Run all GraphQL queries in the component tree and extract the resulting data
const apollo = initApollo({}, cookie)
let appProps = {}
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx)
}
if (!process.browser) {
try {
// Run all GraphQL queries
await getDataFromTree(
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
)
} 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 = apollo.cache.extract()
return {
...appProps,
apolloState,
cookie
}
}
static propTypes = {
apolloState: PropTypes.object,
cookie: PropTypes.string
}
constructor(props) {
super(props)
this.apolloClient = initApollo(props.apolloState, props.cookie)
}
render() {
return <App {...this.props} apolloClient={this.apolloClient} />
}
}
}
@NinjaOnRails
Copy link

This is the only thing that works for me so far. At least on localhost it does. For some reason, once deployed to a custom domain, the token gets wiped with a refresh or when tab is closed. What about you? Thanks

@neil-gebbie-smarterley
Copy link
Author

Good to hear - Unless your persisting and then rehydrating state somehow that is the correct behaviour. I can't really go too much into detail about our stack but if you can let me know a little bit about the stack your working on I might be able to offer a suggestion. @NinjaOnRails

@NinjaOnRails
Copy link

NinjaOnRails commented Aug 10, 2019

I use the same stack on the FR as you mention in the Github thread (Next/Apollo Client/Apollo Server), on the backend I use GraphQL Prisma with Yoga server. So the cookie staying on localhost in the dev environment even on page refresh/tab close, but not in production (deployed to NOW) is the correct behaviour? This is what I got on Chrome and Firefox. On Safari the cookie doesn't even get saved for some reason. SSR luckily works. How is it working for you in production? There's another setup that allows the cookie to be passed properly (using the canary branch of the official Next.js with-apollo example available here on Github), but SSR breaks. Thanks
btw, my project is deployed here

@NinjaOnRails
Copy link

This is strange, now it looks like the cookie is still there. It just doesn't get passed around...

@neil-gebbie-smarterley
Copy link
Author

Our local and production set up is very different, but I can't really talk about that.

Are you passing th cookie to a REST API? Where is it's final destination?

What happens when you go directly to the API Vs through Apollo/graphql?

Are you expecting your response to be different with or without cookies?

Add:
error: ({ error }) => {
console.log(error)
return { error }
}

To ApolloServer.js and see if you get any errors

@NinjaOnRails
Copy link

NinjaOnRails commented Aug 12, 2019

Here's my latest FR server setup
on the backend I use GraphQL Prisma with Yoga server like this (so it's a graphql endpoint, not REST api):

const jwt = require('jsonwebtoken');

require('dotenv').config({ path: 'variables.env' });
const createServer = require('./createServer');
const db = require('./db');

const server = createServer();

// Handle cookies (JWT)
server.express.use(cookieParser());

// Decode JWT to get User ID on each request
server.express.use((req, res, next) => {
  const { token } = req.cookies;
  if (token) {
    const { userId } = jwt.verify(token, process.env.APP_SECRET);
    // Put userId onto req for future requests to access
    req.userId = userId;
  }
  next();
});

// Populate user on each request
server.express.use(async (req, res, next) => {
  if (!req.userId) return next();
  const user = await db.query.user({ where: { id: req.userId } });
  req.user = user;
  next();
});

server.start(
  {
    cors: {
      credentials: true,
      origin: [
        process.env.FRONTEND_URL,
        process.env.FRONTEND_URL_2,
        process.env.FRONTEND_URL_3,
      ],
    },
  },
  deets => {
    console.log(`Server is now running on port http://localhost:${deets.port}`);
  }
);

Every request that comes to my backend from my frontend is supposed to contain headers with cookies. If there's token and it validates, user is set on the FR, if not, nothing happens. I'm only able to get this work with SSR broken. Or SSR work and cookie passing broken. Not both to work at once

@NinjaOnRails
Copy link

I've fixed it for myself using the latest libraries next@9.0.3, next-with-apollo@4.2.0 and react-apollo@3.0.0. Cookies are passed with every request, SSR working as expected and no errors. Code is here. I had to remove .restore(initialState || {}) so it's just cache: new InMemoryCache() and now it's fully working. Only thing still not working is Safari

@louisrli
Copy link

I also had some difficulty with this until I found this gist from a variety of Googling.

I had started off with something along these lines from this repo, but was running into this issue.
https://github.com/brunocrosier/next-with-apollo-auth

After banging my head on various supposed solutions, what finally worked for me was setting prop.cookies in the HOC, e.g., enabling this line in particular.
this.apolloClient = initApollo(props.apolloState, props.cookie)

Also see:
lfades/next-with-apollo#26
apollographql/apollo-client#5089

@NinjaOnRails solution with the inmemorycache change didn't work, unfortunately, but YMMV as my code is slightly different from his (as I mentioned, it's based off the @brunocrosier repo)

@Akumzy
Copy link

Akumzy commented Jan 24, 2020

Wow thanks a lot

@josuevalrob
Copy link

God... why it is soooo complicated??

@neil-gebbie-smarterley
Copy link
Author

@josuevalrob Just take it one step it at a time, log values out along the way, and everything will fall into place. You only need to set it up once then everything will be fine.

@josuevalrob
Copy link

Thanks

@josuevalrob Just take it one step it at a time, log values out along the way, and everything will fall into place. You only need to set it up once then everything will be fine.

But the problem is that I have a different server configuration. Can you check it, please?
Client: https://github.com/josuevalrob/CLE
Server: https://github.com/josuevalrob/cle_api

Just a quick look to know if this approach is fine for me. It means to refactor a lot of code.

So far it is impossible to send the cookie in the login and validate it in the context of my graphQl.

Please if you can give me a hand, contact me: josue.valrob@gmail.com

@neil-gebbie-smarterley
Copy link
Author

@josuevalrob I'm giving you a hand by providing this gist.

What have you tried? I've looked at your repo and you haven't implemented any of the code above. I don't know if the approach will work for you, this gist is for NextJS and Apollo. I can't determine what's possible or not possible as I know nothing about your local, development or production environments. But it seems like a fairly common scenario to pass cookies, so I can't see why it would be an issue.

Best of luck.

@josuevalrob
Copy link

Thanks @neil-gebbie-smarterley for share the repo and you help.

I had explain a little more about my issue in this question from StackOverflow.

My problem is that I need to understand what is happening, this is not magic you know(?).

Thanks to the comments from the StackOverflow questions, it looks like yes, the cookie is traveling in both direction, I have access to if I inspect the network request, but I am lost in the validation from the client and server-side.

Screenshot 2020-04-12 at 16 54 15

I really appreciate your help, any comment is welcome.

@neil-gebbie-smarterley
Copy link
Author

@josuevalrob Are you expecting a different response based on the logged-in status of the user? How are you handling that on the server side? Do something simple like (this is pseudo code) - IF has cookie return a response {cookie: true} from the server. I suspect your trying to do many things at once and you need to just slow down and do one thing at a time.

Also add the authlink I have got here: https://gist.github.com/neil-gebbie-smarterley/cd8356df4c786c4c9dacfc9d46e890ac#file-initapollo-js-L25

@lionelyoung
Copy link

This one worked for me: https://gist.github.com/lionelyoung/6f9020de23f257599bdabfdb0bf40bff found in another github thread

@lionelyoung
Copy link

How do I pass getServerSideProp contexts? i.e.

export async function getServerSideProps(context) {
  const myVars = {
    orderId: context["params"]["order"]
  }

  return {
    props: {
      myVars: myVars,
    },
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment