Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to with-apollo-auth #9274

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 7 additions & 10 deletions examples/with-apollo-auth/README.md
Expand Up @@ -47,23 +47,20 @@ This is an extention of the _[with Apollo](https://github.com/zeit/next.js/tree/
>
> 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.
>
> This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
> This example uses [Photon](https://photonjs.prisma.io/) as ORM connected to Postgres
>
> _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._

[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 `me` 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.

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 `register` mutation are executed in `/api/graphql`, which returns a token that can be used to authenticate the user. 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_).

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 using `login` mutation.

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 `client.cache.reset()` method after signing in and signing out to ensure that no user data is kept in the browser's memory.

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).
To get this example running locally, you will need to create a Postgres database, and database url in [the `schema.prisma` file](https://github.com/zeit/next.js/blob/master/examples/with-apollo-auth/prisma/schema.prisma).
Photon supports other databases if you don't wish to use Postgres see [Photon](https://photonjs.prisma.io/) for more information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to add dummy login credentials if we're going to leave this database in the example 🤔

### Note:

Expand Down
12 changes: 3 additions & 9 deletions examples/with-apollo-auth/components/RegisterBox.js
Expand Up @@ -5,14 +5,8 @@ import cookie from 'cookie'
import redirect from '../lib/redirect'

const CREATE_USER = 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 }) {
mutation register($email: String, $name: String, $password: String) {
register(email: $email, name: $name, password: $password) {
token
}
}
Expand All @@ -23,7 +17,7 @@ const RegisterBox = () => {

const onCompleted = data => {
// Store the token in cookie
document.cookie = cookie.serialize('token', data.signinUser.token, {
document.cookie = cookie.serialize('token', data.register.token, {
maxAge: 30 * 24 * 60 * 60, // 30 days
path: '/' // make cookie available for all routes underneath "/"
})
Expand Down
7 changes: 3 additions & 4 deletions examples/with-apollo-auth/components/SigninBox.js
Expand Up @@ -5,8 +5,8 @@ import cookie from 'cookie'
import redirect from '../lib/redirect'

const SIGN_IN = gql`
mutation Signin($email: String!, $password: String!) {
signinUser(email: { email: $email, password: $password }) {
mutation login($email: String, $password: String) {
login(email: $email, password: $password) {
token
}
}
Expand All @@ -18,8 +18,7 @@ const SigninBox = () => {

const onCompleted = data => {
// Store the token in cookie
document.cookie = cookie.serialize('token', data.signinUser.token, {
sameSite: true,
document.cookie = cookie.serialize('token', data.login.token, {
path: '/',
maxAge: 30 * 24 * 60 * 60 // 30 days
})
Expand Down
2 changes: 1 addition & 1 deletion examples/with-apollo-auth/lib/apollo.js
Expand Up @@ -152,7 +152,7 @@ function createApolloClient (initialState = {}, { getToken }) {
}

const httpLink = new HttpLink({
uri: 'https://api.graph.cool/simple/v1/cj5geu3slxl7t0127y8sity9r', // Server URL (must be absolute)
uri: 'http://localhost:3000/api/graphql', // Server URL (must be absolute)
credentials: 'same-origin',
fetch,
fetchOptions
Expand Down
10 changes: 5 additions & 5 deletions examples/with-apollo-auth/lib/checkLoggedIn.js
Expand Up @@ -4,18 +4,18 @@ export default apolloClient =>
apolloClient
.query({
query: gql`
query getUser {
user {
query me {
me {
id
name
}
}
`
})
.then(({ data }) => {
return { loggedInUser: data }
return { ...data }
})
.catch(() => {
.catch(e => {
// Fail gracefully
return { loggedInUser: {} }
return {}
})
31 changes: 19 additions & 12 deletions examples/with-apollo-auth/package.json
Expand Up @@ -6,23 +6,30 @@
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
"start": "next start",
"postinstall": "prisma2 generate"
},
"dependencies": {
"@apollo/react-hooks": "^3.0.1",
"@apollo/react-ssr": "^3.0.1",
"@apollo/react-hooks": "^3.1.3",
"@apollo/react-ssr": "^3.1.3",
"apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.4",
"apollo-link-context": "^1.0.8",
"apollo-link-http": "1.5.15",
"cookie": "^0.3.1",
"graphql": "14.0.2",
"apollo-link-context": "^1.0.19",
"apollo-link-http": "^1.5.16",
"apollo-server-micro": "^2.9.7",
"bcryptjs": "^2.4.3",
"cookie": "^0.4.0",
"graphql": "^14.5.8",
"graphql-tag": "2.10.1",
"https-proxy-agent": "^2.2.1",
"isomorphic-unfetch": "^2.0.0",
"https-proxy-agent": "^3.0.1",
"isomorphic-unfetch": "^3.0.0",
"jsonwebtoken": "^8.5.1",
"next": "latest",
"prop-types": "^15.6.1",
"react": "^16.7.0",
"react-dom": "^16.7.0"
"prop-types": "^15.7.2",
"react": "^16.11.0",
"react-dom": "^16.11.0"
},
"devDependencies": {
"prisma2": "^2.0.0-preview015"
}
}
103 changes: 103 additions & 0 deletions examples/with-apollo-auth/pages/api/graphql.js
@@ -0,0 +1,103 @@
import { ApolloServer, gql } from 'apollo-server-micro'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { Photon } from '@generated/photon'

const photon = new Photon()

const JWT_SECRET = 'PleaseUseBetterStorageForThisSecret'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DevSpeak why not stick this in env?


const getUserId = (req) => {
const Authorization = req.headers && req.headers.authorization || ''
if (Authorization) {
const token = Authorization.replace('Bearer ', '')
const verifiedToken = jwt.verify(token, JWT_SECRET)
return verifiedToken.userId
}
}

const typeDefs = gql`
type Query {
me: User!
}
type Mutation {
register(email: String, name: String, password: String): AuthPayload!
login(email: String, password: String): AuthPayload!
}
type AuthPayload {
token: String
}
type User {
id: String
name: String
email: String
}
`

const resolvers = {
Query: {
async me (parent, args, context) {
const id = context.user
const user = await context.photon.users.findOne({ where: { id } }).then(user => user)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the .then(user => user) necessary?


if (!user) throw new Error('No such user found.')

return { ...user }
}
},
Mutation: {
async register (parent, { email, name, password }, context) {
const hashedPassword = await bcrypt.hash(password, 10)

const user = await context.photon.users.create({
data: {
email,
name,
password: hashedPassword
}
}).then(user => user)

if (!user) throw new Error('No such user found.')

const token = jwt.sign({
userId: user.id
}, JWT_SECRET)

return { token }
},
async login (parent, { email, password }, context) {
const user = await context.photon.users.findOne({ where: { email } }).then(user => user)

if (!user) throw new Error('No such user found.')

const valid = await bcrypt.compare(password, user.password)

if (valid) {
const token = jwt.sign({
userId: user.id
}, JWT_SECRET)

return { token }
} else {
throw new Error('Invalid password.')
}
}
}
}

const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const user = getUserId(req)
return { req, user, photon }
}
})

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

export default apolloServer.createHandler({ path: '/api/graphql' })
7 changes: 3 additions & 4 deletions examples/with-apollo-auth/pages/create-account.js
Expand Up @@ -18,11 +18,10 @@ const CreateAccountPage = () => (
)

CreateAccountPage.getInitialProps = async context => {
const { loggedInUser } = await checkLoggedIn(context.apolloClient)
const data = await checkLoggedIn(context.apolloClient)

if (loggedInUser.user) {
// Already signed in? No need to continue.
// Throw them back to the main page
if (data.me) {
// If not signed in, send them somewhere more useful
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like registering accounts isn't working
Screen Shot 2019-11-10 at 23 41 44

redirect(context, '/')
}

Expand Down
8 changes: 4 additions & 4 deletions examples/with-apollo-auth/pages/index.js
Expand Up @@ -23,21 +23,21 @@ const IndexPage = ({ loggedInUser }) => {

return (
<div>
Hello {loggedInUser.user.name}!<br />
Hello {loggedInUser.name}!<br />
<button onClick={signout}>Sign out</button>
</div>
)
}

IndexPage.getInitialProps = async context => {
const { loggedInUser } = await checkLoggedIn(context.apolloClient)
const data = await checkLoggedIn(context.apolloClient)

if (!loggedInUser.user) {
if (!data.me) {
// If not signed in, send them somewhere more useful
redirect(context, '/signin')
}

return { loggedInUser }
return { loggedInUser: data.me }
}

export default withApollo(IndexPage)
7 changes: 3 additions & 4 deletions examples/with-apollo-auth/pages/signin.js
Expand Up @@ -18,11 +18,10 @@ const SigninPage = () => (
)

SigninPage.getInitialProps = async context => {
const { loggedInUser } = await checkLoggedIn(context.apolloClient)
const data = await checkLoggedIn(context.apolloClient)

if (loggedInUser.user) {
// Already signed in? No need to continue.
// Throw them back to the main page
if (data.me) {
// If not signed in, send them somewhere more useful
redirect(context, '/')
}

Expand Down
15 changes: 15 additions & 0 deletions examples/with-apollo-auth/prisma/schema.prisma
@@ -0,0 +1,15 @@
generator photon {
provider = "photonjs"
}

datasource db {
provider = "postgresql"
url = "postgresql://yevnnxduqfivsa:d91c5ef154ae01e7ef8e7add436065c15491dd2245e6a83149c01f0e4a85b1b0@ec2-54-235-163-246.compute-1.amazonaws.com/dd90ll8j0inem8?schema=next&sslmode=prefer"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DB url seems unintended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was actually intended for example use purpose.
Its just a free heroku instance of Postgres. That isn't used for anything.

}

model User {
id String @default(cuid()) @id
email String @unique
name String?
password String
}
22 changes: 0 additions & 22 deletions examples/with-apollo-auth/project.graphcool

This file was deleted.