File

src/app/core/permissions/permission-guard/abstract-permission.guard.ts

Description

Abstract base class common to all guards that check configurable user permissions or roles.

Use this as an injection token to provide guards from feature modules that should be used to hide menu items.

Index

Properties
Methods

Methods

Protected Abstract canAccessRoute
canAccessRoute(routeData: DynamicComponentConfig)

Implement specific permission checks here, based on the given route data (from config) and any required services provided by Angular dependency injection.

Parameters :
Name Type Optional Description
routeData DynamicComponentConfig No

The route data object defined either in routing code or loaded from config by the RouterService.

Returns : Promise<boolean>
Async canActivate
canActivate(route: ActivatedRouteSnapshot)

Check if current navigation is allowed. This is used by Angular Router.

Parameters :
Name Type Optional
route ActivatedRouteSnapshot No
Returns : Promise<boolean>
Public checkRoutePermissions
checkRoutePermissions(path: string)

Pre-check if access to the given route would be allowed. This is used by components and services to evaluate permissions without actual navigation.

Parameters :
Name Type Optional
path string No
Returns : Promise<boolean>
Protected Async ensureAbilityInitialized
ensureAbilityInitialized()
Returns : Promise<void>
import {
  ActivatedRouteSnapshot,
  CanActivate,
  Route,
  Router,
} from "@angular/router";
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
import { inject, Injectable } from "@angular/core";
import { EntityAbility } from "../ability/entity-ability";

/**
 * Abstract base class common to all guards that check configurable user permissions or roles.
 *
 * Use this as an injection token to provide guards from feature modules that should be used to hide menu items.
 */
@Injectable()
export abstract class AbstractPermissionGuard implements CanActivate {
  protected readonly router = inject(Router);
  protected readonly ability = inject(EntityAbility, { optional: true });

  protected async ensureAbilityInitialized(): Promise<void> {
    if (this.ability && !this.ability.initialized) {
      await new Promise((res) => this.ability.on("updated", res));
    }
  }

  /**
   * Check if current navigation is allowed. This is used by Angular Router.
   * @param route
   */
  async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
    const routeData: DynamicComponentConfig = route.data;
    if (await this.canAccessRoute(routeData)) {
      return true;
    } else {
      if (route instanceof ActivatedRouteSnapshot) {
        // Route should only change if this is a "real" navigation check (not the check in the NavigationComponent)
        this.router.navigate(["/404"]);
      }
      return false;
    }
  }

  /**
   * Implement specific permission checks here, based on the given route data (from config)
   * and any required services provided by Angular dependency injection.
   *
   * @param routeData The route data object defined either in routing code or loaded from config by the RouterService.
   * @protected
   */
  protected abstract canAccessRoute(
    routeData: DynamicComponentConfig,
  ): Promise<boolean>;

  /**
   * Pre-check if access to the given route would be allowed.
   * This is used by components and services to evaluate permissions without actual navigation.
   *
   * @param path
   */
  public checkRoutePermissions(path: string) {
    let routeData = this.getRouteDataFromRouter(path, this.router.config);
    return this.canAccessRoute(routeData?.data);
  }

  /**
   * Extract the relevant route from Router, to get a merged route that contains the full trail of `permittedRoles`
   * @param path
   * @param routes
   * @private
   */
  private getRouteDataFromRouter(path: string, routes: Route[]) {
    // removing leading slash
    path = path.replace(/^\//, "");

    function isPathMatch(genericPath: string, path: string) {
      const routeRegex = genericPath
        .replace(/\*/g, ".*") // allow for wildcard routes in regex
        .split("/")
        // replace params with wildcard regex
        .map((part) => (part.startsWith(":") ? "[^/]*" : part))
        .join("/");
      return path.match("^" + routeRegex + "[/.*]*$");
    }

    const pathSections = path.split("/");
    let route = routes.find((r) => isPathMatch(r.path, path));
    if (!route && pathSections.length > 1) {
      route = routes.find((r) => isPathMatch(r.path, pathSections[0]));
    }

    if (route?.children) {
      const childRoute = this.getRouteDataFromRouter(
        pathSections.slice(1).join("/"),
        route.children,
      );
      if (childRoute) {
        childRoute.data = { ...route.data, ...childRoute?.data };
        route = childRoute;
      }
    }

    return route;
  }
}

results matching ""

    No results matching ""