ERAS Role Management ‐ Frontend Implementation - JU-DEV-Bootcamps/ERAS GitHub Wiki

Route Validation

In order to prevent access to certain routes of ERAS application, a route guard based on Keycloak createAuthGuard function was created. The canActivateAuthRole guard checks for the required roles to access a route provided in the route metadata and returns true or false depending on the logged user role.

DOM Validation

To conditionally show or hide a DOM element based on the logged user role, the HasERASRolesDirective was created.

This directive takes the role from the logged user using the UserDataService.user signal data and renders the DOM element if the role matches the input roles provided to the directive.

To use it, add the HasErasRolesDirecive to the imports list of the component and then add the directive to the desired element. It is suggested to create a ViewPermissions object inside the component to map the roles and reference the object in the template.

  • ViewPermissions type

type ViewPermissions = Record<string, ERASRoles[]>;

  • ViewPermissions object
viewPermissions: ViewPermissions = {
    platformSettings: [ERASRoles.ADMIN, ERASRoles.OFFICER],
  };
  • Usage of appHasERASRoles
<button
      *appHasERASRoles="viewPermissions['platformSettings']"
      mat-menu-item
      class="menu-button option-button"
      (click)="redirectToSettings()"
    >
      <mat-icon>settings</mat-icon>
      Platform Settings
    </button>

Component Validation

To determine whether the logged user has permissions to execute an action, permission policies were implemented in the application.

This pattern builds on a Permissions Service that gets the logged user role and evaluates a matching function in the permissions map. The permissions map is a constant of type Record<ERASPermissions, PermissionCheck> where each ERASPermissions key has a PermissionsCheck function that evaluates to a boolean based on the specific validation logic for each permission.

To use permission policies, inject the PermissionsService into the component and then call its can method with the ERASPermissions permission to check and use the result accordingly.

const PermissionChecks: Record<ERASPermissions, PermissionCheck> = {
  CAN_CREATE_PROFESSIONALS: (role, requiredRoles) =>
    requiredRoles.includes(role),
}
//permissions.service.ts

@Injectable({
  providedIn: 'root',
})
export class PermissionsService {
  private userDataService = inject(UserDataService);
  private userRole = computed(() => this.userDataService.user()?.role);

  /**
   * Checks if current user can execute the passed action based on their role.
   * @param permission the action to be evaluated.
   * @param context object containing extra information to validate.
   * @returns { boolean } `true` if user has required role; `false` otherwise.
   */
  can(permission: ERASPermissions, context?: PermissionContext): boolean {
    const currentUserRole = this.userRole();
    if (!currentUserRole) return false;

    const requiredRoles = PermissionsRoles[permission];
    return PermissionChecks[permission](
      currentUserRole,
      requiredRoles,
      context
    );
  }
}
// assessments.component.ts

private readonly permissionsService = inject(PermissionsService);
this.permissionsService.can(ERASPermissions.CAN_CREATE_PROFESSIONALS)

Role-based Fetching

To make a specific fetch request based on the logged user role, a role-based fetch resolver was implemented. Similar to the permissions policies pattern, the role-based fetching builds upon a RoleBasedFetchResolver whose resolve method gets the logged user role, finds a matching function in a stategies map and then invokes it against the given service.

The strategies map is of type Record<ERASRoles, RoleFetchStrategy<TService, TResult>> where the RoleFetchStrategy is a function that receives the reference to a service and a FetchContext object to pass along parameters to the fetch methods. This function should return an Observable of TResult.

// interventions-fetch.strategies.ts

export const InterventionsFetchStrategies: RoleFetchStrategyMap<
  InterventionService,
  InterventionModel[]
> = {
  [ERASRoles.ADMIN]: (service, context) =>
    context?.assessmentId
      ? service.getByAssessment(context.assessmentId)
      : throwError(() => new Error('Assessment ID not provided.')),
}
// role-based-fetch.resolver.ts

@Injectable({ providedIn: 'root' })
export class RoleBasedFetchResolver {
  private userDataService = inject(UserDataService);
  private user = computed(() => this.userDataService.user());

  resolve<TService, TResult>(
    service: TService,
    strategies: RoleFetchStrategyMap<TService, TResult>,
    contextOverride?: FetchContext
  ): Observable<TResult> {
    const currentUser = this.user();

    if (!currentUser)
      return throwError(() => new Error('User is not authenticated.'));

    const role = currentUser.role;
    if (!role)
      return throwError(() => new Error('User does not have a role assigned.'));

    const strategy = strategies[role];

    return strategy(service, {
      currentUserId: currentUser.id ?? '',
      ...contextOverride,
    });
  }
}
// intervention-list.component.ts

private readonly fetchResolver = inject(RoleBasedFetchResolver);
this.fetchResolver
      .resolve(this.interventionService, InterventionsFetchStrategies, {
        assessmentId,
      })
      .subscribe();
⚠️ **GitHub.com Fallback** ⚠️