Friday, August 24, 2018

Build a Health Tracking App with React, GraphQL, and User Authentication

This article was originally published on the Okta developer blog. Thank you for supporting the partners who make SitePoint possible.

I think you’ll like the story I’m about to tell you. I’m going to show you how to build a GraphQL API with Vesper framework, TypeORM, and MySQL. These are Node frameworks, and I’ll use TypeScript for the language. For the client, I’ll use React, reactstrap, and Apollo Client to talk to the API. Once you have this environment working, and you add secure user authentication, I believe you’ll love the experience!

Why focus on secure authentication? Well, aside from the fact that I work for Okta, I think we can all agree that pretty much every application depends upon a secure identity management system. For most developers who are building React apps, there’s a decision to be made between rolling your own authentication/authorization or plugging in a service like Okta. Before I dive into building a React app, I want to tell you a bit about Okta, and why I think it’s an excellent solution for all JavaScript developers.

What Is Okta?

In short, we make identity management a lot easier, more secure, and more scalable than what you’re used to. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

Are you sold? Register for a forever-free developer account, and when you’re done, come on back so we can learn more about building secure apps in React!

Why a Health Tracking App?

In late September through mid-October 2014, I’d done a 21-Day Sugar Detox during which I stopped eating sugar, started exercising regularly, and stopped drinking alcohol. I’d had high blood pressure for over ten years and was on blood pressure medication at the time. During the first week of the detox, I ran out of blood pressure medication. Since a new prescription required a doctor visit, I decided I’d wait until after the detox to get it. After three weeks, not only did I lose 15 pounds, but my blood pressure was at normal levels!

Before I started the detox, I came up with a 21-point system to see how healthy I was each week. Its rules were simple: you can earn up to three points per day for the following reasons:

  1. If you eat healthy, you get a point. Otherwise, zero.
  2. If you exercise, you get a point.
  3. If you don’t drink alcohol, you get a point.

I was surprised to find I got eight points the first week I used this system. During the detox, I got 16 points the first week, 20 the second, and 21 the third. Before the detox, I thought eating healthy meant eating anything except fast food. After the detox, I realized that eating healthy for me meant eating no sugar. I’m also a big lover of craft beer, so I modified the alcohol rule to allow two healthier alcohol drinks (like a greyhound or red wine) per day.

My goal is to earn 15 points per week. I find that if I get more, I’ll likely lose weight and have good blood pressure. If I get fewer than 15, I risk getting sick. I’ve been tracking my health like this since September 2014. I’ve lost weight, and my blood pressure has returned to and maintained normal levels. I haven’t had good blood pressure since my early 20s, so this has been a life-changer for me.

I built 21-Points Health to track my health. I figured it’d be fun to recreate a small slice of that app, just tracking daily points.

Building an API with TypeORM, GraphQL, and Vesper

TypeORM is a nifty ORM (object-relational mapper) framework that can run in most JavaScript platforms, including Node, a browser, Cordova, React Native, and Electron. It’s heavily influenced by Hibernate, Doctrine, and Entity Framework. Install TypeORM globally to begin creating your API.

npm i -g typeorm@0.2.7

Create a directory to hold the React client and GraphQL API.

mkdir health-tracker
cd health-tracker

Create a new project with MySQL using the following command:

typeorm init --name graphql-api --database mysql

Edit graphql-api/ormconfig.json to customize the username, password, and database.

{
    ...
    "username": "health",
    "password": "pointstest",
    "database": "healthpoints",
    ...
}

TIP: To see the queries being executed against MySQL, change the “logging” value in this file to be “all”. Many other logging options are available too.

Install MySQL

Install MySQL if you don’t already have it installed. On Ubuntu, you can use sudo apt-get install mysql-server. On macOS, you can use Homebrew and brew install mysql. For Windows, you can use the MySQL Installer.

Once you’ve got MySQL installed and configured with a root password, login and create a healthpoints database.

mysql -u root -p
create database healthpoints;
use healthpoints;
grant all privileges on *.* to 'health'@'localhost' identified by 'points';

Navigate to your graphql-api project in a terminal window, install the project’s dependencies, then start it to ensure you can connect to MySQL.

cd graphql-api
npm i
npm start

You should see the following output:

Inserting a new user into the database...
Saved a new user with id: 1
Loading users from the database...
Loaded users:  [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ]
Here you can setup and run express/koa/any other framework.

Install Vesper to Integrate TypeORM and GraphQL

Vesper is a Node framework that integrates TypeORM and GraphQL. To install it, use good ol’ npm.

npm i vesper@0.1.9

Now it’s time to create some GraphQL models (that define what your data looks like) and some controllers (that explain how to interact with your data).

Create graphql-api/src/schema/model/Points.graphql:

type Points {
  id: Int
  date: Date
  exercise: Int
  diet: Int
  alcohol: Int
  notes: String
  user: User
}

Create graphql-api/src/schema/model/User.graphql:

type User {
  id: String
  firstName: String
  lastName: String
  points: [Points]
}

Next, create a graphql-api/src/schema/controller/PointsController.graphql with queries and mutations:

type Query {
  points: [Points]
  pointsGet(id: Int): Points
  users: [User]
}

type Mutation {
  pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points
  pointsDelete(id: Int): Boolean
}

Now that your data has GraphQL metadata create entities that will be managed by TypeORM. Change src/entity/User.ts to have the following code that allows points to be associated with a user.

import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm';
import { Points } from './Points';

@Entity()
export class User {

  @PrimaryColumn()
  id: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @OneToMany(() => Points, points => points.user)
  points: Points[];
}

In the same src/entity directory, create a Points.ts class with the following code.

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './User';

@Entity()
export class Points {

  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
  date: Date;

  @Column()
  exercise: number;

  @Column()
  diet: number;

  @Column()
  alcohol: number;

  @Column()
  notes: string;

  @ManyToOne(() => User, user => user.points, { cascade: ["insert"] })
  user: User|null;
}

Note the cascade: ["insert"] option on the @ManyToOne annotation above. This option will automatically insert a user if it’s present on the entity. Create src/controller/PointsController.ts to handle converting the data from your GraphQL queries and mutations.

import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';

@Controller()
export class PointsController {

  constructor(private entityManager: EntityManager) {
  }

  // serves "points: [Points]" requests
  @Query()
  points() {
    return this.entityManager.find(Points);
  }

  // serves "pointsGet(id: Int): Points" requests
  @Query()
  pointsGet({id}) {
    return this.entityManager.findOne(Points, id);
  }

  // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
  @Mutation()
  pointsSave(args) {
    const points = this.entityManager.create(Points, args);
    return this.entityManager.save(Points, points);
  }

  // serves "pointsDelete(id: Int): Boolean" requests
  @Mutation()
  async pointsDelete({id}) {
    await this.entityManager.remove(Points, {id: id});
    return true;
  }
}

Change src/index.ts to use Vesper’s bootstrap() to configure everything.

import { bootstrap } from 'vesper';
import { PointsController } from './controller/PointsController';
import { Points } from './entity/Points';
import { User } from './entity/User';

bootstrap({
  port: 4000,
  controllers: [
    PointsController
  ],
  entities: [
    Points,
    User
  ],
  schemas: [
    __dirname + '/schema/**/*.graphql'
  ],
  cors: true
}).then(() => {
  console.log('Your app is up and running on http://localhost:4000. ' +
    'You can use playground in development mode on http://localhost:4000/playground');
}).catch(error => {
  console.error(error.stack ? error.stack : error);
});

This code tells Vesper to register controllers, entities, GraphQL schemas, to run on port 4000, and to enable CORS (cross-origin resource sharing).

Start your API using npm start and navigate to http://localhost:4000/playground. In the left pane, enter the following mutation and press the play button. You might try typing the code below so you can experience the code completion that GraphQL provides you.

mutation {
  pointsSave(exercise:1, diet:1, alcohol:1, notes:"Hello World") {
    id
    date
    exercise
    diet
    alcohol
    notes
  }
}

Your result should look similar to mine.

GraphQL Playground

You can click the “SCHEMA” tab on the right to see the available queries and mutations. Pretty slick, eh?!

You can use the following points query to verify that data is in your database.

query {
  points {id date exercise diet notes}
}

Fix Dates

You might notice that the date returned from pointsSave and the points query is in a format the might be difficult for a JavaScript client to understand. You can fix that, install graphql-iso-date.

npm i graphql-iso-date@3.5.0

Then, add an import in src/index.ts and configure custom resolvers for the various date types. This example only uses Date, but it’s helpful to know the other options.

import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';

bootstrap({
  ...
  // https://github.com/vesper-framework/vesper/issues/4
  customResolvers: {
    Date: GraphQLDate,
    Time: GraphQLTime,
    DateTime: GraphQLDateTime
  },
  ...
});

Now running the points query will return a more client-friendly result.

{
  "data": {
    "points": [
      {
        "id": 1,
        "date": "2018-06-04",
        "exercise": 1,
        "diet": 1,
        "notes": "Hello World"
      }
    ]
  }
}

You’ve written an API with GraphQL and TypeScript in about 20 minutes. How cool is that?! There’s still work to do though. In the next sections, you’ll create a React client for this API and add authentication with OIDC. Adding authentication will give you the ability to get the user’s information and associate a user with their points.

Get Started with React

One of the quickest ways to get started with React is to use Create React App. Install the latest release using the command below.

npm i -g create-react-app@1.1.4

Navigate to the directory where you created your GraphQL API and create a React client.

cd health-tracker
create-react-app react-client

Install the dependencies you’ll need to talk to integrate Apollo Client with React, as well as Bootstrap and reactstrap.

npm i apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2

Configure Apollo Client for Your API

Open react-client/src/App.js and import ApolloClient from apollo-boost and add the endpoint to your GraphQL API.

import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

That’s it! With only three lines of code, your app is ready to start fetching data. You can prove it by importing the gql function from graphql-tag. This will parse your query string and turn it into a query document.

import gql from 'graphql-tag';

class App extends Component {

  componentDidMount() {
    client.query({
      query: gql`
        {
          points {
            id date exercise diet alcohol notes
          }
        }
      `
    })
    .then(result => console.log(result));
  }
...
}

Make sure to open your browser’s developer tools so you can see the data after making this change. You could modify the console.log() to use this.setState({points: results.data.points}), but then you’d have to initialize the default state in the constructor. But there’s an easier way: you can use ApolloProvider and Query components from react-apollo!

Below is a modified version of react-client/src/App.js that uses these components.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ApolloClient from 'apollo-boost';
import gql from 'graphql-tag';
import { ApolloProvider, Query } from 'react-apollo';
const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

class App extends Component {

  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
          <Query query={gql`
            {
              points {id date exercise diet alcohol notes}
            }
          `}>
            {({loading, error, data}) => {
              if (loading) return <p>Loading...</p>;
              if (error) return <p>Error: {error}</p>;
              return data.points.map(p => {
                return <div key={p.id}>
                  <p>Date: {p.date}</p>
                  <p>Points: {p.exercise + p.diet + p.alcohol}</p>
                  <p>Notes: {p.notes}</p>
                </div>
              })
            }}
          </Query>
        </div>
      </ApolloProvider>
    );
  }
}

export default App;

You’ve built a GraphQL API and a React UI that talks to it - excellent work! However, there’s still more to do. In the next sections, I’ll show you how to add authentication to React, verify JWTs with Vesper, and add CRUD functionality to the UI. CRUD functionality already exists in the API thanks to the mutations you wrote earlier.

Add Authentication for React with OpenID Connect

You’ll need to configure React to use Okta for authentication. You’ll need to create an OIDC app in Okta for that.

Log in to your Okta Developer account (or sign up if you don’t have an account) and navigate to Applications > Add Application. Click Single-Page App, click Next, and give the app a name you’ll remember. Change all instances of localhost:8080 to localhost:3000 and click Done. Your settings should be similar to the screenshot below.

OIDC App Settings

Okta’s React SDK allows you to integrate OIDC into a React application. To install, run the following commands:

npm i @okta/okta-react@1.0.2 react-router-dom@4.2.2

Okta’s React SDK depends on react-router, hence the reason for installing react-router-dom. Configuring routing in client/src/App.tsx is a common practice, so replace its code with the JavaScript below that sets up authentication with Okta.

import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { ImplicitCallback, SecureRoute, Security } from '@okta/okta-react';
import Home from './Home';
import Login from './Login';
import Points from './Points';

function onAuthRequired({history}) {
  history.push('/login');
}

class App extends Component {
  render() {
    return (
      <Router>
        <Security issuer='https://{yourOktaDomain}/oauth2/default'
                  client_id='{clientId}'
                  redirect_uri={window.location.origin + '/implicit/callback'}
                  onAuthRequired={onAuthRequired}>
          <Route path='/' exact={true} component={Home}/>
          <SecureRoute path='/points' component={Points}/>
          <Route path='/login' render={() => <Login baseUrl='https://{yourOktaDomain}'/>}/>
          <Route path='/implicit/callback' component={ImplicitCallback}/>
        </Security>
      </Router>
    );
  }
}

export default App;

Make sure to replace {yourOktaDomain} and {clientId} in the code above. You can find both values in the Okta Developer Console.

The code in App.js references two components that don’t exist yet: Home, Login, and Points. Create src/Home.js with the following code. This component renders the default route, provides a Login button, and links to your points and logout after you’ve logged in.

import React, { Component } from 'react';
import { withAuth } from '@okta/okta-react';
import { Button, Container } from 'reactstrap';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';

export default withAuth(class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {authenticated: null, userinfo: null, isOpen: false};
    this.checkAuthentication = this.checkAuthentication.bind(this);
    this.checkAuthentication();
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      if (authenticated && !this.state.userinfo) {
        const userinfo = await this.props.auth.getUser();
        this.setState({authenticated, userinfo});
      } else {
        this.setState({authenticated});
      }
    }
  }

  async componentDidMount() {
    this.checkAuthentication();
  }

  async componentDidUpdate() {
    this.checkAuthentication();
  }

  async login() {
    this.props.auth.login('/');
  }

  async logout() {
    this.props.auth.logout('/');
    this.setState({authenticated: null, userinfo: null});
  }

  render() {
    if (this.state.authenticated === null) return null;
    const button = this.state.authenticated ?
        <div>
          <Button color="link"><Link to="/points">Manage Points</Link></Button><br/>
          <Button color="link" onClick={this.logout}>Logout</Button>
        </div>:
      <Button color="primary" onClick={this.login}>Login</Button>;

    const message = this.state.userinfo ?
      <p>Hello, {this.state.userinfo.given_name}!</p> :
      <p>Please log in to manage your points.</p>;

    return (
      <div>
        <AppNavbar/>
        <Container fluid>
          {message}
          {button}
        </Container>
      </div>
    );
  }
});

This component uses <Container/> and <Button/> from reactstrap. Install reactstrap, so everything compiles. It depends on Bootstrap, so include it too.

npm i reactstrap@6.1.0 bootstrap@4.1.1

Add Bootstrap’s CSS file as an import in src/index.js.

The post Build a Health Tracking App with React, GraphQL, and User Authentication appeared first on SitePoint.


by Matt Raible via SitePoint

No comments:

Post a Comment