Skip to content

Commit

Permalink
refactor(Guards): Re-implement guards feature to use services (#81)
Browse files Browse the repository at this point in the history
Before we used `provideGuard` to quickly create guard factory functions. In order to play better with the offline compiler, this changes guards to instead use traditional services.

BREAKING CHANGE:

  Before:

  ```ts
  const auth = provideGuard(function(http: Http) {

    return function(route: Route, params: any, isTerminal: boolean): Observable<boolean> {
      return http.get('/auth')
        .map(() => true)
        .catch(() => Observable.of(false));
    };

  }, [ Http ]);
  ```

  After:

  ```ts
  @Injectable()
  export class AuthGuard implements Guard {
    constructor(private http: Http) { }

    protectRoute({ route, params, isTerminal }: TraversalCandidate): Observable<boolean> {
      return this.http.get('/auth')
        .map(() => true)
        .catch(() => Observable.of(false));
    }
  }
  ```
  • Loading branch information
MikeRyanDev authored and brandonroberts committed May 3, 2016
1 parent 2d51638 commit 145b671
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 69 deletions.
66 changes: 41 additions & 25 deletions docs/overview/guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,82 @@ Guards are powerful hooks into the Router's route resolution process. When the l
* Yes: Load the index route
* No: Load the children routes and repeat process for all children

Guards are run before a route is selected to be evaluated. This gives you the opportunity let the router's traversal process know if a route should be considered a candidate or not for traversal.
Guards are run after the router determines it is a partial match but before it evaluates it. This gives you the opportunity let the router's traversal process know if a route should continue to be considered a candidate route.

### Use Cases
A great use case for guards is auth protecting routes. Guards are functions that return an Observable of true or false. If your guard's observable emits true, then traversal continues. If your guard emits false, traversal is canceled immediately and the router moves on to the next candidate route. Note that guards will not finish running until your observable completes. To write an auth guard we'll need to use the `provideGuard` helper:
A great use case for guards is auth protecting routes. Guards are services with a `protectRoute` method that return an Observable of true or false. If your guard's observable emits true, then traversal continues. If your guard emits false, traversal is canceled immediately and the router moves on to the next candidate route. Note that guards will not finish running until your observable completes. To write an auth guard we'll need to use the `provideGuard` helper:

```ts
import 'rxjs/add/observable/of';
import { Http } from 'angular2/http';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { provideGuard, Route } from '@ngrx/router';
import { Guard, Route, TraversalCandidate } from '@ngrx/router';

const authGuard = provideGuard(function(http: Http) {
// Guards are provided with a snapshot of the route params that have been
// parsed so far, the route that is being evaluated, and whether or not
// the matched route is the final match
return function(params: any, route: Route, terminal: boolean) {
return http.get('/auth/check')
@Injectable()
class AuthGuard implements Guard {
constructor(private _http: Http) { }
// Guards are provided with a traversal candidate object which contains a
// snapshot of the route params parsed so far, the parsed query params,
// the route being evaluated, and the location change that caused traversal.
protectRoute(candidate: TraversalCandidate) {
return this._http.get('/auth/check')
// If request succeeds, return true
.map(() => true)
// If request fails, return false
.catch(() => Observable.of(false));
}
}, [ Http ]);
}
```

To use this guard all we have to do is add it to the route's guards:
To use this guard first we have to add it to the route's guards:

```ts
const routes: Routes = [
{
path: '/account',
guards: [ authGuard ],
loadComponent: () => System.import('/pages/account', __moduleName)
guards: [ AuthGuard ],
loadComponent: () => System.import('app/pages/account')
.then(module => module.AccountPage),
loadChildren: () => System.import('/routes/account', __moduleName))
loadChildren: () => System.import('app/routes/account'))
.then(module => module.accountRoutes),
}
]
```

Then we include it in the providers array with the router:

```ts
bootstrap(App, [
provideRouter(routes),
AuthGuard
]);
```

### What Makes Guards Powerful?
Guards are run before the component or children are loaded. This prevents the user from having to load unnecessary code giving you a big win in performance.

While a guard must always return an observable, if a guard dispatches a route change (for instance redirecting to a `400 Not Authorized` route) the current traversal will be immediately canceled:

```ts
const authGuard = provideGuard(function(http: Http, router: Router) {
// Guards are provided with the route that is being evaluated:
return function(route: Route) {
return http.get('/auth/check')
// If request succeeds, return true
@Injectable()
class AuthGuard implements Guard {
constructor(private _http: Http, private _router: Router) { }

protectRoute(candidate: TraversalCandidate) {
return this._http.get('/auth/check')
.map(() => true)
// If request fails, redirect to "not authorized" route
// If request fails, catch the error and redirect
.catch(() => {
router.replace('/not-authorized');
this._router.redirect('/400');

return Observable.of(false);
});
}
}, [ Http, Router ]);
}
```

### Injection
Guards are limited by the services they can inject. They are run in the context of the root injector, meaning if there is a service you want to inject into a guard you must provide that service in the same injector (or a parent of the injector) where you provide the router. Additionally, some router services like `RouteSet`, `RouteParams`, and `QueryParams` do not get updated until after all guards have been run.
### Router Services
Some router services like `RouterInstruction`, `RouteParams`, and `QueryParams` do not get updated until after all guards have been run. For this reason your guards should generally not use these services and instead should get route and query params out of the traversal candidate object provided to the `protectRoute` method.
49 changes: 28 additions & 21 deletions lib/guard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* Guards are simple services that can protect routes from being traversed. They
* are implemented using traversal middleware
* Guards are services that can protect routes from being traversed. They
* are implemented using traversal hooks
*
* A guard is called when the router begins traversing a route configuration file.
* It returns `true` or `false` to let the router know if it should consider
* the route a candidate. Using guards, you can auth protect routes, run data
* fetching, etc.
* A guard's `protectRoute` method is called when the router begins traversing a
* route configuration file. It returns `true` or `false` to let the router know
* if it should consider the route a candidate. Using guards, you can auth
* protect routes, run data fetching, etc.
*
* A limitation of guards is that they are instantiated with the _root_ ReflectiveInjector.
* For more powerful injection, consider looking at render middleware
* A limitation of guards is that they must be provided in the same place you
* provide the router.
*/
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/of';
Expand All @@ -23,36 +23,43 @@ import { TRAVERSAL_HOOKS, TraversalCandidate } from './route-traverser';
import { Hook } from './hooks';

export interface Guard {
(params: any, route: Route, isTerminal: boolean): Observable<boolean>;
protectRoute(candidate: TraversalCandidate): Observable<boolean>;
}

export const provideGuard = createProviderFactory<Guard>('@ngrx/router Guard');


@Injectable()
export class GuardHook implements Hook<TraversalCandidate> {
constructor(@Inject(Injector) private _injector: ReflectiveInjector) { }

apply(route$) {
return route$.mergeMap(({ route, params, isTerminal }) => {
if ( !!route.guards && Array.isArray(route.guards) && route.guards.length > 0 ) {
const guards: Guard[] = route.guards
.map(provider => this._injector.resolveAndInstantiate(provider));
resolveGuard(token: any): Guard {
let guard = this._injector.get(token, null);

if ( guard === null ) {
guard = this._injector.resolveAndInstantiate(token);
}

const resolved = guards.map(guard => guard(params, route, isTerminal));
return guard;
}

apply(route$: Observable<TraversalCandidate>): Observable<TraversalCandidate> {
return route$.mergeMap(candidate => {
const { route } = candidate;
if ( !!route.guards && Array.isArray(route.guards) && route.guards.length > 0 ) {
const guards: Guard[] = route.guards.map(token => this.resolveGuard(token));
const activated = guards.map(guard => guard.protectRoute(candidate));

return Observable.merge(...resolved)
return Observable.merge(...activated)
.every(value => !!value)
.map(passed => {
if ( passed ) {
return { route, params, isTerminal };
return candidate;
}

return { route: null, params, isTerminal };
return Object.assign({}, candidate, { route: null });
});
}

return Observable.of({ route, params, isTerminal });
return Observable.of(candidate);
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface IndexRoute extends BaseRoute {

export interface Route extends IndexRoute {
path?: string;
guards?: Provider[];
guards?: any[];
indexRoute?: IndexRoute;
loadIndexRoute?: Async<IndexRoute>;
children?: Routes;
Expand Down
48 changes: 26 additions & 22 deletions spec/guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import { ReflectiveInjector } from '@angular/core';

import { Route } from '../lib/route';
import { TraversalCandidate } from '../lib/route-traverser';
import { Guard, provideGuard, GuardHook } from '../lib/guard';
import { Guard, GuardHook } from '../lib/guard';


describe('Guard Middleware', function() {
let guardHook: GuardHook;
let injector: ReflectiveInjector;

class PassGuard implements Guard {
protectRoute = () => Observable.of(true);
}

class FailGuard implements Guard {
protectRoute = () => Observable.of(false);
}

function route(route: Route, params = {}, isTerminal = false) {
return Observable.of({ route, params, isTerminal });
}
Expand Down Expand Up @@ -42,30 +50,29 @@ describe('Guard Middleware', function() {

it('should resolve all guards in the context of the injector', function() {
spyOn(injector, 'resolveAndInstantiate').and.callThrough();
const guard = provideGuard(() => () => Observable.of(true));

route({ guards: [ guard ] }).let(t => guardHook.apply(t)).subscribe();
route({ guards: [ PassGuard ] }).let(t => guardHook.apply(t)).subscribe();

expect(injector.resolveAndInstantiate).toHaveBeenCalledWith(guard);
expect(injector.resolveAndInstantiate).toHaveBeenCalledWith(PassGuard);
});

it('should provide guards with the route it has matched', function() {
const testGuard = { run: () => Observable.of(true) };
spyOn(testGuard, 'run').and.callThrough();
const guard = provideGuard(() => testGuard.run);
const nextRoute = { guards: [ guard ] };
const params = { abc: 123 };
const isTerminal = true;

route(nextRoute, params, isTerminal).let(t => guardHook.apply(t)).subscribe();

expect(testGuard.run).toHaveBeenCalledWith(params, nextRoute, isTerminal);
// Intentionally commenting this out because a future PR will refactor
// traversal candidate and will pass that to the guards instead
xit('should provide guards with the TraversalCandidate', function() {
// const testGuard = { run: () => Observable.of(true) };
// spyOn(testGuard, 'run').and.callThrough();
// const guard = provideGuard(() => testGuard.run);
// const nextRoute = { guards: [ guard ] };
// const params = { abc: 123 };
// const isTerminal = true;
//
// route(nextRoute, params, isTerminal).let(t => guardHook.apply(t)).subscribe();
//
// expect(testGuard.run).toHaveBeenCalledWith(params, nextRoute, isTerminal);
});

it('should return true if all of the guards return true', function(done) {
const pass = provideGuard(() => () => Observable.of(true));

route({ guards: [ pass ] })
route({ guards: [ PassGuard ] })
.let<TraversalCandidate>(t => guardHook.apply(t))
.subscribe(({ route }) => {
expect(route).toBeTruthy();
Expand All @@ -75,10 +82,7 @@ describe('Guard Middleware', function() {
});

it('should return false if just one guard returns false', function(done) {
const pass = provideGuard(() => () => Observable.of(true));
const fail = provideGuard(() => () => Observable.of(false));

route({ guards: [ pass, fail ] })
route({ guards: [ PassGuard, FailGuard ] })
.let<any>(t => guardHook.apply(t))
.subscribe(({ route }) => {
expect(route).toBeFalsy();
Expand Down

0 comments on commit 145b671

Please sign in to comment.