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

179 lines (170 loc) 15 kB
import { provideHttpClient, withInterceptors } from '@angular/common/http'; import * as i0 from '@angular/core'; import { InjectionToken, inject, Injectable, input, signal, ElementRef, ChangeDetectionStrategy, Component } from '@angular/core'; import { interval, Subject, shareReplay } from 'rxjs'; import { AsyncPipe } from '@angular/common'; const SEA_INTERRUPT_SERVICE = new InjectionToken('SessionInterruptService'); class SessionTimerService { config = inject(ConfigToken); _timeoutSeconds = this.config.totalMinutes * 60; 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().pipe(shareReplay(1)); startTimer() { this.stopTimer(); this.timerSubscription = this.timer.subscribe((n) => { if (n <= this._timeoutSeconds) { this._remainSeconds.next(this._timeoutSeconds - n); } }); } stopTimer() { this.timerSubscription?.unsubscribe(); } resetTimer() { this.startTimer(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SessionTimerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SessionTimerService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SessionTimerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function sessionTimerHttpInterceptor(req, next) { const timerService = inject(SessionTimerService); timerService.resetTimer(); return next(req); } const ConfigToken = new InjectionToken('config'); function provideSessionExpirationServices(interrupter, config) { return [ { provide: ConfigToken, useValue: config || { totalMinutes: 20 }, }, { provide: SEA_INTERRUPT_SERVICE, useClass: interrupter }, provideHttpClient(withInterceptors([sessionTimerHttpInterceptor])), ]; } class SessionExpirationAlert { /** * Should start the timer or not. Usually, you can set it to true if a user is authenticated. */ startTimer = input(true, ...(ngDevMode ? [{ debugName: "startTimer" }] : [])); /** * Count down seconds. */ alertAt = input(60, ...(ngDevMode ? [{ debugName: "alertAt" }] : [])); showModal = signal(false, ...(ngDevMode ? [{ debugName: "showModal" }] : [])); expired = signal(false, ...(ngDevMode ? [{ debugName: "expired" }] : [])); sessionTimerSubscription; el = inject(ElementRef); sessionInterrupter = inject(SEA_INTERRUPT_SERVICE); sessionTimer = inject(SessionTimerService); ngOnInit() { if (!this.sessionTimerSubscription && this.startTimer()) { this.trackSessionTime(); } 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.set(false); this.sessionTimerSubscription = this.sessionTimer.remainSeconds$.subscribe((t) => { if (t === this.alertAt()) { this.open(); } if (t === 0) { this.expired.set(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.set(true); document.body.classList.add('sea-modal-open'); } close() { this.showModal.set(false); document.body.classList.remove('sea-modal-open'); } cleanUp() { this.sessionTimer.stopTimer(); this.sessionTimerSubscription?.unsubscribe(); } reload() { this.close(); location.reload(); } ngOnDestroy() { this.el.nativeElement.remove(); this.cleanUp(); } handleTabKey(event) { 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(); event.preventDefault(); } } } handleShiftTabKey(event) { 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(); event.preventDefault(); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SessionExpirationAlert, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SessionExpirationAlert, isStandalone: true, selector: "session-expiration-alert", inputs: { startTimer: { classPropertyName: "startTimer", publicName: "startTimer", isSignal: true, isRequired: false, transformFunction: null }, alertAt: { classPropertyName: "alertAt", publicName: "alertAt", isSignal: true, isRequired: false, transformFunction: null } }, 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 @if(expired()) {\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"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 } @else {\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expiring\">\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 }\r\n</div>\r\n\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: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SessionExpirationAlert, decorators: [{ type: Component, args: [{ selector: 'session-expiration-alert', imports: [AsyncPipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { '(document:keydown.tab)': 'handleTabKey($event)', '(document:keydown.shift.tab)': 'handleShiftTabKey($event)', }, 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 @if(expired()) {\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"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 } @else {\r\n <div role=\"document\" class=\"sea-modal-content\" id=\"expiring\">\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 }\r\n</div>\r\n\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"] }] }], propDecorators: { startTimer: [{ type: i0.Input, args: [{ isSignal: true, alias: "startTimer", required: false }] }], alertAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alertAt", required: false }] }] } }); /* * Public API Surface of session-expiration-alert */ /** * Generated bundle index. Do not edit. */ export { ConfigToken, SEA_INTERRUPT_SERVICE, SessionExpirationAlert, SessionTimerService, provideSessionExpirationServices, sessionTimerHttpInterceptor }; //# sourceMappingURL=session-expiration-alert.mjs.map