Сервер Apollo установил cookie для доступа в следующем приложении js - PullRequest
2 голосов
/ 23 октября 2019

У меня есть приложение Next.js, и я использую apollo-server-micro и apollo-client. Я не могу установить сеанс cookie, когда пользователь попадает в мутацию входа в систему. Вот моя настройка server.js (npm start)

const express = require('express');
const next = require('next');
const { apolloServer } = require('pages/api/graphql');
const cookieSession = require('cookie-session');

const dev = process.env.NODE_ENV !== 'production';
const app = nextApp({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.use(express.static('./public'));

  server.use(
        '/api',
        cookieSession({
            httpOnly: false,
            keys: ['super-secret-key'],
            maxAge: 24 * 60 * 60 * 1000,
            name: 'xyz',
        })
    );

  server.get('*', handle);

  server.listen(8000, () => {
    console.log('Server running on port 8000');
  })
})

В pages/api/graphql.js у меня есть следующий фрагмент кода (прямо из документации Next.js)

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })

Вотмутация для входа в систему

const jobs = require('../data/jobs');
const users = require('../data/users');

const getUserByEmail = email => users.find(user => user.email === email);

export const resolvers = {
  Query: {
    viewer (_parent, _args, _context, _info) {
      return { id: 1, name: 'John Smith', status: 'cached' }
    },
    jobs: (parent, args, context, info) => {
      return jobs;
    }
  },

  Mutation: {
    login: (parent, args, context, info) => {
      const { email } = args;
      const user = getUserByEmail(email);
      console.log(context.headers);
      if (user) {
        context.req = {
          ...context.req,
          session: user
        }
        return user;
      }
    }
  }
}

Пока нет шифрования, и все, я просто хочу сейчас запустить это. Как настроить сеанс (чтобы файл cookie появлялся в Chrome и был доступен для будущих входящих запросов)

Обновление: конфигурация клиента Apollo

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'

let apolloClient = null

/**
 * 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

      // 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)

  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache
  })
}

function createIsomorphLink () {
  if (typeof window === 'undefined') {
    const { SchemaLink } = require('apollo-link-schema')
    const { schema } = require('./schema')
    return new SchemaLink({ schema })
  } else {
    const { HttpLink } = require('apollo-link-http')
    return new HttpLink({
      uri: '/apii/graphql',
      credentials: 'same-origin'
    })
  }
}

Обновлено: конфигурация сервера Apollo с использованием apolloмикро

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })

1 Ответ

0 голосов
/ 24 октября 2019

Если вы используете SchemaLink, я думаю вы можете просто получить объект req из PageContext и использовать его для реконструкции вашего объекта контекста GraphQL.

const { AppTree, req } = ctx
// elsewhere
new SchemaLink({ schema, context: { req } })

Вот пример, в котором не используется SchemaLink, который вы можете использовать в своем проекте. Ключ заключается в том, чтобы извлечь значение cookie из PageContext. Затем вы можете использовать это для создания пользовательской реализации fetch, которая заполняет заголовок Cookie, а затем передать эту функцию извлечения вашему конструктору HttpLink.

let apolloClient: ApolloClient<any>

export const getClient = (
  initialState: NormalizedCacheObject = {},
  cookie?: string,
): ApolloClient<any> => {
  const fetch: WindowOrWorkerGlobalScope['fetch'] = async (url, init = {}) => {
    const headers = { ...init.headers } as Record<string, string>
    if (cookie) {
      headers.Cookie = cookie
    }

    const response = await unfetch(url, {
      ...init,
      headers,
    })
    return response
  }

  return new ApolloClient<any>({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'http://localhost:3000/graphql',
      credentials: 'same-origin',
      fetch,
    }),
    cache: new InMemoryCache().restore(initialState),
  })
}

export const initApollo = (
  initialState?: NormalizedCacheObject,
  cookie?: string,
): ApolloClient<any> => {
  if (typeof window === 'undefined') {
    return getClient(initialState, cookie)
  }
  if (!apolloClient) {
    apolloClient = getClient(initialState, cookie)
  }

  return apolloClient
}

export const withApollo = (PageComponent: NextPage, { ssr = true } = {}) => {
  const WithApollo = ({ apolloState, ...pageProps }: WithApolloProps) => {
    const client = apolloClient || initApollo(apolloState)

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

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx: PageContext) => {
      const { AppTree } = ctx
      const cookie = (ctx.req && ctx.req.headers.cookie) || undefined
      const apolloClient = (ctx.apolloClient = initApollo({}, cookie))

      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      if (typeof window === 'undefined') {
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        if (ssr) {
          try {
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
                <AppTree
                    pageProps={{
                      ...pageProps,
                      apolloClient,
                    }}
                />,
            )
          } catch (error) {
            console.error('Error while running `getDataFromTree`', error)
          }

          Head.rewind()
        }
      }

      const apolloState = apolloClient.cache.extract()

      return {
        ...pageProps,
        apolloState,
      }
    }
  }

  return WithApollo
}

В качестве отдельного случая вы хотите убедиться, чтовы правильно устанавливаете контекст при инициализации сервера Apollo. Не просто возвращайте объект req, так как это установит ваш контекст для объекта req. Вместо этого выполните:

context: ({ req }) => {
  return { req };
}

, а затем убедитесь, что вы правильно изменяете сеанс внутри вашего резольвера:

context.req.session.user = user

Таким образом, ваш код переопределяет объект сеанса с пользователем,это не то, что вы хотите сделать. Установка свойства пользователя в сеансе означает, что вы сможете получить к нему доступ с помощью context.req.session.user в другом месте вашего кода.

...