Skip to content

Commit

Permalink
add a signal to track current account
Browse files Browse the repository at this point in the history
  • Loading branch information
mshima committed Feb 7, 2024
1 parent 9baf937 commit 35c5e1a
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import UserManagementDeleteDialogComponent from '../delete/user-management-delet
],
})
export default class UserManagementComponent implements OnInit {
currentAccount = signal<Account | null>(null);
currentAccount = inject(AccountService).trackCurrentAccount();
users = signal<User[] | null>(null);
isLoading = signal(false);
<%_ if (!databaseTypeCassandra) { _%>
Expand All @@ -65,15 +65,13 @@ export default class UserManagementComponent implements OnInit {
<%_ } _%>

private userService = inject(UserManagementService);
private accountService = inject(AccountService);
<%_ if (!databaseTypeCassandra) { _%>
private activatedRoute = inject(ActivatedRoute);
private router = inject(Router);
<%_ } _%>
private modalService = inject(NgbModal);

ngOnInit(): void {
this.accountService.identity().subscribe(account => this.currentAccount.set(account));
<%_ if (databaseTypeCassandra) { _%>
this.loadAll();
<%_ } else { _%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { _%>
Expand All @@ -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<Account | null>(null);
private authenticationState = new ReplaySubject<Account | null>(1);
private accountCache$?: Observable<Account> | null;

Expand All @@ -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<Account | null> {
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<Account | null> {
Expand All @@ -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<Account | null> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default class NavbarComponent implements OnInit {
<%_ } _%>
openAPIEnabled?: boolean;
version = '';
account = signal<Account | null>(null);
account = inject(AccountService).trackCurrentAccount();
entitiesNavbarItems: NavbarItem[] = [];
<%_ if (applicationTypeGateway && microfrontend) { _%>
<%_ for (const remote of microfrontends) { _%>
Expand All @@ -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);

Expand All @@ -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) { _%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,21 +37,20 @@ class TestHasAnyAuthorityDirectiveComponent {

describe('HasAnyAuthorityDirective tests', () => {
let mockAccountService: AccountService;
const authenticationState = new Subject<Account | null>();

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [HasAnyAuthorityDirective],
declarations: [TestHasAnyAuthorityDirectiveComponent],
providers: [AccountService],
});
})
);
let currentAccount: WritableSignal<Account | null>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [HasAnyAuthorityDirective],
declarations: [TestHasAnyAuthorityDirectiveComponent],
providers: [AccountService],
});
}));

beforeEach(() => {
mockAccountService = TestBed.inject(AccountService);
mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable());
currentAccount = signal<Account | null>({ activated: true, authorities: [] } as any);
mockAccountService.trackCurrentAccount = jest.fn(() => currentAccount);
});

describe('set <%= jhiPrefix %>HasAnyAuthority', () => {
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<string | string[]>([]);

private readonly destroy$ = new Subject<void>();

private accountService = inject(AccountService);
private templateRef = inject(TemplateRef<any>);
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);
}
}

0 comments on commit 35c5e1a

Please sign in to comment.