Angular 2+ and Ruby on Rails user authentication Part 4

Written by avatsaev | Published 2017/02/18
Tech Story Tags: javascript | angularjs | angular | router

TLDRvia the TL;DR App

This article is the follow up of Angular 2+ and Ruby on Rails user authentication Part 1: Backend

User Profile, Auth Service and Auth Router Guard

Intro

The premise is to show user profile info (name, nickname, email, etc) on /profile route and make a guard for it, so when someone tries to reach it without being authenticated, gets redirected to home route, we’ll also write an Auth Service to wrap all of our auth related stuff in one place.

Auth Service

First and foremost, we need to create an Authetication Service which will wrap all of Auth related states and actions in one place, in particular login/logout actions and user authentication status (user is logged in/out), and all off it asynchronously as a stream (brace yourselves RxJS is incoming).

ng g s services/auth

s = service

This is our authentication service (./src/app/services/auth.service.ts):

Looks intimidating at first glance, but trust me, there is nothing complicated, so let’s break it down:

Line 9: userSignedIn$ is a RxJs Subject (of type boolean), which means it is an Observer and an Observable at the same time, which means we can control its value in our service, and observe its changes outside of it.

Our constructor will initialize the login state by using TokenService’s validateToken() method, which will validate current token (if exists) against the Rails backend, this operation is asynchronous and returns in its body an object that looks like this:

So we can use success key to determine wether the user is logged in with a valid token or not (line 14).

Dollar sign ($) at the end of the name is a convention to specify that this is not a simple variable, but an observable stream of data which changes in time.

  • Our logOutUser() method takes no params and returns an Observable of Response, we’ll call signOut() on Angular2TokenService, but before returning its response, we’ll map though it to change userSignedIn$ value to false (line 19) in order to notify our observers that user successfully logged out, and then return the response to whomever is observing the result of logOutUser() method (line 20).
  • The logInUser method takes an object with email and password keys, and uses it to sign in from Angular2TokenService signIn() method. And again, before returning the response we will modify userSignedIn$ value to true, which will notify the observers that user logged in successfully**.**
  • The registerUser method is similar to loginUser, but we have an additional password confirmation attribute in the input object.

Let’s not forget to inject it into our main AppModule’s providers (./src/app/app.module.ts):

...

import {AuthService} from "./services/auth.service";

@NgModule({declarations: [AppComponent,HomeComponent,ToolbarComponent,AuthDialogComponent,LoginFormComponent,RegisterFormComponent,ProfileComponent],imports: [BrowserModule,FormsModule,HttpModule,AppRoutingModule,MaterializeModule,],providers: [ Angular2TokenService, AuthService],bootstrap: [AppComponent]})export class AppModule { }

Refactoring Login and Toolbar components to use AuthService

Now we need to make some small changes to our Login and Toolbar components in order to use AuthService instead of Angular2TokenService directly.

Refactoring the Toolbar

Let’s start with ToolBarComponent class (./src/app/toolbar/toolbar.component.ts):

As you can see, we got rid of Angular2TokenService and injected our custom AuthService instead (line 16), we also created a logOut() method which will call logOutUser action on our AuthService, and using Angular Router, redirect the user to home when logout action completes (line 21).

Now let’s change Toolbar’s template (./src/app/toolbar/toolbar.component.html):

Now we are using our AuthService’s userSignedIn$ Subject to change the state of the toolbar when user logs in or out, considering that userSignedIn$ is not a simple value, but an asynchrnous stream of values chaning in time, we need to use Angular’s async pipe to listen to its changes (lines 11, 12, 14, 15). We’ll also link the click event on logout button to toobar component’s logOut() method we created earlier (line 15).

Refactoring the Login From

Changes on login form will be extremely minimal, we just need to change the LoginFormComponent class (./src/app/login-form/login-form.component.ts)

The only thing we needed to do is getting rid of Angular2TokenService and replacing it with our AuthService and its logInUser method.

Don’t forget to apply the same changes to RegisterFormComponent

And that’s it, refactoring is over.

User profile page

Profile component

First step is obvious, let’s generate our ProfileComponent that will display current user’s info:

ng g c profile

The component should be automatically imported in AppModule’s declarations.

Profile route

In our router module (./src/app/app-routing.module.ts), let’s declare the profile route:

Import the profile component:

import {ProfileComponent} from "./profile/profile.component";

Declare the route:

...

const routes: Routes = [{path: '',component: HomeComponent,pathMatch: 'full'},{path: 'home',component: HomeComponent},{path: 'profile',component: ProfileComponent}];...

UI

As for the design of profile page, it’ll be pretty simple, we’ll display user’s email, name, and user name in a Materialize Card and use its footer to add a logout button.

Let’s inject Angular2TokenService into our ProfileComponent, and add a logout method (./app/profile/profile.component.ts):

We’ll be making use of Angular2TokenService to display user’s personal information, AuthService to implment the logout action, and the Router to redirect after logout completes, so we need to inject these in our ProfileComponent (lines 14–16)

Nn line 18, we have a logOut action which redirects the user to home when it completes.

Reimplementing the logOut action for each component seems repetitive, but this is actually useful, because we can implement different post-logout behaviours for each component, for example redirecting to some other route instead of home.

Profile Component’s template, (./src/app/profile/profile.component.html)

Angular2TokenService’s currentUserData attribute provides information about our user if he is logged in (email, name, nickname, etc.), its value is undefined when user is not logged in.

One line 15, we are usign *ngIf structural directive to test if there is any data to display, if not we will not show the user info card.

On line 31 we also have a logout button, linked to logOut method of our ProfileComponent class.

Be sure to add a cleafix class to ./src/app/profile/profile.component.sass because Materialize doesn’t implement it.

.clearfixoverflow: auto

And this is what we get when we navigate to /profile route while being logged in:

User info card

Auth Route Guard

Now the problem is, we still can navigate to /profile route even if we are not logged in:

Angular’s Route Guards can help us to fix this issue.

Route Guards are pretty easy to understand, it’s a simple class which implements CanActivate interface, this interface demands us to implement a canActivate() method**,** which returns a single boolean value (or an Observable of type boolean, although I couldn’t manage to make them work properly), and depending on that boolean value the Router will allow or forbid the activation of a route, the logic is up to us to implement, so let’s do that.

Create an AuthGuard class in (./src/app/guards/auth.guard.ts):

As you can see, our AuthGuard class implements CanActivate interface and its canActivate() method, the logic is simple, Angular2TokenService’s userSignedIn() method returns a signle boolean value, we’ll use it to determine if user is signed in, if so, we allow the activation by returning true, of not, we’ll redirect to home and forbid the activation by returning false.

Don’t forget to inject it in our AppModule’s providers:

...

import {AuthGuard} from "./guards/auth.guard";

@NgModule({declarations: [AppComponent,HomeComponent,ToolbarComponent,AuthDialogComponent,LoginFormComponent,RegisterFormComponent,ProfileComponent],imports: [BrowserModule,FormsModule,HttpModule,AppRoutingModule,MaterializeModule,],providers: [ Angular2TokenService, AuthService, AuthGuard],bootstrap: [AppComponent]})export class AppModule { }

Now let’s protect our /profile route, open ./src/app/app-routing.module.ts and set profile route’s guard:

import { NgModule } from '@angular/core';import { Routes, RouterModule } from '@angular/router';import {HomeComponent} from "./home/home.component";import {ProfileComponent} from "./profile/profile.component";import {AuthGuard} from "./guards/auth.guard";

const routes: Routes = [{path: '',component: HomeComponent,pathMatch: 'full'},{path: 'home',component: HomeComponent},{path: 'profile',component: ProfileComponent,canActivate: [AuthGuard]}];

@NgModule({imports: [RouterModule.forRoot(routes)],exports: [RouterModule],providers: []})

export class AppRoutingModule { }

canActivate key takes an array of guards, in case you have several guards on a single route, all of them must return true in order for the route to be activated.

Now, when we go to /profile without being logged in, we’ll get redirected to home, and any Router Link with /profile as a route, will produce no effect when clicked on.

And there you go, a profile page, protected by a Route Guard.

You can get the final code for this part on GitHub: https://github.com/avatsaev/angular-token-auth-seed/tree/profile

Let me know if you have any remarks about the tutorial, I’ll be happy to help and correct it if something isn’t clear enough :)

Thank you for reading this series, follow me on Twitter (https://twitter.com/avatsaev) for more interesting stuff on Angular 2+ and Rails.


Published by HackerNoon on 2017/02/18