ERAS Role Management ‐ Frontend Implementation - JU-DEV-Bootcamps/ERAS GitHub Wiki
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.
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.
-
ViewPermissionstype
type ViewPermissions = Record<string, ERASRoles[]>;
-
ViewPermissionsobject
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>
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)
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();