UNPKG

session-expiration-alert

Version:

An Angular module to time session expiration. When user session idle time reaches a threshold, then pop up a modal dialog to let user choose to continue session or log out the system. When user session is expired, timer will stop and user will be logged o

267 lines (257 loc) 18.1 kB
import * as i3 from '@angular/common'; import { CommonModule } from '@angular/common'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import * as i0 from '@angular/core'; import { Injectable, InjectionToken, Inject, HostListener, Input, ChangeDetectionStrategy, Component, inject, NgModule } from '@angular/core'; import { interval, Subject } from 'rxjs'; class SessionInterruptService { constructor() { } /** * Method to refresh session cookie. Normally, this method issue a quick API call to server. * * @memberof SessionInterruptService */ continueSession() { console.log('You clicked continue session.'); } /** * Method to remove session cookie. Normally, this method redirect to website logout endpoint. * * @memberof SessionInterruptService */ stopSession() { console.log('You clicked logout.'); } /** * Method to handle expiration event. Can be empty. */ onExpire() { console.log('Session expired.'); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionInterruptService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionInterruptService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionInterruptService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); const ConfigToken = new InjectionToken('config'); class SessionTimerService { config; _timeoutSeconds; _count = 0; timerSubscription; timer = interval(1000); _remainSeconds = new Subject(); /** * Observable to get session remaining time (in seconds). * * Subscribers need to unsubscribe to it before hosting element is destroyed. * * @memberof SessionTimerService */ remainSeconds$ = this._remainSeconds.asObservable(); constructor(config) { this.config = config; this._timeoutSeconds = config.totalMinutes * 60; } startTimer() { this.stopTimer(); this._count = this._timeoutSeconds; this.timerSubscription = this.timer.subscribe((n) => { if (this._count > 0) { this._count--; this._remainSeconds.next(this._count); } }); } stopTimer() { if (this.timerSubscription) { this.timerSubscription.unsubscribe(); } } resetTimer() { this.startTimer(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionTimerService, deps: [{ token: ConfigToken }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionTimerService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionTimerService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [ConfigToken] }] }] }); class SessionExpirationAlertComponent { el; sessionInterrupter; sessionTimer; /** * Should start the timer or not. Usually, you can set it to true if a user is authenticated. */ startTimer = true; /** * Count down seconds. */ alertAt = 60; showModal = false; expired = false; sessionTimerSubscription; constructor(el, sessionInterrupter, sessionTimer) { this.el = el; this.sessionInterrupter = sessionInterrupter; this.sessionTimer = sessionTimer; } ngOnInit() { if (!this.sessionTimerSubscription && this.startTimer) { this.trackSessionTime(); } // move element to bottom of page (just before </body>) // so it can be displayed above everything else document.body.appendChild(this.el.nativeElement); } ngOnChanges(changes) { if (changes['startTimer']) { this.cleanUp(); if (changes['startTimer'].currentValue) { this.trackSessionTime(); } } } trackSessionTime() { this.sessionTimer.startTimer(); this.expired = false; this.sessionTimerSubscription = this.sessionTimer.remainSeconds$.subscribe((t) => { if (t === this.alertAt) { this.open(); } if (t === 0) { this.expired = true; this.cleanUp(); this.sessionInterrupter.onExpire(); } }); } continue() { this.sessionInterrupter.continueSession(); this.sessionTimer.resetTimer(); this.close(); } logout() { this.sessionTimer.stopTimer(); this.close(); this.sessionInterrupter.stopSession(); } open() { this.showModal = true; document.body.classList.add('sea-modal-open'); } close() { this.showModal = false; document.body.classList.remove('sea-modal-open'); } cleanUp() { this.sessionTimer.stopTimer(); if (this.sessionTimerSubscription) { this.sessionTimerSubscription.unsubscribe(); } } reload() { this.close(); location.reload(); } ngOnDestroy() { this.el.nativeElement.remove(); this.cleanUp(); } handleTabKey(e) { const modal = document.querySelector('#session-expiration-alert'); if (modal) { const btn1 = modal.querySelector('button.btn-primary'); const btn2 = modal.querySelector('button.btn-secondary'); if (document.activeElement === btn1) { btn2?.focus(); e.preventDefault(); } } } handleShiftTabKey(e) { const modal = document.querySelector('#session-expiration-alert'); if (modal) { const btn1 = modal.querySelector('button.btn-primary'); const btn2 = modal.querySelector('button.btn-secondary'); if (document.activeElement === btn2) { btn1?.focus(); e.preventDefault(); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlertComponent, deps: [{ token: i0.ElementRef }, { token: SessionInterruptService }, { token: SessionTimerService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.8", type: SessionExpirationAlertComponent, isStandalone: false, selector: "session-expiration-alert", inputs: { startTimer: "startTimer", alertAt: "alertAt" }, host: { listeners: { "document:keydown.tab": "handleTabKey($event)", "document:keydown.shift.tab": "handleShiftTabKey($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div\r\n class=\"sea-modal\"\r\n id=\"session-expiration-alert\"\r\n role=\"dialog\"\r\n tabindex=\"-1\"\r\n aria-modal=\"true\"\r\n aria-label=\"session-expiration-alert\"\r\n [hidden]=\"!showModal\"\r\n>\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expiring\" *ngIf=\"!expired\">\r\n <div class=\"sea-modal-header\">Session is about to expire</div>\r\n <div class=\"sea-modal-body\">\r\n <p>\r\n Your session will expire in\r\n {{ sessionTimer.remainSeconds$ | async }} seconds.\r\n </p>\r\n </div>\r\n <div class=\"sea-modal-footer\">\r\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"logout()\">\r\n Logout\r\n </button>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"continue()\">\r\n Continue Session\r\n </button>\r\n </div>\r\n </div>\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expired\" *ngIf=\"expired\">\r\n <div class=\"sea-modal-header\">Session expired</div>\r\n <div class=\"sea-modal-body\">\r\n <p>\r\n The current page is idle due to lack of activity. You can try to reload\r\n this page or log out of the website.\r\n </p>\r\n </div>\r\n <div class=\"sea-modal-footer\">\r\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"logout()\">\r\n Logout\r\n </button>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"reload()\">\r\n Reload Page\r\n </button>\r\n </div>\r\n </div>\r\n</div>\r\n<div class=\"sea-modal-background\" [hidden]=\"!showModal\"></div>\r\n", styles: [":host{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}.sea-modal{position:fixed;inset:0;z-index:2050;overflow:auto}.sea-modal .sea-modal-content{display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid #0003;outline:0;position:relative;width:auto;margin:.5rem}@media (min-width: 576px){.sea-modal .sea-modal-content{max-width:500px;margin:1.75rem auto}}.sea-modal .sea-modal-content .sea-modal-header{background-color:#ffc107!important;display:flex;align-items:flex-start;justify-content:space-between;padding:1rem;border-bottom:1px solid #dee2e6;font-size:1.5rem;font-weight:500;line-height:1.5}.sea-modal .sea-modal-content .sea-modal-body{position:relative;flex:1 1 auto;padding:1.5rem 1rem;font-size:1.25rem}.sea-modal .sea-modal-content .sea-modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6}.sea-modal .sea-modal-content .sea-modal-footer>*{margin:.25rem}.sea-modal-background{position:fixed;inset:0;background-color:#000;opacity:.75;z-index:2000}body.sea-modal-open{overflow:hidden}\n", ".btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;background-color:initial;border:1px solid #0000;padding:.375rem .75rem;font-size:1rem;line-height:1.5;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:button;cursor:pointer;font-family:inherit}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem #828a917f}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem #3184fd7f}\n"], dependencies: [{ kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlertComponent, decorators: [{ type: Component, args: [{ selector: 'session-expiration-alert', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\r\n class=\"sea-modal\"\r\n id=\"session-expiration-alert\"\r\n role=\"dialog\"\r\n tabindex=\"-1\"\r\n aria-modal=\"true\"\r\n aria-label=\"session-expiration-alert\"\r\n [hidden]=\"!showModal\"\r\n>\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expiring\" *ngIf=\"!expired\">\r\n <div class=\"sea-modal-header\">Session is about to expire</div>\r\n <div class=\"sea-modal-body\">\r\n <p>\r\n Your session will expire in\r\n {{ sessionTimer.remainSeconds$ | async }} seconds.\r\n </p>\r\n </div>\r\n <div class=\"sea-modal-footer\">\r\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"logout()\">\r\n Logout\r\n </button>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"continue()\">\r\n Continue Session\r\n </button>\r\n </div>\r\n </div>\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expired\" *ngIf=\"expired\">\r\n <div class=\"sea-modal-header\">Session expired</div>\r\n <div class=\"sea-modal-body\">\r\n <p>\r\n The current page is idle due to lack of activity. You can try to reload\r\n this page or log out of the website.\r\n </p>\r\n </div>\r\n <div class=\"sea-modal-footer\">\r\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"logout()\">\r\n Logout\r\n </button>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"reload()\">\r\n Reload Page\r\n </button>\r\n </div>\r\n </div>\r\n</div>\r\n<div class=\"sea-modal-background\" [hidden]=\"!showModal\"></div>\r\n", styles: [":host{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}.sea-modal{position:fixed;inset:0;z-index:2050;overflow:auto}.sea-modal .sea-modal-content{display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid #0003;outline:0;position:relative;width:auto;margin:.5rem}@media (min-width: 576px){.sea-modal .sea-modal-content{max-width:500px;margin:1.75rem auto}}.sea-modal .sea-modal-content .sea-modal-header{background-color:#ffc107!important;display:flex;align-items:flex-start;justify-content:space-between;padding:1rem;border-bottom:1px solid #dee2e6;font-size:1.5rem;font-weight:500;line-height:1.5}.sea-modal .sea-modal-content .sea-modal-body{position:relative;flex:1 1 auto;padding:1.5rem 1rem;font-size:1.25rem}.sea-modal .sea-modal-content .sea-modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6}.sea-modal .sea-modal-content .sea-modal-footer>*{margin:.25rem}.sea-modal-background{position:fixed;inset:0;background-color:#000;opacity:.75;z-index:2000}body.sea-modal-open{overflow:hidden}\n", ".btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;background-color:initial;border:1px solid #0000;padding:.375rem .75rem;font-size:1rem;line-height:1.5;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:button;cursor:pointer;font-family:inherit}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem #828a917f}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem #3184fd7f}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: SessionInterruptService }, { type: SessionTimerService }], propDecorators: { startTimer: [{ type: Input }], alertAt: [{ type: Input }], handleTabKey: [{ type: HostListener, args: ['document:keydown.tab', ['$event']] }], handleShiftTabKey: [{ type: HostListener, args: ['document:keydown.shift.tab', ['$event']] }] } }); function sessionTimerHttpInterceptor(req, next) { const timerService = inject(SessionTimerService); timerService.resetTimer(); return next(req); } class SessionExpirationAlert { /** * * @param config SessionExpirationConfig, default value: { totalMinutes: 20 } * */ static forRoot(config = { totalMinutes: 20, }) { return { ngModule: SessionExpirationAlert, providers: [ { provide: ConfigToken, useValue: config, }, ], }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlert, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlert, declarations: [SessionExpirationAlertComponent], imports: [CommonModule], exports: [SessionExpirationAlertComponent] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlert, providers: [ SessionTimerService, provideHttpClient(withInterceptors([sessionTimerHttpInterceptor])), SessionInterruptService, ], imports: [CommonModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: SessionExpirationAlert, decorators: [{ type: NgModule, args: [{ imports: [CommonModule], declarations: [SessionExpirationAlertComponent], providers: [ SessionTimerService, provideHttpClient(withInterceptors([sessionTimerHttpInterceptor])), SessionInterruptService, ], exports: [SessionExpirationAlertComponent], }] }] }); /* * Public API Surface of session-expiration-alert */ /** * Generated bundle index. Do not edit. */ export { ConfigToken, SessionExpirationAlert, SessionExpirationAlertComponent, SessionInterruptService, SessionTimerService, sessionTimerHttpInterceptor }; //# sourceMappingURL=session-expiration-alert.mjs.map