Skip to content

Instantly share code, notes, and snippets.

@jasonaden
Last active September 20, 2018 18:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonaden/12f28e147c252a3effce2be3ca4829e1 to your computer and use it in GitHub Desktop.
Save jasonaden/12f28e147c252a3effce2be3ca4829e1 to your computer and use it in GitHub Desktop.
Route Guard Redirect Options
/**
* 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: '' }
];
/**
* 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: '' }
];
/**
* 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: '' }
];
/**
* 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