diff --git a/examples/api-routes-apollo-server-and-client-auth/.env b/examples/api-routes-apollo-server-and-client-auth/.env new file mode 100644 index 000000000000..f7448eb30a79 --- /dev/null +++ b/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" \ No newline at end of file diff --git a/examples/api-routes-apollo-server-and-client-auth/README.md b/examples/api-routes-apollo-server-and-client-auth/README.md index b6339fb72775..4fad0a97a080 100644 --- a/examples/api-routes-apollo-server-and-client-auth/README.md +++ b/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 @@ -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)). diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/client.js b/examples/api-routes-apollo-server-and-client-auth/apollo/client.js index 494f1fdfdacb..26ae378500bd 100644 --- a/examples/api-routes-apollo-server-and-client-auth/apollo/client.js +++ b/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 ( - - - - ) - } - - // 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( - - ) - } 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 +} diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js b/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js index 50fe47ea0ca6..2afc6c3dfb00 100644 --- a/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js @@ -1,76 +1,39 @@ 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 } } @@ -78,17 +41,7 @@ export const resolvers = { 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 }, }, diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js b/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js index cd77f1e5b26e..bea78a284e80 100644 --- a/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js @@ -4,6 +4,7 @@ export const typeDefs = gql` type User { id: ID! email: String! + createdAt: Int! } input SignUpInput { diff --git a/examples/api-routes-apollo-server-and-client-auth/components/field.js b/examples/api-routes-apollo-server-and-client-auth/components/field.js index 5ad6fa2e3548..c33baa1e17ec 100644 --- a/examples/api-routes-apollo-server-and-client-auth/components/field.js +++ b/examples/api-routes-apollo-server-and-client-auth/components/field.js @@ -1,20 +1,16 @@ -export default function Field(props) { +export default function Field({ name, label, type, autoComplete, required }) { return (
-
) diff --git a/examples/api-routes-apollo-server-and-client-auth/lib/auth-cookies.js b/examples/api-routes-apollo-server-and-client-auth/lib/auth-cookies.js new file mode 100644 index 000000000000..b0fbf5030b7e --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/lib/auth-cookies.js @@ -0,0 +1,41 @@ +import { serialize, parse } from 'cookie' + +const TOKEN_NAME = 'token' + +export const MAX_AGE = 60 * 60 * 8 // 8 hours + +export function setTokenCookie(res, token) { + const cookie = serialize(TOKEN_NAME, token, { + maxAge: MAX_AGE, + expires: new Date(Date.now() + MAX_AGE * 1000), + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + path: '/', + sameSite: 'lax', + }) + + res.setHeader('Set-Cookie', cookie) +} + +export function removeTokenCookie(res) { + const cookie = serialize(TOKEN_NAME, '', { + maxAge: -1, + path: '/', + }) + + res.setHeader('Set-Cookie', cookie) +} + +export function parseCookies(req) { + // For API Routes we don't need to parse the cookies. + if (req.cookies) return req.cookies + + // For pages we do need to parse the cookies. + const cookie = req.headers?.cookie + return parse(cookie || '') +} + +export function getTokenCookie(req) { + const cookies = parseCookies(req) + return cookies[TOKEN_NAME] +} diff --git a/examples/api-routes-apollo-server-and-client-auth/lib/auth.js b/examples/api-routes-apollo-server-and-client-auth/lib/auth.js new file mode 100644 index 000000000000..6dc386d15e6b --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/lib/auth.js @@ -0,0 +1,27 @@ +import Iron from '@hapi/iron' +import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies' + +const TOKEN_SECRET = process.env.TOKEN_SECRET + +export async function setLoginSession(res, session) { + const createdAt = Date.now() + // Create a session object with a max age that we can validate later + const obj = { ...session, createdAt, maxAge: MAX_AGE } + const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults) + + setTokenCookie(res, token) +} + +export async function getLoginSession(req) { + const token = getTokenCookie(req) + + if (!token) return + + const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults) + const expiresAt = session.createdAt + session.maxAge * 1000 + + // Validate the expiration date of the session + if (Date.now() < expiresAt) { + return session + } +} diff --git a/examples/api-routes-apollo-server-and-client-auth/lib/user.js b/examples/api-routes-apollo-server-and-client-auth/lib/user.js new file mode 100644 index 000000000000..e5b82ed635c6 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/lib/user.js @@ -0,0 +1,47 @@ +import crypto from 'crypto' +import uuidv4 from 'uuid/v4' + +/** + * User methods. The example doesn't contain a DB, but for real applications you must use a + * db here, such as MongoDB, Fauna, SQL, etc. + */ + +const users = [] + +export async function createUser({ email, password }) { + // Here you should create the user and save the salt and hashed password (some dbs may have + // authentication methods that will do it for you so you don't have to worry about it): + const salt = crypto.randomBytes(16).toString('hex') + const hash = crypto + .pbkdf2Sync(password, salt, 1000, 64, 'sha512') + .toString('hex') + const user = { + id: uuidv4(), + createdAt: Date.now(), + email, + hash, + salt, + } + + // This is an in memory store for users, there is no data persistence without a proper DB + users.push(user) + + return user +} + +// Here you should lookup for the user in your DB +export async function findUser({ email }) { + // This is an in memory store for users, there is no data persistence without a proper DB + return users.find((user) => user.email === email) +} + +// Compare the password of an already fetched user (using `findUser`) and compare the +// password for a potential match +export async function validatePassword(user, inputPassword) { + const password = crypto + .pbkdf2Sync(user.hash, user.salt, 1000, 64, 'sha512') + .toString('hex') + const passwordsMatch = password === inputPassword + + return passwordsMatch +} diff --git a/examples/api-routes-apollo-server-and-client-auth/next.config.js b/examples/api-routes-apollo-server-and-client-auth/next.config.js deleted file mode 100644 index 35db685d842d..000000000000 --- a/examples/api-routes-apollo-server-and-client-auth/next.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - serverRuntimeConfig: { - JWT_SECRET: 'changeme', - }, -} diff --git a/examples/api-routes-apollo-server-and-client-auth/package.json b/examples/api-routes-apollo-server-and-client-auth/package.json index cc147e35e06f..d0ccec797752 100644 --- a/examples/api-routes-apollo-server-and-client-auth/package.json +++ b/examples/api-routes-apollo-server-and-client-auth/package.json @@ -1,32 +1,29 @@ { - "name": "with-apollo", - "version": "2.0.0", + "name": "api-routes-apollo-server-and-client-auth", + "version": "1.0.0", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "dependencies": { - "@apollo/react-common": "3.1.3", - "@apollo/react-hooks": "3.1.3", - "@apollo/react-ssr": "3.1.3", - "apollo-cache-inmemory": "1.6.5", - "apollo-client": "2.6.8", - "apollo-link-http": "1.5.16", - "apollo-link-schema": "1.2.4", - "apollo-server-micro": "2.9.16", + "@apollo/react-common": "^3.1.4", + "@apollo/react-hooks": "^3.1.5", + "@hapi/iron": "6.0.0", + "apollo-cache-inmemory": "^1.6.6", + "apollo-client": "^2.6.10", + "apollo-link-http": "^1.5.17", + "apollo-link-schema": "^1.2.5", + "apollo-server-micro": "^2.14.2", "apollo-utilities": "^1.3.2", - "bcrypt": "3.0.7", - "cookie": "0.4.0", + "cookie": "^0.4.1", "graphql": "^14.0.2", - "graphql-tag": "2.10.1", - "jsonwebtoken": "8.5.1", + "graphql-tag": "^2.10.3", "next": "latest", "prop-types": "^15.6.2", "react": "^16.7.0", "react-dom": "^16.7.0", - "uuid": "3.4.0" + "uuid": "8.1.0" }, - "author": "", "license": "ISC" } diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/_app.js b/examples/api-routes-apollo-server-and-client-auth/pages/_app.js new file mode 100644 index 000000000000..0345a86b23ea --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/_app.js @@ -0,0 +1,12 @@ +import { ApolloProvider } from '@apollo/react-hooks' +import { useApollo } from '../apollo/client' + +export default function App({ Component, pageProps }) { + const apolloClient = useApollo(pageProps.initialApolloState) + + return ( + + + + ) +} diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/about.js b/examples/api-routes-apollo-server-and-client-auth/pages/about.js index 4d21f1676c4c..36ce20fa93b7 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/about.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/about.js @@ -3,9 +3,9 @@ import Link from 'next/link' export default function About() { return (
- This is a static page goto{' '} + Welcome to the about page. Go to the{' '} - dynamic + Home {' '} page.
diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/index.js b/examples/api-routes-apollo-server-and-client-auth/pages/index.js index 2ea42bf1f70f..11ed53b934de 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/index.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/index.js @@ -1,8 +1,8 @@ -import { withApollo } from '../apollo/client' -import gql from 'graphql-tag' +import { useEffect } from 'react' +import { useRouter } from 'next/router' import Link from 'next/link' +import gql from 'graphql-tag' import { useQuery } from '@apollo/react-hooks' -import { useRouter } from 'next/router' const ViewerQuery = gql` query ViewerQuery { @@ -15,22 +15,27 @@ const ViewerQuery = gql` const Index = () => { const router = useRouter() - const { data, loading } = useQuery(ViewerQuery) + const { data, loading, error } = useQuery(ViewerQuery) + const viewer = data?.viewer + const shouldRedirect = !(loading || error || viewer) + + useEffect(() => { + if (shouldRedirect) { + router.push('/signin') + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldRedirect]) - if ( - loading === false && - data.viewer === null && - typeof window !== 'undefined' - ) { - router.push('/signin') + if (error) { + return

{error.message}

} - if (data && data.viewer) { + if (viewer) { return (
- You're signed in as {data.viewer.email} goto{' '} + You're signed in as {viewer.email} goto{' '} - static + about {' '} page. or{' '} @@ -43,4 +48,4 @@ const Index = () => { return

Loading...

} -export default withApollo(Index) +export default Index diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signin.js b/examples/api-routes-apollo-server-and-client-auth/pages/signin.js index 439fb01741fd..34bc49d790b2 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/signin.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signin.js @@ -1,11 +1,10 @@ import { useState } from 'react' +import { useRouter } from 'next/router' import Link from 'next/link' -import { withApollo } from '../apollo/client' import gql from 'graphql-tag' import { useMutation, useApolloClient } from '@apollo/react-hooks' -import Field from '../components/field' import { getErrorMessage } from '../lib/form' -import { useRouter } from 'next/router' +import Field from '../components/field' const SignInMutation = gql` mutation SignInMutation($email: String!, $password: String!) { @@ -74,4 +73,4 @@ function SignIn() { ) } -export default withApollo(SignIn) +export default SignIn diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signout.js b/examples/api-routes-apollo-server-and-client-auth/pages/signout.js index dc899fe5c265..11e67f9e99d0 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/signout.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signout.js @@ -1,8 +1,7 @@ import { useEffect } from 'react' +import { useRouter } from 'next/router' import { useMutation, useApolloClient } from '@apollo/react-hooks' import gql from 'graphql-tag' -import { useRouter } from 'next/router' -import { withApollo } from '../apollo/client' const SignOutMutation = gql` mutation SignOutMutation { @@ -26,4 +25,4 @@ function SignOut() { return

Signing out...

} -export default withApollo(SignOut) +export default SignOut diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signup.js b/examples/api-routes-apollo-server-and-client-auth/pages/signup.js index 3ded487b7743..70db989347a4 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/signup.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signup.js @@ -1,11 +1,10 @@ import { useState } from 'react' +import { useRouter } from 'next/router' import Link from 'next/link' -import { withApollo } from '../apollo/client' import gql from 'graphql-tag' import { useMutation } from '@apollo/react-hooks' -import Field from '../components/field' import { getErrorMessage } from '../lib/form' -import { useRouter } from 'next/router' +import Field from '../components/field' const SignUpMutation = gql` mutation SignUpMutation($email: String!, $password: String!) { @@ -70,4 +69,4 @@ function SignUp() { ) } -export default withApollo(SignUp) +export default SignUp