Skip to content

Commit

Permalink
feat: add simple auth example
Browse files Browse the repository at this point in the history
This is a demo of client-only routes that require a user to be authenticated before viewing. It does NOT show how to build secure authentication. (This is called out in the README.)

Signed-off-by: Jason Lengstorf <[email protected]>
  • Loading branch information
jlengstorf committed Apr 10, 2018
1 parent d169097 commit 1442b70
Show file tree
Hide file tree
Showing 24 changed files with 562 additions and 0 deletions.
16 changes: 16 additions & 0 deletions examples/simple-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Gatsby Authentication Demo

This is a simplified demo to show how an authentication workflow is implemented in Gatsby.

The short version is:

* Gatsby statically renders all unauthenticated routes as usual
* Authenticated routes are whitelisted as client-only
* Logged out users are redirected to the login page if they attempt to visit private routes
* Logged in users will see their private content

## A Note About Security

This example is less about creating an example of secure, production-ready authentication, and more about showing Gatsby's ability to support dynamic content in client-only routes.

For production-ready authentication solutions, take a look at [Auth0](https://auth0.com) or [Passport.js](http://www.passportjs.org/). Rolling a custom auth system is hard and likely to have security holes. Auth0 and Passport.js are both battle tested and widely used.
6 changes: 6 additions & 0 deletions examples/simple-auth/gatsby-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
siteMetadata: {
title: 'Gatsby Demo Simple Authentication',
},
plugins: ['gatsby-plugin-react-helmet'],
};
18 changes: 18 additions & 0 deletions examples/simple-auth/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/

exports.onCreatePage = async ({ page, boundActionCreators }) => {
const { createPage } = boundActionCreators;

// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = '/app/:path';

// Update the page.
createPage(page);
}
};
25 changes: 25 additions & 0 deletions examples/simple-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "gatsby-demo-simple-auth",
"description": "Gatsby demo of simplified authentication flow.",
"version": "1.0.0",
"author": "Jason Lengstorf <[email protected]>",
"dependencies": {
"gatsby": "latest",
"gatsby-link": "latest",
"gatsby-plugin-react-helmet": "latest",
"prop-types": "^15.6.1",
"react-helmet": "^5.2.0",
"react-router-dom": "^4.2.2"
},
"keywords": ["gatsby"],
"license": "MIT",
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"format": "prettier --write 'src/**/*.js'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"prettier": "^1.11.1"
}
}
19 changes: 19 additions & 0 deletions examples/simple-auth/src/components/Details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react"
import View from "./View"
import { getCurrentUser } from "../utils/auth"

const Details = () => {
const { name, legalName, email } = getCurrentUser()

return (
<View title="Your Details">
<ul>
<li>Preferred name: {name}</li>
<li>Legal name: {legalName}</li>
<li>Email address: {email}</li>
</ul>
</View>
)
}

export default Details
41 changes: 41 additions & 0 deletions examples/simple-auth/src/components/Form/form.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.form {
margin: 1rem 0;
}

.form__label {
display: block;
font-size: 67.5%;
letter-spacing: 0.125em;
text-transform: uppercase;
}

.form__label + .form__label {
margin-top: 0.5rem;
}

.form__input {
display: block;
font-size: 1rem;
padding: 0.25rem;
}

.form__button {
background-color: rebeccapurple;
border: 0;
color: white;
font-size: 1.25rem;
font-weight: bold;
margin-top: 0.5rem;
padding: 0.25rem 1rem;
transition: background-color 150ms linear;
}

.form__button:hover {
cursor: pointer;
}

.form__button:hover,
.form__button:active,
.form__button:focus {
background-color: color(rebeccapurple lightness(-20%));
}
38 changes: 38 additions & 0 deletions examples/simple-auth/src/components/Form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react"
import { withRouter } from "react-router-dom"
import styles from "./form.module.css"

export default withRouter(({ handleSubmit, handleUpdate, history }) => (
<form
className={styles.form}
method="post"
onSubmit={event => {
handleSubmit(event)
history.push(`/app/profile`)
}}
>
<p className={styles[`form__instructions`]}>
For this demo, please log in with the username <code>gatsby</code> and the
password <code>demo</code>.
</p>
<label className={styles[`form__label`]}>
Username
<input
className={styles[`form__input`]}
type="text"
name="username"
onChange={handleUpdate}
/>
</label>
<label className={styles[`form__label`]}>
Password
<input
className={styles[`form__input`]}
type="password"
name="password"
onChange={handleUpdate}
/>
</label>
<input className={styles[`form__button`]} type="submit" value="Log In" />
</form>
))
44 changes: 44 additions & 0 deletions examples/simple-auth/src/components/Header/header.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.header {
background-color: rebeccapurple;
}

.header__wrap {
align-items: baseline;
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 0 auto;
max-width: 640px;
padding: 1rem 0;
}

.header__heading {
margin: 0;
font-size: 2rem;
}

.header__nav {
font-size: 1.25rem;
margin-top: 0;
text-align: right;
}

.header__link {
color: white;
font-weight: bold;
margin-left: 0.75rem;
margin-top: 0;
padding: 0.25rem;
text-decoration: none;
}

.header__link--home {
font-size: 2rem;
margin-left: -0.25rem;
}

.header__link:hover,
.header__link:active,
.header__link:focus {
background: white;
color: rebeccapurple;
}
33 changes: 33 additions & 0 deletions examples/simple-auth/src/components/Header/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react"
import Link from "gatsby-link"
import styles from "./header.module.css"

const Header = () => (
<header className={styles.header}>
<div className={styles[`header__wrap`]}>
<h1 className={styles[`header__heading`]}>
<Link
to="/"
className={`${styles[`header__link`]} ${
styles[`header__link--home`]
}`}
>
Gatsby Profiles
</Link>
</h1>
<nav role="main" className={styles[`header__nav`]}>
<Link to="/" className={styles[`header__link`]}>
Home
</Link>
<Link to="/app/profile" className={styles[`header__link`]}>
Profile
</Link>
<Link to="/app/details" className={styles[`header__link`]}>
Details
</Link>
</nav>
</div>
</header>
)

export default Header
15 changes: 15 additions & 0 deletions examples/simple-auth/src/components/Home.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react"
import View from "./View"
import { getCurrentUser } from "../utils/auth"

const Home = () => {
const { name } = getCurrentUser()

return (
<View title="Your Profile">
<p>Welcome back, {name}!</p>
</View>
)
}

export default Home
40 changes: 40 additions & 0 deletions examples/simple-auth/src/components/Login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react"
import { Redirect } from "react-router-dom"
import Form from "./Form"
import View from "./View"
import { handleLogin, isLoggedIn } from "../utils/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()) {
return <Redirect to={{ pathname: `/app/profile` }} />
}

return (
<View title="Log In">
<Form
handleUpdate={e => this.handleUpdate(e)}
handleSubmit={e => this.handleSubmit(e)}
/>
</View>
)
}
}

export default Login
25 changes: 25 additions & 0 deletions examples/simple-auth/src/components/PrivateRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react"
import PropTypes from "prop-types"
import { Redirect, Route } from "react-router-dom"
import { isLoggedIn } from "../utils/auth"

// More info at https://reacttraining.com/react-router/web/example/auth-workflow
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
!isLoggedIn() ? (
// If we’re not logged in, redirect to the home page.
<Redirect to={{ pathname: `/app/login` }} />
) : (
<Component {...props} />
)
}
/>
)

PrivateRoute.propTypes = {
component: PropTypes.any.isRequired,
}

export default PrivateRoute
35 changes: 35 additions & 0 deletions examples/simple-auth/src/components/Status/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react"
import { Link, withRouter } from "react-router-dom"
import { getCurrentUser, isLoggedIn, logout } from "../../utils/auth"
import styles from "./status.module.css"

export default withRouter(({ history }) => {
let details
if (!isLoggedIn()) {
details = (
<p className={styles[`status__text`]}>
To get the full app experience, you’ll need to{` `}
<Link to="/app/login">log in</Link>.
</p>
)
} else {
const { name, email } = getCurrentUser()

details = (
<p className={styles[`status__text`]}>
Logged in as {name} ({email})!{` `}
<a
href="/"
onClick={event => {
event.preventDefault()
logout(() => history.push(`/app/login`))
}}
>
log out
</a>
</p>
)
}

return <div className={styles.status}>{details}</div>
})
11 changes: 11 additions & 0 deletions examples/simple-auth/src/components/Status/status.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.status {
background: lightgrey;
font-size: 87.5%;
padding: 0.25rem;
}

.status__text {
margin: 0 auto;
max-width: 640px;
text-align: right;
}
16 changes: 16 additions & 0 deletions examples/simple-auth/src/components/View/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react"
import PropTypes from "prop-types"
import styles from "./view.module.css"

const View = ({ title, children }) => (
<section className={styles.view}>
<h1 className={styles[`view__heading`]}>{title}</h1>
{children}
</section>
)

View.propTypes = {
title: PropTypes.string.isRequired,
}

export default View
Loading

0 comments on commit 1442b70

Please sign in to comment.