diff --git a/virtual-desktop/src/app/authentication-manager/authentication-manager.module.ts b/virtual-desktop/src/app/authentication-manager/authentication-manager.module.ts index 1f7568d2c..cab81d379 100644 --- a/virtual-desktop/src/app/authentication-manager/authentication-manager.module.ts +++ b/virtual-desktop/src/app/authentication-manager/authentication-manager.module.ts @@ -18,6 +18,8 @@ import { ZluxPopupManagerModule } from '@zlux/widgets'; import { LoginComponent } from './login/login.component'; import { AuthenticationManager } from './authentication-manager.service'; import { StartURLManagerModule } from '../start-url-manager'; +import { StorageService } from './storage.service'; +import { IdleWarnService } from './idleWarn.service'; @NgModule({ imports: [ @@ -36,7 +38,9 @@ import { StartURLManagerModule } from '../start-url-manager'; providers: [ AuthenticationManager, /* Expose authentication manager to window managers */ - { provide: MVDHosting.Tokens.AuthenticationManagerToken, useExisting: AuthenticationManager } + { provide: MVDHosting.Tokens.AuthenticationManagerToken, useExisting: AuthenticationManager }, + StorageService, + IdleWarnService ] }) export class AuthenticationModule { diff --git a/virtual-desktop/src/app/authentication-manager/idleWarn.service.ts b/virtual-desktop/src/app/authentication-manager/idleWarn.service.ts new file mode 100644 index 000000000..335b55684 --- /dev/null +++ b/virtual-desktop/src/app/authentication-manager/idleWarn.service.ts @@ -0,0 +1,118 @@ +import * as moment from 'moment'; +import { Injectable } from '@angular/core'; +import { ZluxPopupManagerService, ZluxErrorSeverity } from '@zlux/widgets'; +import { TranslationService } from 'angular-l10n'; +import { BaseLogger } from 'virtual-desktop-logger'; +import { Subscription } from 'rxjs/Subscription'; +import { StorageService } from './storage.service'; + +@Injectable() +export class IdleWarnService { + + private report: any; + private readonly logger: ZLUX.ComponentLogger = BaseLogger; + + constructor(private popupManager: ZluxPopupManagerService, + public translation: TranslationService, + private storageService: StorageService + ) { + } + + public createRetryErrorReport(renewSession: any, isIdle: any) { + this.removeErrorReport(); + this.report = this.popupManager.createErrorReport( + ZluxErrorSeverity.WARNING, + this.translation.translate('Session Renewal Error'), + this.translation.translate('Session could not be renewed. Logout will occur unless renewed. Click here to retry.'), + { + blocking: false, + buttons: [this.translation.translate('Retry'), this.translation.translate('Dismiss')] + } + ); + + this.report.subscription = new Subscription(); + this.onUserActionSubscribe(renewSession,'Retry'); + this.onActivitySubscribe(renewSession, isIdle); + } + + onUserActionSubscribe(renewSession: any, action: string) { + if(this.report) { + this.report.subscription.add(this.report.subject.subscribe((buttonName:any)=> { + if (buttonName == this.translation.translate(action)) { + renewSession(); + } + })); + } + } + + public createContinueErrorReport(renewSession: any, isIdle: any, expirationInMS: number, desktopSize: any) { + this.removeErrorReport(); + let popupStyle; + + /* According to the size of the desktop, we move the expiration prompt to align with the app bar */ + switch (desktopSize) { + case 3: { + popupStyle = { + 'margin-bottom': '70px', + 'margin-right': '-5px' + }; + break; + } + case 1: { + popupStyle = { + 'margin-bottom': '15px', + 'margin-right': '-10px' + }; + break; + } + default: { + popupStyle = { + 'margin-bottom': '35px', + 'margin-right': '-5px' + }; + break; + } + } + + this.report = this.popupManager.createErrorReport( + ZluxErrorSeverity.WARNING, + this.translation.translate('Session Expiring Soon'), + //TODO: Add translation + //this.translation.translate('You will be logged out at ', { expirationInMS: moment().add(expirationInMS/1000, 'seconds').format('LT') }), + this.translation.translate('You will be logged out at ' + moment().add(expirationInMS/1000, 'seconds').format('LT')), + { + blocking: false, + buttons: [this.translation.translate('Continue session')], + timestamp: false, + theme: "dark", + style: popupStyle, + callToAction: true + }); + + this.report.subscription = new Subscription(); + this.onUserActionSubscribe(renewSession, 'Continue session'); + this.onActivitySubscribe(renewSession, isIdle); + } + + + onActivitySubscribe(renewSession: any, isIdle: any) { + if(this.report) { + this.report.subscription.add(this.storageService.lastActive.subscribe(()=> { + if (!isIdle()) { + this.logger.info('ZWED5047I', 'renew on activity'); /*this.logger.info('Near session expiration, but renewing session due to activity');*/ + renewSession(); + this.removeErrorReport(); + } + })); + } + } + + removeErrorReport() { + if (this.report) { + this.popupManager.removeReport(this.report.id); + this.report.subscription.unsubscribe(); + this.report = undefined; + } + } + +} \ No newline at end of file diff --git a/virtual-desktop/src/app/authentication-manager/login/login.component.ts b/virtual-desktop/src/app/authentication-manager/login/login.component.ts index cfd0adf7d..195be005a 100644 --- a/virtual-desktop/src/app/authentication-manager/login/login.component.ts +++ b/virtual-desktop/src/app/authentication-manager/login/login.component.ts @@ -14,10 +14,10 @@ import { Component, OnInit, ChangeDetectorRef, Injector } from '@angular/core'; import { AuthenticationManager, LoginExpirationIdleCheckEvent } from '../authentication-manager.service'; import { TranslationService } from 'angular-l10n'; -//import { Observable } from 'rxjs/Observable'; -import { ZluxPopupManagerService, ZluxErrorSeverity } from '@zlux/widgets'; import { BaseLogger } from 'virtual-desktop-logger'; -import * as moment from 'moment'; +import { StorageService } from '../storage.service'; +import { StorageKey } from '../storage-enum'; +import { IdleWarnService } from '../idleWarn.service'; const ACTIVITY_IDLE_TIMEOUT_MS = 300000; //5 minutes const HTTP_STATUS_PRECONDITION_REQUIRED = 428; @@ -43,17 +43,16 @@ export class LoginComponent implements OnInit { confirmNewPassword: string; errorMessage: string; loginMessage: string; - private idleWarnModal: any; - private lastActive: number = 0; expiredPassword: boolean; private passwordServices: Set; private themeManager: any; constructor( private authenticationService: AuthenticationManager, + private storageService: StorageService, public translation: TranslationService, + private idleWarnService: IdleWarnService, private cdr: ChangeDetectorRef, - private popupManager: ZluxPopupManagerService, private injector: Injector ) { this.themeManager = this.injector.get(MVDHosting.Tokens.ThemeEmitterToken); @@ -68,6 +67,8 @@ export class LoginComponent implements OnInit { this.errorMessage = ''; this.expiredPassword = false; this.passwordServices = new Set(); + this.renewSession = this.renewSession.bind(this); + this.isIdle = this.isIdle.bind(this); this.authenticationService.loginScreenVisibilityChanged.subscribe((eventReason: MVDHosting.LoginScreenChangeReason) => { switch (eventReason) { case MVDHosting.LoginScreenChangeReason.UserLogout: @@ -77,6 +78,7 @@ export class LoginComponent implements OnInit { case MVDHosting.LoginScreenChangeReason.UserLogin: this.errorMessage = ''; this.needLogin = false; + this.detectActivity(); break; case MVDHosting.LoginScreenChangeReason.PasswordChange: this.changePassword = true; @@ -89,10 +91,7 @@ export class LoginComponent implements OnInit { break; case MVDHosting.LoginScreenChangeReason.SessionExpired: this.backButton(); - if (this.idleWarnModal) { - this.popupManager.removeReport(this.idleWarnModal.id); - this.idleWarnModal = undefined; - } + this.idleWarnService.removeErrorReport(); this.errorMessage = this.translation.translate('Session Expired'); this.needLogin = true; break; @@ -114,88 +113,24 @@ export class LoginComponent implements OnInit { } private isIdle(): boolean { - const lastActive = parseInt(window.localStorage.getItem('ZoweZLUX.lastActive') || '0'); - let idle = (Date.now() - lastActive) > ACTIVITY_IDLE_TIMEOUT_MS; - this.logger.debug("ZWED5304I", lastActive, Date.now(), idle); //this.logger.debug(`User lastActive=${lastActive}, now=${Date.now()}, idle={idle}`); + const activityTime = parseInt(StorageService.getItem(StorageKey.LAST_ACTIVE) || '0'); + let idle = (Date.now() - activityTime) > ACTIVITY_IDLE_TIMEOUT_MS; + this.logger.debug("ZWED5304I", activityTime, Date.now(), idle); + //this.logger.debug(`User lastActive=${lastActive}, now=${Date.now()}, idle={idle}`); return idle; } renewSession(): void { this.authenticationService.performSessionRenewal().subscribe((result:any)=> { - if (this.idleWarnModal) { - this.idleWarnModal.subject.unsubscribe(); - this.idleWarnModal = undefined; - } + this.idleWarnService.removeErrorReport(); }, (errorObservable)=> { - if (this.idleWarnModal) { - this.idleWarnModal.subject.unsubscribe(); - this.idleWarnModal = this.popupManager.createErrorReport( - ZluxErrorSeverity.WARNING, - this.translation.translate('Session Renewal Error'), - this.translation.translate('Session could not be renewed. Logout will occur unless renewed. Click here to retry.'), - { - blocking: false, - buttons: [this.translation.translate('Retry'), this.translation.translate('Dismiss')] - }); - this.idleWarnModal.subject.subscribe((buttonName:any)=> { - if (buttonName == this.translation.translate('Retry')) { - this.renewSession(); - } - }); - } + this.idleWarnService.createRetryErrorReport(this.renewSession, this.isIdle); }); } spawnExpirationPrompt(expirationInMS: number): void { let desktopSize = this.themeManager.mainSize || 2; - let popupStyle; - - /* According to the size of the desktop, we move the expiration prompt to align with the app bar */ - switch (desktopSize) { - case 3: { - popupStyle = { - 'margin-bottom': '70px', - 'margin-right': '-5px' - }; - break; - } - case 1: { - popupStyle = { - 'margin-bottom': '15px', - 'margin-right': '-10px' - }; - break; - } - default: { - popupStyle = { - 'margin-bottom': '35px', - 'margin-right': '-5px' - }; - break; - } - } - - this.idleWarnModal = this.popupManager.createErrorReport( - ZluxErrorSeverity.WARNING, - this.translation.translate('Session Expiring Soon'), - //TODO: Add translation - //this.translation.translate('You will be logged out at ', { expirationInMS: moment().add(expirationInMS/1000, 'seconds').format('LT') }), - this.translation.translate('You will be logged out at ' + moment().add(expirationInMS/1000, 'seconds').format('LT')), - { - blocking: false, - buttons: [this.translation.translate('Continue session')], - timestamp: false, - theme: "dark", - style: popupStyle, - callToAction: true - }); - - this.idleWarnModal.subject.subscribe((buttonName:any)=> { - if (buttonName == this.translation.translate('Continue')) { - //may fail, so don't touch timers yet - this.renewSession(); - } - }); + this.idleWarnService.createContinueErrorReport(this.renewSession, this.isIdle, expirationInMS, desktopSize) } ngOnInit(): void { @@ -256,13 +191,8 @@ export class LoginComponent implements OnInit { } detectActivity(): void { - this.logger.debug('ZWED5305I'); //this.logger.debug('User activity detected'); - this.lastActive = Date.now(); - window.localStorage.setItem('ZoweZLUX.lastActive',this.lastActive.toString()); - if (this.idleWarnModal) { - this.popupManager.removeReport(this.idleWarnModal.id); - this.idleWarnModal = undefined; - } + this.storageService.updateLastActive(); + this.idleWarnService.removeErrorReport(); } attemptPasswordReset(): void { diff --git a/virtual-desktop/src/app/authentication-manager/storage-enum.ts b/virtual-desktop/src/app/authentication-manager/storage-enum.ts new file mode 100644 index 000000000..fe17c480a --- /dev/null +++ b/virtual-desktop/src/app/authentication-manager/storage-enum.ts @@ -0,0 +1,29 @@ + + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +export const enum StorageKey { + LAST_ACTIVE = 'ZoweZLUX.lastActive', + I18_NEXT_LANG = 'i18nextLng', + USERNAME = 'username' +}; + + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + diff --git a/virtual-desktop/src/app/authentication-manager/storage.service.ts b/virtual-desktop/src/app/authentication-manager/storage.service.ts new file mode 100644 index 000000000..e31a9ea9e --- /dev/null +++ b/virtual-desktop/src/app/authentication-manager/storage.service.ts @@ -0,0 +1,76 @@ + + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ +import { Injectable, EventEmitter } from '@angular/core'; +import { StorageKey } from './storage-enum'; +import { BaseLogger } from 'virtual-desktop-logger'; + + +@Injectable() +export class StorageService { + public static setItem(key: string, newValue: string) { + window.localStorage.setItem(key, newValue); + } + + public static getItem(key: string): string | null { + return window.localStorage.getItem(key); + } + + public static removeItem(key: string) { + return window.localStorage.removeItem(key); + } + + public static clear(key: string) { + return window.localStorage.clear(); + } + + private readonly logger: ZLUX.ComponentLogger = BaseLogger; + public lastActive:EventEmitter; + + constructor() { + this.lastActive = new EventEmitter(); + this.storageEventHandler = this.storageEventHandler.bind(this); + window.addEventListener("storage", this.storageEventHandler); + } + + private storageEventHandler(event: StorageEvent) { + this.logger.debug('storageEventListener'); + if (event.storageArea == localStorage) { + const newValue = event.newValue; + switch(event.key) { + case StorageKey.LAST_ACTIVE: { + this.lastActive.emit(Number(newValue)); + } + break; + default: break; + } + } + } + + public updateLastActive() { + this.logger.debug('ZWED5305I','activity'); //this.logger.debug('User activity detected'); + const activityTime = Date.now(); + StorageService.setItem(StorageKey.LAST_ACTIVE,activityTime.toString()); + this.lastActive.next(activityTime); + } + +} + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ +