0

I am trying to restrict the user access to specific routes depending on the user role. I've created a new role guard to do this but am still able to access the restricted routes.Thanks in advance for your help

Here is my guard:

export class RoleGuard implements CanActivate {

  role: string;
  constructor(private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {


    this.getRole().subscribe(data => {
      this.role = data;
    });
    const userRole = JSON.parse(localStorage.getItem('user_role'));
    if (userRole === 'admin'&& this.role === 'ADMIN') {
      // role not authorised so redirect to home page
      this.router.navigate(['']);
      return false;

    }
    else {
      return true;
    }


  }

  getRole(): Observable<any> {
    return this.router.events.pipe(map(data => {
      if (data instanceof ChildActivationStart) {
        return data.snapshot.data.roles;
      }
    }));
  }


}

When I tried logging the role in the guard, I could see the value coming as undefined after the initial admin value which means canActivate is being called multiple times but the getRole method is called only once. Here is my routing module:

const routes: Routes = [{
  path: '',
  pathMatch: 'prefix',
  component: AppComponent,
  canActivate: [AuthGuard],

  children: [
    {
      path: 'admin',
      canActivate: [RoleGuard],
      data: {
        title: 'Admin Support',
        roles: 'ADMIN'
      },
      loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
    },
    {
      path: 'user',
      loadChildren: () => import('./user/user.module').then(m => m.UserModule)
    }
  ],
}
piqueblinders
  • 99
  • 1
  • 10

2 Answers2

0

You are subscribing to an observable but it already exits the method when you check its value.

When you hit this.role === 'ADMIN' this.role is equal to undefined

You should return an Observable from this method

canActivate(
     next: ActivatedRouteSnapshot,
     state: RouterStateSnapshot): Observable<boolean> {


   return this.getRole().pipe(map(role => {
   const userRole = JSON.parse(localStorage.getItem('user_role'));
   if (userRole === 'admin' && role === 'ADMIN') {
      // role not authorised so redirect to home page
      this.router.navigate(['']);
      return false;
   }
   else {
      return true;
   }));
}

getRole(): Observable<string> {
  return this.router.events.pipe(map(data => {
     if (data instanceof ChildActivationStart) {
        return data.snapshot.data.roles as string;
     }
   }));
}

On a general note you should look into more how observables work

misha130
  • 5,457
  • 2
  • 29
  • 51
  • with this I'm getting the following error in console while trying to navigate - Cannot activate an already activated outlet – piqueblinders Jun 08 '21 at 06:30
  • That error has nothing to do with the guard - https://stackoverflow.com/questions/55617772/angular-7-multiple-outlets-error-cannot-activate-an-already-activated-outle – misha130 Jun 08 '21 at 07:54
  • Also with this approach I have to click twice to navigate to the restricted page if the required permission is there. A navigation cancel event is fired on the first click every time. – piqueblinders Jun 08 '21 at 08:35
0

As I said, the problem is you are returning inside the subscription and the returning value is never getting outside of the subscription. The idea behind the Angular Guard is that it should inform the router whether or not the navigation is allowed, so the Guard must return something ideally a boolean or an Observable or a Promise that resolves to boolean.

With that in mind, have a look at the following approach, it might not work by simply copy-pasting it, but should help you to get on track.

export class RoleGuard implements CanActivate {
  role: string;
  constructor(private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.router.events.pipe(
      filter((event: RouterEvent) => event instanceof ChildActivationStart),
      // use the tap for debugging purposes (I'm not sure if ChildActivationStart will contain what your are looking for)
      tap((event) => console.log(event)),
      map((event: any) => event.snapshot.data.roles),
      map((role: string) => this.isUserAuthorized(role)),
      tap((authorized: boolean) => {
        if (!authorized) { this.router.navigate(['']); }
      })
    );
  }

  private isUserAuthorized(role: string): boolean {
    try {
      // JSON.parse might fail, it's a good practice to catch the errors
      const userRole = JSON.parse(localStorage.getItem('user_role'));
      return userRole === 'admin' && role === 'ADMIN';
    } catch (e) {
      return false;
    }
  }

}

Don't give up and publish any further errors.

WSD
  • 3,243
  • 26
  • 38
  • tried this but nothing's being logged to the console. Also the rerouting is not working. And with the correct permission I'm not able to access the guarded route. – piqueblinders Jun 08 '21 at 11:44
  • Probably because `filter((event: RouterEvent) => event instanceof ChildActivationStart)` is never meet. – WSD Jun 08 '21 at 12:32
  • even without the filter events are not logged. After some research, instead of trying to fetch data from the event, I'm now getting it from the activated route snapshot. – piqueblinders Jun 09 '21 at 09:41
  • I'm glad you're on track! – WSD Jun 09 '21 at 09:58