1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Apollo + Authentication + graph.cool example (#2161)

* Apollo + Authentication + graph.cool example

* Fix linting errors
This commit is contained in:
Jess Telford 2017-07-01 06:18:11 +10:00 committed by Tim Neutkens
parent d6a96a82aa
commit 4a0278b5c0
13 changed files with 671 additions and 0 deletions

View file

@ -0,0 +1,16 @@
{
"env": {
"development": {
"presets": "next/babel"
},
"production": {
"presets": "next/babel"
},
"test": {
"presets": [
["env", { "modules": "commonjs" }],
"next/babel"
]
}
}
}

80
examples/with-apollo-auth/.gitignore vendored Normal file
View file

@ -0,0 +1,80 @@
.next
# Created by https://www.gitignore.io/api/vim,node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### Vim ###
# swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
# End of https://www.gitignore.io/api/vim,node

View file

@ -0,0 +1,56 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-apollo-auth)
# Apollo With Authentication Example
## Demo
https://next-with-apollo-auth.now.sh
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-apollo-auth
cd with-apollo-auth
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
```bash
now
```
## The idea behind the example
This is an extention of the _[with Apollo](https://github.com/zeit/next.js/tree/master/examples/with-apollo#the-idea-behind-the-example)_ example:
> [Apollo](http://dev.apollodata.com) 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* 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`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
>
> This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
>
> *Note: Apollo uses Redux internally; if you're interested in integrating the client with your existing Redux store check out the [`with-apollo-and-redux`](https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux) example.*
[graph.cool](https://www.graph.cool) can be setup with many different
[authentication providers](https://www.graph.cool/docs/reference/integrations/overview-seimeish6e/#authentication-providers), the most basic of which is [email-password authentication](https://www.graph.cool/docs/reference/simple-api/user-authentication-eixu9osueb/#email-and-password). Once email-password authentication is enabled for your graph.cool project, you are provided with 2 useful mutations: `createUser` and `signinUser`.
On loading each route, we perform a `user` query to see if the current visitor is logged in (based on a cookie, more on that in a moment). Depending on the query result, and the route, the user may be [redirected](https://github.com/zeit/next.js/blob/master/examples/with-apollo-auth/lib/redirect.js) to a different page.
When creating an account, both the `createUser` and `signinUser` mutations are executed on graph.cool, which returns a token that can be used to [authenticate the user for future requests](https://www.graph.cool/docs/reference/auth/authentication-tokens-eip7ahqu5o/). The token is stored in a cookie for easy access (_note: This may have security implications. Please understand XSS and JWT before deploying this to production_).
A similar process is followed when signing in, except `signinUser` is the only mutation executed.
It is important to note the use of Apollo's `resetStore()` method after signing in and signing out to ensure that no user data is kept in the browser's memory.
To get this example running locally, you will need to create a graph.cool
account, and provide [the `project.graphcool` schema](https://github.com/zeit/next.js/blob/master/examples/with-apollo-auth/project.graphcool).

View file

@ -0,0 +1,19 @@
import gql from 'graphql-tag'
export default (context, apolloClient) => (
apolloClient.query({
query: gql`
query getUser {
user {
id
name
}
}
`
}).then(({ data }) => {
return { loggedInUser: data }
}).catch(() => {
// Fail gracefully
return { loggedInUser: {} }
})
)

View file

@ -0,0 +1,47 @@
import { ApolloClient, createNetworkInterface } from 'react-apollo'
import fetch from 'isomorphic-fetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
function create (initialState, { getToken }) {
const networkInterface = createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cj3h80ffbllm20162alevpcby'
})
networkInterface.use([{
applyMiddleware (req, next) {
if (!req.options.headers) {
req.options.headers = {} // Create the header object if needed.
}
const token = getToken()
req.options.headers.authorization = token ? `Bearer ${token}` : null
next()
}
}])
return new ApolloClient({
initialState,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
networkInterface
})
}
export default function initApollo (initialState, options) {
// 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, options)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState, options)
}
return apolloClient
}

View file

@ -0,0 +1,13 @@
import Router from 'next/router'
export default (context, target) => {
if (context.res) {
// server
// 303: "See other"
context.res.writeHead(303, { Location: target })
context.res.end()
} else {
// In the browser, we just pretend like this never even happened ;)
Router.replace(target)
}
}

View file

@ -0,0 +1,94 @@
import React from 'react'
import cookie from 'cookie'
import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import initApollo from './init-apollo'
function parseCookies (ctx = {}, options = {}) {
return cookie.parse(
ctx.req && ctx.req.headers.cookie
? ctx.req.headers.cookie
: document.cookie,
options
)
}
export default ComposedComponent => {
return class WithData extends React.Component {
static displayName = `WithData(${ComposedComponent.displayName})`
static propTypes = {
serverState: PropTypes.object.isRequired
}
static async getInitialProps (context) {
let serverState = {}
// Setup a server-side one-time-use apollo client for initial props and
// rendering (on server)
let apollo = initApollo({}, {
getToken: () => parseCookies(context).token
})
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {}
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(context, apollo)
}
// Run all graphql queries in the component tree
// and extract the resulting data
if (!process.browser) {
if (context.res && context.res.finished) {
// When redirecting, the response is finished.
// No point in continuing to render
return
}
// Provide the `url` prop data in case a graphql query uses it
const url = {query: context.query, pathname: context.pathname}
// Run all graphql queries
const app = (
<ApolloProvider client={apollo}>
<ComposedComponent url={url} {...composedInitialProps} />
</ApolloProvider>
)
await getDataFromTree(app)
// Extract query data from the Apollo's store
const state = apollo.getInitialState()
serverState = {
apollo: { // Make sure to only include Apollo's data state
data: state.data
}
}
}
return {
serverState,
...composedInitialProps
}
}
constructor (props) {
super(props)
// Note: Apollo should never be used on the server side beyond the initial
// render within `getInitialProps()` above (since the entire prop tree
// will be initialized there), meaning the below will only ever be
// executed on the client.
this.apollo = initApollo(this.props.serverState, {
getToken: () => parseCookies().token
})
}
render () {
return (
<ApolloProvider client={this.apollo}>
<ComposedComponent {...this.props} />
</ApolloProvider>
)
}
}
}

View file

@ -0,0 +1,33 @@
{
"name": "with-apollo-auth",
"version": "0.0.0",
"author": "",
"license": "ISC",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"test": "NODE_ENV=test ava"
},
"dependencies": {
"cookie": "^0.3.1",
"graphql": "^0.9.3",
"isomorphic-fetch": "^2.2.1",
"next": "latest",
"prop-types": "^15.5.10",
"react": "^15.5.4",
"react-apollo": "^1.1.3",
"react-dom": "^15.5.4"
},
"devDependencies": {
"ava": "^0.19.1",
"clear-require": "^2.0.0",
"glob": "^7.1.2"
},
"ava": {
"require": [
"babel-register"
],
"babel": "inherit"
}
}

View file

@ -0,0 +1,104 @@
import React from 'react'
import { graphql, withApollo, compose } from 'react-apollo'
import cookie from 'cookie'
import Link from 'next/link'
import gql from 'graphql-tag'
import withData from '../lib/with-data'
import redirect from '../lib/redirect'
import checkLoggedIn from '../lib/check-logged-in'
class CreateAccount extends React.Component {
static async getInitialProps (context, apolloClient) {
const { loggedInUser } = await checkLoggedIn(context, apolloClient)
if (loggedInUser.user) {
// Already signed in? No need to continue.
// Throw them back to the main page
redirect(context, '/')
}
return {}
}
render () {
return (
<div>
{/* this.props.create is the mutation function provided by apollo below */}
<form onSubmit={this.props.create}>
<input type='text' placeholder='Your Name' name='name' /><br />
<input type='email' placeholder='Email' name='email' /><br />
<input type='password' placeholder='Password' name='password' /><br />
<button>Create account</button>
</form>
<hr />
Already have an account? <Link prefetch href='/signin'><a>Sign in</a></Link>
</div>
)
}
};
export default compose(
// withData gives us server-side graphql queries before rendering
withData,
// withApollo exposes `this.props.client` used when logging out
withApollo,
graphql(
// The `createUser` & `signinUser` mutations are provided by graph.cool by
// default.
// Multiple mutations are executed by graphql sequentially
gql`
mutation Create($name: String!, $email: String!, $password: String!) {
createUser(name: $name, authProvider: { email: { email: $email, password: $password }}) {
id
}
signinUser(email: { email: $email, password: $password }) {
token
}
}
`,
{
// Use an unambiguous name for use in the `props` section below
name: 'createWithEmail',
// Apollo's way of injecting new props which are passed to the component
props: ({
createWithEmail,
// `client` is provided by the `withApollo` HOC
ownProps: { client }
}) => ({
// `create` is the name of the prop passed to the component
create: (event) => {
/* global FormData */
const data = new FormData(event.target)
event.preventDefault()
event.stopPropagation()
createWithEmail({
variables: {
email: data.get('email'),
password: data.get('password'),
name: data.get('name')
}
}).then(({ data: { signinUser: { token } } }) => {
// Store the token in cookie
document.cookie = cookie.serialize('token', token, {
maxAge: 30 * 24 * 60 * 60 // 30 days
})
// Force a reload of all the current queries now that the user is
// logged in
client.resetStore().then(() => {
// Now redirect to the homepage
redirect({}, '/')
})
}).catch((error) => {
// Something went wrong, such as incorrect password, or no network
// available, etc.
console.error(error)
})
}
})
}
)
)(CreateAccount)

View file

@ -0,0 +1,49 @@
import React from 'react'
import cookie from 'cookie'
import { withApollo, compose } from 'react-apollo'
import withData from '../lib/with-data'
import redirect from '../lib/redirect'
import checkLoggedIn from '../lib/check-logged-in'
class Index extends React.Component {
static async getInitialProps (context, apolloClient) {
const { loggedInUser } = await checkLoggedIn(context, apolloClient)
if (!loggedInUser.user) {
// If not signed in, send them somewhere more useful
redirect(context, '/signin')
}
return { loggedInUser }
}
signout = () => {
document.cookie = cookie.serialize('token', '', {
maxAge: -1 // Expire the cookie immediately
})
// Force a reload of all the current queries now that the user is
// logged in, so we don't accidentally leave any state around.
this.props.client.resetStore().then(() => {
// Redirect to a more useful page when signed out
redirect({}, '/signin')
})
}
render () {
return (
<div>
Hello {this.props.loggedInUser.user.name}!<br />
<button onClick={this.signout}>Sign out</button>
</div>
)
}
};
export default compose(
// withData gives us server-side graphql queries before rendering
withData,
// withApollo exposes `this.props.client` used when logging out
withApollo
)(Index)

View file

@ -0,0 +1,97 @@
import React from 'react'
import { graphql, withApollo, compose } from 'react-apollo'
import cookie from 'cookie'
import Link from 'next/link'
import gql from 'graphql-tag'
import withData from '../lib/with-data'
import redirect from '../lib/redirect'
import checkLoggedIn from '../lib/check-logged-in'
class Signin extends React.Component {
static async getInitialProps (context, apolloClient) {
const { loggedInUser } = await checkLoggedIn(context, apolloClient)
if (loggedInUser.user) {
// Already signed in? No need to continue.
// Throw them back to the main page
redirect(context, '/')
}
return {}
}
render () {
return (
<div>
{/* this.props.signin is the mutation function provided by apollo below */}
<form onSubmit={this.props.signin}>
<input type='email' placeholder='Email' name='email' /><br />
<input type='password' placeholder='Password' name='password' /><br />
<button>Sign in</button>
</form>
<hr />
New? <Link prefetch href='/create-account'><a>Create account</a></Link>
</div>
)
}
};
export default compose(
// withData gives us server-side graphql queries before rendering
withData,
// withApollo exposes `this.props.client` used when logging out
withApollo,
graphql(
// The `signinUser` mutation is provided by graph.cool by default
gql`
mutation Signin($email: String!, $password: String!) {
signinUser(email: { email: $email, password: $password }) {
token
}
}
`,
{
// Use an unambiguous name for use in the `props` section below
name: 'signinWithEmail',
// Apollo's way of injecting new props which are passed to the component
props: ({
signinWithEmail,
// `client` is provided by the `withApollo` HOC
ownProps: { client }
}) => ({
// `signin` is the name of the prop passed to the component
signin: (event) => {
/* global FormData */
const data = new FormData(event.target)
event.preventDefault()
event.stopPropagation()
signinWithEmail({
variables: {
email: data.get('email'),
password: data.get('password')
}
}).then(({ data: { signinUser: { token } } }) => {
// Store the token in cookie
document.cookie = cookie.serialize('token', token, {
maxAge: 30 * 24 * 60 * 60 // 30 days
})
// Force a reload of all the current queries now that the user is
// logged in
client.resetStore().then(() => {
// Now redirect to the homepage
redirect({}, '/')
})
}).catch((error) => {
// Something went wrong, such as incorrect password, or no network
// available, etc.
console.error(error)
})
}
})
}
)
)(Signin)

View file

@ -0,0 +1,22 @@
# projectId: cj3h80ffbllm20162alevpcby
# version: 3
type File implements Node {
contentType: String!
createdAt: DateTime!
id: ID! @isUnique
name: String!
secret: String! @isUnique
size: Int!
updatedAt: DateTime!
url: String! @isUnique
}
type User implements Node {
createdAt: DateTime!
email: String @isUnique
id: ID! @isUnique
name: String!
password: String
updatedAt: DateTime!
}

View file

@ -0,0 +1,41 @@
const clearRequire = require('clear-require')
const glob = require('glob')
const test = require('ava')
/**
* Motivations:
*
* - Client-side getInitialProps() wont have access to the apollo client for
* that page (because it's not shared across page bundles), so wont be able to
* reset the state, leaving all the logged in user data there :(
* - So, we have to have a shared module. BUT; next's code splitting means the
* bundle for each page will include its own copy of the module, _unless it's
* inlcuded in every page_.
* - https://github.com/zeit/next.js/issues/659#issuecomment-271824223
* - https://github.com/zeit/next.js/issues/1635#issuecomment-292236785
* - Therefore, this test ensures that every page includes that module, and
* hence it will be shared across every page, giving us a global store in
* Apollo that we can clear, etc
*/
const apolloFilePath = require.resolve('../lib/init-apollo')
test.beforeEach(() => {
// Clean up the cache
clearRequire.all()
})
glob.sync('./pages/**/*.js').forEach((file) => {
test(`.${file} imports shared apollo module`, (t) => {
t.falsy(require.cache[apolloFilePath])
try {
require(`.${file}`)
} catch (error) {
// Don't really care if it fails to execute, etc, just want to be
// certain the expected require call was made
}
t.truthy(require.cache[apolloFilePath])
})
})