Creating AuthGuard for RecorderModule

One of our app's requirements is that recording features should be locked away and inaccessible until a user is authenticated. This provides us with the ability to have a user base and potentially introduce paid features down the road if we so desire.

Angular provides the ability to insert guards on our routes, which would only activate under certain conditions. This is exactly what we need to implement this feature requirement, since we have isolated the '/record' route to lazily load RecorderModule, which will contain all the recording features. We want to only allow access to that '/record' route if the user is authenticated.

Let's create app/guards/auth-guard.service.ts in a new folder for scalability, since we could grow and create other guards as necessary here:

import { Injectable } from '@angular/core';
import { Route, CanActivate, CanLoad } from '@angular/router';
import { AuthService } from '../modules/core/services/auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

constructor(private authService: AuthService) { }

canActivate(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (this._isAuth()) {
resolve(true);
} else {
// login sequence to continue prompting
let promptSequence = (usernameAttempt?: string) => {
this.authService.promptLogin(
'Authenticate to record.',
usernameAttempt
).then(() => {
resolve(true);
}, (usernameAttempt) => {
if (usernameAttempt === false) {
// user canceled prompt
resolve(false);
} else {
// initiate sequence again
promptSequence(usernameAttempt);
}
});
};
// start login prompt sequence
// require auth before activating
promptSequence();
}
});
}

canLoad(route: Route): Promise<boolean> {
// reuse same logic to activate
return this.canActivate();
}

private _isAuth(): boolean {
// just get the latest value from our BehaviorSubject
return this.authService.authenticated$.getValue();
}
}

We are able to take advantage of BehaviorSubject of AuthService to grab the latest value using this.authService.authenticated$.getValue() to determine the auth state. We use this to immediately activate the route via the canActivate hook (or load the module via the canLoad hook) if the user is authenticated. Otherwise, we display the login prompt via the service's method, but this time we wrap it in a reprompt sequence, which will continue to prompt on failed attempts until a successful authentication, or ignore it if the user cancels the prompt.

For the book, we aren't wiring up to any backend service to do any real authentication with a service provider. We will leave that part up to you in your own app. We will just be persisting the e-mail and password you enter into the login prompt as a valid user after doing very simple validation on the input.

Notice that AuthGuard is an Injectable service like other services, so we will want to make sure it is added to the providers metadata of AppRoutingModule. We can now guard our route with the following highlighted modifications to app/app.routing.ts to use it:

...
import { AuthGuard } from './guards/auth-guard.service';

const routes: Routes = [
...
{
path: 'record',
loadChildren:
'./modules/recorder/recorder.module#RecorderModule',
canLoad: [AuthGuard]
}
];

@NgModule({
...
providers: [
AuthGuard,
...
],
...
})
export class AppRoutingModule { }

To try this out, we need to add child routes to our RecorderModule, since we have not done that yet. Open app/modules/recorder/recorder.module.ts and add the following highlighted sections:

// nativescript
import { NativeScriptModule } from 'nativescript-angular/nativescript.module';
import { NativeScriptRouterModule } from 'nativescript-angular/router';

// angular
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { Routes } from '@angular/router';

// app
import { SharedModule } from '../shared/shared.module';
import { PROVIDERS } from './services';
import { RecordComponent } from './components/record.component';

const COMPONENTS: any[] = [
RecordComponent
]

const routes: Routes = [
{
path: '',
component: RecordComponent
}
];

@NgModule({
imports: [
SharedModule,
NativeScriptRouterModule.forChild(routes)
],
declarations: [ ...COMPONENTS ],
providers: [ ...PROVIDERS ],
schemas: [ NO_ERRORS_SCHEMA ]
})
export class RecorderModule { }

We now have a proper child route configuration that will display the single RecordComponent when the user navigates to the '/record' path. We won't show the details of RecordComponent, as you can refer to the Chapter 5Routing and Lazy Loading branch on the repo for the book. However, it is just a stubbed out component at this point inside app/modules/recorder/components/record.component.html, which just shows a simple label, so we can try this out.

Lastly, we need a button that will route to our '/record' path. If we look back at our original sketch, we wanted a Record button to display in the top right corner of ActionBar, so let's implement that now.

Open app/modules/mixer/components/mixer.component.html and add the following:

<ActionBar title="TNSStudio" class="action-bar">
<ActionItem nsRouterLink="/record" ios.position="right">
<Button text="Record" class="action-item"></Button>
</ActionItem>
</ActionBar>
<GridLayout rows="*, 100" columns="*" class="page">
<track-list row="0" col="0"></track-list>
<player-controls row="1" col="0"></player-controls>
</GridLayout>

Now, if we were to run this in the iOS Simulator, we would notice that our Record button in ActionBar does not do anything! This is because MixerModule only imports the following:

@NgModule({
imports: [
PlayerModule,
NativeScriptRouterModule.forChild(routes)
],
...
})
export class MixerModule { }

The NativeScriptRouterModule.forChild(routes) method just configures the routes but does not make various routing directives, such as nsRouterLink, available to our components.

Since you learned earlier that SharedModule should be used to declare various directives, components, and pipes you want to share throughout your modules (lazy loaded or not), this is a perfect opportunity to take advantage of that.

Open app/modules/shared/shared.module.ts and make the following highlighted modifications:

...
import { NativeScriptRouterModule } from 'nativescript-angular/router';
...

@NgModule({
imports: [
NativeScriptModule,
NativeScriptRouterModule
],
declarations: [
...PIPES
],
exports: [
NativeScriptModule,
NativeScriptRouterModule,
...PIPES
],
schemas: [NO_ERRORS_SCHEMA]
})
export class SharedModule { }

Now, back in MixerModule, we can adjust the imports to use SharedModule:

...
import { SharedModule } from '../shared/shared.module';

@NgModule({
imports: [
PlayerModule,
SharedModule,
NativeScriptRouterModule.forChild(routes)
],
...
})
export class MixerModule { }

This ensures all the directives exposed via NativeScriptRouterModule are now included and available for use in  MixerModule by utilizing our app-wide SharedModule.

Running our app again, we now see the login prompt when we tap the Record button in ActionBar. If we enter a properly formatted e-mail address and any password, it will persist the details, log us in, and display RecordComponent as follows on iOS:

You might notice something rather interesting. ActionBar changed from the background color we assigned via CSS and the button color now displays the default blue color. This is because RecordComponent does not define ActionBar; therefore, it is reverting to a default styled ActionBar with a default back button, which takes on the title of the page it just navigated from. The '/record' route is also using the ability of page-router-outlet to push components onto the mobile navigation stack. RecordComponent is animated into view while allowing the user to choose the top left button to navigate back (to pop the navigation history back one).

To fix ActionBar, let's just add ActionBar to the RecordComponent view with a custom NavigationButton (a NativeScript view component simulating a mobile device's default back navigation button). We can make the adjustments to app/modules/record/components/record.component.html:

<ActionBar title="Record" class="action-bar">
<NavigationButton text="Back"
android.systemIcon="ic_menu_back">
</NavigationButton>
</ActionBar>
<StackLayout class="p-20">
<Label text="TODO: Record" class="h1 text-center"></Label>
</StackLayout>

Now, this looks a lot better:

If we run this on Android and log in using any e-mail/password combo to persist a user, it will display the same RecordComponent view; however, you will notice another interesting detail. We have set up Android to display a standard back arrow system icon as NavigationButton, but when tapping that arrow, it does not do anything. Android's default behavior relies on the device's physical hardware back button next to the home button. However, we can provide a consistent experience by just adding a tap event to NavigationButton, so both iOS and Android react the same to tapping the back button. Make the following modification to the template:

<ActionBar title="Record" icon="" class="action-bar">
<NavigationButton (tap)="back()" text="Back"
android.systemIcon="ic_menu_back">
</NavigationButton>
</ActionBar>
<StackLayout class="p-20">
<Label text="TODO: Record" class="h1 text-center"></Label>
</StackLayout>

Then, we can implement the back() method in app/modules/recorder/components/record.component.ts using NativeScript for Angular's RouterExtensions service:

// angular
import { Component } from '@angular/core';
import { RouterExtensions } from 'nativescript-angular/router';

@Component({
moduleId: module.id,
selector: 'record',
templateUrl: 'record.component.html'
})
export class RecordComponent {

constructor(private router: RouterExtensions) { }

public back() {
this.router.back();
}

}

Now, Android's back button can be tapped to navigate back in addition to the hardware back button. iOS simply ignores the tap event handler, since it uses the default native behavior for NavigationButton. Pretty nice. Here is how RecordComponent looks on Android:

We will implement a nice recording view in upcoming chapters.

We are surely cruising down Route 66 by now!

We have implemented lazily loaded routes, provided AuthGuard to protect unauthorized use of our app's recording features, and learned a ton in the process. However, we've just realized we are missing a very important feature late in the game. We need a way to work on several different mixes over time. By default, our app may launch the last opened mix, but we would like to create new mixes (let's consider them compositions) and record entirely new mixes of individual tracks as separate compositions. We need a new route to display these compositions that we can name appropriately, so we can jump back and forth and work on different material.

..................Content has been hidden....................

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