Thank you very much. I will try to improve my implementation using this change. I think I may close the ticket now.
OK - so I abandoned the idea to override the RoutesService
and implemented the following handler (retaining the idea to hide the elements ASAP and unhide them after getting service data) - please have a look if there could be a more efficient solution.
import { ABP, RoutesService } from '@abp/ng.core';
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, ReplaySubject, Subscription, combineLatest } from 'rxjs';
import { distinctUntilChanged, filter, takeLast, takeUntil } from 'rxjs/operators';
import { ModulePermissionsService } from '../services/module-permissions.service';
@Injectable({
providedIn: 'root'
})
export class ModulePermissionHandler implements OnDestroy {
modulePermissionMap$: BehaviorSubject<{[key: string]: string[]}> = new BehaviorSubject<{[key: string]: string[]}>({});
private destroy: ReplaySubject<any> = new ReplaySubject<any>(1);
private stopMenuScan$: ReplaySubject<any> = new ReplaySubject<any>(1);
private mapBindingComplete: boolean = false;
constructor(
private routesService: RoutesService,
private modulePermissionsService: ModulePermissionsService
) {
}
ngOnDestroy(): void {
this.deinit();
}
init() {
this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
this.routesService.refresh();
combineLatest([this.modulePermissionMap$, this.routesService.flat$])
.pipe
(
filter(result => !this.mapBindingComplete && Object.keys(result[0]).length > 0 && Object.keys(result[1]).length > 0),
takeUntil(this.destroy),
takeUntil(this.stopMenuScan$)
)
.subscribe(([modulePermissionMap, nonLazyLoadedRoute]) => {
let permissionProhibitedPageIds: string[] = [];
nonLazyLoadedRoute.filter(node => node.requiredPolicy).forEach((nonLazyRouteItem: ABP.Route) => {
let moduleId = (nonLazyRouteItem as any).data?.moduleId;
if (moduleId) {
const moduleIdPolicyViolated = !modulePermissionMap[moduleId] || modulePermissionMap[moduleId] && !modulePermissionMap[moduleId].includes(nonLazyRouteItem.requiredPolicy as string);
const ordinaryRolePolicyViolated = !modulePermissionMap['_ordinaryRole'] || modulePermissionMap['_ordinaryRole'] && !modulePermissionMap['_ordinaryRole'].includes(nonLazyRouteItem.requiredPolicy as string);
if (moduleIdPolicyViolated && ordinaryRolePolicyViolated) {
permissionProhibitedPageIds.push(nonLazyRouteItem.name);
}
else {
nonLazyRouteItem.invisible = false;
}
}
});
this.stopMenuScan$.next(null);
this.stopMenuScan$.complete();
this.routesService.remove(permissionProhibitedPageIds);
this.mapBindingComplete = true;
this.stopMenuScan$ = new ReplaySubject(1);
});
this.modulePermissionsService.getModulePermissionMap()
.pipe(takeUntil(this.destroy))
.subscribe(modulePermissionMap => {
this.mapBindingComplete = false;
this.modulePermissionMap$.next(modulePermissionMap);
});
}
deinit() {
this.destroy.next(null);
this.destroy.complete();
}
}
Usage - app.component.ts:
ngOnInit(): void {
this.oAuthService.events
.pipe(filter(event => event?.type === 'logout'))
.subscribe(() => {
this.modulePermissionHandler.deinit();
});
this.currentUser$.subscribe(currentUser => {
if (currentUser?.isAuthenticated) {
this.modulePermissionHandler.init();
}
});
}
And - yes - I had also to override PermissionGuard
: I have copy-pasted this from route.provider.ts to app-routing.module.ts:
and check this.modulePermissionMap$
in CanActivate
method.
So far I have not found a better approach than hiding (setting invisible
to an item with requiredPolicy
and moduleId
) in the createTree
method) the items prior to removing or showing them inside subcription of combineLatest
.
UPDATE: i've checked this approach and it has some drawbacks - after filtering the menu structure is broken (some intermediate parent nodes disappeared) and I cannot navigate to the displayed pages with "no matching route" error. I don't know if I am doing something wrong or it is bad idea to override RoutesService
and hide the items in the createTree
call.
Anjali, do you know where should I put my "hide" method? The small issue now is that initially the menu is shown fully - pause for API mapping call - and then it is filtered out accordingly. I want the menu was shown after it has been filtered out...
this.currentUser$.subscribe(currentUser => {
if (currentUser?.isAuthenticated) {
...
this.modulePermissionHandler.init(); // it is too late :(
...
});
I've tried to replace RoutesService
:
export class AbxRoutesService extends AbstractNavTreeService<ABP.Route>
and make filtration inside
public override createTree(items: ABP.Route[]): TreeNode<ABP.Route>[]
But this method is synchronous and I cannot await mapping API call result. On other hand, I don't want to do something about flat$
getter, because the filtration needs to be done just before building the tree.
It is so frustrating...
Hi. Seems like I have eventually nailed it. My steps were:
adding moduleId
to non-lazy node collection in route.provider.ts
(I copied the corresponding values from lazy-loading nodes (path: ''
in the routing module). I applied your as any
recipe, thank you;
returning the following multiple permission result from back-end to be able to handle (hide) nodes in Angular app using mapping from back-end:
public override Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)
{
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToList();
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
return Task.FromResult(new MultiplePermissionGrantResult(permissionNames.ToArray(), PermissionGrantResult.Granted));
}
making API request retrieving the mapping between moduleId
(or all simple roles) and their permissions for a current user (modulePermissionMap$);
filtering out all the menu items which need to be hidden based on modulePermissionMap$, RoutersService.flat$
(combineLatest) and eventually applying RoutersService.remove
method;
I will keep the ticket opened so far - in case our testing team finds something wrong during the nearest time...
Hmm, probably it would work this way. I would give it a try. However, I just discovered I was not right in my suggestions.
ABP RoutesService
does not return all nodes in the menu - it returns only non-lazy nodes. So what I need is to load a whole structure of my menu into a variable and then check each node module ID inside the loop. The question is - what is a proper way to load all the nodes? Are there ready ABP methods for it? Or I need to use Angular methods? Or I need to create my own recursive method? I know it won't be complex, but don't want to re-invent a wheel...
I need to receive all nodes which are present in the menu, both lazy and non-lazy. And obtain data
property of each node.
Angular Router
config DOES return data, but it does not return lazy nodes.
ABP RoutesService
flat$ / tree$ / visible$ DOES return all nodes, but it does not return data
.
So I need something which would return BOTH.
The problem is that this.router.config
does not return lazy-loading routing module nodes where actually my data with moduleId
resides.
It only returns the ordinary nodes.