CodyBugstein
CodyBugstein

Reputation: 23322

How can you use Angular's canActivate to negate the result of a guard?

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!

Upvotes: 21

Views: 9432

Answers (5)

Yevhenii Ostash
Yevhenii Ostash

Reputation: 41

Put data in guards

  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    loadChildren: () => import('./modules/home/home.module').then(m => m.HomeModule),
    canActivate: [isAuthenticatedGuard],
  },
  {
    path: 'login',
    loadComponent: () => import('./modules/login/login.component').then(m => m.LoginComponent),
    canActivate: [isAuthenticatedGuard],
    data: { disableAccess: true }
  },
  {
    path: 'register',
    loadComponent: () => import('./modules/register/register.component').then(m => m.RegisterComponent),
    canActivate: [isAuthenticatedGuard],
    data: { disableAccess: true }
  }

data and property name is disableAccess, decided that allowAccess name for property is not good code:


  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    const result = this.checkLogin(url);
    const { disableAccess = false } = route.data;
    const redirectMessage =
      "User is not logged - This routing guard prevents redirection to any routes that needs logging."; // TODO: Move it out

    if (result) {
      if (disableAccess) {
        console.log(redirectMessage);
        this.router.navigate(['home/main',]); //TODO: redirect to logout, and prepare application for logout
      }
    } else {
      console.log(redirectMessage);
      if (!disableAccess) {
        this.router.navigate(['login',]); //TODO: redirect to logout, and prepare application for logout
      }
    }

    return disableAccess ? !result : result;
  }

And then just manipulate with it like this, or any other way enter image description here

First: solution to create new guard and reuse canActivate from opposite guard is obviously bad. There can be redirects which can be triggered from another guard, and in general idea to inject guard to another guard - not fine.

Will glad to see your comments if you have any words for me.

Upvotes: 0

SeriousLee
SeriousLee

Reputation: 1371

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
  }
}

Upvotes: 0

Jim ReesPotter
Jim ReesPotter

Reputation: 465

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;
      }
    }
  }
}

Upvotes: 4

Jota.Toledo
Jota.Toledo

Reputation: 28434

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

Upvotes: 25

Mike Tung
Mike Tung

Reputation: 4821

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
    }
}

Upvotes: 2

Related Questions