Skip to content

Reach router and GoTrue.js - full Authentication demo #18

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

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
.env.development.local
.env.test.local
.env.production.local
.netlify

npm-debug.log*
yarn-debug.log*
110 changes: 7 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,109 +1,13 @@
> ⚠️You may not need `netlify-lambda`. [Netlify Dev](https://github.com/netlify/netlify-dev-plugin) works with `create-react-app` out of the box, give it a try! Only use `netlify-lambda` if you need a build step for your functions. [See its README for details](https://github.com/netlify/netlify-lambda/blob/master/README.md#netlify-lambda).
#Netlify Identity + Reach Router by wrapping the goTrue API in a React Hook.

This project is based on latest versions of [Create React App v3](https://github.com/facebookincubator/create-react-app) and [netlify-lambda v1](https://github.com/netlify/netlify-lambda).
[Deployed site here](https://unruffled-roentgen-04c3b8.netlify.com/)

The main addition to base Create-React-App is a new folder: `src/lambda`. Each JavaScript file in there will be built for Lambda function deployment in `/built-lambda`, specified in [`netlify.toml`](https://www.netlify.com/docs/netlify-toml-reference/).
this is a demo of using Netlify Identity with Reach Router by wrapping the goTrue API in a React Hook.

As an example, we've included a small `src/lambda/hello.js` function, which will be deployed to `/.netlify/functions/hello`. We've also included an async lambda example using async/await syntax in `async-chuck-norris.js`.
⚠️Make sure Netlify Identity is enabled!!! or demo wont work

[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/create-react-app-lambda)
Reach Router is used to show authentication as per [Ryan Florence](https://twitter.com/ryanflorence/status/1060361144701833216).

## Video
If you want to fork/deploy this yourself on Netlfiy, click this:

Learn how to set this up yourself (and why everything is the way it is) from scratch in a video: https://www.youtube.com/watch?v=3ldSM98nCHI

## Babel/webpack compilation

All functions are compiled with webpack using the Babel Loader, so you can use modern JavaScript, import npm modules, etc., without any extra setup.

## Local Development

Before developing, clone the repository and run `yarn` from the root of the repo to install all dependencies.

### Option 1: Starting both servers at once

Most people should be able to get up and running just by running:

```bash
yarn start
```

This uses [npm-run-all](https://github.com/mysticatea/npm-run-all#readme) to run the functions dev server and app dev server concurrently.

### Option 2: Start each server individually

**Run the functions dev server**

From inside the project folder, run:

```
yarn start:lambda
```

This will open a local server running at `http://localhost:9000` serving your Lambda functions, updating as you make changes in the `src/lambda` folder.

You can then access your functions directly at `http://localhost:9000/{function_name}`, but to access them with the app, you'll need to start the app dev server. Under the hood, this uses `react-scripts`' [advanced proxy feature](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#configuring-the-proxy-manually) with the `setupProxy.js` file.

**Run the app dev server**

While the functions server is still running, open a new terminal tab and run:

```
yarn start:app
```

This will start the normal create-react-app dev server and open your app at `http://localhost:3000`.

Local in-app requests to the relative path `/.netlify/functions/*` will automatically be proxied to the local functions dev server.

## Typescript

<details>
<summary>
<b id="typescript">Click for instructions</b>
</summary>
You can use Typescript in both your React code (with `react-scripts` v2.1+) and your lambda functions )with `netlify-lambda` v1.1+). Follow these instructions:

1. `yarn add -D typescript @types/node @types/react @types/react-dom @babel/preset-typescript @types/aws-lambda`
2. convert `src/lambda/hello.js` to `src/lambda/hello.ts`
3. use types in your event handler:

```ts
import { Handler, Context, Callback, APIGatewayEvent } from "aws-lambda"

interface HelloResponse {
statusCode: number
body: string
}

const handler: Handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
const params = event.queryStringParameters
const response: HelloResponse = {
statusCode: 200,
body: JSON.stringify({
msg: `Hello world ${Math.floor(Math.random() * 10)}`,
params
})
}

callback(undefined, response)
}

export { handler }
```

rerun and see it work!

You are free to set up your `tsconfig.json` and `tslint` as you see fit.

</details>

**If you want to try working in Typescript on the client and lambda side**: There are a bunch of small setup details to get right. Check https://github.com/sw-yx/create-react-app-lambda-typescript for a working starter.

## Routing and authentication

For a full demo of routing and authentication, check this branch: https://github.com/netlify/create-react-app-lambda/pull/18 This example will not be maintained but may be helpful.

## Service Worker

The service worker does not work with lambda functions out of the box. It prevents calling the function and returns the app itself instead ([Read more](https://github.com/facebook/create-react-app/issues/2237#issuecomment-302693219)). To solve this you have to eject and enhance the service worker configuration in the webpack config. Whitelist the path of your lambda function and you are good to go.
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/create-react-app-lambda/tree/reachRouterAndGoTrueDemo&stack=cms)
14,781 changes: 0 additions & 14,781 deletions package-lock.json

This file was deleted.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -3,9 +3,12 @@
"version": "0.4.0",
"private": true,
"dependencies": {
"@reach/router": "^1.2.1",
"gotrue-js": "^0.9.22",
"node-fetch": "^2.3.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-netlify-identity": "^0.0.12",
"react-scripts": "^3.0.0"
},
"scripts": {
@@ -29,7 +32,6 @@
],
"devDependencies": {
"@babel/plugin-transform-object-assign": "^7.0.0",
"babel-loader": "8.0.4",
"http-proxy-middleware": "^0.19.0",
"netlify-lambda": "^1.4.5",
"npm-run-all": "^4.1.5"
1 change: 1 addition & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* /index.html 200
28 changes: 20 additions & 8 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />

<link
href="https://fonts.googleapis.com/css?family=Lato:100"
rel="stylesheet"
type="text/css"
/>
<link
href="https://fonts.googleapis.com/css?family=Lato:100italic"
rel="stylesheet"
type="text/css"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@@ -22,9 +36,7 @@
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<!--
This HTML file is a template.
164 changes: 146 additions & 18 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -2,31 +2,159 @@
text-align: center;
}

.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
}

.App-header {
background-color: #282c34;
min-height: 100vh;
.title {
color: #d44b15;
text-align: center;
background-color: #e5e4e4;
font-family: Lato, serif;
margin-top: 0px;
display: flex;
flex-direction: column;

align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}

.App-link {
color: #61dafb;
.title .italic {
color: #fff;
font-size: 2em;
font-style: italic;
vertical-align: bottom;
margin: 0 -0.3em;
}
pre {
text-align: left;
overflow-x: scroll;
overflow-y: hidden;
}

form {
text-align: left;
margin: 0 auto;
display: inline-block;
}
form > div {
margin-bottom: 1rem;
}

nav {
background: #ffe259; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#ffa751,
#ffe259
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#ffa751,
#ffe259
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
padding: 1rem;
font-family: Lato, serif;
font-weight: bold;
margin-bottom: 1rem;
}

nav a {
color: brown;
text-decoration: none;
}

/* http://tobiasahlin.com/spinkit/ */

.sk-folding-cube {
margin: 20px auto;
width: 40px;
height: 40px;
position: relative;
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}

@keyframes App-logo-spin {
from {
transform: rotate(0deg);
.sk-folding-cube .sk-cube {
float: left;
width: 50%;
height: 50%;
position: relative;
-webkit-transform: scale(1.1);
-ms-transform: scale(1.1);
transform: scale(1.1);
}
.sk-folding-cube .sk-cube:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #333;
-webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
animation: sk-foldCubeAngle 2.4s infinite linear both;
-webkit-transform-origin: 100% 100%;
-ms-transform-origin: 100% 100%;
transform-origin: 100% 100%;
}
.sk-folding-cube .sk-cube2 {
-webkit-transform: scale(1.1) rotateZ(90deg);
transform: scale(1.1) rotateZ(90deg);
}
.sk-folding-cube .sk-cube3 {
-webkit-transform: scale(1.1) rotateZ(180deg);
transform: scale(1.1) rotateZ(180deg);
}
.sk-folding-cube .sk-cube4 {
-webkit-transform: scale(1.1) rotateZ(270deg);
transform: scale(1.1) rotateZ(270deg);
}
.sk-folding-cube .sk-cube2:before {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.sk-folding-cube .sk-cube3:before {
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
.sk-folding-cube .sk-cube4:before {
-webkit-animation-delay: 0.9s;
animation-delay: 0.9s;
}
@-webkit-keyframes sk-foldCubeAngle {
0%,
10% {
-webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}

@keyframes sk-foldCubeAngle {
0%,
10% {
-webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
to {
transform: rotate(360deg);
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
220 changes: 186 additions & 34 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,202 @@
import React, { Component } from "react"
import logo from "./logo.svg"
import React from "react"
import { Router, Link, navigate } from "@reach/router"
import "./App.css"
import { useNetlifyIdentity } from "react-netlify-identity"
import useLoading from "./useLoading"

class LambdaDemo extends Component {
constructor(props) {
super(props)
this.state = { loading: false, msg: null }
}
let IdentityContext = React.createContext()

handleClick = api => e => {
e.preventDefault()
function PrivateRoute(props) {
const identity = React.useContext(IdentityContext)
let { as: Comp, ...rest } = props
return identity.user ? (
<Comp {...rest} />
) : (
<div>
<h3>You are trying to view a protected page. Please log in</h3>
<Login />
</div>
)
}

this.setState({ loading: true })
fetch("/.netlify/functions/" + api)
.then(response => response.json())
.then(json => this.setState({ loading: false, msg: json.msg }))
function Login() {
const { loginUser, signupUser } = React.useContext(IdentityContext)
const formRef = React.useRef()
const [msg, setMsg] = React.useState("")
const [isLoading, load] = useLoading()
const signup = () => {
const email = formRef.current.email.value
const password = formRef.current.password.value
load(signupUser(email, password))
.then(user => {
console.log("Success! Signed up", user)
navigate("/dashboard")
})
.catch(err => console.error(err) || setMsg("Error: " + err.message))
}
return (
<form
ref={formRef}
onSubmit={e => {
e.preventDefault()
const email = e.target.email.value
const password = e.target.password.value
load(loginUser(email, password))
.then(user => {
console.log("Success! Logged in", user)
navigate("/dashboard")
})
.catch(err => console.error(err) || setMsg("Error: " + err.message))
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
{isLoading ? (
<Spinner />
) : (
<div>
<input type="submit" value="Log in" />
<button onClick={signup}>Sign Up </button>
{msg && <pre>{msg}</pre>}
</div>
)}
</form>
)
}

render() {
const { loading, msg } = this.state

return (
function Home() {
return (
<div>
<h3>Welcome to the Home page!</h3>
<p>
<button onClick={this.handleClick("hello")}>{loading ? "Loading..." : "Call Lambda"}</button>
<button onClick={this.handleClick("async-dadjoke")}>{loading ? "Loading..." : "Call Async Lambda"}</button>
<br />
<span>{msg}</span>
this is a <b>Public Page</b>, not behind an authentication wall
</p>
)
<div style={{ backgroundColor: "#EEE", padding: "1rem" }}>
<div>
<a
href={`https://app.netlify.com/start/deploy?repository=https://github.com/netlify/create-react-app-lambda/tree/reachRouterAndGoTrueDemo&stack=cms`}
>
<img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify" />
</a>
</div>
This demo is{" "}
<a href="https://github.com/netlify/create-react-app-lambda/tree/reachRouterAndGoTrueDemo">Open Source.</a>{" "}
</div>
</div>
)
}

function About() {
return <div>About</div>
}

function Dashboard() {
const props = React.useContext(IdentityContext)
const { isConfirmedUser, authedFetch } = props
const [isLoading, load] = useLoading()
const [msg, setMsg] = React.useState("Click to load something")
const handler = () => {
load(authedFetch.get("/.netlify/functions/authEndPoint")).then(setMsg)
}
return (
<div>
<h3>This is a Protected Dashboard!</h3>
{!isConfirmedUser && (
<pre style={{ backgroundColor: "papayawhip" }}>
You have not confirmed your email. Please confirm it before you ping the API.
</pre>
)}
<hr />
<div>
<p>You can try pinging our authenticated API here.</p>
<p>If you are logged in, you should be able to see a `user` info here.</p>
<button onClick={handler}>Ping authenticated API</button>
{isLoading ? <Spinner /> : <pre>{JSON.stringify(msg, null, 2)}</pre>}
</div>
</div>
)
}

function Spinner() {
return (
<div className="sk-folding-cube">
<div className="sk-cube1 sk-cube" />
<div className="sk-cube2 sk-cube" />
<div className="sk-cube4 sk-cube" />
<div className="sk-cube3 sk-cube" />
</div>
)
}
function Nav() {
const { isLoggedIn } = React.useContext(IdentityContext)
return (
<nav>
<Link to="/">Home</Link> | <Link to="dashboard">Dashboard</Link>
{" | "}
<span>{isLoggedIn ? <Logout /> : <Link to="login">Log In/Sign Up</Link>}</span>
</nav>
)
}
function Logout() {
const { logoutUser } = React.useContext(IdentityContext)
return <button onClick={logoutUser}>You are signed in. Log Out</button>
}

class App extends Component {
render() {
return (
function App() {
// TODO: SUPPLY A URL EITHER FROM ENVIRONMENT VARIABLES OR SOME OTHER STRATEGY
// e.g. 'https://unruffled-roentgen-04c3b8.netlify.com'
const [url, setUrl] = React.useState(window.location.origin)
const handler = e => setUrl(e.target.value)
const identity = useNetlifyIdentity(url)
console.log({ identity, url })
return (
<IdentityContext.Provider value={identity}>
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<LambdaDemo />
</header>
<div className="Appheader">
<h1 className="title">
<span>Netlify Identity</span>
<span className="italic">&</span> <span>Reach Router</span>
</h1>
<label>
<a href="https://www.netlify.com/docs/identity/">Netlify Identity</a> Instance:{" "}
<input
type="text"
placeholder="your instance here e.g. https://unruffled-roentgen-04c3b8.netlify.com"
value={url}
onChange={handler}
size={50}
/>
<div>
<div style={{ display: "inline-block" }}>
{window.location.hostname === "localhost" ? (
<pre>WARNING: this demo doesn't work on localhost</pre>
) : (
<pre>your instance here e.g. https://unruffled-roentgen-04c3b8.netlify.com</pre>
)}
</div>
</div>
</label>
</div>
<Nav />
<Router>
<Home path="/" />
<About path="/about" />
<Login path="/login" />
<PrivateRoute as={Dashboard} path="/dashboard" />
</Router>
</div>
)
}
</IdentityContext.Provider>
)
}

export default App
9 changes: 0 additions & 9 deletions src/App.test.js

This file was deleted.

6 changes: 0 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -2,11 +2,5 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
35 changes: 35 additions & 0 deletions src/lambda/authEndPoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// example of async handler using async-await
// https://github.com/netlify/netlify-lambda/issues/43#issuecomment-444618311

import fetch from 'node-fetch';
export async function handler(event, context) {
if (!context.clientContext || !context.clientContext.identity) {
return {
statusCode: 500,
body: JSON.stringify({
msg:
'No identity instance detected. Did you enable it? Also, Netlify Identity is not supported on local dev yet.'
}) // Could be a custom message or object i.e. JSON.stringify(err)
};
}
const { identity, user } = context.clientContext;
try {
const response = await fetch('https://api.chucknorris.io/jokes/random');
if (!response.ok) {
// NOT res.status >= 200 && res.status < 300
return { statusCode: response.status, body: response.statusText };
}
const data = await response.json();

return {
statusCode: 200,
body: JSON.stringify({ identity, user, msg: data.value })
};
} catch (err) {
console.log(err); // output to netlify function log
return {
statusCode: 500,
body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err)
};
}
}
11 changes: 0 additions & 11 deletions src/lambda/hello.js

This file was deleted.

127 changes: 0 additions & 127 deletions src/serviceWorker.js

This file was deleted.

18 changes: 18 additions & 0 deletions src/useLoading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

export default function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = aPromise => {
setState(true);
return aPromise
.then((...args) => {
setState(false);
return Promise.resolve(...args);
})
.catch((...args) => {
setState(false);
return Promise.reject(...args);
});
};
return [isLoading, load];
}
26 changes: 26 additions & 0 deletions src/useLocalState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

const noop = () => {};
export default function useLocalStorage(key, optionalCallback = noop) {
const [state, setState] = React.useState(null);
React.useEffect(() => {
// chose to make this async
const existingValue = localStorage.getItem(key);
if (existingValue) {
const parsedValue = JSON.parse(existingValue);
setState(parsedValue);
optionalCallback(parsedValue);
}
}, []);
const removeItem = () => {
setState(null);
localStorage.removeItem(key);
optionalCallback(null);
};
const setItem = obj => {
setState(obj);
localStorage.setItem(key, JSON.stringify(obj));
optionalCallback(obj);
};
return [state, setItem, removeItem];
}
1,900 changes: 601 additions & 1,299 deletions yarn.lock

Large diffs are not rendered by default.