Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Apollo Server and Client Auth Example
- Loading branch information
1 parent
2a70b26
commit 5199e0b
Showing
20 changed files
with
791 additions
and
0 deletions.
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
examples/api-routes-apollo-server-and-client-auth/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Apollo Server and Client Auth Example | ||
|
||
## How to use | ||
|
||
### Using `create-next-app` | ||
|
||
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app | ||
# or | ||
yarn create next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app | ||
``` | ||
|
||
### Download manually | ||
|
||
Download the example: | ||
|
||
```bash | ||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/api-routes-apollo-server-and-client-auth | ||
cd api-routes-apollo-server-and-client-auth | ||
``` | ||
|
||
Install it and run: | ||
|
||
```bash | ||
npm install | ||
npm run dev | ||
# or | ||
yarn | ||
yarn dev | ||
``` | ||
|
||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)): | ||
|
||
```bash | ||
now | ||
``` | ||
|
||
## The idea behind the 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. | ||
|
||
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 |
168 changes: 168 additions & 0 deletions
168
examples/api-routes-apollo-server-and-client-auth/apollo/client.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
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(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 (!apolloClient) { | ||
apolloClient = createApolloClient(ctx, initialState) | ||
} | ||
|
||
return apolloClient | ||
} | ||
|
||
/** | ||
* 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/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient | ||
return new ApolloClient({ | ||
ssrMode, | ||
link: createIsomorphLink(ctx), | ||
cache, | ||
}) | ||
} | ||
|
||
function createIsomorphLink(ctx) { | ||
if (typeof window === 'undefined') { | ||
const { SchemaLink } = require('apollo-link-schema') | ||
const { schema } = require('./schema') | ||
const { context } = require('./context') | ||
return new SchemaLink({ schema, context: context(ctx) }) | ||
} else { | ||
const { HttpLink } = require('apollo-link-http') | ||
const { setContext } = require('apollo-link-context') | ||
const { ApolloLink } = require('apollo-link') | ||
const cookie = require('js-cookie') | ||
|
||
return ApolloLink.from([ | ||
setContext(() => { | ||
const token = cookie.get('token') | ||
if (token) { | ||
return { | ||
headers: { | ||
authorization: `Bearer ${token}`, | ||
}, | ||
} | ||
} | ||
}), | ||
new HttpLink({ | ||
uri: '/api/graphql', | ||
credentials: 'same-origin', | ||
}), | ||
]) | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
examples/api-routes-apollo-server-and-client-auth/apollo/context.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import models from '../models' | ||
import jwt from 'jsonwebtoken' | ||
import getConfig from 'next/config' | ||
|
||
export const context = prevContext => ({ | ||
...prevContext, | ||
...getConfig(), | ||
models, | ||
jwt, | ||
}) |
68 changes: 68 additions & 0 deletions
68
examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
AuthenticationError, | ||
ApolloError, | ||
UserInputError, | ||
} from 'apollo-server-micro' | ||
|
||
const getToken = headers => { | ||
if (headers.authorization) { | ||
return headers.authorization.replace(/bearer\s/i, '') | ||
} else if (headers.cookie.includes('token=')) { | ||
let part = headers.cookie.slice(headers.cookie.indexOf('token=') + 6) | ||
return part.slice(0, part.indexOf(';')) | ||
} | ||
} | ||
|
||
export const resolvers = { | ||
Query: { | ||
async viewer(_parent, _args, context, _info) { | ||
const token = getToken(context.req.headers) | ||
if (token) { | ||
try { | ||
const { id, email } = context.jwt.verify( | ||
token, | ||
context.serverRuntimeConfig.JWT_SECRET | ||
) | ||
|
||
return await context.models.User.findOne({ where: { id, email } }) | ||
} catch { | ||
throw new AuthenticationError( | ||
'Authentication token is invalid, please log in' | ||
) | ||
} | ||
} | ||
|
||
return null | ||
}, | ||
}, | ||
Mutation: { | ||
async signUp(parent, args, context, info) { | ||
const user = await context.models.User.create(args.input).catch(error => { | ||
return new ApolloError('ValidationError', 'VALIDATION_ERROR', { | ||
errors: error.errors, | ||
fields: error.fields, | ||
}) | ||
}) | ||
return { user } | ||
}, | ||
async signIn(parent, args, context, info) { | ||
const user = await context.models.User.findOne({ | ||
where: { email: args.input.email }, | ||
}) | ||
|
||
if (user && user.validPassword(args.input.password)) { | ||
const token = context.jwt.sign( | ||
{ email: user.email, id: user.id, time: new Date() }, | ||
context.serverRuntimeConfig.JWT_SECRET, | ||
{ | ||
expiresIn: '6h', | ||
} | ||
) | ||
|
||
return { user, token } | ||
} | ||
|
||
throw new UserInputError('Invalid email and password combination') | ||
}, | ||
}, | ||
} |
8 changes: 8 additions & 0 deletions
8
examples/api-routes-apollo-server-and-client-auth/apollo/schema.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { makeExecutableSchema } from 'graphql-tools' | ||
import { typeDefs } from './type-defs' | ||
import { resolvers } from './resolvers' | ||
|
||
export const schema = makeExecutableSchema({ | ||
typeDefs, | ||
resolvers, | ||
}) |
38 changes: 38 additions & 0 deletions
38
examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import gql from 'graphql-tag' | ||
|
||
export const typeDefs = gql` | ||
type User { | ||
id: ID! | ||
email: String! | ||
} | ||
input SignUpInput { | ||
email: String! | ||
password: String! | ||
} | ||
input SignInInput { | ||
email: String! | ||
password: String! | ||
} | ||
type SignUpPayload { | ||
user: User! | ||
} | ||
type SignInPayload { | ||
user: User! | ||
token: String! | ||
} | ||
type Query { | ||
user(id: ID!): User! | ||
users: [User]! | ||
viewer: User | ||
} | ||
type Mutation { | ||
signUp(input: SignUpInput!): SignUpPayload! | ||
signIn(input: SignInInput!): SignInPayload! | ||
} | ||
` |
22 changes: 22 additions & 0 deletions
22
examples/api-routes-apollo-server-and-client-auth/components/field.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export default function Field(props) { | ||
return ( | ||
<div> | ||
<label | ||
id={[props.name, 'label'].join('-')} | ||
htmlFor={[props.name, 'input'].join('-')} | ||
> | ||
{props.label}{' '} | ||
{props.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} | ||
/> | ||
{props.status ? <p>{props.status.message}</p> : undefined} | ||
</div> | ||
) | ||
} |
Oops, something went wrong.