Create a route Guard to manage permissions
Tags: angular
Published: 2023-01-24 08:09:24

## Welcome to Angular challenges #6 : part 2
The sixth challenge is about managing permissions within an application. Many applications have to handle different types of user permissions such as admins, managers… In this challenge, we will learn how to use a guard in Angular to restrict access to certain pages based on a user's permissions.

If you haven't done the challenge yet, I invite you to try it first by going to [Angular Challenges](https://github.com/tomalaforge/angular-challenges/blob/main/apps/permissions/README.md) and coming back afterward to compare your solution with mine. _(You can also submit a PR that I'll review)_

---

We begin the challenge with the following application. We have a list of buttons to log in as different users, each with their own set of permissions. We also have a button to enter the application. Depending on the user's permissions, we will display the appropriate dashboard.

![dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wvjceypc9x85zr2vodu5.png)


Angular provides a list of built-in guards to protect our routes: `canLoad`, `canActivate` , `canDeactivate` , `canActivateChild` and `canMatch` .

If you want to learn more about each guard and how to implement them, I invite you to read [this article](https://dev.to/this-is-angular/everything-you-need-to-know-about-route-guard-in-angular-2hkn).
{% embed https://dev.to/this-is-angular/everything-you-need-to-know-about-route-guard-in-angular-2hkn %}

For the purpose of this challenge, we will use the `canMatch` guard which fits our use case perfectly.

We can write the same route multiple times. If the first route in the file is matched, the `router` will navigate to this route. Otherwise it will check the following one. Let's see how `canMatch` works with a very simpler example:

```typescript
  {
    path: 'enter',
    canMatch: [() => false],
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [() => true],
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },
```

In this example, if the user navigates to `enter`, the `router` will try to match the first route. However the `canMatch` guard returns false., so the `router` will try the second route which returns true. Finally the `router` will navigate to the `ClientComponent` and will not execute of follow up routes.

---

Now, let's apply our knowledge to our needs. First we need to create an injectable service to implement our guard logic for managing user permissions.
Here is the implementation:

```typescript
@Injectable({ providedIn: 'root' })
export class HasPermissionGuard implements CanMatch {
  private router = inject(Router);
  private userStore = inject(UserStore);

  canMatch(route: Route): Observable<boolean | UrlTree> {
    const accessRolesList: Role[] = route.data?.['roles'] ?? [];
    const isAdmin: boolean = route.data?.['isAdmin'] ?? false;
    return this.hasPermission$(isAdmin, accessRolesList);
  }

  private hasPermission$(isAdmin: boolean, accessRolesList: Role[]) {
    return this.userStore.isUserLoggedIn$.pipe(
      mergeMap((hasUser) => {
        if (hasUser) {
          if (isAdmin) {
            return this.userStore.isAdmin$.pipe(map(Boolean));
          } else if (accessRolesList.length > 0) {
            return this.userStore
              .hasAnyRole(accessRolesList)
              .pipe(map(Boolean));
          }
          return of(false);
        } else {
          return of(this.router.parseUrl('no-user'));
        }
      })
    );
  }
}
```
We retrieve the `roles` and `isAdmin` properties from the route's `data` attribute.

Then we first check if a user has been logged in. If not, we navigate to the `no-user` page.

If a user has been logged in, we compare the user's roles with the ones set on the route to determine whether the router should navigate to this route or check the next one.

Once our guard as a service is complete, we can create our routes as follow:

```typescript
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      isAdmin: true,
    },
    loadComponent: () => import('./dashboard/admin.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['MANAGER'],
    },
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['WRITER', 'READER'],
    },
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['CLIENT'],
    },
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },
```
Since **Angular v.14.2**, we can use functional programming. Additionally, the guard as a service will be deprecated in version **15.2**.

This gives us the following function. To inject our service using the Dependency Injection system of Angular, we can now take advantage of the `inject` function. _(Note: The `inject` function only works inside a dependency context)_

```typescript
export const hasAdminPermission = (isAdmin: boolean, accessRolesList: Role[]) => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) => {
      if (hasUser) {
        if (isAdmin) {
          return userStore.isAdmin$.pipe(map(Boolean));
        } else if (accessRolesList.length > 0) {
          return userStore.hasAnyRole(accessRolesList).pipe(map(Boolean));
        }
        return of(false);
      } else {
        return of(router.parseUrl('no-user'));
      }
    })
  );
};
```
Since we are using function, we can split our logic into two separate functions.

```typescript
export const isAdmin = () => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.isAdmin$.pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};

export const hasRole = (accessRolesList: Role[]) => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};
```
This way, the code is easier to understand, easier to maintain and less prone to errors. If we want to use both functions to protect one route, we can, since a guard takes an array as input.
We can now refactor our `route` definition

```typescript
  {
    path: 'enter',
    canMatch: [() => isAdmin()],
    loadComponent: () => import('./dashboard/admin.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['MANAGER'])],
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['WRITER', 'READER'])],
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['CLIENT'])],
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },
```
- Better Developer eXperience
- Less prone to errors _(No need for the `data` property of type `any` anymore)_
- Less boilerplate code
- Easier to read
- More maintainable

**One more improvement**
Using the _inject_ function inside a function can lead to errors if someone wants to reuse the function outside a dependency context. To avoid this, we can set the two injected dependencies as optional parameters:

```typescript
export const hasRole = (accessRolesList: Role[], userStore = inject(UserStore), router = inject(Router)) => {
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};
```
**Note:** There is one more advantage by doing so: you can now easily test your function without the need of `TestBed` 

---

That's it for this article. I hope you enjoyed this sixth challenge and learned new techniques from it.

If you want to read the first part of this challenge about managing permissions with structural directives, follow [this link](https://dev.to/this-is-angular/create-a-custom-structural-directive-to-manage-permissions-like-a-pro-5293).

{% embed https://dev.to/this-is-angular/create-a-custom-structural-directive-to-manage-permissions-like-a-pro-5293 %}

_👉 Other challenges are waiting for you at [Angular challenges](https://github.com/tomalaforge/angular-challenges). Come and try them. I'll be happy to review you!_

Follow me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge) to read more about upcoming Challenges! Don't hesitate to ping me if you have more questions.