Open Closed

After switching between tenants, the navigation menu items are hidden as expected, but later not shown back #5637


User avatar
0
alexander.nikonov created
  • ABP Framework version: v7.0.1
  • UI Type: Angular
  • Database System: EF Core (Oracle)
  • Auth Server Separated

After switching the tenant I need to reload navigation menu according to the current tenant's user permissions. I cannot manage to do this: when I switch to the tenant who does not have the proper permissions - the menu items are hidden. However, when I switch back - the menu items which need to be visible are not shown:

      switch(tenant: Models.Common.Lookup<string>) {
        this.oAuthService.configure(this.environment.oAuthConfig);
        let loadingToasterId = Number(this.toaster.info('::Tenants:PleaseWait', '::Tenants:Switching', { ...this.toasterOptions && { sticky: true } }));
        return from(this.oAuthService.loadDiscoveryDocument())
          .pipe
          (
            switchMap(() => from(this.oAuthService.fetchTokenUsingGrant('switch_tenant', { token: this.oAuthService.getAccessToken(), tenant: tenant.id }))),
            take(1)
          )
          .subscribe({
            complete: () => {
              this.toaster.remove(loadingToasterId);
              this.toaster.success('::Tenants:SwitchingSucceeded', '::Tenants:Switching', { ...this.toasterOptions && { life: 2000 } });
              this.sessionStateService.setTenant({ id: tenant.id, name: tenant.displayName, isAvailable: true } as CurrentTenantDto);
              this.configStateService.refreshAppState().subscribe(x => {
                this.router.navigate(['/'], { skipLocationChange: false, onSameUrlNavigation: 'reload' }).then(ready => {
                  if (ready) {
                    //TODO: is it really needed? What to do here?
                  }
                });
              });
            },
            error: () => {
              this.toaster.remove(loadingToasterId);
              this.toaster.error('::Tenants:SwitchingFailed', '::Tenants:Switching', { ...this.toasterOptions && { life: 2000 } });
            }
          });
      }
      

The method which is triggered after switching the tenant:

      init() {
        this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
        this.routesService.refresh();
        combineLatest([this.configStateService.getDeep$('extraProperties.modulePermissionMap'), this.routesService.flat$.pipe(take(1))])
          .pipe
          (
            filter(([modulePermissionMap, route]) => Object.keys(modulePermissionMap).length > 0 && Object.keys(route).length > 0),
            takeUntil(this.destroy)
          )
          .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.routesService.remove(permissionProhibitedPageIds);
            this.routesService.refresh(); //tried this, but it does not help - nonLazyLoadedRoute seem to contain menu items, however they are not displayed as expected
          });
      }
      

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) {
            if (!this.appSettingInitialized) {
              this.abxTenantSwitcher.init(); // tenant switching functionality
              this.modulePermissionHandler.init(); // custom permission handling - init method shown previously
              this.appSettingInitialized = true;
            }
          }
        });
      }

13 Answer(s)
  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi,

    Can you provide the tenant switching context like after a tenant switch are you reloading the page? because once you do this.routesService.remove(permissionProhibitedPageIds);

    then this code nonLazyRouteItem.invisible = false; will not work on the items that you have removed.

    if you can provide step to reproduce this i can look into it more clearly.

  • User Avatar
    0
    alexander.nikonov created

    Can you provide the tenant switching context like after a tenant switch are you reloading the page?

    After a successful tenant switch I'm calling the piece of code in complete callback (see the switch method code above). Also - due do the fact that switching a tenant causes changing the value of this.currentUser$ observable - the if (currentUser?.isAuthenticated) part is called (also shown above). Eventually, the aim of init method (as shown above too) is to filter out the available menu items according to user permission map in this.configStateService.getDeep$('extraProperties.modulePermissionMap') (you proposed this approach - using extraProperties- in another ticket and it was a good suggestion, everything works good, except when switching to another tenant).

    Probably my code is incorrect, but I cannot share the project, sorry. So I only can write what I'd like to do:

    when I switch to another tenant - the left navigation menu is refreshed and displays only those items which are available according to this current tenant users' permissions. Also I need to navigate to root (home) page, because it is the only page which is guaranteed to be available no matter what permissions are.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    From the code that you have provided I guess menu items are not visible. because you have removed it from the RouteService (this.routesService.remove(permissionProhibitedPageIds);) so now routes service doesn't have those menu. in the previous question I asked you "are you doing a reload after tenant switch?" i am guessing you are just using router to route to default root page. can you replace following code

     this.router.navigate(['/'], { skipLocationChange: false, onSameUrlNavigation: 'reload' }).then(ready => {
                      if (ready) {
                        //TODO: is it really needed? What to do here?
                      }
                    });
    

    with this

    window.location.href = '/'
    
  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali.

    Thank you for the response.

    First - window.location.href = '/' does not help. The menu is still empty. But I was already able to make it work this way before using window.location.reload() or something like that. However, I want to avoid this approach. The reason is that this Angular thing is a Single Page Application and it does not look nice to reload a whole page instead of just refreshing a navigation menu - this is exactly what I want to get.

    The idea is the following: after I receive this.configStateService.getDeep$('extraProperties.modulePermissionMap') from server, I want to make decision myself, what to show in the menu and what - not. Since ABP builds its menu based on ordinary permissions - I had to use the approach with

    this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
    this.routesService.refresh();
    

    to hide ALL the menu items from the very beginning and then start applying my logic (combineLatest([this.configStateService.getDeep$('extraProperties.modulePermissionMap'), this.routesService.flat$.pipe(take(1))])). Probably my approach is incorrect - then please advice me how to obtain my goal.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi,

    can you create a new abp io solution reproducing the issue and share it on support@abp.io with ticket id mentioned. that way i can provide you with the best approach.

  • User Avatar
    0
    alexander.nikonov created

    I'm afraid I can't do that. This is a commercial project - I can just show some pieces of code. Besides - we don't even have SQL Server Express setup (we use another DB) and we don't use test ABP app generation, so all preparation work would take a way more time than we are given for bug-fixing.

    Probably you have some ready testing environment at your side set up? In this case even tenant switching functionality itself can be omitted - you just need to rerun this API call (by the UI button?):

    And in the extra-properties return something like hidePageMap dictionary: { [moduleId of the page]: true, [moduleId of the page]: true } - moduleIDs can be randomly selected from the predefined collection you use (see below), so it imitates different users permissions when switching tenants.

    Module ID for any relevant page is supplied (and read in Angular app) here: and here:

    So each time you click the button and return result from application configuration - the menu needs to be shown according to the "visible" module IDs only.

    P.S. Maybe in future I will ask our management to give us time for building original ABP solution test environment to be able to create bug reproduction scenarios, so it will be easier for all.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi,

    we need to check this remotely, as we are unable understand the tenant switch scenario. is that okay with you?

  • User Avatar
    0
    alexander.nikonov created

    Hi,

    we need to check this remotely, as we are unable understand the tenant switch scenario. is that okay with you?

    As I wrote before, probably to reproduce the issue, tenant switching is not needed: it can be just UI button click which triggers https://localhost:44337/[test-app]/application-configuration?includeLocalizationResources=false call. Where the overriden AbpApplicationConfigurationAppService GetAsync() method returns the mentioned structure in extraProperties, so if some Module ID has false - the relevant page needs to be removed from ABP navigation menu. If the Module ID is not there - the page needs to be shown again. And this structure is randomly generated on each button click (giving different Module IDs from the existing ones), imitating tenant switching. Does it make sense to you?

    Anyway, our policies do not allow us to share the code or show it via screen-sharing... Sorry.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    HI,

    thanks for the information we will reproduce this on our end and get back to you asap

    Thanks

  • User Avatar
    1
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    can you try replacing

      if (moduleIdPolicyViolated && ordinaryRolePolicyViolated) {
                      permissionProhibitedPageIds.push(nonLazyRouteItem.name);
                    }
                    else {
                      nonLazyRouteItem.invisible = false;
                    }
    

    with below code

      if (!(moduleIdPolicyViolated && ordinaryRolePolicyViolated)) {
                                   nonLazyRouteItem.invisible = false;
                    }
    

    and remove the this.routesService.remove(permissionProhibitedPageIds);

  • User Avatar
    0
    alexander.nikonov created

    Thank you! I've also removed this.routesService.refresh(); both occurences and seems to be working fine now.

  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali.

    I have to reopen this ticket, because without using this.routesService.refresh() it appeared, that sometimes just setting items to invisible is not enough: menu is still shown complete (suprisingly - sometimes it DOES work properly - when navigating to specific page menu is rebuilt according to invisibility of items). But using this.routesService.refresh() makes combineLatest trigger again. Could you please suggest the changed code - where the menu always refreshes properly, but there are no extra unnecessary invocations?

          init() {
            this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
            this.routesService.refresh(); // HAVE TO USE IT to make invisibility to have effect
            combineLatest([this.configStateService.getDeep$('extraProperties.modulePermissionMap'), this.routesService.flat$.pipe(take(1))])
              .pipe
              (
                filter(([modulePermissionMap, route]) => Object.keys(modulePermissionMap).length > 0 && Object.keys(route).length > 0),
                takeUntil(this.destroy)
              )
              .subscribe(([modulePermissionMap, nonLazyLoadedRoute])  => {
                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) {
                      nonLazyRouteItem.invisible = false;
                    }
                  }
                });
                this.routesService.refresh(); // HAVE TO USE IT to make invisibility to have effect, but combineLatest is called again... :(
            });
          }
          
    

    UPDATE: this is a modified version - does it look ok?

          init() {
            this.routesService.flat.filter(x => x.requiredPolicy && (x as any).data?.moduleId).forEach(x => x.invisible = true);
            this.routesService.refresh();
            let routeStateBefore = JSON.stringify(this.routesService.flat, ['name', 'invisible']);
            combineLatest([this.configStateService.getDeep$('extraProperties.modulePermissionMap'), this.routesService.flat$.pipe(take(1))])
              .pipe
              (
                filter(([modulePermissionMap, route]) => Object.keys(modulePermissionMap).length > 0 && Object.keys(route).length > 0),
                takeUntil(this.destroy)
              )
              .subscribe(([modulePermissionMap, nonLazyLoadedRoute])  => {
                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) {
                      nonLazyRouteItem.invisible = false;
                    }
                  }
                });
                let routeStateNow = JSON.stringify(this.routesService.flat, ['name', 'invisible']);
                if (routeStateNow !== routeStateBefore) {
                  routeStateBefore = routeStateNow;
                  this.routesService.refresh();
                }
              });
          }
    
  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello

    does the updated solution work?

Made with ❤️ on ABP v9.1.0-preview. Updated on December 13, 2024, 06:09