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
JavaScript
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