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