Last active
September 20, 2018 18:06
-
-
Save jasonaden/12f28e147c252a3effce2be3ca4829e1 to your computer and use it in GitHub Desktop.
Route Guard Redirect Options
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Currently most people run guards in this way, with redirects happening inside the guard itself. | |
* There are a few issues with this, but one is that guards run asynchronously. So if multiple | |
* `CanActivate` guards perform a redirect, there isn't a way to guarantee the sequence and | |
* therefore difficult to guarantee what page the user will land on. | |
* | |
* NOTE: Guards run async, but all CanDeactivate guards will run before CanActivate | |
*/ | |
// Simple async auth guard | |
@Injectable() | |
export class AuthGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(): Promise<boolean> { | |
return this.auth.isAuthenticated() | |
.then(isAuth => { | |
if (isAuth) { | |
return true; | |
} else { | |
this.router.navigate(['login']); | |
return false; | |
} | |
}); | |
} | |
} | |
// Simple promise based role guard | |
@Injectable() | |
export class RoleGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { | |
this.auth.getToken() | |
.then(token => { | |
if (this.auth.decode(token).role !== route.data.expectedRole) { | |
// Redirect within the guard can cause collisions | |
this.router.navigate(['login']); | |
return false; | |
} | |
return true; | |
}); | |
} | |
} | |
// Synchronous guard to check if experiments is enabled in a query param | |
@Injectable() | |
export class ExperimentsGuardService implements CanActivate { | |
constructor(public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot): boolean { | |
if (route.queryParams.exp !== 'enabled') { | |
this.router.navigate(['error', {message: "Experiments are not enabled"}]); | |
return false; | |
} | |
return true; | |
} | |
} | |
export const ROUTES: Routes = [ | |
// Entire app is behind the `AuthGuardService`, and re-run on all route changes | |
{ path: '', component: HomeComponent, canActivate: [AuthGuardService], runGuardsAndResolvers: 'always', children: [ | |
{ path: 'profile', component: ProfileComponent }, | |
{ | |
path: 'admin', | |
component: AdminComponent, | |
canActivate: [RoleGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
{ | |
path: 'admin-new', | |
component: AdminNewComponent, | |
canActivate: [RoleGuardService, ExperimentsGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
] }, | |
{ path: '**', redirectTo: '' } | |
]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* One option would be to have fixed guarantees on what sequence guard failures are handled. | |
* We could then add to the CanActivate interface to allow passing in a failure handler | |
* that will get called in the guaranteed sequence. | |
*/ | |
@Injectable() | |
export class AuthGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(): Promise<boolean> { | |
return this.auth.isAuthenticated(); | |
} | |
onCanActivateFail() { | |
this.router.navigate(['login']); | |
return false; | |
} | |
} | |
@Injectable() | |
export class RoleGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { | |
this.auth.getToken() | |
.then(token => this.auth.decode(token).role === route.data.expectedRole); | |
} | |
canActivateFail() { | |
this.router.navigate(['login']); | |
return false; | |
} | |
} | |
@Injectable() | |
export class ExperimentsGuardService implements CanActivate { | |
constructor(public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { | |
return route.queryParams.exp === 'enabled'; | |
} | |
canActivateFail() { | |
this.router.navigate(['error', {message: "Experiments are not enabled"}]); | |
return false; | |
} | |
} | |
/** | |
* We would still run all guards in parallel, but if a CanActivate guard for a child returns a failure before a parent, | |
* we would wait for the parent to return success or failure, then run the failure callbacks serially, ignoring any | |
* failure callbacks after any failure callback returns `false`. | |
* | |
* Example: | |
* | |
* Start on `/` as authenticated user | |
* Log out in another tab | |
* Come back to original tab | |
* Click link to `/admin-new`, without the `exp=enabled` query option (Experimental guard will fail synchronously) | |
* Guards to execute in parallel: AuthGuardService, RoleGuardService, ExperimentsGuardService | |
* ExperimentsGuardService will return first (sync). Do NOT call canActivateFail handler | |
* Wait for AuthGuardService to return a result | |
* Run AuthGuardService.canActivateFail, causing redirect and returning `false` | |
* Cancel navigation immediately, not running other canActivateFail handlers | |
* | |
* NOTE: If recovery is possible in the canActivateFail handler, returning `true` would allow execution of the next | |
* canActivateFail handler, in this case for `RoleGuardService` | |
* | |
*/ | |
export const ROUTES: Routes = [ | |
{ path: '', component: HomeComponent, canActivate: [AuthGuardService], runGuardsAndResolvers: 'always', children: [ | |
{ path: 'profile', component: ProfileComponent }, | |
{ | |
path: 'admin', | |
component: AdminComponent, | |
canActivate: [RoleGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
{ | |
path: 'admin-new', | |
component: AdminNewComponent, | |
canActivate: [RoleGuardService, ExperimentsGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
] }, | |
{ path: '**', redirectTo: '' } | |
]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Pass a `redirect` callback function to guards. Similar to Option B, but wouldn't | |
* require a new method on the guard. Would still guarantee execution in the correct | |
* sequence by collecting the values passed to the redirect callback and only executing | |
* the highest priority on (from the top-down for CanActivate, and bottom up for | |
* CanDeactivate). | |
* | |
* The `redirect` callback would accept anything that can be passed to `router.navigate` | |
* or `router.navigateByUrl`, which includes string|UrlTree|any[] (the last is a set | |
* of values that get concatinated together to form the target URL tree). | |
*/ | |
declare type NavigationTarget = string|UrlTree|any[]; | |
declare type Redirection = (target: NavigationTarget) => false; | |
@Injectable() | |
export class AuthGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
// Ignore first two args of `ActivatedRouteSnapshot` and `RouterStateSnapshot` | |
canActivate(_, __, redirect: Redirection): Promise<boolean> { | |
return this.auth.isAuthenticated() | |
.then(isAuth => isAuth || redirect(['login']); | |
} | |
} | |
// Simple promise based role guard | |
@Injectable() | |
export class RoleGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot, _, redirect): Promise<boolean> { | |
this.auth.getToken() | |
.then(token => this.auth.decode(token).role === route.data.expectedRole || redirect(['login']); | |
} | |
} | |
// Synchronous guard to check if experiments is enabled in a query param | |
@Injectable() | |
export class ExperimentsGuardService implements CanActivate { | |
constructor(public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot, _, redirect): boolean { | |
return route.queryParams.exp === 'enabled' || redirect(['error', {message: "Experiments are not enabled"}]); | |
} | |
} | |
export const ROUTES: Routes = [ | |
// Entire app is behind the `AuthGuardService`, and re-run on all route changes | |
{ path: '', component: HomeComponent, canActivate: [AuthGuardService], runGuardsAndResolvers: 'always', children: [ | |
{ path: 'profile', component: ProfileComponent }, | |
{ | |
path: 'admin', | |
component: AdminComponent, | |
canActivate: [RoleGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
{ | |
path: 'admin-new', | |
component: AdminNewComponent, | |
canActivate: [RoleGuardService, ExperimentsGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
] }, | |
{ path: '**', redirectTo: '' } | |
]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Same as the options above, but this would simplify things by allowing a guard to | |
* return either a boolean or a URL to redirect to. It would change the guard interface | |
* as defined below. | |
*/ | |
declare type GuardReturnTypes = boolean|string|UrlTree|any[]; | |
export interface CanActivate { | |
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): | |
Observable<GuardReturnTypes>|Promise<GuardReturnTypes>|GuardReturnTypes; | |
} | |
@Injectable() | |
export class AuthGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
// Ignore first two args of `ActivatedRouteSnapshot` and `RouterStateSnapshot` | |
canActivate(): Promise<boolean> { | |
return this.auth.isAuthenticated() | |
.then(isAuth => isAuth || ['login']; | |
} | |
} | |
// Simple promise based role guard | |
@Injectable() | |
export class RoleGuardService implements CanActivate { | |
constructor(public auth: AuthService, public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { | |
this.auth.getToken() | |
.then(token => this.auth.decode(token).role === route.data.expectedRole || ['login']; | |
} | |
} | |
// Synchronous guard to check if experiments is enabled in a query param | |
@Injectable() | |
export class ExperimentsGuardService implements CanActivate { | |
constructor(public router: Router) {} | |
canActivate(route: ActivatedRouteSnapshot, _, redirect): boolean { | |
return route.queryParams.exp === 'enabled' || ['error', {message: "Experiments are not enabled"}]; | |
} | |
} | |
export const ROUTES: Routes = [ | |
// Entire app is behind the `AuthGuardService`, and re-run on all route changes | |
{ path: '', component: HomeComponent, canActivate: [AuthGuardService], runGuardsAndResolvers: 'always', children: [ | |
{ path: 'profile', component: ProfileComponent }, | |
{ | |
path: 'admin', | |
component: AdminComponent, | |
canActivate: [RoleGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
{ | |
path: 'admin-new', | |
component: AdminNewComponent, | |
canActivate: [RoleGuardService, ExperimentsGuardService], | |
data: { | |
expectedRole: 'admin' | |
} | |
}, | |
] }, | |
{ path: '**', redirectTo: '' } | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment