How I created a directive in Angular that renders components only if user is authorized?

ยท

3 min read

Table of contents

No heading

No headings in the article.

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>

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

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() { }

}

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>

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() { }

}

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>
//...
//...

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

    Note that:

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

    Is a shourthand of:

    <ng-template>

    <app-review-offer [appAuthorized]="'AGENT'"></app-review-offer>
    </ng-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
  ) { }

}

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

  ) { }

}

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>

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.

ย