Skip to content

Commit

Permalink
[Examples] Move api-routes-apollo-server-and-client-auth to SSG (verc…
Browse files Browse the repository at this point in the history
…el#13849)

Related to vercel#11014

The authentication method has been replaced with an improved version of the passport example.
  • Loading branch information
lfades authored and rokinsky committed Jul 11, 2020
1 parent 8300c90 commit b817fa4
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 271 deletions.
3 changes: 3 additions & 0 deletions examples/api-routes-apollo-server-and-client-auth/.env
@@ -0,0 +1,3 @@
# Secrets like the one below should go into `.env.local` instead to avoid pushing
# them to a repository, this is an exception for the sake of the example
TOKEN_SECRET="this-is-a-secret-value-with-at-least-32-characters"
11 changes: 3 additions & 8 deletions examples/api-routes-apollo-server-and-client-auth/README.md
@@ -1,13 +1,8 @@
# Apollo Server and Client Auth Example

[Apollo](https://www.apollographql.com/client/) is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run, fetching more results from the server.
[Apollo](https://www.apollographql.com/client/) is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run.

In this simple example, we integrate Apollo seamlessly with Next by wrapping our _pages/\_app.js_ inside a [higher-order component (HOC)](https://facebook.github.io/react/docs/higher-order-components.html). Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application.

On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.

Note: Do not be alarmed that you see two renders being executed. Apollo recursively traverses the React render tree looking for Apollo query components. When it has done that, it fetches all these queries and then passes the result to a cache. This cache is then used to render the data on the server side (another React render).
https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree
In this simple example, we integrate Apollo seamlessly with [Next.js data fetching methods](https://nextjs.org/docs/basic-features/data-fetching) to fetch queries in the server and hydrate them in the browser.

## How to use

Expand Down Expand Up @@ -40,4 +35,4 @@ yarn
yarn dev
```

> If you have issues installing `bcrypt`, follow this instructions: https://github.com/kelektiv/node.bcrypt.js/wiki/Installation-Instructions
Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
169 changes: 33 additions & 136 deletions examples/api-routes-apollo-server-and-client-auth/apollo/client.js
@@ -1,151 +1,48 @@
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { useMemo } from 'react'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'

let globalApolloClient = null
let apolloClient

/**
* 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(undefined, 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({
res: ctx.res,
req: ctx.req,
}))

// 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(ctx, 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(ctx, initialState)
}

// Reuse client on the client-side
if (!globalApolloClient) {
globalApolloClient = createApolloClient(ctx, initialState)
}

return globalApolloClient
}

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

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

function createIsomorphLink(ctx) {
function createIsomorphLink() {
if (typeof window === 'undefined') {
const { SchemaLink } = require('apollo-link-schema')
const { schema } = require('./schema')
return new SchemaLink({ schema, context: ctx })
return new SchemaLink({ schema })
} else {
const { HttpLink } = require('apollo-link-http')

return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
}
}

function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(),
cache: new InMemoryCache(),
})
}

export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()

// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
_apolloClient.cache.restore(initialState)
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient

return _apolloClient
}

export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
@@ -1,94 +1,47 @@
import { AuthenticationError, UserInputError } from 'apollo-server-micro'
import cookie from 'cookie'
import jwt from 'jsonwebtoken'
import getConfig from 'next/config'
import bcrypt from 'bcrypt'
import v4 from 'uuid/v4'

const JWT_SECRET = getConfig().serverRuntimeConfig.JWT_SECRET

const users = []

function createUser(data) {
const salt = bcrypt.genSaltSync()

return {
id: v4(),
email: data.email,
hashedPassword: bcrypt.hashSync(data.password, salt),
}
}

function validPassword(user, password) {
return bcrypt.compareSync(password, user.hashedPassword)
}
import { createUser, findUser, validatePassword } from '../lib/user'
import { setLoginSession, getLoginSession } from '../lib/auth'
import { removeTokenCookie } from '../lib/auth-cookies'

export const resolvers = {
Query: {
async viewer(_parent, _args, context, _info) {
const { token } = cookie.parse(context.req.headers.cookie ?? '')
if (token) {
try {
const { id, email } = jwt.verify(token, JWT_SECRET)
try {
const session = await getLoginSession(context.req)

return users.find((user) => user.id === id && user.email === email)
} catch {
throw new AuthenticationError(
'Authentication token is invalid, please log in'
)
if (session) {
return findUser({ email: session.email })
}
} catch (error) {
throw new AuthenticationError(
'Authentication token is invalid, please log in'
)
}
},
},
Mutation: {
async signUp(_parent, args, _context, _info) {
const user = createUser(args.input)

users.push(user)

const user = await createUser(args.input)
return { user }
},

async signIn(_parent, args, context, _info) {
const user = users.find((user) => user.email === args.input.email)
const user = await findUser({ email: args.input.email })

if (user && validPassword(user, args.input.password)) {
const token = jwt.sign(
{ email: user.email, id: user.id, time: new Date() },
JWT_SECRET,
{
expiresIn: '6h',
}
)
if (user && validatePassword(user, args.input.password)) {
const session = {
id: user.id,
email: user.email,
}

context.res.setHeader(
'Set-Cookie',
cookie.serialize('token', token, {
httpOnly: true,
maxAge: 6 * 60 * 60,
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
})
)
await setLoginSession(context.res, session)

return { user }
}

throw new UserInputError('Invalid email and password combination')
},
async signOut(_parent, _args, context, _info) {
context.res.setHeader(
'Set-Cookie',
cookie.serialize('token', '', {
httpOnly: true,
maxAge: -1,
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
})
)

removeTokenCookie(context.res)
return true
},
},
Expand Down
Expand Up @@ -4,6 +4,7 @@ export const typeDefs = gql`
type User {
id: ID!
email: String!
createdAt: Int!
}
input SignUpInput {
Expand Down
@@ -1,20 +1,16 @@
export default function Field(props) {
export default function Field({ name, label, type, autoComplete, required }) {
return (
<div>
<label
id={[props.name, 'label'].join('-')}
htmlFor={[props.name, 'input'].join('-')}
>
{props.label}{' '}
{props.required ? <span title="Required">*</span> : undefined}
<label id={[name, 'label'].join('-')} htmlFor={[name, 'input'].join('-')}>
{label} {required ? <span title="Required">*</span> : undefined}
</label>
<br />
<input
autoComplete={props.autoComplete}
id={[props.name, 'input'].join('-')}
name={props.name}
required={props.required}
type={props.type}
autoComplete={autoComplete}
id={[name, 'input'].join('-')}
name={name}
required={required}
type={type}
/>
</div>
)
Expand Down

0 comments on commit b817fa4

Please sign in to comment.