Skip to content

Commit

Permalink
NAS-129885: Add customer designed login banner
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko committed Jul 12, 2024
1 parent 8f6a8c3 commit d162cb2
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 26 deletions.
1 change: 1 addition & 0 deletions src/app/core/testing/utils/mock-auth.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function mockAuth(
}),
createSpyObject(Store),
createSpyObject(WebSocketService),
createSpyObject(Window),
);

mockService.setUser(user as LoggedInUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ <h3>{{ 'Access' | translate }}</h3>
<mat-list-item [ixUiSearch]="searchableElements.elements.loginBanner">
<span class="label">{{ 'Login Banner' | translate }}:</span>
<span *ixWithLoadingState="loginBanner$ as loginBanner" class="value">
{{ loginBanner || ('None' | translate) }}
{{ loginBanner || '–' }}
</span>
</mat-list-item>
</mat-list>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { LocaleService } from 'app/services/locale.service';
import { SystemGeneralService } from 'app/services/system-general.service';
import { WebSocketService } from 'app/services/ws.service';
import { selectPreferences } from 'app/store/preferences/preferences.selectors';
import { selectGeneralConfig } from 'app/store/system-config/system-config.selectors';
import { selectAdvancedConfig, selectGeneralConfig } from 'app/store/system-config/system-config.selectors';

describe('AccessCardComponent', () => {
let spectator: Spectator<AccessCardComponent>;
Expand Down Expand Up @@ -79,6 +79,12 @@ describe('AccessCardComponent', () => {
ds_auth: true,
},
},
{
selector: selectAdvancedConfig,
value: {
login_banner: 'Hello World!',
},
},
],
}),
mockProvider(DialogService, {
Expand Down Expand Up @@ -113,6 +119,11 @@ describe('AccessCardComponent', () => {
expect(await allowed.getFullText()).toBe('Allow Directory Service users to access WebUI: Yes');
});

it('shows current login banner', async () => {
const loginBanner = (await loader.getAllHarnesses(MatListItemHarness))[2];
expect(await loginBanner.getFullText()).toBe('Login Banner: Hello World!');
});

it('opens Token settings form when Configure is pressed', async () => {
const configure = await loader.getHarness(MatButtonHarness.with({ text: 'Configure' }));
await configure.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { SystemGeneralService } from 'app/services/system-general.service';
import { WebSocketService } from 'app/services/ws.service';
import { lifetimeTokenUpdated } from 'app/store/preferences/preferences.actions';
import { selectPreferences } from 'app/store/preferences/preferences.selectors';
import { generalConfigUpdated } from 'app/store/system-config/system-config.actions';
import { selectGeneralConfig } from 'app/store/system-config/system-config.selectors';
import { advancedConfigUpdated, generalConfigUpdated, loginBannerUpdated } from 'app/store/system-config/system-config.actions';
import { selectAdvancedConfig, selectGeneralConfig } from 'app/store/system-config/system-config.selectors';

describe('AccessFormComponent', () => {
let spectator: Spectator<AccessFormComponent>;
Expand All @@ -37,9 +37,13 @@ describe('AccessFormComponent', () => {
localStorage: {
setItem: jest.fn,
},
sessionStorage: {
setItem: jest.fn,
},
}),
mockWebSocket([
mockCall('system.general.update'),
mockCall('system.advanced.update'),
]),
mockProvider(IxChainedSlideInService, {
open: jest.fn(() => of(true)),
Expand All @@ -55,6 +59,9 @@ describe('AccessFormComponent', () => {
}, {
selector: selectGeneralConfig,
value: { ds_auth: true },
}, {
selector: selectAdvancedConfig,
value: { login_banner: 'test' },
}],
}),
mockProvider(ChainedRef, chainedRef),
Expand All @@ -73,6 +80,7 @@ describe('AccessFormComponent', () => {

expect(values).toEqual({
'Token Lifetime': '300',
'Login Banner': 'test',
'Allow Directory Service users to access WebUI': true,
});
});
Expand All @@ -84,6 +92,7 @@ describe('AccessFormComponent', () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Token Lifetime': '60',
'Login Banner': '',
'Allow Directory Service users to access WebUI': false,
});

Expand All @@ -94,7 +103,12 @@ describe('AccessFormComponent', () => {
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('system.general.update', [{
ds_auth: false,
}]);
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('system.advanced.update', [{
login_banner: '',
}]);
expect(store$.dispatch).toHaveBeenCalledWith(generalConfigUpdated());
expect(store$.dispatch).toHaveBeenCalledWith(advancedConfigUpdated());
expect(store$.dispatch).toHaveBeenCalledWith(loginBannerUpdated({ loginBanner: '' }));
expect(chainedRef.close).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AppState } from 'app/store';
import { defaultPreferences } from 'app/store/preferences/default-preferences.constant';
import { lifetimeTokenUpdated } from 'app/store/preferences/preferences.actions';
import { selectPreferences } from 'app/store/preferences/preferences.selectors';
import { advancedConfigUpdated, generalConfigUpdated } from 'app/store/system-config/system-config.actions';
import { advancedConfigUpdated, generalConfigUpdated, loginBannerUpdated } from 'app/store/system-config/system-config.actions';
import { selectAdvancedConfig, selectGeneralConfig } from 'app/store/system-config/system-config.selectors';

@UntilDestroy()
Expand Down Expand Up @@ -89,12 +89,21 @@ export class AccessFormComponent implements OnInit {
).subscribe(() => {
this.store$.dispatch(lifetimeTokenUpdated({ lifetime: this.form.value.token_lifetime }));

if (this.form.controls.login_banner.dirty || this.isEnterprise) {
const bannerChanged = this.form.controls.login_banner.dirty;

if (bannerChanged || this.isEnterprise) {
const requests$ = [];
this.isLoading = true;
forkJoin([
this.updateLoginBanner(),
this.updateEnterpriseDsAuth(),
])

if (bannerChanged) {
requests$.push(this.updateLoginBanner());
}

if (this.isEnterprise) {
requests$.push(this.updateEnterpriseDsAuth());
}

forkJoin(requests$)
.pipe(untilDestroyed(this))
.subscribe({
next: () => {
Expand All @@ -115,8 +124,12 @@ export class AccessFormComponent implements OnInit {
}

private updateLoginBanner(): Observable<unknown> {
return this.ws.call('system.advanced.update', [{ login_banner: this.form.value.login_banner }])
.pipe(finalize(() => this.store$.dispatch(advancedConfigUpdated())));
const loginBanner = this.form.value.login_banner;
return this.ws.call('system.advanced.update', [{ login_banner: loginBanner }])
.pipe(finalize(() => {
this.store$.dispatch(advancedConfigUpdated());
this.store$.dispatch(loginBannerUpdated({ loginBanner }));
}));
}

private updateEnterpriseDsAuth(): Observable<unknown> {
Expand Down
1 change: 1 addition & 0 deletions src/app/store/system-config/system-config.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export const systemConfigLoaded = createAction(

export const generalConfigUpdated = createAction('[System Config] General Config Updated');
export const advancedConfigUpdated = createAction('[System Config] Advanced Config Updated');
export const loginBannerUpdated = createAction('[System Config] Login Banner Updated', props<{ loginBanner: string }>());
14 changes: 14 additions & 0 deletions src/app/views/sessions/signin/signin.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MockComponents } from 'ng-mocks';
import { BehaviorSubject, of } from 'rxjs';
import { FailoverDisabledReason } from 'app/enums/failover-disabled-reason.enum';
import { FailoverStatus } from 'app/enums/failover-status.enum';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { CopyrightLineComponent } from 'app/modules/layout/components/copyright-line/copyright-line.component';
import { WebSocketConnectionService } from 'app/services/websocket-connection.service';
import {
Expand Down Expand Up @@ -33,6 +34,7 @@ describe('SigninComponent', () => {
const canLogin$ = new BehaviorSubject<boolean>(undefined);
const isConnected$ = new BehaviorSubject<boolean>(undefined);
const managedByTrueCommand$ = new BehaviorSubject<boolean>(undefined);
const loginBanner$ = new BehaviorSubject<string>(undefined);

const createComponent = createComponentFactory({
component: SigninComponent,
Expand All @@ -50,6 +52,9 @@ describe('SigninComponent', () => {
),
],
providers: [
mockProvider(DialogService, {
fullScreenDialog: jest.fn(),
}),
mockProvider(WebSocketConnectionService, {
isConnected$,
}),
Expand All @@ -59,6 +64,7 @@ describe('SigninComponent', () => {
failover$,
hasFailover$,
canLogin$,
loginBanner$,
isLoading$: of(false),
init: jest.fn(),
}),
Expand All @@ -73,6 +79,7 @@ describe('SigninComponent', () => {
canLogin$.next(true);
isConnected$.next(true);
managedByTrueCommand$.next(false);
loginBanner$.next('');
});

it('initializes SigninStore on component init', () => {
Expand Down Expand Up @@ -128,5 +135,12 @@ describe('SigninComponent', () => {
expect(failoverStatus.status).toEqual(FailoverStatus.Error);
expect(failoverStatus.failoverIps).toEqual(['123.44.1.22', '123.44.1.34']);
});

it('checks login banner and shows full dialog if set', () => {
loginBanner$.next('HELLO USER');
spectator.detectChanges();

expect(spectator.inject(DialogService).fullScreenDialog).toHaveBeenCalled();
});
});
});
22 changes: 9 additions & 13 deletions src/app/views/sessions/signin/signin.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Component, OnInit, ChangeDetectionStrategy,
Inject,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest } from 'rxjs';
import {
Expand Down Expand Up @@ -30,7 +29,6 @@ export class SigninComponent implements OnInit {
readonly hasLoadingIndicator$ = combineLatest([this.signinStore.isLoading$, this.isConnected$]).pipe(
map(([isLoading, isConnected]) => isLoading || !isConnected),
);
readonly loginBanner = toSignal(this.signinStore.loginBanner$);

constructor(
private wsManager: WebSocketConnectionService,
Expand All @@ -46,16 +44,14 @@ export class SigninComponent implements OnInit {
this.signinStore.init();
});

this.signinStore.loginBanner$
.pipe(
filter(Boolean),
filter(() => this.window.sessionStorage.getItem('loginBannerDismissed') !== 'true'),
switchMap((text) => this.dialog.fullScreenDialog(null, text, true, true)),
take(1),
filter(Boolean),
untilDestroyed(this),
).subscribe(() => {
this.window.sessionStorage.setItem('loginBannerDismissed', 'true');
});
this.signinStore.loginBanner$.pipe(
filter(Boolean),
filter(() => this.window.sessionStorage.getItem('loginBannerDismissed') !== 'true'),
switchMap((text) => this.dialog.fullScreenDialog(null, text, true, true).pipe(take(1))),
filter(Boolean),
untilDestroyed(this),
).subscribe(() => {
this.window.sessionStorage.setItem('loginBannerDismissed', 'true');
});
}
}
25 changes: 25 additions & 0 deletions src/app/views/sessions/signin/store/signin.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('SigninStore', () => {
mockCall('auth.twofactor.config', { enabled: false } as GlobalTwoFactorConfig),
mockCall('failover.disabled.reasons', [FailoverDisabledReason.NoLicense]),
mockCall('truenas.managed_by_truecommand', false),
mockCall('system.advanced.login_banner', ''),
]),
mockProvider(WebSocketConnectionService, {
isConnected$: of(true),
Expand Down Expand Up @@ -88,11 +89,16 @@ describe('SigninStore', () => {
wasAdminSet: true,
managedByTrueCommand: false,
isLoading: false,
loginBanner: '',
};
beforeEach(() => {
spectator.service.setState(initialState);
});

it('loginBanner$', async () => {
expect(await firstValueFrom(spectator.service.loginBanner$)).toBe('');
});

it('hasRootPassword$', async () => {
expect(await firstValueFrom(spectator.service.wasAdminSet$)).toBe(true);
});
Expand Down Expand Up @@ -120,6 +126,21 @@ describe('SigninStore', () => {
});

describe('init', () => {
it('checks login banner and show if set', async () => {
websocket.mockCall('system.advanced.login_banner', 'HELLO USER');
spectator.service.init();

expect(await firstValueFrom(spectator.service.state$)).toEqual({
wasAdminSet: true,
managedByTrueCommand: false,
loginBanner: 'HELLO USER',
isLoading: false,
failover: {
status: FailoverStatus.Single,
},
});
});

it('checks if root password is set and loads failover status', async () => {
spectator.service.init();

Expand All @@ -129,6 +150,7 @@ describe('SigninStore', () => {
expect(await firstValueFrom(spectator.service.state$)).toEqual({
wasAdminSet: true,
managedByTrueCommand: false,
loginBanner: '',
isLoading: false,
failover: {
status: FailoverStatus.Single,
Expand All @@ -147,6 +169,7 @@ describe('SigninStore', () => {
wasAdminSet: true,
isLoading: false,
managedByTrueCommand: false,
loginBanner: '',
failover: {
disabledReasons: [FailoverDisabledReason.NoLicense],
ips: ['123.23.44.54'],
Expand Down Expand Up @@ -190,6 +213,7 @@ describe('SigninStore', () => {
wasAdminSet: true,
isLoading: false,
managedByTrueCommand: false,
loginBanner: '',
failover: {
disabledReasons: [FailoverDisabledReason.NoLicense],
ips: ['123.23.44.54'],
Expand Down Expand Up @@ -221,6 +245,7 @@ describe('SigninStore', () => {
wasAdminSet: true,
managedByTrueCommand: false,
isLoading: false,
loginBanner: '',
failover: {
disabledReasons: [FailoverDisabledReason.DisagreeVip],
ips: ['123.23.44.54'],
Expand Down
Loading

0 comments on commit d162cb2

Please sign in to comment.