diff --git a/docs/docs/authentication-tutorial.md b/docs/docs/authentication-tutorial.md index 757600808ef4c..34b21efe38c1e 100644 --- a/docs/docs/authentication-tutorial.md +++ b/docs/docs/authentication-tutorial.md @@ -2,7 +2,463 @@ title: Making a site with user authentication --- -This is a stub. Help our community expand it. +Sometimes, you need to create a site with gated content, available only to authenticated users. Using Gatsby, you may achieve this using the concept of [client-only routes](https://www.gatsbyjs.org/docs/building-apps-with-gatsby/#client-only-routes), to define which pages a user can view only after logging in. -Please use the [Gatsby Style Guide](/docs/gatsby-style-guide/) to ensure your -pull request gets accepted. +## Prerequisites + +You should have already configured your environment to be able to use the `gatsby-cli`. A good starting point is the [main tutorial](https://www.gatsbyjs.org/tutorial/). + +## Security notice + +In production, you should use a tested and robust solution to handle the authentication. [Auth0](https://www.auth0.com), [Firebase](https://firebase.google.com), and [Passport.js](passportjs.org) are good examples. This tutorial will only cover the authentication workflow, but you should take the security of your app as seriously as possible. + +## Building your Gatsby app + +Start by creating a new Gatsby project: + +```shell +gatsby new gatsby-auth +cd gatsby-auth +``` + +Then, add a more apt title to your newly created site, changing the content of `gatsby-config.js`: + +```javascript:title=gatsby-config.js +module.exports = { + siteMetadata: { + title: "Gatsby Authentication Tutorial", + }, + plugins: ["gatsby-plugin-react-helmet", "gatsby-plugin-offline"], +} +``` + +Create a new component to hold the links. For now, it will act as a placeholder: + +```jsx:title=src/components/navBar.js +import React from "react" +import { Link } from "gatsby" + +export default () => ( +
+ You are not logged in + + +
+) +``` + +And edit the layout component to include it: + +```jsx{7,41}:title=src/components/layout.js +import React from "react" +import PropTypes from "prop-types" +import Helmet from "react-helmet" +import { StaticQuery, graphql } from "gatsby" + +import Header from "./header" +import NavBar from "./navBar" +import "./layout.css" + +const Layout = ({ children }) => ( + ( + <> + + + +
+
+ + {children} +
+ + )} + /> +) + +Layout.propTypes = { + children: PropTypes.node.isRequired, +} + +export default Layout +``` + +Lastly, change the index page to include this new content: + +```jsx{9-11}:title=src/pages/index.js +import React from "react" +import { Link } from "gatsby" + +import Layout from "../components/layout" + +const IndexPage = () => ( + +

Hi people

+

+ You should log in to see restricted content +

+
+) + +export default IndexPage +``` + +## Authentication service + +For this tutorial you will use a hardcoded user/password. Create the folder `src/services` and add the following content to the file `auth.js`: + +```javascript:title=src/services/auth.js +export const isBrowser = () => typeof window !== "undefined" + +export const getUser = () => + isBrowser() && window.localStorage.getItem("gatsbyUser") + ? JSON.parse(window.localStorage.getItem("gatsbyUser")) + : {} + +const setUser = user => + window.localStorage.setItem("gatsbyUser", JSON.stringify(user)) + +export const handleLogin = ({ username, password }) => { + if (username === `john` && password === `pass`) { + return setUser({ + username: `john`, + name: `Johnny`, + email: `johnny@example.org`, + }) + } + + return false +} + +export const isLoggedIn = () => { + const user = getUser() + + return !!user.username +} + +export const logout = callback => { + setUser({}) + callback() +} +``` + +## Creating client-only routes + +At the beginning of this tutorial, you created a "default" Gatsby site, which includes the `@reach/router` library. Now, using the [@reach/router](https://reach.tech/router/) library, you can create routes available only to logged-in users. This library is used by Gatsby under the hood, so you don't even have to install it. + +First, edit `gatsby-node.js`. You will define that any route that starts with `/app/` is part of your restricted content and the page will be created on demand: + +```javascript:title=gatsby-config.js +// Implement the Gatsby API “onCreatePage”. This is +// called after every page is created. +exports.onCreatePage = async ({ page, actions }) => { + const { createPage } = actions + + // page.matchPath is a special key that's used for matching pages + // only on the client. + if (page.path.match(/^\/app/)) { + page.matchPath = "/app/*" + + // Update the page. + createPage(page) + } +} +``` + +> Note: There is a convenient plugin that already does this work for you: [gatsby-plugin-create-client-paths](https://www.gatsbyjs.org/packages/gatsby-plugin-create-client-paths/) + +Now, you must create a generic page that will have the task to generate the restricted content: + +```jsx:title=src/pages/app.js +import React from "react" +import { Router } from "@reach/router" +import Layout from "../components/layout" +import Profile from "../components/profile" +import Login from "../components/login" + +const App = () => ( + + + + + + +) + +export default App +``` + +Next, add the components regarding those new routes. The profile component to show the user data: + +```jsx:title=src/components/profile.js +import React from "react" + +const Profile = () => ( + <> +

Your profile

+ + +) + +export default Profile +``` + +The login component will handle - as you may have guessed - the login process: + +```jsx:title=src/components/login.js +import React from "react" +import { navigate } from "gatsby" +import { handleLogin, isLoggedIn } from "../services/auth" + +class Login extends React.Component { + state = { + username: ``, + password: ``, + } + + handleUpdate = event => { + this.setState({ + [event.target.name]: event.target.value, + }) + } + + handleSubmit = event => { + event.preventDefault() + handleLogin(this.state) + } + + render() { + if (isLoggedIn()) { + navigate(`/app/profile`) + } + + return ( + <> +

Log in

+
{ + this.handleSubmit(event) + navigate(`/app/profile`) + }} + > + + + +
+ + ) + } +} + +export default Login +``` + +Though the routing is working now, you still can access all routes without restriction. + +## Controlling private routes + +To check if a user can access the content, you can wrap the restricted content inside a PrivateRoute component: + +```jsx:title=scr/components/privateRoute.js +import React from "react" +import { navigate } from "gatsby" +import { isLoggedIn } from "../services/auth" + +const PrivateRoute = ({ component: Component, location, ...rest }) => { + if (!isLoggedIn() && location.pathname !== `/app/login`) { + // If the user is not logged in, redirect to the login page. + navigate(`/app/login`) + return null + } + + return +} + +export default PrivateRoute +``` + +And now you can edit your Router to use the PrivateRoute component: + +```jsx{4,11}:title=src/pages/app.js +import React from "react" +import { Router } from "@reach/router" +import Layout from "../components/layout" +import PrivateRoute from "../components/privateRoute" +import Profile from "../components/profile" +import Login from "../components/login" + +const App = () => ( + + + + + + +) + +export default App +``` + +## Refactoring to use new routes and user data + +With the client-only routes in place, you must now refactor some files to account for the user data available. + +The navigation bar will show the user name and logout option to registered users: + +```jsx{2-3,5-12,21,26,28-38,42}:title=src/components/navBar.js +import React from "react" +import { Link, navigate } from "gatsby" +import { getUser, isLoggedIn, logout } from "../services/auth" + +export default () => { + const content = { message: "", login: true } + if (isLoggedIn()) { + content.message = `Hello, ${getUser().name}` + } else { + content.message = "You are not logged in" + } + return ( +
+ {content.message} + + +
+ ) +} +``` + +The index page will suggest to login or check the profile accordingly: + +```jsx{3,7-8,10-23,26}:title=src/pages/index.js +import React from "react" +import { Link } from "gatsby" +import { getUser, isLoggedIn } from "../services/auth" + +import Layout from "../components/layout" + +const IndexPage = () => { + return ( + +

Hi {isLoggedIn() ? getUser().name : "people"}

+

+ {isLoggedIn() ? ( + <> + You are logged in, so check your{" "} + profile + + ) : ( + <> + You should log in to see restricted + content + + )} +

+
+ ) +} + +export default IndexPage +``` + +And the profile will show the user data: + +```jsx{2,8,9}:title=src/components/profile.js +import React from "react" +import { getUser } from "../services/auth" + +const Profile = () => ( + <> +

Your profile

+
    +
  • Name: {getUser().name}
  • +
  • E-mail: {getUser().email}
  • +
+ +) + +export default Profile +``` + +You should now have a complete authentication workflow, functioning with both login and a user-restricted area! + +## Further reading + +If you want to learn more about using production-ready auth solutions, these links may help: + +- [Gatsby repo simple auth example](https://github.com/gatsbyjs/gatsby/tree/master/examples/simple-auth) +- [A Gatsby email _application_](https://github.com/DSchau/gatsby-mail), using React Context API to handle authentication +- [The Gatsby store for swag and other Gatsby goodies](https://github.com/gatsbyjs/store.gatsbyjs.org) +- [Building a blog with Gatsby, React and Webtask.io!](https://auth0.com/blog/building-a-blog-with-gatsby-react-and-webtask/) +- [JAMstack PWA — Let’s Build a Polling App. with Gatsby.js, Firebase, and Styled-components Pt. 2](https://medium.com/@UnicornAgency/jamstack-pwa-lets-build-a-polling-app-with-gatsby-js-firebase-and-styled-components-pt-2-9044534ea6bc)