This chapter covers
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.
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.
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.
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.
import { InjectionToken } from '@angular/core'; 1 export const BROWSER_STORAGE = new InjectionToken<Storage> 2 ('Browser Storage',{ 2 providedIn: 'root', factory: () => localStorage 3 });
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.
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 } }
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.
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.
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.
export class User { email: string; 1 name: string; 1 }
Listing 12.4 provides the definition for your AuthResponse object, which at this time holds the token string.
export class AuthResponse { token: string; 1 }
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.
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)); } }
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.
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); } ... }
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.
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.
public logout(): void { this.storage.removeItem('loc8r-token'); 1 }
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.
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.
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
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().
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; } } }
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.
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.
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 } }
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.
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.
The Login page should look like figure 12.2. You’ll begin with the Register page.
To develop a working registration page, you have a few things to do:
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.
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 { }
With that done, look at the details of the component template and methods that link this template to the services that you built earlier.
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.
<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>
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.
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.
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() { }
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.
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); }
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.
You’ve added the ability for a new user to register. Next, you’ll enable a returning user to log in.
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.
<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>
Finally, you make changes in the login component, which is similar to the register component. The changes you need to make are these:
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.
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 }); } }
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.
When you have a way to authenticate users, the next step is making use of that information. In Loc8r, you’ll do two things:
You’ll tackle the navigation first.
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.
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.
<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>
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:
The following listing shows how this is done.
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'; } }
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.
<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>
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.
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 ) { } ...
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:
The next listing shows this in action.
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]; }); } ... }
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.
public getPreviousUrl(): string { const length = this.urls.length; return length > 1 ? this.urls[length – 2] : '/'; 1 }
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.
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 }); } ...
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.
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 }
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).
private doRegister(): void { this.authenticationService.register(this.credentials) .then( () => { this.router.navigateByUrl( this.historyService.getLastNonLoginUrl() 1 ); }) .catch( (message) => { this.formError = message }); }
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:
First, do the necessary importing of the AuthenticationService, and then inject into the component constructor.
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() {} ... }
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.
public isLoggedIn(): boolean { 1 return this.authenticationService.isLoggedIn(); } public getUsername(): string { 2 const { name } = this.authenticationService.getCurrentUser(); return name ? name : 'Guest'; 3 }
To complete this part of the exercise, you need to update the template by
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.
<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">
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.
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.
<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.
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'; } } ... }
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.
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 ) { }
Finally, you need to update the addReviewByLocationId() function to include the Authorization header in submissions to the API. The following listing shows the changes.
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); }
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!
In this chapter, you learned
3.137.218.215