Merge remote-tracking branch 'origin/dev-latest' into feature/dds-agent-settlement-reports

# Conflicts:
#	src/app/core/shell/shell.component.html
#	src/app/organization/organization-routing.module.ts
#	src/environments/.env.ts
This commit is contained in:
tarunmalviya
2026-09-23 12:11:12 +05:30
605 changed files with 74890 additions and 8182 deletions
@@ -11,6 +11,7 @@ import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angula
/** Custom Services */
import { AuthenticationService } from '../../core/authentication/authentication.service';
import { TenantMenuAccessService } from './tenant-menu-access.service';
/** Environment */
import { environment } from '../../../environments/environment';
@@ -23,6 +24,7 @@ export class HasPermissionDirective {
private templateRef = inject<TemplateRef<any>>(TemplateRef);
private viewContainer = inject(ViewContainerRef);
private authenticationService = inject(AuthenticationService);
private tenantMenuAccessService = inject(TenantMenuAccessService);
/** User Permissions */
private userPermissions: any[];
@@ -75,6 +77,13 @@ export class HasPermissionDirective {
// Menu & Sidebar permissions are ALWAYS enforced (hide/show nav items based on role)
const isMenuPermission = permission.startsWith('MENU_') || permission.startsWith('SIDEBAR_');
// The tenant's own ceiling on which menu sections it may ever show — checked before anything
// else below, ALL_FUNCTIONS included, because ALL_FUNCTIONS bypassing this is exactly the gap
// this exists to close (see TenantMenuAccessApiResource). Non-menu permissions never touch this.
if (isMenuPermission && !this.tenantMenuAccessService.isMenuAllowed(permission)) {
return false;
}
// For non-menu permissions: if RBAC is disabled, show everything (backward compatibility)
if (!this.rbacEnabled && !isMenuPermission) {
return true;
@@ -0,0 +1,61 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of, tap } from 'rxjs';
/**
* The current tenant's own cap on which top-nav menu codes may ever show — see
* `TenantMenuAccessApiResource` on the backend for why this exists separately from a user's own
* permission list: a role carrying `ALL_FUNCTIONS` bypasses every ordinary permission check, so
* `HasPermissionDirective` needs a second, tenant-level list that check cannot see past.
*
* Loaded once per session, into a plain `Set` rather than an `Observable` the directive would have
* to subscribe to — `mifosxHasPermission`'s setter runs synchronously and once per binding, so
* `ShellComponent` loads this before the nav renders (see `ready`) and every directive instance
* afterwards just reads the already-populated set.
*/
@Injectable({ providedIn: 'root' })
export class TenantMenuAccessService {
private http = inject(HttpClient);
/** Menu codes this tenant has explicitly turned off. Absence from this set means "shown" —
* a tenant that never configured anything (every tenant, until an operator narrows one) has
* nothing here, which is what keeps every existing deployment behaving exactly as it does today. */
private disabledCodes = new Set<string>();
/** True once the initial load has settled, success or failure. `ShellComponent` waits on this
* before rendering the nav, so a menu is never shown-then-hidden a moment later. */
private loaded = false;
isReady(): boolean {
return this.loaded;
}
isMenuAllowed(menuCode: string): boolean {
return !this.disabledCodes.has(menuCode);
}
/** Fetches this tenant's list once. A failed request is treated as "nothing disabled" — the same
* as a tenant that never configured this at all — so a transient network hiccup on login never
* makes the whole nav disappear. */
load() {
return this.http.get<{ menuCode: string; enabled: boolean }[]>('/tenant-menu-access').pipe(
tap((rows) => {
this.disabledCodes = new Set((rows || []).filter((r) => r.enabled === false).map((r) => r.menuCode));
this.loaded = true;
}),
catchError(() => {
this.disabledCodes = new Set();
this.loaded = true;
return of([]);
})
);
}
}