NgRx Use Cases, Part I: Restricting Access
Tags: javascript, typescript, angular, ngrx
Published: 2022-10-24 08:24:54

_Original cover photo by [Sandy Millar](https://unsplash.com/@sandym10) on Unsplash._

## What is access restricting?

Restricting access in an application is essentially modifying how different users may interact with our platform. Some users may have permissions to see some pages and some might not; authenticated users may be able to visit certain segments of an app, and maybe not able to visit others; or it can be more granular, like disabling sections in pages that are otherwise available, or even performing some actions, but not in their entirety. Today, we are going to examine how this can be done in the most convenient way using NgRx in Angular apps.

## What tools do we have?

First of all, we have to keep in mind, that Angular itself already provides some pretty powerful tools hat help us achieve access restriction. Let's first reiterate them:

1. [Guards](https://angular.io/api/router/CanActivate) - special classes that restrict access to pages via routing based on a condition we provide
2. [Interceptors](https://angular.io/guide/http#intercepting-requests-and-responses) - while they do not come as specifically targeting access restriction, they can be used to modify and even prevent network requests
3. [NgIf](https://angular.io/guide/built-in-directives#adding-or-removing-an-element-with-ngif) directive - yes, this is our main tool when dealing with granular access restriction
4. [NgSwitch](https://angular.io/guide/built-in-directives#switching-cases-with-ngswitch) directive - main tool when we need to provide different components to the end user based on some role

Aim of this article is not to try and replace these tools with NgRx, but rather make our reactive store interplay with these tools to achieve best UX with as little and concrete code as possible.

## What are our use cases?

We are going to show access restriction on three main scenarios:

1. Authentication with NgRx
2. Handling permissions
3. Showing certain features depending on roles/permissions stored in NgRx Store

Let's begin with authorization.

## Authorization with NgRx

When dealing with apps that use NgRx, the most important thing to remember is not to duplicate data and keep our interface access to the Store data simple and efficient. For this, we need to understand what sort of scenario we are dealing with. For the purpose of this example, we will implement authorization with following characteristics:

1. Using a [JSON Web Token](https://jwt.io/)
2. Storing the token in cookies
3. Retrieving the user data using the token for a call to a special API
4. Checking the auth status of the user
5. Getting user info (for a profile page or for displaying somewhere)

This is a somewhat specific scenario, but it can be easily extrapolated to other use cases, as in using `localStorage` instaead of `cookies` (keep in mind that `localStorage` is [considered insecure](https://dev.to/rdegges/please-stop-using-local-storage-1i04)), or using an entirely different authorization strategy.

Now let's understand what sort of data we would need to keep in the Store. At first glance, we will probably need to store the following:

- Current user data
- a boolean indicating if the user is logged in
- the auth token itself

Seems alright, doesn't it? **Wrong!**. We do not need to store anything other than the user data itself, because:

1. The existence of user data already proves the user is logged in
2. The token itself contains the user data, just needs to be decoded
3. The call to API will be made immediately when the app starts

So we will just store the user data in our `auth` section of the store, and write several selectors that access the logged in state, the token itself, and decoded user data. So our `AuthState` will look very simple, like this:

```ts
export interface AuthState {
  token: string;
  user: User;
}

export const initialState: AuthState = {
  token: "",
  user: null,
};
```

Now, we need actions to put the token into the state, remove it and so on:

```ts
import { createAction, props } from "@ngrx/store";

export const setToken = createAction(
  "[Auth] Set Token",
  props<{ token: string }>()
);

export const setUser = createAction(
  "[Auth] Set user",
  props<{ user: User }>(),
);

export const removeToken = createAction("[Auth] Remove Token");
```

And then a simple reducer which will handle interactions:

```ts
import { createReducer, on } from "@ngrx/store";
import { removeToken, setToken } from "./actions";
import { AuthState, initialState } from "./state";

export const authReducer = createReducer(
  initialState,
  on(setToken, (state, { token }): AuthState => ({ ...state, token })),
  on(removeToken, (state): AuthState => ({ ...state, token: "" })),
  on(setUser, (state, { user }): AuthState => ({ ...state, user }))
);
```

Now, let's register this in our `AppModule`:

```ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";

import { AppComponent } from "./app.component";
import {
  UserDashboardComponent,
} from "./user-dashboard/user-dashboard.component";
import { AppRoutingModule } from "./routing.module";
import { LoginComponent } from "./login/login.component";
import { StoreModule } from "@ngrx/store";
import { authReducer } from "./store/reducer";
import { CommonModule } from "@angular/common";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    CommonModule,
    StoreModule.forRoot({ auth: authReducer }),
  ],
  declarations: [AppComponent, UserDashboardComponent, LoginComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

Now we need to write the selectors to pick up the data we want:

```ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { AuthState } from "./state";
import { decode } from "some-jwt-library";

export const authFeature = createFeatureSelector<AuthState>("auth");

export const selectToken = createSelector(
  authFeature,
  (state) => state.token,
);
export const selectIsAuth = createSelector(
  authFeature,
  (state) => !!state.token
);
export const selectUserData = createSelector(
  authFeature,
  (state) => state.user
);
```

And that's it for the initial part, we now can store the data about the user and retrieve it safely. Now let's discuss how we get that data into the store. For this, we will use three effects:

1. Log in: send the login data to backend, get the token, store in cookies and then the Store itself
2. Log out: remove the token, redirect to the login page
3. Retrieve token: when the app; starts, check the cookies for a token and store it in the Store

First we add relevant actions:

```ts
export const login = createAction(
  "[Auth] Login",
  props<{ email: string; password: string }>()
);

export const loginError = createAction(
  "[Auth] Login",
  props<{ message: string }>()
);

export const logout = createAction("[Auth] Log Out");
```

And the effects themselves. Pay special attention to the last one:

```ts
@Injectable()
export class AuthEffects {
  // on login, send auth data to backend,
  // get the token and put into the store and cookies
  login$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(login),
      mergeMap(({ email, password }) => {
        return this.authService.login(email, password).pipe(
          tap(({ token }) => this.cookieService.set("token", token)),
          map(({ token }) => setToken({ token })),
          catchError(() => of(loginError({ message: "Login failed" })))
        );
      })
    );
  });

  // on logout, just remove the token
  // and navigate to login page
  // no need to dispatch any actions after that
  logout$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(logout),
        tap(() => {
          this.cookieService.remove("token");
          this.router.navigateByUrl("/login");
        })
      );
    },
    { dispatch: false }
  );

  // when app has started, get the user data
  // using the token from cookies
  // and put into the store
  init$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(ROOT_EFFECTS_INIT),
      mergeMap(({ email, password }) => {
        return this.authService.getCurrentUser().pipe(
          map(({ token }) => setUser({ user })),
          catchError(() => of(setUserError({ message: "Error" })))
        );
      })
    );
  });

  constructor(
    private readonly actions$: Actions,
    private readonly authService: AuthService,
    private readonly router: Router,
    private readonly cookieService: CookieService
  ) {}
}
```

First two effects are fairly straightforward, so let's focus on the last one. Here, we have taken advantage of the built-in `ROOT_EFFECTS_INIT` action which gets dispatched by NgRx itself when it subscribes to our effects - essentially acting as a notifier for our app start in this scenario. Then we put the user data into our state, essentially rehydrating it.

So now we have our auth in NgRx ready, the final addition will be creating a special guard that will check the existence of our auth token:

```ts
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly store: Store,
    private readonly router: Router,
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ) {
    return this.store.select(selectIsAuth).pipe(
      map((isAuth) => {
        return isAuth ? true : this.router.parseUrl("/login");
      })
    );
  }
}
```

Essentially, all the logic behind autgorization has been delegated to the NgRx Store, and the guard just asks the Store whether to proceed or not.

This is mostly it. Now, let's discuss permission based access restrictions.

## Handling permissions with NgRx

Different applications have different approaches to permissions, different ways to store them and validate their existence. The cool thing about NgRx is that we do not care what is the shape of the permissions - we can always write selectors that provide a similar interface for checking permissions and granting access **regardless** of what the permissions look like.

For the sake of simplicity, for this example we will imagine that the permissions in our app are stored in the JWT token together with the user data in an array of strings - if the name of the permission in present then the user has that permission, otherwise not. So, our user object might look something like this:

```ts
interface UserData {
  firstName: string;
  lastName: string;
  permissions: string[];
}
```

Now, let's add the relevant selectors:

```ts
export const selectPermissions = createSelector(
  selectUserData,
  (userData) => userData?.permissions ?? []
);
```

Now this will return the array that we have, but in certain cases we need to check the existence of a particular permission. For this, we can write a selector with an argument:

```ts
export const selectHasPermission = (permission: string) =>
  createSelector(selectPermissions, (permissions) =>
    permissions.includes(permission)
  );
```

Then in our components we can use this selector to check whether the user has the permission to perform a certain action:

```ts
export class UserDashboardComponent implements OnInit {
  canCreateOrder$ = this.store.select(
    selectHasPermission("CreateOrder"),
  );
  constructor(private readonly store: Store) {}
}
```

And then in the template:

```html
<button *ngIf="canCreateOrder$ | async" (click)="createOrder()">
  Create Order
</button>
```

Starting from Angular 14, using the `inject` function, we can create a customizable functional guard for permission checkings:

```ts
export function hasPermissionGuard(permission: string) {
  return function () {
    const store = inject(Store);
    return store.select(selectHasPermission(permission));
  };
}
```

And then we can use it in our routes:

```ts
const routes: Routes = [
  {
    path: "orders",
    component: OrdersComponent,
    canActivate: [hasPermissionGuard("ViewOrders")],
  },
  {
    path: "orders/create",
    component: CreateOrderComponent,
    canActivate: [hasPermissionGuard("CreateOrder")],
  },
];
```

> Note: we can customize the guard to accept multiple permissions and check whether the user has all of them, and also add another argument to handle redirects if needed.

**Bonus point**: finally, we visit the section where we provide granular access to certain parts of the UI:

## Handling granular access to UI elements

For this, can create a directive that will hide the element if the user does not have the permission. This directive will:

1. Be a structural directive - `*appHasPermission="permission"`
2. Take a string as an input - the name of the permission
3. Use the `Store` to access permissions
4. Use the `selectHasPermission` selector to check whether the user has the permission
5. Hide the element if the user does not have the permission
6. Show the element if the user has the permission
7. Unsubscribe from the store when the element is destroyed

Here we go:

```ts
@Directive({
  selector: "[appHasPermission]",
})
export class HasPermissionDirective implements OnInit, OnDestroy {
  @Input("appHasPermission") permission: string;
  destroy$ = new Subject<void>();

  constructor(
    private readonly templateRef: TemplateRef<any>,
    private readonly viewContainer: ViewContainerRef,
    private readonly store: Store
  ) {}

  ngOnInit() {
    this.store
      .select(selectHasPermission(this.permission))
      .pipe(takeUntil(this.destroy$))
      .subscribe((hasPermission) => {
        if (hasPermission) {
          this.viewContainer.createEmbeddedView(this.templateRef);
        } else {
          this.viewContainer.clear();
        }
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
  }
}
```

And then in the template:

```html
<a *appHasPermission="'CreateOrder'" routerLink="/orders/create">
  Create Order
</a>
```

## Conclusion

Access restriction has a very improtant role in multiple applications, especially enterprise apps. NgRx is a great tool for handling authorization and permissions, and it can be used in a variety of ways. In this article, we have covered the most common use cases, but there are many more, so never hesitate to explore new ways of doing things with NgRx.