import { Injectable , OnDestroy } from '@angular/core' ; import { AuthService } from './auth.service' ; import { interval , Observable , ReplaySubject , Subject , Subscription } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { Token } from '../models/token' ; import { NgbModal , NgbModalRef } from '@ng-bootstrap/ng-bootstrap' ; import { SessionTimeoutNotificationModalComponent } from '../modals/session-timeout-notification-modal/session-timeout-notification-modal.component' ; import { ToastService } from '../../layout/toast/toast.service' ; @ Injectable ( { providedIn : 'root' } ) export class SessionTimeoutService implements OnDestroy { /**
     * In which ms interval should the session be checked.
     * Default is every 1 second.
     */ static readonly TIME_CHECK_INTERVAL : number = 1000 ; /**
     * Which time distance should the notification modal appear.
     * Default is 15 minutes before the timeout.
     */ static readonly NOTIFICATION_THRESHOLD : number = 1000 * 60 * 15 ; private private subscriber : ReplaySubject < void > = new ReplaySubject < void > ( ) ; private private notificationModalRef : NgbModalRef ; private private notified : boolean = false ; private private intervalTimer$ : Observable < number > = interval ( SessionTimeoutService . TIME_CHECK_INTERVAL ) ; private private timerSubscription = new Subscription ( ) ; /**
     * Timeout time emitter.
     */ public sessionTimeout : Subject < number > ; /**
     * Same as timeout emitter but human readable.
     */ public sessionTimeoutRemaining : Subject < string > ; private private tokenExpire : number ; constructor ( private authService , private modal , private toast ) { this . sessionTimeout = new Subject < number > ( ) ; this . sessionTimeoutRemaining = new Subject < string > ( ) ; this . authService . currentUserToken . subscribe ( ( token ) => { if ( token ) { this . tokenExpire = token . expire * 1000 ; this . startTimer ( ) ; } else { this . stopTimer ( ) ; } } ) ; } param msecs public static convertToCountdown ( msecs ) { const secsInt = Math . floor ( parseInt ( msecs , 10 ) / 1000 ) ; const days = Math . floor ( secsInt / 86400 ) % 7 ; const hours = Math . floor ( secsInt / 3600 ) % 24 ; const minutes = Math . floor ( secsInt / 60 ) % 60 ; const seconds = secsInt % 60 ; return [ days , hours , minutes , seconds ] . map ( v => v < 10 ? '0' + v : v ) . filter ( ( v , i ) => v !== '00' || i > 0 ) . join ( ':' ) ; } /**
     * Remaining token lifetime.
     */ public get remaining ( ) : number { if ( ! this . tokenExpire ) { return 0 ; } const currentTime = Date . now ( ) ; const distance = this . tokenExpire - currentTime ; if ( distance > 0 ) { return distance ; } return 0 ; } private private startTimer ( ) { if ( this . timerSubscription ) { this . stopTimer ( ) ; } if ( ! this . intervalTimer$ ) { this . intervalTimer$ = interval ( SessionTimeoutService . TIME_CHECK_INTERVAL ) ; } this . notified = false ; this . timerSubscription = this . intervalTimer$ . subscribe ( ( t ) => { this . sessionTimeout . next ( this . remaining ) ; this . sessionTimeoutRemaining . next ( SessionTimeoutService . convertToCountdown ( this . remaining ) ) ; if ( this . remaining <= 0 ) { this . cleanUpTimer ( ) ; this . authService . logout ( ) ; } else if ( ( this . remaining <= SessionTimeoutService . NOTIFICATION_THRESHOLD ) && ! this . notified ) { this . notified = true ; this . notifyPossibleTimeout ( ) ; } } ) ; } private private notifyPossibleTimeout ( ) { this . notificationModalRef = this . modal . open ( SessionTimeoutNotificationModalComponent ) ; this . notificationModalRef . componentInstance . remainingTime$ = this . sessionTimeoutRemaining ; this . notificationModalRef . result . then ( ( result ) => { if ( result . renewed ) { this . toast . success ( 'Session renewed!' ) ; } else { this . displayLogoutWarning ( ) ; } } , ( ) => { this . displayLogoutWarning ( ) ; } ) ; } private private displayLogoutWarning ( ) { const logoutTime = new Date ( this . tokenExpire ) ; this . toast . warning ( `You will be logged out at ${ logoutTime }` ) ; } private private stopTimer ( ) { this . timerSubscription . unsubscribe ( ) ; } private private cleanUpTimer ( ) { this . sessionTimeout . complete ( ) ; this . sessionTimeoutRemaining . complete ( ) ; this . stopTimer ( ) ; if ( this . notificationModalRef ) { this . notificationModalRef . close ( ) ; } } public ngOnDestroy ( ) { this . cleanUpTimer ( ) ; this . subscriber . next ( ) ; this . subscriber . complete ( ) ; } }