Chapter 12. Using an authentication API in Angular applications

This chapter covers

  • Using local storage and Angular to manage a user session
  • Managing user sessions in Angular
  • Using JWT in Angular Applications

In this chapter, you’ll integrate the work that you completed in chapter 11 on authentication via API and use the API endpoints in your Angular application. Specifically, you’ll look at how to use the Angular HTTP client library and localStorage.

12.1. Creating an Angular authentication service

In an Angular app, as in any other application, authentication is likely to be needed across the board. The obvious thing to do is create an authentication service that can be used anywhere it’s needed. This service should be responsible for everything related to authentication, including saving and reading a JWT, returning information about the current user, and calling the login and register API endpoints.

You’ll start by looking at how to manage the user session.

12.1.1. Managing a user session in Angular

Assume for a moment that a user has just logged in and the API has returned a JWT. What should you do with the token? Because you’re running an SPA, you could keep it in the browser’s memory. This approach is okay unless the user decides to refresh the page, which reloads the application, losing everything in memory—not ideal.

Next, you’ll look to save the token somewhere a bit more robust, allowing the application to read it whenever it needs to. The question is whether to use cookies or local storage.

Cookies vs. local storage

The traditional approach to saving user data in a web application is to save a cookie, and that’s certainly an option. But cookies are there to be used by server applications, with each request to the server sending the cookies along in the HTTP header to be read. In an SPA, you don’t need cookies; the API endpoints are stateless and don’t get or set cookies.

You need to look somewhere else, toward local storage, which is designed for client-side applications. With local storage, the data stays in the browser and doesn’t automatically get transmitted with requests, as would happen with cookies.

Local storage is also easy to use with JavaScript. Look at the following snippet, which would set and get some data:

window.localStorage['my-data'] = 'Some information';
window.localStorage['my-data']; // Returns 'Some information'

Right, so that’s settled; you’ll use local storage in Loc8r to save the JWT. If localStorage isn’t familiar territory, head to the Mozilla developer documentation at http://mng.bz/0WKz to find out more.

To facilitate the use of localStorage in the Angular application, you’ll first create an Injectable called BROWSER_STORAGE that you can use in components. You’ll hook into the localStorage, but you’ll do so via a factory service that you inject into components that require access to the localStorage.

To start, generate the class file

$ ng generate class storage

and place the following code in it.

Listing 12.1. storage.ts
import { InjectionToken } from '@angular/core';                 1

export const BROWSER_STORAGE = new InjectionToken<Storage>      2
('Browser Storage',{                                          2
  providedIn: 'root',
  factory: () => localStorage                                   3
});

  • 1 Uses the InjectionToken class
  • 2 Creates a new InjectionToken
  • 3 factory function wrapping localStorage
Creating a service to save and read a JWT in local storage

You’ll start building the authentication service by creating the methods to save a JWT in local storage and read it back out again. You’ve seen how easy it is to work with localStorage in JavaScript, so now you need to wrap it in an Angular service that exposes two methods: saveToken() and getToken(). No real surprises here, but the saveToken() method should accept a value to be saved, and getToken() should return a value.

First, generate a new service called Authentication inside the Angular application:

$ ng generate service authentication

The following listing shows the contents of the new service, including the first two methods.

Listing 12.2. Creating the authentication service with the first two methods
import { Inject, Injectable } from '@angular/core';
import { BROWSER_STORAGE } from './storage';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {

  constructor(@Inject(BROWSER_STORAGE) private storage: Storage) { }   1

  public getToken(): string {                                          2
    return this.storage.getItem('loc8r-token');                        2
  }

  public saveToken(token: string): void {                              3
    this.storage.setItem('loc8r-token', token);                        3
  }
}

  • 1 Injects the importer BROWSER_STORAGE wrapper
  • 2 Creates the getToken function
  • 3 Creates the saveToken function

And there you have a simple service to handle saving loc8r-token to localStorage and reading it back out again. Next, you’ll look at logging in and registering.

12.1.2. Allowing users to sign up, sign in, and sign out

To use the service to enable users to register, log in, and log out, you’ll need to add three more methods. Start with registering and logging in.

Calling the API to register and log in

You’ll need two methods to register and log in, which post the form data to the register and login API endpoints you created earlier in this chapter. When successful, both these endpoints return a JWT, so you can use the saveToken method to save them.

To prepare, you’ll generate two simple auxiliary classes to help manage the data that you need across the application—a User class (listing 12.3) and an AuthResponse class (listing 12.4):

$ ng generate class user
$ ng generate class authresponse

The following two listings show the simple classes that you’ll use to maintain the given data. Listing 12.3 provides your User class definition, which is a simple class to hold the name and email as strings.

Listing 12.3. user.ts
export class User {
  email: string;        1
  name: string;         1
}

  • 1 Tells typescript that you require strings here

Listing 12.4 provides the definition for your AuthResponse object, which at this time holds the token string.

Listing 12.4. authresponse.ts
export class AuthResponse {
  token: string;              1
}

  • 1 Sets the token to be a string

With these classes in place, you can add the aforementioned register() and login() methods to the authentication service, as shown in the next listing. As these methods rely on the Loc8rDataService, you’ll inject that too.

Listing 12.5. authentication.service.ts
import { Inject, Injectable } from '@angular/core';
import { BROWSER_STORAGE } from './storage';
import { User } from './user';                             1
import { AuthResponse } from './authresponse';             1
import { Loc8rDataService } from './loc8r-data.service';   1

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {

  constructor(
    @Inject(BROWSER_STORAGE) private storage: Storage,
    private loc8rDataService: Loc8rDataService             2
  ) { }

  ...

  public login(user: User): Promise<any> {                 3
    return this.loc8rDataService.login(user)
      .then((authResp: AuthResponse) => this.saveToken(authResp.token));
  }

  public register(user: User): Promise<any> {              4
    return this.loc8rDataService.register(user)
      .then((authResp: AuthResponse) => this.saveToken(authResp.token));
  }
}

  • 1 Imports the relevant classes and services
  • 2 Injects the data service
  • 3 The login function
  • 4 The register function

Take a quick look at the two methods that you’ve added. What you’re doing is providing a wrapper for the login() and register() methods from the Loc8rDataService that you’re about to write and ensuring that a Promise gets returned so data can be passed back to the UI. You don’t necessarily care what’s in the Promise—only that it’s returned. Then the token from the AuthResponse object that the methods receive is saved, using the functions already in place.

Finally, you need to add the aforementioned methods to the Loc8rDataService that are required to communicate with the API endpoints. Changes are highlighted in bold in the next listing.

Listing 12.6. Changes to Loc8rDataService
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Location, Review } from './location';
import { User } from './user';                                 1
import { AuthResponse } from './authresponse';                 1

@Injectable({
  providedIn: 'root'
})
export class Loc8rDataService {

...

  public login(user: User): Promise<AuthResponse> {            2
    return this.makeAuthApiCall('login', user);
  }

  public register(user: User): Promise<AuthResponse> {         3
    return this.makeAuthApiCall('register', user);
  }

  private makeAuthApiCall(urlPath: string, user: User):
  Promise<AuthResponse> {                                    4
    const url: string = `${this.apiBaseUrl}/${urlPath}`;
    return this.http
      .post(url, user)                                         5
      .toPromise()                                             5
      .then(response => response as AuthResponse)
      .catch(this.handleError);
  }

  ...
}

  • 1 Imports for User and AuthResponse classes
  • 2 Login method returning the AuthResponse Promise
  • 3 Register method returning the AuthResponse Promise
  • 4 The actual call. login and register are similar enough to make DRY.
  • 5 Uses the HttpClient POST request Observable that you convert to a Promise object

The call out to the API in both cases of login and register are essentially the same call; the only difference is the URL that you’re required to hit to perform the action you need. In listing 12.6, you POST a payload containing the user details that you’re attempting to use and returning an AuthResponse object on success or handling the error on failure. To that end, you have a private method (makeAuthApiCall()) to manage the call and public methods login() and register() to handle the specific details of which API endpoint URL you want to call.

With these methods in place, you can address signing out.

Deleting localStorage to sign out

The user session in the Angular application is managed by saving the JWT in localStorage. If the token is there, is valid, and hasn’t expired, you can say that the user is logged in. You can’t change the expiry date of the token from within the Angular app; only the server can do that. What you can do is delete it.

To give users the ability to log out, you can create a new logout method in the authentication service to remove the Loc8r JWT.

Listing 12.7. Removing the token from location storage
  public logout(): void {
    this.storage.removeItem('loc8r-token');        1
  }

  • 1 Deletes the token from localStorage

This code removes the loc8r-token item from the browser’s localStorage.

Now you have methods to get a JWT from the server, save it in localStorage, read it from localStorage, and delete it. The next question is how to use it in the application to see that a user is logged in and to get data out of it.

12.1.3. Using the JWT data in the Angular service

The JWT saved in the browser’s localStorage is what you use to manage a user session. The JWT is used to validate whether a user is logged in. If a user is logged in, the application can also read the user information stored inside.

First, add a method to check whether somebody is logged in.

Checking the logged-in status

To check whether a user is currently logged in to the application, you need to check whether the loc8r-token exists in localStorage. You can use the getToken() method for that task. But the existence of a token isn’t enough. Remember that the JWT has expiry data embedded in it, so if a token exists, you’ll need to check that too.

The expiration date and time of the JWT is part of the payload, which is the second chunk of data. Remember that this part is an encoded JSON object; it’s encoded rather than encrypted, so you can decode it. In fact, we’ve already talked about the function to do this: atob.

Stitching everything together, you want to create a method that

  1. Gets the stored token
  2. Extracts the payload from the token
  3. Decodes the payload
  4. Validates that the expiry date hasn’t passed

This method, added to the AuthenticationService, should return true if a user is logged in and false if not. The next listing shows this behavior in a method called isLoggedIn().

Listing 12.8. isLoggedIn method for the authentication service
public isLoggedIn(): boolean {
  const token: string = this.getToken();                      1
    if (token) {                                              2
      const payload = JSON.parse(atob(token.split('.')[1]));  2
      return payload.exp > (Date.now() / 1000);               3
    } else {
      return false;
    }
  }
}

  • 1 Gets the token from storage
  • 2 If the token exists, gets the payload, decodes it, and parses it to JSON
  • 3 Validates whether expiry is passed

That isn’t much code, but it’s doing a lot. After you’ve referenced it in the return statement in the service, the application can quickly check whether a user is logged in at any point.

The next and final method to add to the authentication service gets some user information from the JWT.

Getting user information from the JWT

You want the application to be able to get a user’s email address and name from the JWT. You saw in the isLoggedIn() method how to extract data from the token, and your new method does exactly the same thing.

Create a new method called getCurrentUser(). The first thing that this method does is validate that a user is logged in by calling the isLoggedIn() method. If a user is logged in, it gets the token by calling the getToken() method before extracting and decoding the payload and returning the data you’re after. The following listing shows how this looks.

Listing 12.9. getCurrentUser() method (authentication.service.ts)
public getCurrentUser(): User {                                     1
  if (this.isLoggedIn()) {                                          2
    const token: string = this.getToken();
    const { email, name } = JSON.parse(atob(token.split('.')[1]));
    return { email, name } as User;                                 3
  }
}

  • 1 Returns the type of User
  • 2 Ensures that the user is logged in
  • 3 Typecasts object to the User type

With that done, the Angular authentication service is complete. Looking back over the code, you can see that it’s generic and easy to copy from one application to another. All you’ll probably have to change are the name of the token and the API URLs, so you’ve got a nice, reusable Angular service.

Now that the service is in the application, you can use it. Keep moving forward by creating the Login and Register pages.

12.2. Creating the Register and Login pages

Everything you’ve done so far is great, but without a way for visitors to the website to register and log in, it would be useless. So that’s what you’ll address now.

In terms of functionality, you want a Register page where new users can set their details and sign up, and a Login page where users return to input their username and password. When users have gone through either of these processes and are successfully authenticated, the application should send them back to the page they were on when they started the process.

At the end of the following sections, you’d expect your Register page to look a lot like figure 12.1.

Figure 12.1. Register page

The Login page should look like figure 12.2. You’ll begin with the Register page.

Figure 12.2. Login page

12.2.1. Building the Register page

To develop a working registration page, you have a few things to do:

  1. Create the register component and add it to the routing.
  2. Build the template.
  3. Flesh out the component body, including redirection.

And, of course, you’ll want to test the page when you’re done.

Step 1 is creating the component. Reach for the Angular generator:

$ ng generate component register

With that done, amend the application routing by adding entries to app_routing/app_routing.module.ts. Point the register component at the /register route, as the next listing shows.

Listing 12.10. Registration routing
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { AboutComponent } from '../about/about.component';
import { HomepageComponent } from '../homepage/homepage.component';
import { DetailsPageComponent } from '../details-page/details-page.
component';
import { RegisterComponent } from '../register/
register.component';                            1

const routes: Routes = [
...
  {                                               2
    path: 'register',
    component: RegisterComponent
  }
];

...
})
export class AppRoutingModule { }

  • 1 Imports the newly created register component
  • 2 Adds the path information

With that done, look at the details of the component template and methods that link this template to the services that you built earlier.

Building the registration template

Okay, now you’ll build the template for the registration page. Aside from the normal header and footer, you’ll need a few things. Primarily, you need a form to allow visitors to input their name, email address, and password. In this form, you should also have an area to display any errors. You’ll also pop in a link to the Login page, in case users realize that they’re already registered.

The next listing shows the template pieced together. Notice that the input fields have the credentials in the view model bound to them via ngModel.

Listing 12.11. Full template for the registration page (register/register.component.html)
<app-page-header [content]="pageContent.header"></app-page-header>
<div class="row">
  <div class="col-12 col-md-8">
    <p class="lead">Already a member? Please <a routerLink="/login">
    log in</a> instead</p>                                             1
    <form (submit)="onRegisterSubmit()">
      <div role="alert" *ngIf="formErrors" class="alert alert-danger">
      {{ formError }}</div>                                            2
      <div class="form-group">
        <label for="name">Full Name</label>
        <input class="form-control" id="name" name="name" placeholder=
        "Enter your name" [(ngModel)]="credentials.name">              3
      </div>
      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" class="form-control" id="email" name="email"
        placeholder="Enter email address" [(ngModel)]=
        "credentials.email">                                           4
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="pasword" class="form-control" id="password"
        name="password" placeholder="e.g 12+ alphanumerics"
        [(ngModel)]="credentials.password">                            5
      </div>
      <button type="submit" role="button" class="btn
      btn-primary">Register!</button>
    </form>
  </div>
  <app-sidebar [content]="pageContent.sidebar"  class=
  "col-12 col-md-4"></app-sidebar>
</div>

  • 1 Link to switch to Login page
  • 2 A <div> to display errors
  • 3 Input for username
  • 4 Input for email address
  • 5 Input for password

Again, the important thing to note is that a user’s name, email, and password are bound to the view model in the object credentials.

Next, you look at the flip side and code the component methods.

Creating the registration component skeleton

Based on the template, you’ll set up a few things in the register component. You’ll need the title text for the page header and an onRegisterSubmit() function to handle form submission. You’ll also give all the credentials properties a default empty string value.

The next listing shows the initial setup.

Listing 12.12. Starting the register component
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';                            1
import { AuthenticationService } from '../authentication.service';   2

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {

  public formError: string = '';                                    3

  public credentials = {                                            4
    name: '',
    email: '',
    password: ''
  };

  public pageContent = {                                            5
    header: {
      title: 'Create a new account',
      strapline: ''
    },
    sidebar: ''
  };

  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) { }

  ngOnInit() {
  }

  • 1 Imports the services required from the Router
  • 2 Imports the authentication service
  • 3 Error string initialization
  • 4 credentials object to hold model data
  • 5 Page content object for the usual page data

There’s nothing new here—a couple of public properties to manage the component’s internal data and injection of the services that you’ll need to use in the component.

Add the contents of the next listing to the component that you’ve created.

Listing 12.13. Registration submission handler
  public onRegisterSubmit(): void {                                   1
    this.formError = '';
    if (
      !this.credentials.name ||                                       2
      !this.credentials.email ||                                      2
      !this.credentials.password                                      2
    ) {
      this.formError = 'All fields are required, please try again';   3
    } else {
      this.doRegister();
    }
  }

  private doRegister(): void {                                        4
    this.authenticationService.register(this.credentials)
      .then(() => this.router.navigateByUrl('/'))
      .catch((message) => this.formError = message);
  }

  • 1 Submits an event handler
  • 2 Checks that you’ve received all the relevant information
  • 3 Returns messaging in case of an error
  • 4 Performs the registration

With this code in place, you can try out the Register page and functionality by starting the application running and heading to http://localhost:4200/register.

When you’ve done this and successfully registered as a user, open the browser development tools, and look for the resources. As illustrated in figure 12.3, you should see a loc8r-token below the local storage folder.

Figure 12.3. Finding the loc8r-token in the browser

You’ve added the ability for a new user to register. Next, you’ll enable a returning user to log in.

12.2.2. Building the Login page

The approach to the Login page is similar to the approach to the Register page. Nothing here should be unfamiliar, so you’ll go through it quickly.

First, generate the new component:

$ ng generate component login

Add the following to the routes object in the router (app-routing/app-routing.module.ts):

{
  path: 'login',
  component: LoginComponent
}

With this code in place, you can build up the component template file: login/login-component.html. You can see from the route where you want this file to be. It’s similar to the register template, so it’s probably easiest to duplicate and edit that template. All you need to do is remove the name input and change a couple of pieces of text. The following listing highlights in bold the changes you need to make in the login template.

Listing 12.14. Changes for the login template
<app-page-header [content]="pageContent.header"></app-page-header>
<div class="row">
  <div class="col-12 col-md-8">
    <p class="lead">Not a member? Please
      <a routerLink="/register">register</a> first
    </p>                                                                1
    <form (ngSubmit)="onLoginSubmit(evt)">                              2
      <div role="alert" *ngIf="formError" class="alert alert-danger">
        {{ formError }}                                                 3
      </div>
      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" class="form-control" id="email" name="email"
        placeholder="Enter email address" [(ngModel)]=
        "credentials.email">
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="pasword" class="form-control" id="password"
         name="password" placeholder="e.g 12+ alphanumerics"
        [(ngModel)]="credentials.password">
      </div>
      <button type="submit" role="button" class="btn btn-default">
      Sign in!</button>                                               4
    </form>
  </div>
  <app-sidebar [content]="pageContent.sidebar" class="col-12 col-md-4">
  </app-sidebar>
</div>

  • 1 Changes the link to register
  • 2 Updates the submit event function call
  • 3 Note that the name input is removed.
  • 4 Changes the text on the button

Finally, you make changes in the login component, which is similar to the register component. The changes you need to make are these:

  • Change the name of the component controller.
  • Change the page title.
  • Remove references to the name field.
  • Rename doRegisterSubmit() to doLoginSubmit(), and doRegister to doLogin.
  • Call the login() method of the AuthenticationService instead of the register() method.

Copy the main body of the component class code from register/register-component.ts, and make the following changes. The next listing shows the content of the file and highlights the changes in bold.

Listing 12.15. Changes required for the login component
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '../authentication.service';

@Component({
  selector: 'app-login',                                   1
  templateUrl: './login.component.html',                   1
  styleUrls: ['./login.component.css']                     1
})
export class LoginComponent implements OnInit {            2

  public formError: string = '';

  public credentials = {
    name: '',
    email: '',
    password: ''
  };

  public pageContent = {
    header: {
      title: 'Sign in to Loc8r',                           3
      strapline: ''
    },
    sidebar: ''
  };

  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) { }

  ngOnInit() {
  }

  public onLoginSubmit(): void {                           4
    this.formError = '';
    if (!this.credentials.email || !this.credentials.password) {
      this.formError = 'All fields are required, please try again';
    } else {
      this.doLogin();
    }
  }

  private doLogin(): void {                                5
    this.authenticationService.login(this.credentials)
      .then( () => this.router.navigateByUrl('/'))
      .catch( (message) => {
        this.formError = message
      });
  }
}

  • 1 Updates the component definition block
  • 2 Changes the component name
  • 3 Changes the page title
  • 4 Changes the submit event method
  • 5 Changes the doRegister method to doLogin and updates the authentication service call

That was easy! There’s no need to dwell on this component as, functionally, it works like the register controller.

Now you’ll move to the final stage and use the authenticated session in the Angular application.

12.3. Working with authentication in the Angular app

When you have a way to authenticate users, the next step is making use of that information. In Loc8r, you’ll do two things:

  • Change the navigation based on whether the visitor is logged in.
  • Use the user information when creating reviews.

You’ll tackle the navigation first.

12.3.1. Updating the navigation

One thing that’s currently missing from the navigation is a Sign-in link, so you’ll add one in the conventional place: the top-right corner of the screen. But when a user is logged in, you don’t want to display a sign-in message; it would be better to display the user’s name and give them an option to sign out.

That’s what you’ll do in this section, starting by adding a right-side section to the navigation bar.

12.3.2. Adding a right-side section to the navigation

The navigation for Loc8r is set up in the framework component that acts as a layout for every page. You may remember from chapter 9 that this is the root component that defines the router outlet; the files are in app_public/src/app/framework. The following listing highlights in bold the markup you need to add to the template (framework.component.html) to put a Sign-in link on the right side.

Listing 12.16. Changes for the framework component
<div id="navbarMain" class="navbar-collapse collapse">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item" routerLinkActive="active">
        <a routerLink="about" class="nav-link">About</a>
      </li>
    </ul>
    <ul class="navbar-nav justify-content-end">                  1
      <li class="nav-item" routerLinkActive="active">
        <a routerLink="login" class="nav-link">Sign in</a>       2
      </li>
      <li class="nav-item dropdown" routerLinkActive="active">
        <a class="nav-link dropdown-toggle"
data-toggle="dropdown">Username</a>                              3
          <div class="dropdown-menu">
            <a class="dropdown-item">Logout</a>                  4
          </div>
      </li>
    </ul>
  </div>
</div>

  • 1 Adds a navbar to the header, and pushes it to the right
  • 2 The Sign-in link
  • 3 Area for the username when logged in
  • 4 Link for logging out

The login nav option navigates to the freshly minted login component you’ve built.

Currently, however, an added link in the drop-down menu doesn’t work, and the Logout link needs to be fleshed out.

To make this link work, you need to inject the Authentication service into the Framework component. You also need to add three methods:

  • A click event to trigger the logout (doLogout())
  • A method to check the current user login status
  • A method to get the current user name

The following listing shows how this is done.

Listing 12.17. Changes to Framework for logout
import { Component, OnInit } from '@angular/core';
import { AuthenticationService } from '../authentication.service';  1
import { User } from '../user';                                     2

@Component({
  selector: 'app-framework',
  templateUrl: './framework.component.html',
  styleUrls: ['./framework.component.css']
})
export class FrameworkComponent implements OnInit {

  constructor(
    private authenticationService: AuthenticationService            3
  ) { }

  ngOnInit() {
  }

  public doLogout(): void {                                         4
    this.authenticationService.logout();
  }

  public isLoggedIn(): boolean {                                    5
    return this.authenticationService.isLoggedIn();
  }

  public getUsername(): string {                                    6
    const user: User = this.authenticationService.getCurrentUser();
    return user ? user.name : 'Guest';
  }

}

  • 1 Imports the authentication service
  • 2 Imports the User class for type checking
  • 3 Injects the imported service
  • 4 doLogout wrapper for the authentication service logout method
  • 5 isLoggedIn wrapper
  • 6 getUsername wrapper

When these functions are in place, you’ll add them to the framework HTML template. You need to add an *ngIf to toggle the display of the username drop-down menu, depending on the result of isLoggedIn(). When isLoggedIn() returns true, you’ll want to show the user’s name in the HTML. Finally, you need to hook in the doLogout() function to the click event for the Logout link.

Listing 12.18. Changes to the framework component template
  <ul class="navbar-nav justify-content-end">
    <li class="nav-item" routerLinkActive="active">
      <a routerLink="login" class="nav-link" *ngIf="!isLoggedIn()">
      Sign in</a>                                                   1
    </li>
    <li class="nav-item dropdown" routerLinkActive="active"
    *ngIf="isLoggedIn()">                                           2
      <a class="nav-link dropdown-toggle" data-toggle="dropdown">
        {{ getUsername() }}                                           3
      </a>
      <div class="dropdown-menu">
        <a class="dropdown-item" (click)="doLogout()">Logout</a>
      </div>
    </li>
  </ul>

  • 1 Doesn’t show if logged in
  • 2 Shows if logged in
  • 3 Shows username if available

With the logout functionality in place, now is a good time to consider a user-experience issue. Currently, the login and register components redirect the user to the homepage on a successful response, which is not a great experience for the user. What you’ll do is return the user back to the page that they were on before logging in or registering.

To do this, create a service that takes advantage of the Angular router events property. The events property keeps a record of the routing events that occur while the user navigates the application. To start, generate a service called history:

$ ng generate service history

Add this new service to the framework component so that the reference is in place before you fill in body of the history service.

Listing 12.19. Adding a history service to the framework component
import { Component, OnInit } from '@angular/core';
import { AuthenticationService } from '../authentication.service';
import { HistoryService } from '../history.service';                1
import { User } from '../user';

@Component({
  selector: 'app-framework',
  templateUrl: './framework.component.html',
  styleUrls: ['./framework.component.css']
})
export class FrameworkComponent implements OnInit {

  constructor(
    private authenticationService: AuthenticationService,
    private historyService: HistoryService                            2
  ) { }
...

  • 1 Imports the service
  • 2 Injects it into the component

With this code in place, fill in the logic for the HistoryService. You need to do several things to track a user’s navigation history:

  • Import the Angular Router module.
  • Subscribe to the events property to track each navigation event.
  • Create a public method to get access to the navigation history.

The next listing shows this in action.

Listing 12.20. Adding a history service
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';    1
import { filter } from 'rxjs/operators';                    2

@Injectable({
  providedIn: 'root'
})
export class HistoryService {
  private urls: string[] = [];

  constructor(private router: Router) {
    this.router.events                                      3
      .pipe(filter(routerEvent => routerEvent instanceof NavigationEnd))
      .subscribe((routerEvent: NavigationEnd) => {
        const url = routerEvent.urlAfterRedirects;
        this.urls = [...this.urls, url];
      });
  }
  ...
}

  • 1 Imports the Router and NavigationEnd classes
  • 2 Brings in the filter from rxjs
  • 3 The events property subscription

The functionality in the constructor function as given in listing 12.20 probably needs a closer look. The router events property returns an Observable that emits several event types, but you’re interested only in the NavigationEnd event, which you imported from the @angular/router.

To get these event types from the Observable (events stream), you need to filter them out, which is where the RxJS filter function comes into play. This function is piped to your events stream via the Observable pipe method. As we’re not covering RxJS in this book, we recommend RxJS in Action (https://www.manning.com/books/rxjs-in-action) for further detail.

The events of this pipe after you subscribe to them are of type NavigationEnd, which is exactly what you need. NavigationEnd events have a urlAfterRedirects property, which is a string that you can push to your array of urls that you hold in your HistoryService.

Last, you need to add a method that returns the previous URL from the collected URL history. Add the following method to the HistoryService.

Listing 12.21. getPreviousUrl function
public getPreviousUrl(): string {
  const length = this.urls.length;
  return length > 1 ? this.urls[length – 2] : '/';      1
}

  • 1 Returns the default location if there’s no other entry

Now that you have a history service that keeps track of where the user was before login or registration, implement it as part of your login and register components.

You’ll add this to the register component as shown in the next listing and change the login component later as an exercise to be completed, as the operation is identical. The solution is available on GitHub.

Listing 12.22. Changes required in the register component
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '../authentication.service';
import { HistoryService } from '../history.service';                      1

...
  constructor(
    private router: Router,
    private authenticationService: AuthenticationService,
    private historyService: HistoryService                                2
  ) { }

  ...

  private doRegister(): void {
    this.authenticationService.login(this.credentials)
      .then( () => {
        this.router.navigateByUrl(this.historyService.getPreviousUrl());  3
      })
      .catch( (message) => {
        this.formError = message
      });
  }
...

  • 1 Imports the history service
  • 2 Injects the history service into constructor
  • 3 Uses the provided getPreviousUrl function to redirect, using the router

After completing this change, and maybe through a little testing, you may have noticed that the page that the register component returns you to is the Login page—not what you’re looking for. After registering, as a user you’d like to be returned to the page before Login, because that’s where you entered the login/registration loop. From a user perspective, it’s not a great experience.

To avoid this experience, add a new method to the history service that returns the last URL encountered before either login or register. This way, it doesn’t matter whether the user travels between these two pages several times before performing the desired action.

You’ll achieve this by using a filter across the list of URLs already navigated, removing all the URLs that match in the exclusions list. Then, pick the last one, safe in the knowledge that you’ve removed all the register and login items.

Listing 12.23. getLastNonLoginUrl()
public getLastNonLoginUrl(): string {
  const exclude: string[] = ['/register', '/login'];                   1
  const filtered = this.urls.filter(url => !exclude.includes(url));    2
  const length = filtered.length;
  return length > 1 ? filtered[length – 1] : '/';                      3
}

  • 1 List of strings that you need to exclude
  • 2 Filters the collected list of URLs, and returns only those not in exclude
  • 3 Returns the last element of the filtered array or a default value

Add this code the history service, and change the function doLogin() in login.component.ts and doRegister() in register.component.ts to use it instead, as shown in the following listing (from register.component.ts).

Listing 12.24. Updating the doRegister function
private doRegister(): void {
  this.authenticationService.register(this.credentials)
    .then( () => {
      this.router.navigateByUrl(
        this.historyService.getLastNonLoginUrl()     1
      );
    })
    .catch( (message) => {
      this.formError = message
    });
}

  • 1 Changes getPreviousUrl() to getLastNonLoginUrl()

Now you can reap the benefits of being logged in. You’ll inject the authentication service into location-details.component.ts so you can check to see whether a user is logged in and present functionality accordingly.

You’re going to do a couple of things:

  • Inject the authentication service into the component to check the user’s login state.
  • Modify the component to take advantage of the logged-in state.

First, do the necessary importing of the AuthenticationService, and then inject into the component constructor.

Listing 12.25. location-details.component.ts changes
import { Component, OnInit, Input } from '@angular/core';
import { Location, Review } from '../location';
import { Loc8rDataService } from '../loc8r-data.service';
import { AuthenticationService } from '../authentication.service';    1

...
  constructor(
    private loc8rDataService: Loc8rDataService,
    private authenticationService: AuthenticationService              2
  ) { }

  ngOnInit() {}

...
}

  • 1 Imports the AuthenticationService
  • 2 Injects the AuthenticationService into the component

Next, add some methods that make use of the functionality provided by the AuthenticationService. Add the two methods in listing 12.26 to the location-details component.

Listing 12.26. Methods to add to location-details.component.ts
public isLoggedIn(): boolean {                                    1
  return this.authenticationService.isLoggedIn();
}

public getUsername(): string {                                    2
  const { name } = this.authenticationService.getCurrentUser();
  return name ? name : 'Guest';                                   3
}

  • 1 Wrapper function for isLoggedIn from AuthenticationService
  • 2 Wrapper function for getCurrentUser from AuthenticationService
  • 3 If name isn’t available, returns Guest

To complete this part of the exercise, you need to update the template by

  • Ensuring that the user is authenticated to leave a review
  • Removing the need to enter the author name when writing a review
  • Providing the username as the author from the authentication service when submitting a review and preventing validation from failing

First, change the template so that, in the logged-out state, you present a button inviting the user to log in to post a review. When the user is logged in, the page presents a button to allow them to add a review.

Change the location-details template (location-details.component.html) as shown next.

Listing 12.27. Changes to location-details.component.html
<div class="row">
  <div class="col-12">
    <div class="card card-primary review-card">
      <div class="card-block" [ngSwitch]="isLoggedIn()">               1
        <button (click)="formVisible=true" class="btn btn-primary
        float-right"*ngSwitchCase="true">Add review</button>         2
        <a routerLink="/login" class="btn btn-primary float-right"
        *ngSwitchDefault>Log in to add review</a>                    3
        <h2 class="card-title">Customer reviews</h2>
        <div *ngIf="formVisible">

  • 1 ngSwitch around the logged-in status
  • 2 Shows whether user is logged in
  • 3 Default state

The ngSwitch directive checks whether the user is logged in and displays the appropriate call to action. Both states are shown in figure 12.4.

Figure 12.4. The two states of the Add Review button, depending on whether the user is logged in

Now that a user needs to be logged in to post a review, you no longer need users to enter their names in the review form, as this data can now be retrieved from the JWT. As a result, you need to delete code from the location-details.component.html template. See the following listing for the elements to remove.

Listing 12.28. Code to remove from location-details.component.html
<div class="form-group row">
  <label for="name" class="col-sm-2 col-form-label">Name</label>
  <div class="col-sm-10">
    <input [(ngModel)]="newReview.author" id="name" name="name"
    required="required" class="form-control">
  </div>
</div>

Without the form field, you need to pull the author name from the getUsername() function that you conveniently created earlier. Listing 12.29 highlights in bold the pieces to be changed in onReviewSubmit() in location-details.component.ts. Figure 12.5 shows the final review form.

Figure 12.5. The final review form without a name field

Listing 12.29. Removing name validation from location-details.component.ts
public onReviewSubmit(): void {
    this.formError = '';
    this.newReview.author = this.getUsername();          1
    if (this.formIsValid()) {
      this.loc8rDataService.addReviewByLocationId(this.location._id,
      this.newReview)
        .then((review: Review) => {
           console.log('Review saved', review);
           let reviews = this.location.reviews.slice(0);
           reviews.unshift(review);
           this.location.reviews = reviews;
           this.resetAndHideReviewForm();
        });
    } else {
      this.formError = 'All fields required, please try again';
    }
  }
  ...
}

  • 1 Gets the username from the component

If you try this now, you still encounter a problem. If you check the web browser’s development console, you’ll see that the API returns a 401 Unauthorized response, because you haven’t updated the review submission API call with the JWT to allow the API to accept the request.

To make this work, you need to get access to the JWT stored in localStorage and pass it forward as a Bearer token in the Authorization request header.

Listing 12.30. Adding AuthenticationService to loc8r-data.service.ts
import { Injectable, Inject } from '@angular/core';
...
import { AuthResponse } from './authresponse';
import { BROWSER_STORAGE } from './storage';                 1

@Injectable({
  providedIn: 'root'
})
export class Loc8rDataService {

  constructor(
    private http: HttpClient,
    @Inject(BROWSER_STORAGE) private storage: Storage        2
  ) { }

  • 1 Imports the AuthenticationService
  • 2 Injects the imported service into the component

Finally, you need to update the addReviewByLocationId() function to include the Authorization header in submissions to the API. The following listing shows the changes.

Listing 12.31. Adding Authorization headers to API call
public addReviewByLocationId(locationId: string, formData: Review):
Promise<Review> {
  const url: string = `${this.apiBaseUrl}/locations/${locationId}/
  reviews`;
  const httpOptions = {                                                  1
    headers: new HttpHeaders({
      'Authorization': `Bearer ${this.storage.getItem('loc8r-token')}`   2
    })
  };
  return this.http
    .post(url, formData, httpOptions)                                    3
    .toPromise()
    .then(response => response as Review)
    .catch(this.handleError);
}

  • 1 Creates an httpOptions object for HttpHeaders
  • 2 String Template used here
  • 3 Adds httpOptions to the API call

With that update, you’ve completed the authentication section. Users must be logged in to add a review, and through the authentication system, the review will be given the correct username.

This brings you to the end of the book. By now, you should have a good idea of the power and capabilities of the MEAN stack and be empowered to start building some cool stuff!

You have a platform to build REST APIs, server-side web applications, and browser-based single-page applications. You can create database-driven sites, APIs, and applications, and then publish them to a live URL.

When starting your next project, remember to take a little time to think about the best architecture and user experience. Spend a little time planning to make your development time more productive and enjoyable. And never be afraid to refactor and improve your code and application as you go.

You’ve only scratched the surface of what these amazing technologies can offer. So please dive in, build things, try stuff, keep learning, and (most important) have fun!

Summary

In this chapter, you learned

  • How to use local storage to manage a user session in the browser
  • How to use JWT data inside Angular
  • How to pass a JWT from Angular to an API via HTTP headers
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.218.215