add jwt + auth0 todo example app (#445)

This commit is contained in:
Anon Ray 2018-09-14 04:17:08 +00:00 committed by Shahidh K Muhammed
parent d2decec32b
commit f7dbf2a6f1
33 changed files with 10216 additions and 0 deletions

View File

@ -0,0 +1,6 @@
package-lock.json
node_modules
.gitignore
exec.sh
exec.ps1
README.md

View File

@ -0,0 +1 @@
ws

View File

@ -0,0 +1,89 @@
# Integrating Todo app with Auth0 and JWT authorization with Hasura GraphQL Engine
In this example, we use Hasura GraphQL engine's JWT authorization mode. We use
Auth0 as our authentication and JWT token provider.
## Create an application in Auth0
1. Create an application in Auth0 dashboard
2. In the settings of the application, add `http://localhost:3000/callback` as
"Allowed Callback URLs" and "Allowed Web Origins"
## Add rules for custom JWT claims
In the Auth0 dashboard, navigate to "Rules". Add the following rules to add our custom JWT claims:
```javascript
function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
context.idToken[namespace] =
{
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': user.email.match(/foobar.com/) ? ['user', 'admin'] : ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}
```
## Get your JWT signing certificate
Download your JWT signing X509 certificate by visiting URL:
`https://<YOUR-AUTH0-DOMAIN>/pem`
Convert the file into one-line, this will be required later:
```shell
$ cat filename.pem | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g'
```
## Deploy Hasura GraphQL Engine
[![Deploy HGE on heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
After deploying, add the following environment variables to configure JWT mode:
```
HASURA_GRAPHQL_ACCESS_KEY : yoursecretaccesskey
```
```
HASURA_GRAPHQL_JWT_SECRET: <the-certificate-in-one-line>
```
For example, (copy the certificate from above step):
```
HASURA_GRAPHQL_JWT_SECRET : {"type":"RS256", "key": "-----BEGIN CERTIFICATE-----\nMIIDDTCCAfWgAwIBAgIJPhNlZ11IDrxbMA0GCSqGSIb3DQEBCQxIjAgNV\nBAMTGXRlc3QtaGdlLWp3dC5ldS5hdXRoMC5jb20wHhcNMTgwNzMwMTM1MjM1WhcN\nMzIwNDA3MTM1MjM1WjAkMSIwIAYDVQQDExl0ZXN0LWhnZS1qd3QuZXUuYXV0aDAu\nY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA13CivdSkNzRnOnR5iReDb+AgbL7BWjRiw3tRwjxRp5PYzvAGuj94y+R6LRh3QybYtsMFbSg5J7fNq6\nLd6yMpRMrUu8CBOnYY45D6b/2jlf+Vp8vEQuKvPMOOw8Ev6x7X3blcuXCELSwyL3\nAGHq9OpP2RV6V6CIE863IzzuYH5HDLzU35oMZqozgJVRJM0+6besH6TnSTNiA7xi\nBAqFaiQRNQRVi1CAUa0bLkN1XRp4AFy7d63VldO9sM+8QnCNHySdDr1XevVuq6DK\nLQyGexFFy4niALgHV0Q7QA+xP1c2G6rJomZmn4jl1avnlBpU87E58JMrRHOCj+5m\nXj22AQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6FvNkuUgu\YQ/i4lo5aOgwazAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEB\nADCLj+/L22pEKyqaIUlhHUJh7DAiDSLafy0fw56UCntzPhqiZVVRlhxeAKidkCLVIEbRLuxUoXiQSezPqMp//9xHegMp0f2VauVCFbg7EpUanYwvqFqjy9LWgH+SBz\n4uroLSYZ5g1EPsHtlArLRChA90caTX4e7Z7Xlu8vG2kHRJB5nC7ycdbMUvEWBMeI\ntn/pcb4mZ3/vlgj4UTEnCURe2UPmSJpxmPwXqBctvwdKHRMgFXhZxojWCi0z4ftf\nf8t8UJSIcbEblnkYe7wzRYy8tOXoMMHqGSisCdkWp/866029rJsKbwd8rVIyKNC5\nfrGYawv+0cxO6/Sir0meA=\n-----END CERTIFICATE-----"}
```
Save changes.
## Configure the application
Setup values in `todo-app/src/constants.js`:
1. Auth0 domain
2. GraphQL engine deployed URL, e.g: `https://hasura-todo-auth0-jwt.herokuapp.com/v1alpha1/graphql`
3. Auth0 application's client id
## Create the initial tables
1. Add your database URL and access key in `hasura/config.yaml`
```yaml
endpoint: https://<hge-heroku-url>
access_key: <your-access-key>
```
2. Run `hasura migrate apply` to create the required tables and permissions for the todo app
## Run the application
`$ npm install && npm start`
> The app runs on port 3000 by default. You can change the port number, but you will also have to reconfigure the callback
## Code
- All the Auth0 related code is in `todo-app/src/Auth`
- In `todo-app/src/routes.js`, we get the `id_token` from localstorage, and send
as `Authorization` header to HGE.

View File

@ -0,0 +1,2 @@
endpoint: https://hasura-todo-auth0-jwt.herokuapp.com
access_key: xxxxxxxxx

View File

@ -0,0 +1,72 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 10.5 (Debian 10.5-1.pgdg90+1)
-- Dumped by pg_dump version 10.5 (Ubuntu 10.5-0ubuntu0.18.04)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: todo; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.todo (
id integer NOT NULL,
task text NOT NULL,
completed boolean NOT NULL,
user_id text NOT NULL
);
--
-- Name: todo_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.todo_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: todo_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.todo_id_seq OWNED BY public.todo.id;
--
-- Name: todo id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.todo ALTER COLUMN id SET DEFAULT nextval('public.todo_id_seq'::regclass);
--
-- Name: todo todo_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.todo
ADD CONSTRAINT todo_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--

View File

@ -0,0 +1,45 @@
- type: replace_metadata
args:
query_templates: []
tables:
- array_relationships: []
delete_permissions:
- comment: null
permission:
filter:
user_id:
_eq: X-HASURA-USER-ID
role: user
event_triggers: []
insert_permissions:
- comment: null
permission:
allow_upsert: true
check:
user_id:
_eq: X-HASURA-USER-ID
role: user
object_relationships: []
select_permissions:
- comment: null
permission:
columns:
- task
- completed
- user_id
- id
filter:
user_id:
_eq: X-HASURA-USER-ID
role: user
table: todo
update_permissions:
- comment: null
permission:
columns:
- completed
filter:
user_id:
_eq: X-HASURA-USER-ID
role: user

View File

@ -0,0 +1,15 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
node_modules
# testing
coverage
# production
build
# misc
.DS_Store
.env
npm-debug.log

View File

@ -0,0 +1,12 @@
FROM node:8.7-alpine
WORKDIR /home/app
RUN npm install -g create-react-app
ADD package.json /home/app
RUN npm install
ADD . /home/app
CMD ["npm", "start"]
EXPOSE 3000

View File

@ -0,0 +1,2 @@
docker build -t auth0-react-01-login .
docker run -p 3000:3000 -it auth0-react-01-login

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
docker build -t auth0-react-01-login .
docker run -p 3000:3000 -it auth0-react-01-login

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "centralized-login",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "0.9.5"
},
"dependencies": {
"apollo-boost": "^0.1.10",
"apollo-link-context": "^1.0.8",
"auth0-js": "^9.0.0",
"bootstrap": "^3.3.7",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"react": "^15.5.4",
"react-apollo": "^2.1.6",
"react-bootstrap": "^0.31.0",
"react-dom": "^15.5.4",
"react-router": "^4.1.1",
"react-router-dom": "^4.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Todo app - Hasura & Auth0</title>
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>

View File

@ -0,0 +1,13 @@
.btn-margin {
margin: 7px 3px;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.container {
padding-top: 50px;
}

View File

@ -0,0 +1,74 @@
import React, { Component } from 'react';
import { Navbar, Button } from 'react-bootstrap';
import './App.css';
class App extends Component {
goTo(route) {
this.props.history.replace(`/${route}`)
}
login() {
this.props.auth.login();
}
logout() {
this.props.auth.logout();
}
componentDidMount() {
if (this.props.auth.isAuthenticated()) {
this.props.history.push('/home');
}
}
render() {
const { isAuthenticated } = this.props.auth;
return (
<div>
<Navbar fluid>
<Navbar.Header>
<Navbar.Brand>
<a href="#">Auth0 - React</a>
</Navbar.Brand>
<Button
bsStyle="primary"
className="btn-margin"
onClick={this.goTo.bind(this, 'home')}
>
Home
</Button>
{
!isAuthenticated() && (
<Button
id="qsLoginBtn"
bsStyle="primary"
className="btn-margin"
onClick={this.login.bind(this)}
>
Log In
</Button>
)
}
{
isAuthenticated() && (
<Button
id="qsLogoutBtn"
bsStyle="primary"
className="btn-margin"
onClick={this.logout.bind(this)}
>
Log Out
</Button>
)
}
</Navbar.Header>
</Navbar>
</div>
);
}
}
export default App;

View File

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

View File

@ -0,0 +1,67 @@
import history from '../history';
import auth0 from 'auth0-js';
import { AUTH_CONFIG } from './auth0-variables';
export default class Auth {
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: 'token id_token',
scope: 'openid'
});
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
}
login() {
this.auth0.authorize();
}
handleAuthentication() {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
history.replace('/home');
} else if (err) {
history.replace('/home');
console.error(err);
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
}
setSession(authResult) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
localStorage.setItem('auth0:access_token', authResult.accessToken);
localStorage.setItem('auth0:id_token', authResult.idToken);
localStorage.setItem('auth0:expires_at', expiresAt);
localStorage.setItem('auth0:id_token:sub', authResult.idTokenPayload.sub)
// navigate to the home route
history.replace('/home');
}
logout() {
// Clear access token and ID token from local storage
localStorage.removeItem('auth0:access_token');
localStorage.removeItem('auth0:id_token');
localStorage.removeItem('auth0:expires_at');
localStorage.removeItem('auth0:id_token:sub');
// navigate to the home route
history.replace('/home');
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem('auth0:expires_at'));
return new Date().getTime() < expiresAt;
}
}

View File

@ -0,0 +1,10 @@
import {
authDomain,
authClientId
} from '../constants';
export const AUTH_CONFIG = {
domain: authDomain,
clientId: authClientId,
callbackUrl: 'http://localhost:3000/callback'
};

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import loading from './loading.svg';
class Callback extends Component {
render() {
const style = {
position: 'absolute',
display: 'flex',
justifyContent: 'center',
height: '100vh',
width: '100vw',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
}
return (
<div style={style}>
<img src={loading} alt="loading"/>
</div>
);
}
}
export default Callback;

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><defs><filter id="uil-ring-shadow" x="-100%" y="-100%" width="300%" height="300%"><feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"></feOffset><feGaussianBlur result="blurOut" in="offOut" stdDeviation="0"></feGaussianBlur><feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend></filter></defs><path d="M10,50c0,0,0,0.5,0.1,1.4c0,0.5,0.1,1,0.2,1.7c0,0.3,0.1,0.7,0.1,1.1c0.1,0.4,0.1,0.8,0.2,1.2c0.2,0.8,0.3,1.8,0.5,2.8 c0.3,1,0.6,2.1,0.9,3.2c0.3,1.1,0.9,2.3,1.4,3.5c0.5,1.2,1.2,2.4,1.8,3.7c0.3,0.6,0.8,1.2,1.2,1.9c0.4,0.6,0.8,1.3,1.3,1.9 c1,1.2,1.9,2.6,3.1,3.7c2.2,2.5,5,4.7,7.9,6.7c3,2,6.5,3.4,10.1,4.6c3.6,1.1,7.5,1.5,11.2,1.6c4-0.1,7.7-0.6,11.3-1.6 c3.6-1.2,7-2.6,10-4.6c3-2,5.8-4.2,7.9-6.7c1.2-1.2,2.1-2.5,3.1-3.7c0.5-0.6,0.9-1.3,1.3-1.9c0.4-0.6,0.8-1.3,1.2-1.9 c0.6-1.3,1.3-2.5,1.8-3.7c0.5-1.2,1-2.4,1.4-3.5c0.3-1.1,0.6-2.2,0.9-3.2c0.2-1,0.4-1.9,0.5-2.8c0.1-0.4,0.1-0.8,0.2-1.2 c0-0.4,0.1-0.7,0.1-1.1c0.1-0.7,0.1-1.2,0.2-1.7C90,50.5,90,50,90,50s0,0.5,0,1.4c0,0.5,0,1,0,1.7c0,0.3,0,0.7,0,1.1 c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.9-0.2,1.8-0.4,2.8c-0.2,1-0.5,2.1-0.7,3.3c-0.3,1.2-0.8,2.4-1.2,3.7c-0.2,0.7-0.5,1.3-0.8,1.9 c-0.3,0.7-0.6,1.3-0.9,2c-0.3,0.7-0.7,1.3-1.1,2c-0.4,0.7-0.7,1.4-1.2,2c-1,1.3-1.9,2.7-3.1,4c-2.2,2.7-5,5-8.1,7.1 c-0.8,0.5-1.6,1-2.4,1.5c-0.8,0.5-1.7,0.9-2.6,1.3L66,87.7l-1.4,0.5c-0.9,0.3-1.8,0.7-2.8,1c-3.8,1.1-7.9,1.7-11.8,1.8L47,90.8 c-1,0-2-0.2-3-0.3l-1.5-0.2l-0.7-0.1L41.1,90c-1-0.3-1.9-0.5-2.9-0.7c-0.9-0.3-1.9-0.7-2.8-1L34,87.7l-1.3-0.6 c-0.9-0.4-1.8-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.4-1.5c-3.1-2.1-5.9-4.5-8.1-7.1c-1.2-1.2-2.1-2.7-3.1-4c-0.5-0.6-0.8-1.4-1.2-2 c-0.4-0.7-0.8-1.3-1.1-2c-0.3-0.7-0.6-1.3-0.9-2c-0.3-0.7-0.6-1.3-0.8-1.9c-0.4-1.3-0.9-2.5-1.2-3.7c-0.3-1.2-0.5-2.3-0.7-3.3 c-0.2-1-0.3-2-0.4-2.8c-0.1-0.4-0.1-0.8-0.1-1.2c0-0.4,0-0.7,0-1.1c0-0.7,0-1.2,0-1.7C10,50.5,10,50,10,50z" fill="#337ab7" filter="url(#uil-ring-shadow)"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" repeatCount="indefinite" dur="1s"></animateTransform></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import TodoComponent from '../Todo/TodoComponent';
class Home extends Component {
login() {
this.props.auth.login();
}
render() {
const { isAuthenticated } = this.props.auth;
if (isAuthenticated()) {
return <TodoComponent />
}
return (
<div className="container">
<h4>
You are not logged in! Please{' '}
<a
style={{ cursor: 'pointer' }}
onClick={this.login.bind(this)}
>
Log In
</a>
{' '}to continue.
</h4>
</div>
);
}
}
export default Home;

View File

@ -0,0 +1,35 @@
.parentContainer .header {
font-size: 40px;
}
.parentContainer input {
padding: 10px;
font-size: 16px;
width: 90%;
border: 2px solid #003399;
}
.parentContainer .todoList {
list-style: none;
padding-left: 0;
margin-top: 50px;
width: 93%;
}
.parentContainer .todoList li {
color: #333;
background-color: #dedede;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
cursor: pointer;
}
.parentContainer .todoLabel {
width: 80%;
}
.parentContainer .deleteLabel {
color: #ff0000;
position: absolute;
cursor: pointer;
right: 0px;
width: 150px;
}

View File

@ -0,0 +1,84 @@
import React from 'react';
import { Mutation } from "react-apollo";
import './Todo.css';
import {
QUERY_TODO,
MUTATION_TODO_UPDATE,
MUTATION_TODO_DELETE
} from './graphQueries/todoQueries';
const handleTodoToggle = (toggleTodo, todo) => {
toggleTodo({
variables: {
todoId: todo.id,
set: {
completed: !todo.completed
}
},
update: (cache, { data: { update_todo }}) => {
const data = cache.readQuery({ query: QUERY_TODO })
const toggledTodo = data.todo.find(t => t.id === todo.id)
toggledTodo.completed = !todo.completed;
cache.writeQuery({
query: QUERY_TODO,
data
})
}
})
}
const handleTodoDelete = (deleteTodo, todo) => {
deleteTodo({
variables: {
todoId: todo.id
},
update: (cache, { data: { update_todo }}) => {
const data = cache.readQuery({ query: QUERY_TODO })
data.todo = data.todo.filter(t => {
return t.id !== todo.id
})
cache.writeQuery({
query: QUERY_TODO,
data
})
}
})
}
const Todo = ({ todo }) => (
<Mutation mutation={MUTATION_TODO_UPDATE}>
{(updateTodo) => {
return (
<div className="parentContainer">
<li className="todoItem"
onClick={e => {
handleTodoToggle(updateTodo, todo)
}}>
{
todo.completed ?
<strike className="todoLabel">{todo.task}</strike> :
<label className="todoLabel">{todo.task}</label>
}
<Mutation mutation={MUTATION_TODO_DELETE}>
{(deleteTodo) => {
return (
<label className="deleteLabel"
onClick={e => {
e.preventDefault();
e.stopPropagation();
handleTodoDelete(deleteTodo, todo)
}}>
Delete
</label>
)
}}
</Mutation>
</li>
</div>
)
}}
</Mutation>
)
export default Todo;

View File

@ -0,0 +1,18 @@
import React from 'react';
import './Todo.css';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
export default class TodoComponent extends React.Component {
render() {
const userId = localStorage.getItem('auth0:id_token:sub');
return (
<div className="parentContainer">
<h1 className="header">Todos</h1>
<TodoInput userId={userId}/>
<TodoList />
</div>
)
}
}

View File

@ -0,0 +1,71 @@
import React from 'react';
import { Mutation } from "react-apollo";
import './Todo.css';
import {
QUERY_TODO,
MUTATION_TODO_ADD
} from './graphQueries/todoQueries';
export default class TodoInput extends React.Component {
constructor() {
super();
this.state = {
textboxValue: ''
}
}
handleTextboxValueChange = (e) => {
this.setState({
...this.state,
textboxValue: e.target.value
});
}
handleTextboxKeyPress = (e, addTodo) => {
if (e.key === 'Enter') {
const newTask = this.state.textboxValue;
const userId = this.props.userId;
addTodo({
variables: {
objects: [{
task: newTask,
user_id: userId,
completed: false
}]
},
update: (store, { data: { insert_todo }}) => {
const data = store.readQuery({ query: QUERY_TODO })
const insertedTodo = insert_todo.returning;
data.todo.splice(0, 0, insertedTodo[0])
store.writeQuery({
query: QUERY_TODO,
data
})
this.setState({
...this.state,
textboxValue: ''
});
}
})
}
}
render() {
return (
<Mutation mutation={MUTATION_TODO_ADD}>
{(addTodo, { data, loading, called, error }) => {
return (
<div className="parentContainer">
<input className="input" placeholder="Add a todo" value={this.state.textboxValue} onChange={this.handleTextboxValueChange} onKeyPress={e => {
this.handleTextboxKeyPress(e, addTodo);
}}/>
<br />
</div>
)
}}
</Mutation>
)
}
}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Query } from "react-apollo";
import Todo from './Todo';
import {
QUERY_TODO,
} from './graphQueries/todoQueries';
const TodoList = () => (
<Query query={QUERY_TODO}>
{({loading, error, data}) => {
if (loading) {
return (
<div>Loading. Please wait...</div>
);
}
if (error) {
return (
<div>{""}</div>
);
}
return (
<div className="parentContainer">
<ul className="todoList">
{
data.todo.map((todo, index) => {
return (
<Todo key={index} todo={todo} />
);
})
}
</ul>
</div>
)
}}
</Query>
);
export default TodoList;

View File

@ -0,0 +1,47 @@
import gql from 'graphql-tag';
const QUERY_TODO = gql`
query fetch_todos {
todo {
id
task
completed
}
}
`;
const MUTATION_TODO_ADD = gql`
mutation insert_todo ($objects: [todo_insert_input!]){
insert_todo(objects: $objects) {
affected_rows
returning {
id
task
completed
}
}
}
`;
const MUTATION_TODO_UPDATE = gql`
mutation update_todo ($todoId: Int, $set: todo_set_input!) {
update_todo(where: {id: {_eq: $todoId}} _set: $set) {
affected_rows
}
}
`;
const MUTATION_TODO_DELETE = gql`
mutation delete_todo ($todoId: Int) {
delete_todo(where: {id: {_eq: $todoId}}) {
affected_rows
}
}
`;
export {
QUERY_TODO,
MUTATION_TODO_ADD,
MUTATION_TODO_UPDATE,
MUTATION_TODO_DELETE
};

View File

@ -0,0 +1,3 @@
export const GRAPHQL_URL = 'https://hasura-todo-auth0-jwt.herokuapp.com/v1alpha1/graphql';
export const authClientId = "xxxxxxxxxxxxxxxxxxxxx";
export const authDomain = "<your-auth0-domain>.auth0.com";

View File

@ -0,0 +1,3 @@
import createHistory from 'history/createBrowserHistory'
export default createHistory()

View File

@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

View File

@ -0,0 +1,10 @@
import ReactDOM from 'react-dom';
import './index.css';
import 'bootstrap/dist/css/bootstrap.css';
import { makeMainRoutes } from './routes';
const routes = makeMainRoutes();
ReactDOM.render(
routes,
document.getElementById('root')
);

View File

@ -0,0 +1,74 @@
import React from 'react';
import { Route, Router } from 'react-router-dom';
import App from './App';
import Home from './Home/Home';
import Callback from './Callback/Callback';
import Auth from './Auth/Auth';
import history from './history';
import ApolloClient from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { ApolloProvider } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { GRAPHQL_URL } from './constants';
const httpLink = createHttpLink({
uri: GRAPHQL_URL,
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('auth0:id_token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
addTypename: false
})
});
const provideClient = (component) => {
return (
<ApolloProvider client={client}>
{component}
</ApolloProvider>
);
};
const auth = new Auth();
const handleAuthentication = ({location}) => {
if (/access_token|id_token|error/.test(location.hash)) {
auth.handleAuthentication();
}
}
export const makeMainRoutes = () => {
return (
<Router history={history}>
<div className="container">
<Route
path="/"
render={(props) => provideClient(<App auth={auth} {...props} />)}
/>
<Route
path="/home"
render={(props) => provideClient(<Home auth={auth} {...props} />)} />
<Route path="/callback" render={(props) => {
handleAuthentication(props);
return <Callback {...props}/>
}}/>
</div>
</Router>
);
}