The third part of this tutorial (Part 3a and Part 3b) covered fetching, filtering, and displaying API data.

The fourth installment in the series covers access management with Angular, displaying admin data, and setting up detail pages with tabs.

Angular: Access Management

In the Admin Authorization section of Part 2, we enabled administrative rights for a specific user login. In the API Events section, we authorized an /api/event/:id API endpoint that required authentication and an /api/events/admin endpoint that required authentication and admin access. We'll now take measures to protect authorized routes on the front-end and manage access to components utilizing protected API routes.

Route Guards

We'll implement two route guards in our application. Route guards determine whether a user should be allowed to access a specific route or not. If the guard evaluates to true , navigation is allowed to complete. If the guard evaluates to false , the route is not activated and the user's attempted navigation does not take place. Multiple route guards can be used on a single route in a chaining fashion, meaning a user can be required to pass multiple checks before they're granted access to a route—similar to how we added multiple middleware functions to our Node API endpoints.

We have two levels of authorization for various routes that require guards:

Is the user authenticated? Does the authenticated user have admin privileges?

Redirecting To and From Login

There will be an important extra feature in our authentication route guard that will require some updates to our authentication service: redirection to and from login. When an unauthenticated user arrives at our app, we want to limit disruptions to their experience as much as possible. This means that we'll surface links to authenticated routes in our public event listing, and then prompt the user to log in when such links are clicked. After they authenticate, they'll be redirected to the protected route.

This reduces friction in the application because we're taking care not to redirect users back to a homepage. This would force them to try to find their own way back to the route they were originally trying to access. It also helps keep the user on track if they manually typed in a URL or clicked a link someone else sent them.

Note: Route guards are for the UI only. They don't confer any security when it comes to accessing an API. However, we are enforcing authentication and authorization in our API (as you should do in all your apps), so we can take advantage of guards to authenticate and redirect users as well as stop unauthorized navigation.

Create an Authenticated Route Guard

Let's create a new route guard. The first thing we need to know is whether or not the user is logged in. We'll call this route guard AuthGuard .

Create a new guard file with the following command:

$ ng g guard auth/auth

Add the following code to your new auth.guard.ts file:

// src/app/auth/auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private auth: AuthService) { } canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (this.auth.tokenValid) { return true; } else { // Send guarded route to redirect to after logging in this.auth.login(state.url); return false; } } }

The boilerplate imports the CanActivate interface to implement the logic declaring whether or not the user should be allowed access to the route. We also need both ActivatedRouteSnapshot and RouterStateSnapshot to gain access to the route information for redirection. RxJS provides Observable for type annotation, and, finally, we need to add the AuthService to access its methods.

The logic in the canActivate() function is pretty straightforward. Route guards operate on returning true or false based on a condition that has to be fulfilled to permit navigation. Our condition is auth.tokenValid from our AuthService . If the user is authenticated with an unexpired token, we can return true and navigation continues.

However, if the user is not authenticated, we'll send the guarded route to the auth.login() method. This will allow us to redirect after returning from the hosted Auth0 login, which is outside the application. We'll prompt the user to log in to continue with navigation and return false to ensure navigation cannot complete.

Update Authentication Service to Manage Redirects

The route guard contains a URL to redirect to on successful authentication, so our auth.service.ts needs to utilize it. Let's make the necessary changes to this file:

// src/app/auth/auth.service.ts ... export class AuthService { ... login(redirect?: string) { // Set redirect after login const _redirect = redirect ? redirect : this.router.url; localStorage.setItem('authRedirect', _redirect); // Auth0 authorize request ... } handleAuth() { // When Auth0 hash parsed, get profile this._auth0.parseHash((err, authResult) => { ... } else if (err) { this._clearRedirect(); this.router.navigate(['/']); console.error(`Error authenticating: ${err.error}`); } }); } private _getProfile(authResult) { // Use access token to retrieve user's profile and set session this._auth0.client.userInfo(authResult.accessToken, (err, profile) => { if (profile) { ... this.router.navigate([localStorage.getItem('authRedirect') || '/']); this._clearRedirect(); } else if (err) { ... }); } ... private _clearRedirect() { // Remove redirect from localStorage localStorage.removeItem('authRedirect'); } logout() { // Ensure all auth items removed from localStorage ... this._clearRedirect(); // Reset local properties, update loggedIn$ stream ... // Return to homepage this.router.navigate(['/']); } ...

In the login() method, we'll now check for a redirect parameter. If there isn't one, this means the user initialized the login() method from the header link and not from the route guard. In this case, we'll set _redirect to the current URL so the user returns here after authenticating. We'll then set the _redirect in local storage.

If the hash is successfully parsed with the appropriate tokens in the handleAuth() function, we'll redirect the user in the _getProfile() method. If an error occurs, we'll clear the redirect (method declared further down in the code), navigate to the homepage, and display the error in the console.

As mentioned above, the _getProfile() method will now navigate to the stored redirect URL (or as a failsafe, to the homepage). It will then clear the redirect from local storage to ensure no lingering data is left behind.

The _clearRedirect() method is simply a shortcut that removes the authRedirect item from local storage since we do this several times throughout the service.

Finally, on logout() we'll clear the redirect. Since Home is the only component that does not require authentication to view, we'll navigate to the homepage.

Create an Admin Route Guard

Now that we have our authentication guard and service updated, the admin guard will be simple by comparison. Create a new guard:

$ ng g guard auth/admin

Add the following code to the generated admin.guard.ts file:

// src/app/auth/admin.guard.ts import { Injectable } from '@angular/core'; import { Router, CanActivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { AuthService } from './auth.service'; @Injectable() export class AdminGuard implements CanActivate { constructor( private auth: AuthService, private router: Router) { } canActivate(): Observable<boolean> | Promise<boolean> | boolean { if (this.auth.isAdmin) { return true; } this.router.navigate(['/']); return false; } }

The admin guard will run after the authentication guard, so we'll get all the benefits of the authentication guard too (such as auth checking and redirection). All the admin guard needs to do is check if the authenticated user is an admin and if not, navigate to the homepage.

Import Guards in Routing Module

Finally, in order to use our route guards, we need to import them in our app-routing.module.ts :

// src/app/app-routing.module.ts ... import { AuthGuard } from './auth/auth.guard'; import { AdminGuard } from './auth/admin.guard'; const routes: Routes = [ ... ]; @NgModule({ ..., providers: [ AuthGuard, AdminGuard ], ... }) ...

We'll import our two route guards and then add them to the providers array.

We're now ready to guard routes. The next step is to create protected routes!

Angular: Admin Component Event List

We want an Admin component to display a list of all events, including past and private events (unlike the Home component, which only shows upcoming, public events). The Admin component also needs to be protected by both the authentication guard and the admin guard.

Let's create an Admin component with the CLI now:

$ ng g component pages/admin

Admin Component Route

Now we'll add the Admin component to our routes in the app-routing.module.ts file:

// src/app/app-routing.module.ts ... import { AdminComponent } from './pages/admin/admin.component'; const routes: Routes = [ ..., { path: 'admin', canActivate: [ AuthGuard, AdminGuard ], children: [ { path: '', component: AdminComponent } ] }, ... ]; @NgModule({ ... }) ...

Import the new AdminComponent . You'll notice we've set up this route a bit differently. The /admin route will eventually have other child routes, including pages to create and update events. We want all routes under the admin URL segment to be protected, so we'll add a canActivate array containing our two route guards, AuthGuard and AdminGuard . For now, we just have a root child route which uses the Admin component. We'll add the other children later.

Add Admin Link in Navigation

Let's add a link to the Admin page in our off-canvas navigation. To do this, open the header.component.html template:

<!-- src/app/header/header.component.html --> <header id="header" class="header"> ... <nav id="nav" class="nav" role="navigation"> <ul class="nav-list"> ... <li> <a *ngIf="auth.loggedIn && auth.isAdmin" routerLink="/admin" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Admin</a> </li> </ul> </nav> </header>

We'll add an "Admin" link that only shows if the user is auth.loggedIn and auth.isAdmin . Because our admin route has children, we'll also add exact: true to the [routerLinkActiveOptions] directive to prevent the parent "Admin" link from being marked as active when any of its children are active.

This link should now appear in the navigation when an admin user is logged in.

Show All Events in Admin Component

Open the new admin.component.ts file:

// src/app/pages/admin/admin.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { AuthService } from './../../auth/auth.service'; import { ApiService } from './../../core/api.service'; import { UtilsService } from './../../core/utils.service'; import { FilterSortService } from './../../core/filter-sort.service'; import { Subscription } from 'rxjs/Subscription'; import { EventModel } from './../../core/models/event.model'; @Component({ selector: 'app-admin', templateUrl: './admin.component.html', styleUrls: ['./admin.component.scss'] }) export class AdminComponent implements OnInit, OnDestroy { pageTitle = 'Admin'; eventsSub: Subscription; eventList: EventModel[]; filteredEvents: EventModel[]; loading: boolean; error: boolean; query = ''; constructor( private title: Title, public auth: AuthService, private api: ApiService, public utils: UtilsService, public fs: FilterSortService) { } ngOnInit() { this.title.setTitle(this.pageTitle); this._getEventList(); } private _getEventList() { this.loading = true; // Get all (admin) events this.eventsSub = this.api .getAdminEvents$() .subscribe( res => { this.eventList = res; this.filteredEvents = res; this.loading = false; }, err => { console.error(err); this.loading = false; this.error = true; } ); } searchEvents() { this.filteredEvents = this.fs.search(this.eventList, this.query, '_id', 'mediumDate'); } resetQuery() { this.query = ''; this.filteredEvents = this.eventList; } ngOnDestroy() { this.eventsSub.unsubscribe(); } }

This is very similar to the Home component we set up in Angular: Home Component Event List. The only difference is that we'll import the Auth service. Other than that, we'll set the title, get the full admin events list, implement search functionality, and unsubscribe on the destruction of the component.

Admin Component Template

Before we create our template, let's add a couple of icons to our assets folder. Download this calendar icon SVG and eye icon SVG. Right-click the links and save both icons to your src/assets/images folder.

Now let's open our admin.component.html template:

<!-- src/app/pages/admin/admin.component.html --> <h1 class="text-center"> {{pageTitle}} </h1> <app-loading *ngIf="loading"></app-loading> <ng-template [ngIf]="utils.isLoaded(loading)"> <p class="lead">Welcome, {{auth.userProfile?.name}} ! You can create and administer events below.</p> <!-- Events --> <ng-template [ngIf]="eventList"> <ng-template [ngIf]="eventList.length"> <!-- Search events --> <section class="search input-group mb-3"> <label class="input-group-addon" for="search">Search</label> <input id="search" type="text" class="form-control" [(ngModel)]="query" (keyup)="searchEvents()" /> <span class="input-group-btn"> <button class="btn btn-danger" (click)="resetQuery()" [disabled]="!query">×</button> </span> </section> <!-- No search results --> <p *ngIf="fs.noSearchResults(filteredEvents, query)" class="alert alert-warning"> No events found for <em class="text-danger"> {{query}} </em>, sorry! </p> <!-- Events listing --> <section class="list-group"> <div *ngFor="let event of fs.orderByDate(filteredEvents, 'startDatetime')" class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <a [routerLink]="['/event', event._id]"> <h5 class="mb-1" [innerHTML]="event.title"></h5> </a> <div class="event-icons"> <img *ngIf="!event.viewPublic" class="event-icon" title="Private" src="/assets/images/eye.svg"> <img *ngIf="utils.eventPast(event.endDatetime)" class="event-icon" title="Event is over" src="/assets/images/calendar.svg"> </div> </div> <p class="mb-1"> <strong>Date:</strong> {{utils.eventDates(event.startDatetime, event.endDatetime)}} </p> </div> </section> </ng-template> <!-- No events available --> <p *ngIf="!eventList.length" class="alert alert-info"> No events have been created yet. </p> </ng-template> <!-- Error loading events --> <p *ngIf="error" class="alert alert-danger"> <strong>Oops!</strong> There was an error retrieving event data. </p> </ng-template>

Again, this is very similar to our Home component's implementation. However, we'll start with a paragraph greeting our admin user. We're showing icons in our event list indicating if an event is in the past (with the calendar icon) or if it has viewPublic: false . Also, we're only linking the title of the event to its detail page instead of the entire list item because we'll be adding "Edit" and "Delete" buttons to each event later.

Now open admin.component.scss to add a few styles for our event icons:

/* src/app/pages/admin/admin.component.scss */ /*-------------------- ADMIN COMPONENT --------------------*/ .event-icon { display: inline-block; height: 16px; margin: 0 4px; width: 16px; }

Because our Admin component and API route are protected, you'll have to log in to the app with the admin user you specified in the Admin Authorization section of Part 2 in order to view the page. Once you're logged in, the Admin component should look something like this:

If unauthenticated users attempt to access this page, they'll be prompted to log in. If they are admin upon logging in, they'll be granted access. If they don't have admin rights, they'll be redirected to the homepage by our admin route guard. If a user logs out from this page, they'll also be redirected to the homepage. Try it out!

Security Note: Even if a user was somehow able to circumvent the front-end protection, the Node API would not return the events data without the correct admin role concealed in the access token.

That's all we'll do with the Admin page for now. Later on, we'll add links to create and update events.