Angular - Conceal Protected Links on the Template

In my AuthGuard, I have implemented CanActivate which returns either true or false based on custom logic:

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthService } from '../services/auth.service';

import { UserTypes } from '../model/user-type';

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

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const allowedRoles = route.data['allowedRoles'] as Array<UserTypes>;
        if (!allowedRoles) {
            // All users allowed
            return true;
        }
        // Check user's role against the allowed roles defined
        const canActivate = (allowedRoles.indexOf(this.authService.userData.UserTypeId) !== -1);
        if (!canActivate) {
            this.router.navigate(['/portal']);
        }
        return canActivate;
    }
}

I require both canActivate parameters (

route: ActivatedRouteSnapshot, state: RouterStateSnapshot
) for processing (not all code is shown).

My routes are set up with specific data indicating which user types can access a particular route. This works perfectly when attempting to access routes:

const routes: Routes = [
  {
    path: '',
    component: PortalLayoutComponent,
    children: [
      {
        path: '',
        canActivate: [AuthGuard],
        component: PortalDashboardComponent
      },
      {
        path: 'customers',
        canActivate: [AuthGuard],
        data: { allowedRoles: [UserTypes.Admin] },
        children: [
          { path: '', component: PortalCustomerListComponent }
        ]
      }
    ]
  }
];

If canActivate returns false for any given link, I want to hide that link in the template. I am unsure of how to achieve this. Here's an example of a typical link in a template:

<a [routerLink]="['customers']">List Customers</a>

How can I disable this without duplicating the user type logic present in the AuthGuard? I have tried injecting AuthGuard into my components but struggle with providing the necessary params for canActivate. Below is a sample component with some test code I am working on:

import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Constants } from '../../app.constants';

import { AuthGuard } from '../../guards/auth.guard';

@Component({
  templateUrl: './portal-dashboard.component.html',
  styleUrls: ['./portal-dashboard.component.scss']
})
export class PortalDashboardComponent {
  constructor(
    private authGuard: AuthGuard,
    private activatedRoute: ActivatedRoute,
    private router: Router
  ) { }

  public get canActivateLink(targetUrl: string): boolean {
    **// TODO: Use targetLink here to get user role rules from the route data?** 
    return this.authGuard.canActivate(this.activatedRoute.snapshot, this.router.routerState.snapshot);
  }
}

If I am able to make the above code work, I could simply do the following in the template (although not completely DRY, it is better than repeating the role logic in every link controller):

<a [routerLink]="['customers']" *ngIf="canActivateLink('customers')">List Customers</a>

UPDATE

Thanks to Yordan, I have managed to turn this into a directive. However, I still face the same challenge of easily obtaining Route information (specifically the route data) for a specific URL. Here's where I stand currently:

import { Input, OnInit, Directive, ViewContainerRef, TemplateRef } from '@angular/core';
import { LocationStrategy } from '@angular/common';
import { Router, ActivatedRoute, UrlTree } from '@angular/router';

import { AuthService, AuthState } from '../../services/auth.service';

import { UserTypes } from '../../model/user-type';

@Directive({
    selector: '[hiddenIfUnauthorised]'
})
export class HiddenIfUnauthorisedDirective implements OnInit {

    private commands: any[] = [];

    constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef,
        private locationStrategy: LocationStrategy,
        private router: Router,
        private route: ActivatedRoute,
        private auth: AuthService
    ) { }

    @Input()
    set hiddenIfUnauthorised(commands: any[] | string) {
        if (commands != null) {
            this.commands = Array.isArray(commands) ? commands : [commands];
        } else {
            this.commands = [];
        }
        console.log(this.commands);
    }

    get urlTree(): UrlTree {
        // TODO: Mimic the rest of the relevant options as per the RouterLink source
        return this.router.createUrlTree(this.commands, {
            relativeTo: this.route
        });
    }

    public ngOnInit() {
        const urlTree = this.urlTree;
        const sUrl = this.router.serializeUrl(urlTree);
        const url = this.locationStrategy.prepareExternalUrl(sUrl);

        // TODO: I need to generate an ActivatedRoute object for the "url" created above
        // so I can get 'allowedRoles' from route data - or get it some other way.
        // Are there any helper methods anywhere?
        const targetRoute = this.route;

        const userTypes = targetRoute.data['allowedRoles'] as Array<UserTypes>;

        const authState = this.auth.getAuthState(userTypes);

        if (authState !== AuthState.Authorised) {
            // Not authorised, remove the DOM container
            this.viewContainer.clear();
        } else {
            // Show the DOM container
            this.viewContainer.createEmbeddedView(this.templateRef);
        }
    }
}

This directive is registered in my module and utilized in a template like so:

<div *hiddenIfUnauthorised="['customers']">
Secure Content!
</div>

Still searching for a solution. Any ideas would be greatly appreciated.

Answer №1

Below is a snippet of code demonstrating the concept

import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[permission]' })
export class IfDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private auth: AuthService
    ) { }

  @Input() permission: string;

  public ngOnInit() {
    if(this.auth.hasPermission) {
      // Render template in the DOM if condition is met
      this.viewContainer.createEmbeddedView(this.templateRef);
     } else {
     // Remove template from the DOM if condition is not met
      this.viewContainer.clear();
    }
  }

}

Modified version

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the process for retrieving paginated data from the store or fetching new data from an API service within an Angular 2 application using ngrx-effects?

After coming across this insightful question and answer about the structure of paginated data in a redux store, I found myself pondering how to implement similar principles using ngrx/store in an angular 2 application. { entities: { users: { 1 ...

Error: Attempting to modify a constant value for property 'amount' within object '#<Object>'

After fetching data from an API, I stored an array in a state. Upon trying to update a specific field within an object inside the array using user input, I encountered the error message: 'Uncaught TypeError: Cannot assign to read only property 'a ...

What could be causing the conditional div to malfunction in Angular?

There are three conditional div elements on a page, each meant to be displayed based on specific conditions. <div *ngIf="isAvailable=='true'"> <form> <div class="form-group"> <label for ...

A guide to iterating over an array and displaying individual elements in Vue

In my application, there is a small form where users can add a date with multiple start and end times which are then stored in an array. This process can be repeated as many times as needed. Here is how the array structure looks: datesFinal: {meetingName: ...

What is the best way to alter the Date format in Typescript?

Within my response, the field createdate: "2019-04-19T15:47:48.000+0000" is of type Date. I am looking to display it in my grid with a different format such as createdate: "19/04/2019, 18:47:48" while maintaining its original data type. To achieve this, I ...

Angular: Execute a function once all BehaviorSubject subscriptions have been initialized

In order to facilitate the sharing of parameters across components in my Angular application, I have implemented a "global" service as shown below: import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs/BehaviorSu ...

Angular 8 does not allow for the assignment of type '{}' to a parameter

I have a unique approach for managing errors: private handleErrors<T>(operation = 'operation', result?: T) { return (error: any): Observable<T> => { console.error(error); this.record(`${operation} failed: ${error.m ...

What is the best way to free up memory after receiving responseText in a continuous streaming request?

Utilizing xmlHTTPRequest to fetch data from a continuous motion JPEG data stream involves an interesting trick where responseText can populate data even before the request is completed, since it will never actually finish. However, I have encountered some ...

What are the disadvantages of nesting CSS Grids within each other?

I have been exploring component-driven front-end frameworks like Angular and honing my skills in CSS Grid. My query is: Is it considered a bad practice to nest CSS Grids? In my main/root component, I have utilized CSS grid to create two elements: the nav ...

What is the best way to work with the INITIAL_STATE injection token when it has dependencies

I have a dynamic module that loads lazily and uses ngrx/store with the feature StoreModule.forFeature('tasks', tasksReducer). To initialize this module, I need to set some initial values that are obtained through dependency injection from another ...

How can we make type assertions consistent without sacrificing brevity?

In the project I am currently working on, we have implemented a warning for typescript-eslint/consistent-type-assertions with specific options set to { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }. While I generally appr ...

The sequence of activities within the Primeng Multiselect component

Lately, I've encountered an issue while using the multiselect component from the primeng library in Angular. Everything was going smoothly until I noticed a strange problem with the order of events. Here is an example that showcases the issue: https:/ ...

Employing an unchanging Map format for observation

I'm currently working on implementing a synchronization mechanism using observable and Map structures from Immutable.js. However, I'm encountering an issue where the Map is unable to function as an observable or perhaps I might be approaching it ...

There are no HTTP methods available in the specified file path. Make sure to export a distinct named export for each HTTP method

Every time I attempt to run any code, I encounter the following error message: No HTTP methods exported in 'file path'. Export a named export for each HTTP method. Below is the content of my route.ts file: import type { NextApiRequest, NextApi ...

Calculating the Angular Sum of Multiple Input Fields

There are multiple input fields on this page. <div class="form-group w-100"> <label class="col-md-3 text-left" for="">Box 2</label> <input class="form-control ml-2 mr-2" [value]="MenuBox2" [style.backgrou ...

Guide on defining a data type for the response payload in a Next.js route controller

interface DefaultResponse<T> { success: boolean; message?: string; user?: T; } export async function POST(req: Request) { const body: Pick<User, 'email' | 'password'> = await req.json(); const user = await prisma ...

Leverage the Angular2 component property when initializing a jQuery function

I'm currently developing a web app with Angular 2 and utilizing jQuery autocomplete. When making requests to the remote server for completion data, I found that the server address is hardcoded in the autocomplete function. Even though I tried using co ...

Mocking callback or nested function with jest

My code snippet looks like this: public async waitForElementSelected(element: WebdriverIO.Element) { /** * Maximum number of milliseconds to wait for * @type {Int} */ const ms = 10000; await browser.waitUntil(async () =>{ ...

Retrieve a formatted item from a JSON document

Within my Next.js project, I have implemented a method for loading translations and passing them into the component. Here is an example: import "server-only"; import i18nConfig from "../../i18n-config"; const dictionaries = { en: () ...

Angular: Excessive mat-menu items causing overflow beyond the page's boundaries

Within my mat-menu, there is a div for each mat-menu item. The number of items varies depending on the data retrieved. However, I noticed that when there are more items than can fit in the menu, it extends beyond the boundaries of the mat-menu instead of ...