19

From the Angular documentation on canActivate, it seems you can only use canActivate guards to allow proceeding to a route if the canActivate function ultimately returns true.

Is there some way to say, "only proceed to this route if the canActivate class evaluates to false" ?

For example, to not allow logged in users to visit the log in page, I tried this but it did not work:

export const routes: Route[] = [
    { path: 'log-in', component: LoginComponent, canActivate: [ !UserLoggedInGuard ] },

I got this error in the console:

ERROR Error: Uncaught (in promise): Error: StaticInjectorError[false]: 
  StaticInjectorError[false]: 
    NullInjectorError: No provider for false!
Error: StaticInjectorError[false]: 
  StaticInjectorError[false]: 
    NullInjectorError: No provider for false!
AlexMelw
  • 2,406
  • 26
  • 35
CodyBugstein
  • 21,984
  • 61
  • 207
  • 363
  • 4
    you cant negate a type, thats completely wrong – Jota.Toledo Jan 14 '18 at 15:27
  • 3
    With this particular case of the login page. I make a totally separate guard, something like `AuthRedirectGuard` which checks if the user is logged in. If true, redirect to the landing page, else continue to login. – LLai Jan 17 '18 at 21:58

4 Answers4

24

The interesting thing in your question is the formulation:

Is there some way to say, "only proceed to this route if the canActivate class evaluates to false" ?

And how you expressed the "intuitive" solution:

{ path: 'log-in', component: LoginComponent, canActivate: [ !UserLoggedInGuard ] },

Which basically says, you need to negate the result of UserLoggedInGuard@canActivate

Lets consider the following implementation of the UserLoggedInGuard:

@Injectable()
export class UserLoggedInGuard implements CanActivate {
   constructor(private _authService: AuthService) {}

   canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        return this._authService.isLoggedIn();
    }
} 

Next, lets look at the solution proposed by @Mike

@Injectable()
export class NegateUserLoggedInGuard implements CanActivate {    
    constructor(private _authService: AuthService) {}

   canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        return !this._authService.isLoggedIn();
    }
}

Now, the approach is ok, but is tightly coupled to the (internal) implementation of UserLoggedInGuard . If for some reason the implementation of UserLoggedInGuard@canActivate changes, NegateUserLoggedInGuard will break.

How can we avoid that? Simple, abuse dependency injection:

@Injectable()
export class NegateUserLoggedInGuard implements CanActivate {    
  constructor(private _userLoggedInGuard: UserLoggedInGuard) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
     return !this._userLoggedInGuard.canActivate(route,state);
  }
}

Now this is doing exactly what you expressed with

canActivate: [ !UserLoggedInGuard ]

And the best part:

  • It isnt tightly coupled with the internal implementation of the UserLoggedInGuard
  • Can be expanded to manipulate the result of more than one Guard class
Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • Great idea. I would also put both exported classes in the same file, and maybe make the actual `canActivate` function definition a separate external function common between them – CodyBugstein Jan 15 '18 at 02:02
  • I dont really see a good reason to share the implemention through a function. I would simply use DI to compose this negate guard wrapper – Jota.Toledo Jan 15 '18 at 07:17
  • I also wouldnt suggest to put both classes in one file. What if the negation guard now negates the result of more than one guard? In which file would you put it? – Jota.Toledo Jan 15 '18 at 07:21
  • 1
    Perfect answer! – Alon Shmiel Apr 21 '20 at 13:51
  • 3
    This however doesn't work when the 'isLoggedIn' method returns an Observable – karimaounn Dec 31 '20 at 00:22
3

I had a similar problem - wanted to make a login page that was only accessible if not authenticated and a dashboard only accessible if you are authenticated (and redirect user to appropriate one automatically). I solved it by making the guard itself login+route sensitive:

The Routes:

const appRoutes: Routes = [
  { path: 'login', component: LoginComponent, canActivate: [AuthGuard] },
  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },

The Guard:

export class AuthGuard implements CanActivate {

  private login: UrlTree;
  private dash: UrlTree;

  constructor(private authSvc: AuthenticationService, private router: Router ) {
    this.login = this.router.parseUrl('login');
    this.dash = this.router.parseUrl('dashboard');
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
    if (this.authSvc.isSignedIn()) {
      if (route.routeConfig.path === 'login') {
        return this.dash;
      } else {
        return true;
      }
    } else {
      if (route.routeConfig.path === 'login') {
        return true;
      } else {
        return this.login;
      }
    }
  }
}
Jim ReesPotter
  • 445
  • 1
  • 3
  • 10
2

Thinking about your problem, one solution could be to implement a route guard that does the logic in reverse.

import { MyService } from "./myservice.service";
import { CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot } from "@angular/router";
import { Injectable } from "@angular/core";

@Injectable()
export class MyGuard implements CanActivate {

    constructor(private myService: MyService) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.myService.isNotLoggedIn(); //if user not logged in this is true
    }
}
Mike Tung
  • 4,735
  • 1
  • 17
  • 24
0

I added a data.auth: boolean option to all of my routes (except the ones that should be accessible either way), like so:

const routes: Routes = [
  {
    path: '', // all
    component: HomeComponent,
  },
  {
    path: 'authenticate', // not authenticated
    component: AuthComponent,
    data: { auth: false },
    canActivate: [AuthGuard],
  },
  {
    path: 'account', // authenticated
    component: AccountComponent,
    data: { auth: true },
    canActivate: [AuthGuard],
  },
]

And then in my auth guard, I check for this data property and act accordingly:

export class AuthGuard implements CanActivate {
  constructor(private readonly supabase: SupabaseService) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return route.data['auth'] ? !!this.supabase.session : !this.supabase.session
  }
}
SeriousLee
  • 1,301
  • 4
  • 20
  • 42