Skip to content

Commit dd195e3

Browse files
committed
Add user register/login flow with components
Copy pasted from user tabomors https://github.com/tabomors/next.js/tree/update-with-apollo-auth
1 parent 39ef7d0 commit dd195e3

File tree

9 files changed

+350
-17
lines changed

9 files changed

+350
-17
lines changed

README.md

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# Apollo Example
2-
3-
## Demo
4-
5-
https://next-with-apollo.now.sh
1+
# Apollo With Authentication Example
62

73
## How to use
84

@@ -11,18 +7,18 @@ https://next-with-apollo.now.sh
117
Execute [`create-next-app`](https://github.com/segmentio/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:
128

139
```bash
14-
npx create-next-app --example with-apollo with-apollo-app
10+
npx create-next-app --example with-apollo-auth with-apollo-auth-app
1511
# or
16-
yarn create next-app --example with-apollo with-apollo-app
12+
yarn create next-app --example with-apollo-auth with-apollo-auth-app
1713
```
1814

1915
### Download manually
2016

2117
Download the example:
2218

2319
```bash
24-
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-apollo
25-
cd with-apollo
20+
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-apollo-auth
21+
cd with-apollo-auth
2622
```
2723

2824
Install it and run:
@@ -43,13 +39,34 @@ now
4339

4440
## The idea behind the example
4541

46-
[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.
42+
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:
43+
44+
> [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.
45+
>
46+
> 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.
47+
>
48+
> On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](https://www.apollographql.com/docs/react/features/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.
49+
>
50+
> This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
51+
>
52+
> _Note: 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._
53+
54+
[graph.cool](https://www.graph.cool) can be setup with many different
55+
[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`.
56+
57+
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.
58+
59+
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_).
60+
61+
A similar process is followed when signing in, except `signinUser` is the only mutation executed.
62+
63+
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.
4764

48-
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.
65+
To get this example running locally, you will need to create a graph.cool
66+
account, and provide [the `project.graphcool` schema](https://github.com/zeit/next.js/blob/master/examples/with-apollo-auth/project.graphcool).
4967

50-
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.
68+
### Note:
5169

52-
This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
70+
Solved by providing `static defaultProp`
5371

54-
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).
55-
https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree
72+
In these _with-apollo_ examples, the `withData()` HOC must wrap a top-level component from within the `pages` directory. Wrapping a child component with the HOC will result in a `Warning: Failed prop type: The prop 'serverState' is marked as required in 'WithData(Apollo(Component))', but its value is 'undefined'` error. Down-tree child components will have access to Apollo, and can be wrapped with any other sort of `graphql()`, `compose()`, etc HOC's.

components/RegisterBox.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useMutation, useApolloClient } from '@apollo/react-hooks'
2+
import gql from 'graphql-tag'
3+
import cookie from 'cookie'
4+
import redirect from '../lib/redirect'
5+
6+
const CREATE_USER = gql`
7+
mutation Create($name: String!, $email: String!, $password: String!) {
8+
createUser(
9+
name: $name
10+
authProvider: { email: { email: $email, password: $password } }
11+
) {
12+
id
13+
}
14+
signinUser(email: { email: $email, password: $password }) {
15+
token
16+
}
17+
}
18+
`
19+
20+
const RegisterBox = () => {
21+
const client = useApolloClient()
22+
23+
const onCompleted = data => {
24+
// Store the token in cookie
25+
document.cookie = cookie.serialize('token', data.signinUser.token, {
26+
maxAge: 30 * 24 * 60 * 60 // 30 days
27+
})
28+
// Force a reload of all the current queries now that the user is
29+
// logged in
30+
client.cache.reset().then(() => {
31+
redirect({}, '/')
32+
})
33+
}
34+
const onError = error => {
35+
// If you want to send error to external service?
36+
console.error(error)
37+
}
38+
const [create, { error }] = useMutation(CREATE_USER, { onCompleted, onError })
39+
let name, email, password
40+
41+
return (
42+
<form
43+
onSubmit={e => {
44+
e.preventDefault()
45+
e.stopPropagation()
46+
47+
create({
48+
variables: {
49+
name: name.value,
50+
email: email.value,
51+
password: password.value
52+
}
53+
})
54+
55+
name.value = email.value = password.value = ''
56+
}}
57+
>
58+
{error && <p>Issue occurred while registering :(</p>}
59+
<input
60+
name='name'
61+
placeholder='Name'
62+
ref={node => {
63+
name = node
64+
}}
65+
/>
66+
<br />
67+
<input
68+
name='email'
69+
placeholder='Email'
70+
ref={node => {
71+
email = node
72+
}}
73+
/>
74+
<br />
75+
<input
76+
name='password'
77+
placeholder='Password'
78+
ref={node => {
79+
password = node
80+
}}
81+
type='password'
82+
/>
83+
<br />
84+
<button>Register</button>
85+
</form>
86+
)
87+
}
88+
89+
export default RegisterBox

components/SigninBox.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useMutation, useApolloClient } from '@apollo/react-hooks'
2+
import gql from 'graphql-tag'
3+
import cookie from 'cookie'
4+
import redirect from '../lib/redirect'
5+
6+
const SIGN_IN = gql`
7+
mutation Signin($email: String!, $password: String!) {
8+
signinUser(email: { email: $email, password: $password }) {
9+
token
10+
}
11+
}
12+
`
13+
14+
// TODO: Find a better name for component.
15+
const SigninBox = () => {
16+
const client = useApolloClient()
17+
18+
const onCompleted = data => {
19+
// Store the token in cookie
20+
document.cookie = cookie.serialize('token', data.signinUser.token, {
21+
maxAge: 30 * 24 * 60 * 60 // 30 days
22+
})
23+
// Force a reload of all the current queries now that the user is
24+
// logged in
25+
client.cache.reset().then(() => {
26+
redirect({}, '/')
27+
})
28+
}
29+
30+
const onError = error => {
31+
// If you want to send error to external service?
32+
console.error(error)
33+
}
34+
35+
const [signinUser, { error }] = useMutation(SIGN_IN, {
36+
onCompleted,
37+
onError
38+
})
39+
40+
let email, password
41+
42+
return (
43+
<form
44+
onSubmit={e => {
45+
e.preventDefault()
46+
e.stopPropagation()
47+
48+
signinUser({
49+
variables: {
50+
email: email.value,
51+
password: password.value
52+
}
53+
})
54+
55+
email.value = password.value = ''
56+
}}
57+
>
58+
{error && <p>No user found with that information.</p>}
59+
<input
60+
name='email'
61+
placeholder='Email'
62+
ref={node => {
63+
email = node
64+
}}
65+
/>
66+
<br />
67+
<input
68+
name='password'
69+
placeholder='Password'
70+
ref={node => {
71+
password = node
72+
}}
73+
type='password'
74+
/>
75+
<br />
76+
<button>Sign in</button>
77+
</form>
78+
)
79+
}
80+
81+
export default SigninBox

lib/checkLoggedIn.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import gql from 'graphql-tag'
2+
3+
export default apolloClient =>
4+
apolloClient
5+
.query({
6+
query: gql`
7+
query getUser {
8+
user {
9+
id
10+
name
11+
}
12+
}
13+
`
14+
})
15+
.then(({ data }) => {
16+
return { loggedInUser: data }
17+
})
18+
.catch(() => {
19+
// Fail gracefully
20+
return { loggedInUser: {} }
21+
})

lib/init-apollo.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ if (typeof window === 'undefined') {
1414

1515
function create (initialState, { getToken }) {
1616
const httpLink = createHttpLink({
17-
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
17+
uri: 'https://api.graph.cool/simple/v1/cj5geu3slxl7t0127y8sity9r',
1818
credentials: 'same-origin',
1919
})
2020

lib/redirect.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Router from 'next/router'
2+
3+
export default (context, target) => {
4+
if (context.res) {
5+
// server
6+
// 303: "See other"
7+
context.res.writeHead(303, { Location: target })
8+
context.res.end()
9+
} else {
10+
// In the browser, we just pretend like this never even happened ;)
11+
Router.replace(target)
12+
}
13+
}

pages/create-account.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react'
2+
import Link from 'next/link'
3+
4+
import redirect from '../lib/redirect'
5+
import checkLoggedIn from '../lib/checkLoggedIn'
6+
7+
import RegisterBox from '../components/RegisterBox'
8+
9+
export default class CreateAccount extends React.Component {
10+
static async getInitialProps (context) {
11+
const { loggedInUser } = await checkLoggedIn(context.apolloClient)
12+
13+
if (loggedInUser.user) {
14+
// Already signed in? No need to continue.
15+
// Throw them back to the main page
16+
redirect(context, '/')
17+
}
18+
19+
return {}
20+
}
21+
22+
render () {
23+
return (
24+
<>
25+
{/* RegisterBox handles all register logic. */}
26+
<RegisterBox />
27+
<hr />
28+
Already have an account?{' '}
29+
<Link prefetch href='/signin'>
30+
<a>Sign in</a>
31+
</Link>
32+
</>
33+
)
34+
}
35+
}

pages/index.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,43 @@
1-
export default () => <div>Hello</div>
1+
import React from 'react'
2+
import cookie from 'cookie'
3+
import { useApolloClient } from '@apollo/react-hooks'
4+
5+
import redirect from '../lib/redirect'
6+
import checkLoggedIn from '../lib/checkLoggedIn'
7+
8+
const Index = ({ loggedInUser }) => {
9+
const apolloClient = useApolloClient()
10+
11+
const signout = () => {
12+
document.cookie = cookie.serialize('token', '', {
13+
maxAge: -1 // Expire the cookie immediately
14+
})
15+
16+
// Force a reload of all the current queries now that the user is
17+
// logged in, so we don't accidentally leave any state around.
18+
apolloClient.cache.reset().then(() => {
19+
// Redirect to a more useful page when signed out
20+
redirect({}, '/signin')
21+
})
22+
}
23+
24+
return (
25+
<div>
26+
Hello {loggedInUser.user.name}!<br />
27+
<button onClick={signout}>Sign out</button>
28+
</div>
29+
)
30+
}
31+
32+
Index.getInitialProps = async context => {
33+
const { loggedInUser } = await checkLoggedIn(context.apolloClient)
34+
35+
if (!loggedInUser.user) {
36+
// If not signed in, send them somewhere more useful
37+
redirect(context, '/signin')
38+
}
39+
40+
return { loggedInUser }
41+
}
42+
43+
export default Index

0 commit comments

Comments
 (0)