How to create a directive in Angular that renders components only if user is authorized?

gassaihamza

Gassai Hamza

Posted on January 18, 2023

How to create a directive in Angular that renders components only if user is authorized?

Recently, I was working on an Angular project where certain components were public meaning that everyone should see them and others should be rendered only if the user is authenticated or have a specific role, so I was doing something like this:

<!-- file name: offers.component.html -->
<div>
    <app-nav-bar></app-nav-bar>
    <app-profile-image *ngIf="user.isAuthenticatd"
    ></app-profile-image>
    <div>
        <app-search></app-search>
        <app-offers-list></app-offers-list>
    </div>
    <div>
        <app-review-offer *ngIf="user?.roles.include('AGENT')"
        ></app-review-offer>
    </div>
<div>

Enter fullscreen mode Exit fullscreen mode

This doesn't sounds clean to me, especially the last one: *ngIf="user?.roles.include('AGENT')"

Thankfully Angular gives us the ability to create our custom directives. so let's create one!

First, we need to create a class and annotate it with @Directive annotation (Angular CLI made it easy 😋)

ng generate directive authorized 
# or use the shourhand: ng g d authorized

Enter fullscreen mode Exit fullscreen mode

The above command will generate a class as follows:

// file name: authorized.directive.ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[appAuthorized]'
})
export class AuthorizedDirective {

  constructor() { }

}

Enter fullscreen mode Exit fullscreen mode

Remember we need to pass/bind the role as an input to use it in our directive something like this:

<div>
    <app-review-offer *appAuthorized="'AGENT'"></app-review-offer>
</div>

Enter fullscreen mode Exit fullscreen mode

Like in component classes, we will create a property and annotate it with the @Input() decorator.

// file name: authorized.directive.ts
import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[appAuthorized]'
})
export class AuthorizedDirective {
  @Input appAuthorized

  constructor() { }

}

Enter fullscreen mode Exit fullscreen mode

Now we want to perform our login whenever the appAuthorized input is changed, for this, we will use a setter:

// file name: authorized.directive.ts
import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[appAuthorized]'
})
export class AuthorizedDirective {
  @Input set appAuthorized(role: string){
      console.log(role) // this will print: AGENT
  }

  constructor() { }

}

// file name: offers.component.html
//...
<div>
    <app-review-offer *appAuthorized="'AGENT'"></app-review-offer>
</div>
//...
//...

Enter fullscreen mode Exit fullscreen mode

The above code will simply print AGENT in the console

** Note that appAuthorized is still a property learn more about component-interaction

To proceed we need to inject some objects:

  1. TemplateRef : to access our template

  2. ViewContainerRef : to create a new embedded view.

  3. AuthService: This one could be different from one project to another, for me, I have a service called authService where I had stored the currently logged-in user with all its information such as username, roles...

import {Directive, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {AuthService} from "../services/auth/auth.service";
import {Principal} from "../models/principal";

@Directive({
  selector: '[appAuthorized]'
})
export class AuthorizedDirective {

   @Input set appAuthorized(role: string){
      console.log(role) // this will print: AGENT
   }

  constructor(
    private auth: AuthService,
    private templateRef: TemplateRef<any>,
    private viewContainerRef: ViewContainerRef
  ) { }

}

Enter fullscreen mode Exit fullscreen mode

And finally, add the logic that handles the authorization:

import {Directive, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {AuthService} from "../services/auth/auth.service";
import {Principal} from "../models/principal";

@Directive({
  selector: '[appAuthorized]'
})
export class AuthorizedDirective {

  @Input() set appAuthorized(role: string){
    this.auth.principal$.subscribe({
      next:(value)=>{
        // Check if user is authenticated
        if(!(<Principal>value).authenticated){
          this.viewContainerRef.clear();
          return;
        }

        // Check if the user have the given role
        if (role !== '*' && !(<Principal>value).roles.includes(`ROLE_${role}`)){
          this.viewContainerRef.clear();
          return;
        }

        this.viewContainerRef.createEmbeddedView(this.templateRef)
      }
    })

  }

  constructor(
    private auth: AuthService,
    private templateRef: TemplateRef<any>,
    private viewContainerRef: ViewContainerRef

  ) { }

}

Enter fullscreen mode Exit fullscreen mode

As you noticed in the above code we used the clear method this.viewContainerRef.clear() to destroy the view if the user is not authenticated or doesn't have the desired role.

We also used this.viewContainerRef.createEmbeddedView(this.templateRef) to instantiate an embedded view and inserts it into our container.

Now we can use our directive anywhere in our application:

<!-- file name: offers.component.html -->
<div>
    <app-nav-bar></app-nav-bar>
    <app-profile-image *appAuthorized="'*'"
    ></app-profile-image>
    <div>
        <app-search></app-search>
        <app-offers-list></app-offers-list>
    </div>
    <div>
        <app-review-offer *appAuthorized="'AGENT'"
        ></app-review-offer>
    </div>
<div>

Enter fullscreen mode Exit fullscreen mode

That was it, I hope you found it helpful. If you have any feedback or suggestions for improvements, I would like to hear from you.

💖 💪 🙅 🚩
gassaihamza
Gassai Hamza

Posted on January 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related