Angular 2's router has been through some serious changes lately, I think the current version is the 2nd rewrite of the original beta router. I've been using it in a medium sized project at work and I'm really liking it so far.
One thing I needed was to be able to lock people out of a route based on permissions. In previous versions of the router, components could implement the OnActivate interface and provide some hints to the router. But in v3 of the router, that is no longer an option. Instead they have a concept called Route/Auth Guards.
Now, you implement the CanActivate interface in it's own class and use that when configuring routes.
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate() {
if (this.authService.isLoggedIn()) {
return Observable.of(true);
}
return this.authService.logIn();
}
}
The canActivate method can return either a boolean
, or an Observable<boolean>
. In the above example, my AuthService.logIn()
method will return an Observable<boolean>
once the login process has completed, so I can pass that onto the router. If the user is already logged in, I just wrap true
in an Observable
and send that back.
UPDATE: If you're following along in your own code, make sure that the AuthService and AuthGuard are registered as providers somewhere in you DI tree. In my case, I configure all my services in main.ts when bootstrapping, for example:
import {AuthGuard} from './authguard';
import {AuthService} from './auth.service';
let APP_SERVICES = [
AuthService
...
]
boostrap(AppComponent, [APP_ROUTER_PROVIDERS, APP_SERVICES, AuthGuard]);
In my case, I'm using Active Directory authentication on the backend, so logging in is just a matter of hitting an endpoint and getting back the user's infomation. In most other cases, you would want to redirect the user to a login page if they are not yet authenticated. You can do that as well.
import { Router } from '@angular/router'
...
canActivate() {
if (this.authService.isLoggedIn()) {
return true;
}
//Redirect the user before denying them access to this route
this.router.navigate(['/login']);
return false;
}
Now in my route configuration, I tell the router to use this AuthGuard
class to decide if a route can be activated.
import { provideRouter, RouterConfig } from '@angular/router';
import { AuthGuard } from './authguard';
import { HomeComponent } from './home';
export const routes: RouterConfig = [
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent /* Make sure to not guard this route */ },
];
export const APP_ROUTER_PROVIDERS = [
provideRouter(routes), AuthGuard
];
I haven't found a way to tell the router to just use the guard for all routes, so not sure if thats an option.
The official documentation explains this concept quite well
Bonus Extra Tip
In my case, while waiting for the login to happen in the background, I wanted to give the user some feedback.
In my root component class:
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs/Observable';
export clas AppComponent implements OnInit {
constructor(private authService : AuthService){
}
loggedIn : Observable<boolean>;
ngOnInit() {
this.loggedIn = this.userService.logIn();
}
}
You'll notice that I call logIn()
here, even though it will be called in the AuthGuard
. My AuthService
actually stores the pending Observable
and returns that one instead of starting a new request everytime. See this post for more on how I did that as there are some gotcha's when re-using Observables
.
Then in the template:
<router-outlet></router-outlet>
<div class="overlay" *ngIf="!(loggedIn | async)">
<div class="loading">
<img class="loading" src="imgs/splashScreen.png">
</div>
<div class="text">Please wait...logging you in</div>
<i class="fa fa-circle-o-notch fa-spin"></i>
</div>
I used thes following styles to make the loading dialog cover the empty router. The spinner is from a FontAwesome font and stylesheet.
.overlay {
position:fixed;
z-index: 1000;
top : 0;
right: 0;
width:100%;
height:100%;
background-color: black;
opacity: 0.8;
}
.overlay div.text {
width:100%;
text-align: center;
color:white;
font-size: 20px;
top: 40%;
position: absolute;
}
.overlay div.loading {
width:100%;
text-align: center;
margin-top:5%;
}
.overlay i {
font-size:48px;
color:white;
top: 48%;
position: absolute;
left: 49%;
}