All too often I become frustrated when building an authentication component. The asynchronous flow is hard to manage and it makes my template a mess of ngIfs and Elvis operators:
{{ (user | async)?.uid }}
The problem is always same. I'm mixing too many concerns in one place. Organizing the right code in the right place is what makes a happy codebase.
The solution: Store route specific code in Angular's Route Guards.
The AngularFireAuth service is a great match for routing code. It contains information about a current user, whether they are coming from a redirect, and all sorts of other things.
AngularFireAuth
Take a simple signInWithRedirect example:
signInWithRedirect
import { Component, OnInit } from '@angular/core'; import { AngularFireAuth } from 'angularfire2/auth'; import { Router } from '@angular/router'; import * as firebase from 'firebase/app'; @Component({ selector: 'app-login', template: ` <button class="btn" (click)="auth()"> Login </button> `, styleUrls: ['./login.scss'] }) export class LoginComponent implements OnInit { constructor( private afAuth: AngularFireAuth, private router: Router ) { } ngOnInit() { this.afAuth.getRedirectResult().then(result => { if(result) { this.router.navigateByUrl('profile'); } }); } auth() { const provider = new firebase.auth.GoogleAuthProvider(); this.afAuth.auth.signInWithRedirect(provider); } }
Here's what's going in this example:
auth()
signInWithRedirect()
getRedirectResult()
ngOnInit()
navigateByUrl()
This isn't a problem with one component. However, imagine having to manage redirects in multiple components.
The code in ngOnInit belongs in a Route Guard. This will clean up the component of routing logic and remove the Router dependency entirely.
ngOnInit
Router
A Route Guard is used for all sorts of useful things. In this case a guard can handle the redirect code.
@Injectable() export class RedirectGuard implements CanActivate { constructor( private authService: AngularFireAuth, private router: Router ) {} canActivate() { this.authService.auth.getRedirectResult().then(result => { if(result.user != null) { this.router.navigateByUrl('profile'); return false; } return true; }); } }
The canActivate method determined if the user has access to the route. Here's what happens:
canActivate
'profile'
This code is now reusable. The Angular Router lets you specify which routes you want to listen for a redirect.
import { Routes, RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { RedirectGuard } from './redirect.guard'; const ROUTES: Routes = [ { path: '', component: LoginComponent, canActivate: [RedirectGuard] }, { path: 'profile', loadChildren: 'app/profile/profile.module#ProfileModule' } ]; const AppRoutes = RouterModule.forRoot(ROUTES); export { AppRoutes }
Now the LoginComponent is free of routing code:
LoginComponent
@Component({ selector: 'app-login', template: ` <button class="btn" (click)="auth()"> Login </button> `, styleUrls: ['./login.scss'] }) export class LoginComponent { constructor(private afAuth: AngularFireAuth) { } auth() { const provider = new firebase.auth.GoogleAuthProvider(); this.afAuth.auth.signInWithRedirect(provider); } }
No more async flow mess. The right code is organized in the right place. You can add the RedirectGuard to more routes as the app grows, which keeps the complexity out of your components.
RedirectGuard