diff --git a/generators/angular/templates/src/main/webapp/app/admin/user-management/list/user-management.component.ts.ejs b/generators/angular/templates/src/main/webapp/app/admin/user-management/list/user-management.component.ts.ejs index 31f2fc658140..ca8926cd44bc 100644 --- a/generators/angular/templates/src/main/webapp/app/admin/user-management/list/user-management.component.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/admin/user-management/list/user-management.component.ts.ejs @@ -53,7 +53,7 @@ import UserManagementDeleteDialogComponent from '../delete/user-management-delet ], }) export default class UserManagementComponent implements OnInit { - currentAccount = signal(null); + currentAccount = inject(AccountService).trackCurrentAccount(); users = signal(null); isLoading = signal(false); <%_ if (!databaseTypeCassandra) { _%> @@ -65,7 +65,6 @@ export default class UserManagementComponent implements OnInit { <%_ } _%> private userService = inject(UserManagementService); - private accountService = inject(AccountService); <%_ if (!databaseTypeCassandra) { _%> private activatedRoute = inject(ActivatedRoute); private router = inject(Router); @@ -73,7 +72,6 @@ export default class UserManagementComponent implements OnInit { private modalService = inject(NgbModal); ngOnInit(): void { - this.accountService.identity().subscribe(account => this.currentAccount.set(account)); <%_ if (databaseTypeCassandra) { _%> this.loadAll(); <%_ } else { _%> diff --git a/generators/angular/templates/src/main/webapp/app/core/auth/account.service.ts.ejs b/generators/angular/templates/src/main/webapp/app/core/auth/account.service.ts.ejs index d19c2efaa82d..c7d7133410e9 100644 --- a/generators/angular/templates/src/main/webapp/app/core/auth/account.service.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/core/auth/account.service.ts.ejs @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. -%> -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable, Signal, signal } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; <%_ if (enableTranslation) { _%> @@ -26,12 +26,12 @@ import { Observable, ReplaySubject, of } from 'rxjs'; import { shareReplay, tap, catchError } from 'rxjs/operators'; import { StateStorageService } from 'app/core/auth/state-storage.service'; -import { ApplicationConfigService } from '../config/application-config.service'; import { Account } from 'app/core/auth/account.model'; +import { ApplicationConfigService } from '../config/application-config.service'; @Injectable({ providedIn: 'root' }) export class AccountService { - private userIdentity: Account | null = null; + private userIdentity = signal(null); private authenticationState = new ReplaySubject(1); private accountCache$?: Observable | null; @@ -50,21 +50,26 @@ export class AccountService { <%_ } _%> authenticate(identity: Account | null): void { - this.userIdentity = identity; - this.authenticationState.next(this.userIdentity); + this.userIdentity.set(identity); + this.authenticationState.next(this.userIdentity()); if (!identity) { this.accountCache$ = null; } } + trackCurrentAccount(): Signal { + return this.userIdentity.asReadonly(); + } + hasAnyAuthority(authorities: string[] | string): boolean { - if (!this.userIdentity) { + const userIdentity = this.userIdentity(); + if (!userIdentity) { return false; } if (!Array.isArray(authorities)) { authorities = [authorities]; } - return this.userIdentity.authorities.some((authority: string) => authorities.includes(authority)); + return userIdentity.authorities.some((authority: string) => authorities.includes(authority)); } identity(force?: boolean): Observable { @@ -84,14 +89,14 @@ export class AccountService { this.navigateToStoredUrl(); }), - shareReplay() + shareReplay(), ); } return this.accountCache$.pipe(catchError(() => of(null))); } isAuthenticated(): boolean { - return this.userIdentity !== null; + return this.userIdentity() !== null; } getAuthenticationState(): Observable { diff --git a/generators/angular/templates/src/main/webapp/app/layouts/navbar/navbar.component.ts.ejs b/generators/angular/templates/src/main/webapp/app/layouts/navbar/navbar.component.ts.ejs index 5562c084e17f..c512ce976865 100644 --- a/generators/angular/templates/src/main/webapp/app/layouts/navbar/navbar.component.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/layouts/navbar/navbar.component.ts.ejs @@ -62,7 +62,7 @@ export default class NavbarComponent implements OnInit { <%_ } _%> openAPIEnabled?: boolean; version = ''; - account = signal(null); + account = inject(AccountService).trackCurrentAccount(); entitiesNavbarItems: NavbarItem[] = []; <%_ if (applicationTypeGateway && microfrontend) { _%> <%_ for (const remote of microfrontends) { _%> @@ -78,7 +78,9 @@ export default class NavbarComponent implements OnInit { private injector = inject(Injector); <%_ } _%> <%_ } _%> +<%_ if (applicationTypeGateway && microfrontend) { _%> private accountService = inject(AccountService); +<%_ } _%> private profileService = inject(ProfileService); private router = inject(Router); @@ -95,12 +97,11 @@ export default class NavbarComponent implements OnInit { this.openAPIEnabled = profileInfo.openAPIEnabled; }); - this.accountService.getAuthenticationState().subscribe(account => { - this.account.set(account); <%_ if (applicationTypeGateway && microfrontend) { _%> + this.accountService.getAuthenticationState().subscribe(account => { this.loadMicrofrontendsEntities(); -<%_ } _%> }); +<%_ } _%> } <%_ if (enableTranslation) { _%> diff --git a/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts.ejs b/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts.ejs index 7951d75ceb02..7f5a4737774d 100644 --- a/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts.ejs @@ -18,9 +18,8 @@ -%> jest.mock('app/core/auth/account.service'); -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Component, ElementRef, Signal, ViewChild, WritableSignal, signal } from '@angular/core'; import { TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { Subject } from 'rxjs'; import { AccountService } from 'app/core/auth/account.service'; @@ -38,21 +37,20 @@ class TestHasAnyAuthorityDirectiveComponent { describe('HasAnyAuthorityDirective tests', () => { let mockAccountService: AccountService; - const authenticationState = new Subject(); - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HasAnyAuthorityDirective], - declarations: [TestHasAnyAuthorityDirectiveComponent], - providers: [AccountService], - }); - }) - ); + let currentAccount: WritableSignal; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HasAnyAuthorityDirective], + declarations: [TestHasAnyAuthorityDirectiveComponent], + providers: [AccountService], + }); + })); beforeEach(() => { mockAccountService = TestBed.inject(AccountService); - mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + currentAccount = signal({ activated: true, authorities: [] } as any); + mockAccountService.trackCurrentAccount = jest.fn(() => currentAccount); }); describe('set <%= jhiPrefix %>HasAnyAuthority', () => { @@ -61,6 +59,7 @@ describe('HasAnyAuthorityDirective tests', () => { mockAccountService.hasAnyAuthority = jest.fn(() => true); const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); const comp = fixture.componentInstance; + fixture.detectChanges(); // WHEN fixture.detectChanges(); @@ -74,6 +73,7 @@ describe('HasAnyAuthorityDirective tests', () => { mockAccountService.hasAnyAuthority = jest.fn(() => false); const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); const comp = fixture.componentInstance; + fixture.detectChanges(); // WHEN fixture.detectChanges(); @@ -86,9 +86,10 @@ describe('HasAnyAuthorityDirective tests', () => { describe('change authorities', () => { it('should show or not show restricted content correctly if user authorities are changing', () => { // GIVEN - mockAccountService.hasAnyAuthority = jest.fn(() => true); + mockAccountService.hasAnyAuthority = jest.fn((): boolean => Boolean(currentAccount())); const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); const comp = fixture.componentInstance; + fixture.detectChanges(); // WHEN fixture.detectChanges(); @@ -97,55 +98,20 @@ describe('HasAnyAuthorityDirective tests', () => { expect(comp.content).toBeDefined(); // GIVEN - mockAccountService.hasAnyAuthority = jest.fn(() => false); + currentAccount.set(null); // WHEN - authenticationState.next(null); fixture.detectChanges(); // THEN expect(comp.content).toBeUndefined(); - // GIVEN - mockAccountService.hasAnyAuthority = jest.fn(() => true); - // WHEN - authenticationState.next(null); + currentAccount.set({ activated: true, authorities: ['foo'] } as any); fixture.detectChanges(); // THEN expect(comp.content).toBeDefined(); }); }); - - describe('ngOnDestroy', () => { - it('should destroy authentication state subscription on component destroy', () => { - // GIVEN - mockAccountService.hasAnyAuthority = jest.fn(() => true); - const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); - const div = fixture.debugElement.queryAllNodes(By.directive(HasAnyAuthorityDirective))[0]; - const hasAnyAuthorityDirective = div.injector.get(HasAnyAuthorityDirective); - - // WHEN - fixture.detectChanges(); - - // THEN - expect(mockAccountService.hasAnyAuthority).toHaveBeenCalled(); - - // WHEN - jest.clearAllMocks(); - authenticationState.next(null); - - // THEN - expect(mockAccountService.hasAnyAuthority).toHaveBeenCalled(); - - // WHEN - jest.clearAllMocks(); - hasAnyAuthorityDirective.ngOnDestroy(); - authenticationState.next(null); - - // THEN - expect(mockAccountService.hasAnyAuthority).not.toHaveBeenCalled(); - }); - }); }); diff --git a/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.ts.ejs b/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.ts.ejs index 09d0051ee625..68b54f1e48f3 100644 --- a/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/shared/auth/has-any-authority.directive.ts.ejs @@ -16,9 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. -%> -import { Directive, inject, Input, TemplateRef, ViewContainerRef, OnDestroy } from '@angular/core'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Directive, inject, Input, TemplateRef, ViewContainerRef, effect, signal, computed } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; @@ -37,38 +35,28 @@ import { AccountService } from 'app/core/auth/account.service'; standalone: true, selector: '[<%= jhiPrefix %>HasAnyAuthority]', }) -export default class HasAnyAuthorityDirective implements OnDestroy { - private authorities!: string | string[]; +export default class HasAnyAuthorityDirective { + private authorities = signal([]); - private readonly destroy$ = new Subject(); - - private accountService = inject(AccountService); private templateRef = inject(TemplateRef); private viewContainerRef = inject(ViewContainerRef); - @Input() - set <%= jhiPrefix %>HasAnyAuthority(value: string | string[]) { - this.authorities = value; - this.updateView(); - // Get notified each time authentication state changes. - this.accountService - .getAuthenticationState() - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.updateView(); - }); + constructor() { + const accountService = inject(AccountService); + const currentAccount = accountService.trackCurrentAccount(); + const hasPermission = computed(() => currentAccount()?.authorities && accountService.hasAnyAuthority(this.authorities())); + + effect(() => { + if (hasPermission()) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } else { + this.viewContainerRef.clear(); + } + }); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private updateView(): void { - const hasAnyAuthority = this.accountService.hasAnyAuthority(this.authorities); - this.viewContainerRef.clear(); - if (hasAnyAuthority) { - this.viewContainerRef.createEmbeddedView(this.templateRef); - } + @Input() + set <%= jhiPrefix %>HasAnyAuthority(value: string | string[]) { + this.authorities.set(value); } }