Best way to show/hide a link in angular 5
Asked Answered
N

1

5

I currently have a home page that routes to all the different workflows my company runs. We have about 15 different workflows and each workflow is guarded by a user role. If you don't have the correct user role in the database you wont see corresponding link to that page. We protect the server end points, but what I am worried about is what is the best way to show people the links or not show them I would prefer to not duplicate code.

Here is one way to do it:

I have a html page like this

<ul>
  <li *ngIf="authService.hasRequiredRole('users.user-admin')" routerLink="/user">Users</li>
  <li *ngIf="authService.hasRequiredRole('users.role-admin')" routerLink="/role">Roles</li>
</ul>

I have an auth service like this:

hasRequiredRole(roles: string | [string]) {
    if (typeof roles === 'string') {
      roles = [roles];
    }
    for (const roleSlug of roles) {
      if (this.user.roles.find((role: any) => {
          return role.slug === roleSlug;
      })) {
        return true;
      }
    }
    return false;
  }

and I have a router with routes like this:

const routes: Routes = [{

  path: 'user',
  data: {
    allowedRoles: 'users.user-admin'
  },
  loadChildren: 'app/user/user.module#UserModule',
  canActivate: [AuthGuard]
},
{ path: 'home', component: HomeComponent }]

The AuthGuard just checks if the user is logged in and then uses the data in the route to check if the user has the correct role.

As you can see there are two separate locations where we use the string 'users.user-admin'.


I think we should have a router with two guards. One guard will check if the user is logged in, and another to check if the user has the correct role. The role will be hard coded there like this:

export class UserAdminGuard implements CanActivate {
  constructor(private router: Router, private authService: AuthService) {}

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

    return this.authService.hasRequiredRole('users.user-admin');
  }
}

and then the html would look something like this:

<ul>
  <li *ngIf="userAdminGuard.canActivate()" routerLink="/user">Users</li>
  <li *ngIf="roleAdminGuard.canActivate()" routerLink="/role">Roles</li>
</ul>

Would something like my method work or is there a better more angular way of doing this? I could not find anything in the documentation about anything like this. If you could also provide documentation extra points.

Nitro answered 6/11, 2017 at 18:52 Comment(3)
As an aside, did you really mean the tuple [string] (an Array of length exactly 1 containing a string) or rather string[] or Array<string>? I ask because, while perfectly permissible, I have never seen it in production Angular.Aliphatic
I meant it could be an array of strings or a string so you could provide ['role1','role2'] or 'role'Nitro
Ok, an array of strings in TypeScript is declared as either string[] or Array<string>. [string], on the other hand, is a tuple type, which means you declare the only valid type is an array of exactly the form you define. In this case, an array of length exactly 1 with 1 string (see link in original comment). Similarly, you could declare something like x: [string, number, number] for which ["a", 1, 2] is valid, but ["a", "b", "c"] is not.Aliphatic
D
9

I think one way of doing that is by creating a Structural Directive. You can create a RolesDirective which handles the logic of showing/hiding content as per the role(s) you pass it.

For example:

Your directive will look something like this

@Directive({
  selector: '[roles]',
  inputs: ['roles']
})
export class RolesDirective {

  constructor(private _templateRef: TemplateRef<any>,
              private _viewContainer: ViewContainerRef,
              private userService: UserService) {

  }

  @Input() set roles(allowedRoles: Array<string>) {
    let shouldShow: boolean = false;
    let userRoles:Array<string> = this.userService.getUserRoles();
    for(let role of userRoles){
      if(role.toUpperCase() == "ADMIN"){
        shouldShow = true;
        break;
      }
      for(let allowedRole of allowedRoles){
        allowedRole = allowedRole.toUpperCase();
        if(allowedRole.toUpperCase() == role.toUpperCase()){
          shouldShow = true;
          break;
        }
      }
    }
    if (shouldShow) {
      this._viewContainer.createEmbeddedView(this._templateRef);
    } else {
      this._viewContainer.clear();
    }
  }

}

And in your html just pass the allowed roles for that component

<ul>
  <li *roles="['admin']" routerLink="/user">Users</li>
  <li *roles="['other','roles']" routerLink="/role">Roles</li>
</ul>

Here's the documentation to Structural Directive: https://angular.io/guide/structural-directives

Deshawndesi answered 6/11, 2017 at 19:22 Comment(7)
Very nice solution!Aliphatic
how would route guarding be handled? Would you use the directive inside a main auth guard? how would you pass the role to the route guard? Would you just get rid of the route guard all together?Nitro
@SamuelThompson I have an AdminAuthGuard same as yours, it just checks if the user is admin. I use it for routes which are exclusive for admins and for other pages where I want to hide some content of a page I use the directive. Use the auth guard if you want to make the page exclusive to admin and the roles directive if you want to control the visibility of components in a page.Deshawndesi
@Aliphatic Glad you liked it, I am also curious if there's a better or optimized way to do this. Directives are pretty dope though.Deshawndesi
@RohanArora I like your solution, but I have a lot of people sharing devices, so it is very possible for someone to try accessing a page they don't have authorization to access. So I need some way of also checking the routes inside a guard. Not just restricting access to the link.Nitro
To be more precise I want to not have to change the auth permission in two locations, i just want to change it in one location. Maybe just use a role config JSON file with a map to roles allowed?Nitro
@SamuelThompson Attaching roles to routes as data attribute and retrieving that roles in the Guard and applying the same logic seems like the way to do for now. Example in this ans: https://mcmap.net/q/143368/-pass-parameter-into-route-guardDeshawndesi

© 2022 - 2024 — McMap. All rights reserved.