Real-Time Charts using Angular, D3, and Socket.IO

June 14, 2018 0 Comments

Real-Time Charts using Angular, D3, and Socket.IO

 

 

TL;DR: Charts create some of the most catchy sections on any business applications. A chart that updates in real time is even more catchy/useful and adds huge value for users. Here, you will see how to create real-time charts using Angular, D3, and Socket.IO. You can find the final code produced throughout this article in this GitHub repository.

Introduction

With the evolution of the web, needs of users are also increasing. The capabilities of the web in the present era can be used to build very rich interfaces. The interfaces may include widgets in the dashboards, huge tables with incrementally loading data, different types of charts and anything that you can think of. Thanks to the technologies like WebSockets, users want to see the UI updated as early as possible. This is a good problem for you to know how to deal with.

In this article, you will build a virtual market application that shows a D3 multi-line chart. That chart will consume data from a Node.js backend consisting of an Express API and a SocketIO instance to get this data in real time.

"Learn how to create real-time @Angular apps with D3 and Socket.IO"


Creating a Virtual Market Server

The demo app you are going to build consists of two parts. One is a Node.js server that provides market data and the other is an Angular application consuming this data. As stated, the server will consist of an Express API and a SocketIO endpoint to serve the data continuously.

So, to create this server and this Angular application, you will a directory to hold the source code. As such, create a new directory called virtual-market and, inside this folder, create a sub-directory called server. In a terminal (e.g. Bash or PowerShell), you can move into the server directory and run the following command:

# move into the server directory
cd virtual-market/server/ # initialise it as an NPM project
npm init -y

This command will generate the package.json file with some default properties. After initialising this directory as a NPM project, run the following command to install some dependencies of the server:

npm install express moment socket.io

Once they are installed, you can start building your server.

Building the Express API

First, create a new file called market.js inside the server directory. This file will be used as a utility. It will contain the data of a virtual market and it will contain a method to update the data. For now, you will add the data alone and the method will be added while creating the Socket.IO endpoint. So, add the following code to this file:

const marketPositions = [ {"date": "10-05-2012", "close": 68.55, "open": 74.55}, {"date": "09-05-2012", "close": 74.55, "open": 69.55}, {"date": "08-05-2012", "close": 69.55, "open": 62.55}, {"date": "07-05-2012", "close": 62.55, "open": 56.55}, {"date": "06-05-2012", "close": 56.55, "open": 59.55}, {"date": "05-05-2012", "close": 59.86, "open": 65.86}, {"date": "04-05-2012", "close": 62.62, "open": 65.62}, {"date": "03-05-2012", "close": 64.48, "open": 60.48}, {"date": "02-05-2012", "close": 60.98, "open": 55.98}, {"date": "01-05-2012", "close": 58.13, "open": 53.13}, {"date": "30-04-2012", "close": 68.55, "open": 74.55}, {"date": "29-04-2012", "close": 74.55, "open": 69.55}, {"date": "28-04-2012", "close": 69.55, "open": 62.55}, {"date": "27-04-2012", "close": 62.55, "open": 56.55}, {"date": "26-04-2012", "close": 56.55, "open": 59.55}, {"date": "25-04-2012", "close": 59.86, "open": 65.86}, {"date": "24-04-2012", "close": 62.62, "open": 65.62}, {"date": "23-04-2012", "close": 64.48, "open": 60.48}, {"date": "22-04-2012", "close": 60.98, "open": 55.98}, {"date": "21-04-2012", "close": 58.13, "open": 53.13}
]; module.exports = { marketPositions,
};

Now, add another file and name it index.js. This file will do all the Node.js work required. For now, you will add the code to create an Express REST API to serve the data. So, add the following code to the file index.js:

const app = require('express')();
const http = require('http').Server(app);
const market = require('./market'); const port = 3000; app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); next();
}); app.get('/api/market', (req, res) => { res.send(market.marketPositions);
}); http.listen(port, () => { console.log(Listening on *:${port});
});

After saving this file, you can check if everything is going well. Run the following command to start your Express REST API:

# from the server directory, run the server
node index.js

As this command starts your Node.js server on port 3000, you can visit the http://localhost:3000/api/market URL to see the market updates on last few days.

Fake market data to show on real-time Angular application.

Adding Socket.IO to Serve Data in Real Time

To show a real-time chart, you will need to simulate a real-time market data by updating it every 5 seconds. For this, you will add a new method to the market.js file. This method will be called from a Socket.IO endpoint that you will add to your index.js file. So, open the file market.js and add the following code to it:

const moment = require('moment'); // const marketPositions ... let counter = 0; function updateMarket() { const diff = Math.floor(Math.random() * 1000) / 100; const lastDay = moment(marketPositions[0].date, 'DD-MM-YYYY').add(1, 'days'); let open; let close; if (counter % 2 === 0) { open = marketPositions[0].open + diff; close = marketPositions[0].close + diff; } else { open = Math.abs(marketPositions[0].open - diff); close = Math.abs(marketPositions[0].close - diff); } marketPositions.unshift({ date: lastDay.format('DD-MM-YYYY'), open, close }); counter++;
} module.exports = { marketPositions, updateMarket,
};

The updateMarket method generates a random number every time it is called and adds it to (or subtracts it from) the last market value to generate some randomness in the figures. Then, it adds this entry to the marketPositions array.

Now, open the index.js file, so you can create a Socket.IO connection to it. This connection will call the updateMarket method after every 5 seconds to update the market data and will emit an update on the Socket.IO endpoint to update the latest data for all listeners. In this file, make the following changes:

// ... other import statements ...
const io = require('socket.io')(http); // ... app.use and app.get ... setInterval(function () { market.updateMarket(); io.sockets.emit('market', market.marketPositions[0]);
}, 5000); io.on('connection', function (socket) { console.log('a user connected');
}); // http.listen(3000, ...

With these changes in place, you can start building the Angular client to use this.

Building the Angular Application

To generate your Angular application, you can use Angular CLI. There are two ways to do it. One is to install a local copy of the CLI globally in your machine and the other is to use a tool that comes with NPM that is called npx. Using npx is better because it avoids the need to install the package locally and because you always get the latest version. If you want to use npx, make sure that you have npm 5.2 or above installed.

Then, go back to the main directory of your whole project (i.e. the virtual-market directory) and run the following command to generate the Angular project:

npx @angular/cli new angular-d3-chart

Once the project is generated, you need to install both the D3 and Socket.IO NPM libraries. So, move to the angular-d3-chart directory and run the following command to install these libraries:

npm install d3 socket.io-client

As you will use these libraries with TypeScript, it is good to have their typings installed. So, run the following command to install the typings:

npm i @types/d3 @types/socket.io-client -D

Now that the setup process is done, you can run the application to see if everything is fine:

# from the angular-d3-chart directory
npm start

To see the default Angular application, just point your browser to the http://localhost:4200 URL.

Building a Component to Display the D3 Chart

Now that your Angular application setup is ready, you can start writing its code. First, you will add a component to display the multi-line D3 chart. Second, you will create a service to fetch the data. For now, this service will consume static data from the REST API then, in no time, you will add real-time capabilities to your app.

So, run the following command to add a file for this service:

npx ng generate service market-status

To consume the REST APIs, you need to use the HttpClient service from the HttpClientModule module. This module has to be imported into the application's module for this. As such, open the app.module.ts file and replace its code with this:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {HttpClientModule} from '@angular/common/http'; import {AppComponent} from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [AppComponent]
})
export class AppModule {}

As you can see, the new version of this file does nothing besides adding the HttpClientModule to the imports section of the AppModule module.

Now, open the market-status.service.ts file and add the following code to it:

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {MarketPrice} from './market-price'; @Injectable({ providedIn: 'root'
})
export class MarketStatusService { private baseUrl = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } getInitialMarketStatus() { return this.httpClient.get<MarketPrice[]>(${this.baseUrl}/api/market); }
}

This service uses the MarketPrice class to structure the data received from your backend API (baseUrl = 'http://localhost:3000'). To add this class to your project, create a new file named market-price.ts in the app folder and add the following code to it:

export class MarketPrice { open: number; close: number; date: string | Date;
}

Now, add a new component to the application, so you can show the multi-line D3 chart. The following command adds this component:

npx ng g c market-chart

Then, open the market-chart.component.html file and replace its default content with this:

The D3 chart will be rendered inside this <div #chart> element. As you can see, you created a local reference for the div element (#chart). You will use this reference in your component class while configuring D3.

This component will not use the MarketStatusService to fetch data. Instead, it will accept the data as input. The goal of this approach is to make the market-chart component reusable. For this, the component will have an Input field and the value to this field will be passed from the app-root component. The component will use the ngOnChanges lifecycle hook to render the chart whenever there is a change in the data. It will also use the OnPush change detection strategy to ensure that the chart is re-rendered only when the input changes.

So, open the file market-chart.component.ts and add the following code to it:

import {ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, ViewChild} from '@angular/core';
import * as d3 from 'd3'; import {MarketPrice} from '../market-price'; @Component({ selector: 'app-market-chart', templateUrl: './market-chart.component.html', styleUrls: ['./market-chart.component.css'], changeDetection: ChangeDetectionStrategy.OnPush
})
export class MarketChartComponent implements OnChanges { @ViewChild('chart') chartElement: ElementRef; parseDate = d3.timeParse('%d-%m-%Y'); @Input() marketStatus: MarketPrice[]; private svgElement: HTMLElement; private chartProps: any; constructor() { } ngOnChanges() { } formatDate() { this.marketStatus.forEach(ms => { if (typeof ms.date === 'string') { ms.date = this.parseDate(ms.date); } }); }
}

Now, the MarketChartComponent class has everything required to render the chart. In addition to the local reference for the div (chartElement) and the lifecycle hook, the class has a few fields that will be used while rendering the chart. The parseDate method converts string values to Date objects and the private fields svgElement and chartProps will be used to hold the reference of the SVG element and the properties of the chart respectively. These fields will be quite useful to re-render the chart.

Now, the most complex part of the tutorial. Add the following method to the MarketChartComponent class:

 buildChart() { this.chartProps = {}; this.formatDate(); // Set the dimensions of the canvas / graph var margin = { top: 30, right: 20, bottom: 30, left: 50 }, width = 600 - margin.left - margin.right, height = 270 - margin.top - margin.bottom; // Set the ranges this.chartProps.x = d3.scaleTime().range([0, width]); this.chartProps.y = d3.scaleLinear().range([height, 0]); // Define the axes var xAxis = d3.axisBottom(this.chartProps.x); var yAxis = d3.axisLeft(this.chartProps.y).ticks(5); let this = this; // Define the line var valueline = d3.line<MarketPrice>() .x(function (d) { if (d.date instanceof Date) { return _this.chartProps.x(d.date.getTime()); } }) .y(function (d) { console.log('Close market'); return _this.chartProps.y(d.close); }); // Define the line var valueline2 = d3.line<MarketPrice>() .x(function (d) { if (d.date instanceof Date) { return _this.chartProps.x(d.date.getTime()); } }) .y(function (d) { console.log('Open market'); return _this.chartProps.y(d.open); }); var svg = d3.select(this.chartElement.nativeElement) .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', translate(${margin.left},${margin.top})); // Scale the range of the data this.chartProps.x.domain( d3.extent(this.marketStatus, function (d) { if (d.date instanceof Date) return (d.date as Date).getTime(); })); this.chartProps.y.domain([0, d3.max(this.marketStatus, function (d) { return Math.max(d.close, d.open); })]); // Add the valueline2 path. svg.append('path') .attr('class', 'line line2') .style('stroke', 'green') .style('fill', 'none') .attr('d', valueline2(this.marketStatus)); // Add the valueline path. svg.append('path') .attr('class', 'line line1') .style('stroke', 'black') .style('fill', 'none') .attr('d', valueline(this.marketStatus)); // Add the X Axis svg.append('g') .attr('class', 'x axis') .attr('transform', translate(0,${height})) .call(xAxis); // Add the Y Axis svg.append('g') .attr('class', 'y axis') .call(yAxis); // Setting the required objects in chartProps so they could be used to update the chart this.chartProps.svg = svg; this.chartProps.valueline = valueline; this.chartProps.valueline2 = valueline2; this.chartProps.xAxis = xAxis; this.chartProps.yAxis = yAxis;
}

Refer to the comments added before every section in the above method to understand what the code is doing. Also, if you have any specific doubt, just leave a comment.

Now, you will have to change the ngOnChanges function (still in your MarketChartComponent class) to call this method:

ngOnChanges() { if (this.marketStatus) { this.buildChart(); }
}

Now, you need to insert this component in the app-root component to see the chart. So, open the app.component.html file and replace its content with:

<app-market-chart [marketStatus]="marketStatusToPlot"></app-market-chart>

Then, you have to replace the content of the app.component.ts file with the following code:

import {Component} from '@angular/core';
import {MarketStatusService} from './market-status.service';
import {Observable} from 'rxjs';
import {MarketPrice} from './market-price'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css']
})
export class AppComponent { title = 'app'; marketStatus: MarketPrice[]; marketStatusToPlot: MarketPrice[]; set MarketStatus(status: MarketPrice[]) { this.marketStatus = status; this.marketStatusToPlot = this.marketStatus.slice(0, 20); } constructor(private marketStatusSvc: MarketStatusService) { this.marketStatusSvc.getInitialMarketStatus() .subscribe(prices => { this.MarketStatus = prices; }); }
}

Save these changes and run the application using the ng serve command (or npm start). Now, head to the http://localhost:4200/ URL and you will see a page with a chart similar to the following image:

Showing a D3 chart with static data in an Angular application.

Adding Real-Time Capabilities to the D3 Chart

Now that you have the chart rendered on the page, you can make it receive the market updates from Socket.IO to make it real-time. To receive these updates, you need to add a listener to the Socket.IO endpoint in the market-status.service.ts file. So, open this file and replace its code with:

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {MarketPrice} from './market-price';
import { Subject, from } from 'rxjs';
import * as socketio from 'socket.io-client'; @Injectable({ providedIn: 'root'
})
export class MarketStatusService { private baseUrl = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } getInitialMarketStatus() { return this.httpClient.get<MarketPrice[]>(${this.baseUrl}/api/market); } getUpdates() { let socket = socketio(this.baseUrl); let marketSub = new Subject<MarketPrice>(); let marketSubObservable = from(marketSub); socket.on('market', (marketStatus: MarketPrice) => { marketSub.next(marketStatus); }); return marketSubObservable; }
}

The new method, getUpdates, does three important things:


  • it creates a manager for the Socket.IO endpoint at the given URL;

  • it creates a RxJS Subject and gets an Observable from this subject. This observable is returned from this method so consumers can listen to the updates;

  • The call to the on method on the Socket.IO manager adds a listener to the market event. The callback passed to this method is called whenever the Socket.IO endpoint publishes something new.

Now, you have to make the AppComponent class consume the getUpdates() method. So, open the app.component.ts file and modify the constructor as shown below:

constructor(private marketStatusSvc: MarketStatusService) { this.marketStatusSvc.getInitialMarketStatus() .subscribe(prices => { this.MarketStatus = prices; let marketUpdateObservable = this.marketStatusSvc.getUpdates(); // 1 marketUpdateObservable.subscribe((latestStatus: MarketPrice) => { // 2 this.MarketStatus = [latestStatus].concat(this.marketStatus); // 3 }); // 4 });
}

In the above snippet, the statements marked with the numbers are the new lines added to the constructor. Observe the statement labeled with 3. This statement creates a new array instead of updating the field marketStatus. This is done to let the consuming app-market-chart component know about the change when you have an update.

The last change you will need to do to see the chart working in real time is to make the flowing data hit the chart. To do this, open the market-chart.component.ts file and add the following method the MarketChartComponent class:

updateChart() { let _this = this; this.formatDate(); // Scale the range of the data again this.chartProps.x.domain(d3.extent(this.marketStatus, function (d) { if (d.date instanceof Date) { return d.date.getTime(); } })); this.chartProps.y.domain([0, d3.max(this.marketStatus, function (d) { return Math.max(d.close, d.open); })]); // Select the section we want to apply our changes to this.chartProps.svg.transition(); // Make the changes to the line chart this.chartProps.svg.select('.line.line1') // update the line .attr('d', this.chartProps.valueline(this.marketStatus)); this.chartProps.svg.select('.line.line2') // update the line .attr('d', this.chartProps.valueline2(this.marketStatus)); this.chartProps.svg.select('.x.axis') // update x axis .call(this.chartProps.xAxis); this.chartProps.svg.select('.y.axis') // update y axis .call(this.chartProps.yAxis);
}

The comments added in the snippet explain what you are doing in this method. Now, you have to make the ngOnChanges method call this new method. So, change the ngOnChanges() method in the MarketChartComponent class as shown below:

ngOnChanges() { if (this.marketStatus && this.chartProps) { this.updateChart(); } else if (this.marketStatus) { this.buildChart(); }
}

Now, if you run the application, you will see an error on the browser console saying global is not defined.

Console error saying global is not defined.

This is because Angular CLI 6 removed the global object and SocketIO uses it. To fix this problem, add the following statement to the polyfills.ts file:

(window as any).global = window;

With this, all the changes are done. Save all your files and run the applications again. You can move into the server directory in one terminal and issue node index.js to run your backend API, then move to the angular-d3-chart directory and issue npm start to run the Angular application.

Now, if you head to (http://localhost:4200)[http://localhost:4200], you will see your nice chart with real-time data flowing into it every 5 seconds.

Real-time chart with Angular, D3, and SocketIO.

Awesome, right?

"Adding real-time capabilities to @Angular is easy with D3 and Socket.IO"


Aside: Authenticate an Angular App and Node API with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with an Angular application and a Node API using Auth0. You can clone this sample app and API from the angular-auth0-aside repo on GitHub.

Auth0 login screen

Features

The sample Angular application and API has the following features:


  • Angular application generated with Angular CLI and served at http://localhost:4200

  • Authentication with auth0.js using a login page

  • Node server protected API route http://localhost:3001/api/dragons returns JSON data for authenticated GET requests

  • Angular app fetches data from API once user is authenticated with Auth0

  • Profile page requires authentication for access using route guards

  • Authentication service uses a subject to propagate authentication status events to the entire app

  • User profile is fetched on authentication and stored in authentication service

  • Access token, profile, and token expiration are stored in local storage and removed upon logout

Sign Up for Auth0

You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 application and API so Auth0 can interface with an Angular app and Node API.

Set Up an Auth0 Application


  1. Go to your Auth0 Dashboard and click the "create a new application" button.

  2. Name your new app and select "Single Page Web Applications".

  3. In the Settings for your new Auth0 app, add http://localhost:4200/callback to the Allowed Callback URLs.

  4. Add http://localhost:4200 to the Allowed Logout URLs. Click the "Save Changes" button.

  5. If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure you set up your own social keys and do not leave social connections set to use Auth0 dev keys.

Note: Under the OAuth tab of Advanced Settings (at the bottom of the Settings section) you should see that the JsonWebToken Signature Algorithm is set to RS256. This is the default for new applications. If it is set to HS256, please change it to RS256. You can read more about RS256 vs. HS256 JWT signing algorithms here.

Set Up an API


  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to your API endpoint URL. In this example, this is http://localhost:3001/api/. The Signing Algorithm should be RS256.

  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our Angular client and Node backend API.

Dependencies and Setup

The Angular app utilizes the Angular CLI. Make sure you have the CLI installed globally:

$ npm install -g @angular/cli

Once you've cloned the project, install the Node dependencies for both the Angular app and the Node server by running the following commands in the root of your project folder:

$ npm install
$ cd server
$ npm install

The Node API is located in the /server folder at the root of our sample application.

Find the config.js.example file and remove the .example extension from the filename. Then open the file:

// server/config.js (formerly config.js.example)
module.exports = { CLIENTDOMAIN: '[AUTH0CLIENTDOMAIN]', // e.g. 'you.auth0.com' AUTH0AUDIENCE: 'http://localhost:3001/api/'
};

Change the AUTH0CLIENTDOMAIN identifier to your Auth0 application domain and set the AUTH0AUDIENCE to your audience (in this example, this is http://localhost:3001/api/). The /api/dragons route will be protected with express-jwt and jwks-rsa.

Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.

Our API is now protected, so let's make sure that our Angular application can also interface with Auth0. To do this, we'll activate the src/app/auth/auth0-variables.ts.example file by deleting .example from the file extension. Then open the file and change the [AUTH0CLIENTID] and [AUTH0CLIENT_DOMAIN] strings to your Auth0 information:

// src/app/auth/auth0-variables.ts (formerly auth0-variables.ts.example)
...
export const AUTHCONFIG: AuthConfig = { CLIENTID: '[AUTH0CLIENTID]', CLIENTDOMAIN: '[AUTH0CLIENT_DOMAIN]', ...

Our app and API are now set up. They can be served by running ng serve from the root folder and node server.js from the /server folder.

With the Node API and Angular app running, let's take a look at how authentication is implemented.

Authentication Service

Authentication logic on the front end is handled with an AuthService authentication service: src/app/auth/auth.service.ts file.

// src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import * as auth0 from 'auth0-js';
import { AUTHCONFIG } from './auth0-variables';
import { UserProfile } from './profile.model'; (window as any).global = window; @Injectable()
export class AuthService { // Create Auth0 web auth instance // @TODO: Update AUTHCONFIG and remove .example extension in // src/app/auth/auth0-variables.ts.example private Auth0 = new auth0.WebAuth({ clientID: AUTHCONFIG.CLIENTID, domain: AUTHCONFIG.CLIENTDOMAIN, responseType: 'token', redirectUri: AUTHCONFIG.REDIRECT, audience: AUTHCONFIG.AUDIENCE, scope: AUTHCONFIG.SCOPE }); userProfile: UserProfile; accessToken: string; expiresAt: number; // Create a stream of logged in status to communicate throughout app loggedIn: boolean; loggedIn$ = new BehaviorSubject<boolean>(this.loggedIn); constructor() { // You can restore an unexpired authentication session on init // by using the checkSession() endpoint from auth0.js: // https://auth0.com/docs/libraries/auth0js/v9#using-checksession-to-acquire-new-tokens } private setLoggedIn(value: boolean) { // Update login status subject this.loggedIn$.next(value); this.loggedIn = value; } login() { // Auth0 authorize request this.Auth0.authorize(); } handleLoginCallback() { // When Auth0 hash parsed, get profile this.Auth0.parseHash((err, authResult) => { if (authResult && authResult.accessToken) { window.location.hash = ''; this.getUserInfo(authResult); } else if (err) { console.error(Error: ${err.error}); } }); } getUserInfo(authResult) { // Use access token to retrieve user's profile and set session this.Auth0.client.userInfo(authResult.accessToken, (err, profile) => { this.setSession(authResult, profile); }); } private _setSession(authResult, profile) { // Save session data and update login status subject this.expiresAt = authResult.expiresIn * 1000 + Date.now(); this.accessToken = authResult.accessToken; this.userProfile = profile; this.setLoggedIn(true); } logout() { // Remove token and profile, update login status subject, // and log out of Auth0 authentication session // This does a refresh and redirects back to homepage // Make sure you have the returnTo URL in your Auth0 // Dashboard Application settings in Allowed Logout URLs this.Auth0.logout({ returnTo: 'http://localhost:4200', clientID: AUTHCONFIG.CLIENTID }); } get authenticated(): boolean { // Check if current date is greater than // expiration and user is currently logged in return (Date.now() < this.expiresAt) && this.loggedIn; } }

This service uses the config variables from auth0-variables.ts to instantiate an auth0.js WebAuth instance.

An RxJS BehaviorSubject is used to provide a stream of authentication status events that you can subscribe to anywhere in the app.

The login() method authorizes the authentication request with Auth0 using your config variables. A login page will be shown to the user and they can then log in.

Note: If it's the user's first visit to our app and our callback is on localhost, they'll also be presented with a consent screen where they can grant access to our API. A first party client on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive accessToken and expiresIn in the hash from Auth0 when returning to our app. The handleLoginCallback() method uses Auth0's parseHash() method callback to get the user's profile (getUserInfo()) and set the session (setSession()) by saving the token, profile, and token expiration and updating the loggedIn$ subject so that any subscribed components in the app are informed that the user is now authenticated.

Note: The profile takes the shape of profile.model.ts from the OpenID standard claims.

Finally, we have a logout() method that logs out of the authentication session on Auth0's server and then redirects back to our app's homepage.

We also have an authenticated accessor to return current authentication status based on presence of a token and the token's expiration.

Once AuthService is provided in app.module.ts, its methods and properties can be used anywhere in our app, such as the home component.

Callback Component

The callback component is where the app is redirected after authentication. This component simply shows a loading message until the login process is completed. It executes the handleLoginCallback() method to parse the hash and extract authentication information. It subscribes to the loggedIn$ Behavior Subject from our Authentication service in order to redirect back to the home page once the user is logged in, like so:

// src/app/callback/callback.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { AuthService } from './../auth/auth.service';
import { Router } from '@angular/router'; @Component({ selector: 'app-callback', templateUrl: './callback.component.html', styleUrls: ['./callback.component.css']
})
export class CallbackComponent implements OnInit, OnDestroy { loggedInSub: Subscription; constructor(private auth: AuthService, private router: Router) { // Parse authentication hash auth.handleLoginCallback(); } ngOnInit() { this.loggedInSub = this.auth.loggedIn$.subscribe( loggedIn => loggedIn ? this.router.navigate(['/']) : null ) } ngOnDestroy() { this.loggedInSub.unsubscribe(); } }

Making Authenticated API Requests

In order to make authenticated HTTP requests, we need to add an Authorization header with the access token in our api.service.ts file.

// src/app/api.service.ts
import { Injectable } from '@angular/core';
import { throwError, Observable } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { AuthService } from './auth/auth.service'; @Injectable()
export class ApiService { private baseUrl = 'http://localhost:3001/api/'; constructor( private http: HttpClient, private auth: AuthService ) { } getDragons$(): Observable<any[]> { return this.http .get<any[]>(${this.baseUrl}dragons, { headers: new HttpHeaders().set( 'Authorization', Bearer ${this.auth.accessToken} ) }) .pipe( catchError(this._handleError) ); } private _handleError(err: HttpErrorResponse | any) { const errorMsg = err.message || 'Unable to retrieve data'; return throwError(errorMsg); } }

Final Touches: Route Guard and Profile Page

A profile page component can show an authenticated user's profile information. However, we only want this component to be accessible if the user is logged in.

With an authenticated API request and login/logout implemented, the final touch is to protect our profile route from unauthorized access. The auth.guard.ts route guard can check authentication and activate routes conditionally. The guard is implemented on specific routes of our choosing in the app-routing.module.ts file like so:

// src/app/app-routing.module.ts
...
import { AuthGuard } from './auth/auth.guard';
... { path: 'profile', component: ProfileComponent, canActivate: [ AuthGuard ] },
...

More Resources

That's it! We have an authenticated Node API and Angular application with login, logout, profile information, and protected routes. To learn more, check out the following resources:

Conclusion

As you saw in this tutorial, the web has capabilities that allow you to build very rich applications. Your real-time chart, for example, adds a huge value to your app because there the user knows that they will have the latest data without having to refresh the page or performing any action. This kind of interactivity improves your users' experiences and will contribute to their happiness.

WDYT? Ready to add some real-time data to the web?


Tag cloud