"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionTimeout = void 0; exports.startTimer = startTimer; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _session_expiration_toast = require("./session_expiration_toast"); var _constants = require("../../common/constants"); var _types = require("../../common/types"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ class SessionTimeout { constructor(notifications, sessionExpired, http, tenant) { (0, _defineProperty2.default)(this, "channel", void 0); (0, _defineProperty2.default)(this, "isVisible", document.visibilityState !== 'hidden'); (0, _defineProperty2.default)(this, "isFetchingSessionInfo", false); (0, _defineProperty2.default)(this, "consecutiveErrorCount", 0); (0, _defineProperty2.default)(this, "snoozedWarningState", void 0); (0, _defineProperty2.default)(this, "sessionState$", new _rxjs.BehaviorSubject({ lastExtensionTime: 0, expiresInMs: null, canBeExtended: false })); (0, _defineProperty2.default)(this, "subscription", void 0); (0, _defineProperty2.default)(this, "warningToast", void 0); (0, _defineProperty2.default)(this, "stopActivityMonitor", void 0); (0, _defineProperty2.default)(this, "stopVisibilityMonitor", void 0); (0, _defineProperty2.default)(this, "removeHttpInterceptor", void 0); (0, _defineProperty2.default)(this, "stopRefreshTimer", void 0); (0, _defineProperty2.default)(this, "stopWarningTimer", void 0); (0, _defineProperty2.default)(this, "stopLogoutTimer", void 0); /** * Event handler that receives session information from other browser tabs. */ (0, _defineProperty2.default)(this, "handleChannelMessage", messageEvent => { if (this.isSessionState(messageEvent.data)) { this.sessionState$.next(messageEvent.data); } }); (0, _defineProperty2.default)(this, "isSessionState", data => { return typeof data === 'object' && Object.hasOwn(data !== null && data !== void 0 ? data : {}, 'canBeExtended'); }); /** * HTTP request interceptor which ensures that API calls extend the session only if tab is * visible. */ (0, _defineProperty2.default)(this, "handleHttpRequest", fetchOptions => { // Ignore requests to external URLs if (fetchOptions.path.indexOf('://') !== -1 || fetchOptions.path.startsWith('//')) { return; } if (!fetchOptions.asSystemRequest) { return { ...fetchOptions, asSystemRequest: !this.isVisible }; } }); /** * Event handler that tracks user activity and extends the session if needed. */ (0, _defineProperty2.default)(this, "handleUserActivity", () => { if (this.shouldExtend()) { this.fetchSessionInfo(true); } }); /** * Event handler that tracks page visibility. */ (0, _defineProperty2.default)(this, "handleVisibilityChange", isVisible => { this.isVisible = isVisible; if (isVisible) { this.handleUserActivity(); } }); (0, _defineProperty2.default)(this, "resetTimers", ({ lastExtensionTime, expiresInMs }) => { var _this$stopRefreshTime, _this$stopWarningTime, _this$stopLogoutTimer; this.stopRefreshTimer = (_this$stopRefreshTime = this.stopRefreshTimer) === null || _this$stopRefreshTime === void 0 ? void 0 : _this$stopRefreshTime.call(this); this.stopWarningTimer = (_this$stopWarningTime = this.stopWarningTimer) === null || _this$stopWarningTime === void 0 ? void 0 : _this$stopWarningTime.call(this); this.stopLogoutTimer = (_this$stopLogoutTimer = this.stopLogoutTimer) === null || _this$stopLogoutTimer === void 0 ? void 0 : _this$stopLogoutTimer.call(this); if (expiresInMs !== null) { const logoutInMs = Math.max(expiresInMs - _constants.SESSION_GRACE_PERIOD_MS, 0); // Show warning before session expires. However, do not show warning again if previously // dismissed. The snooze time is the expiration time that was remaining in the warning. const showWarningInMs = Math.max(logoutInMs - _constants.SESSION_EXPIRATION_WARNING_MS, this.snoozedWarningState ? this.snoozedWarningState.lastExtensionTime + this.snoozedWarningState.expiresInMs - _constants.SESSION_GRACE_PERIOD_MS - lastExtensionTime : 0, 0); const fetchSessionInMs = showWarningInMs - _constants.SESSION_CHECK_MS; // Schedule logout when session is about to expire this.stopLogoutTimer = startTimer(() => this.sessionExpired.logout(_types.LogoutReason.SESSION_EXPIRED), logoutInMs); // Hide warning if session has been extended if (showWarningInMs > 0) { this.hideWarning(); } // Schedule warning before session expires if (showWarningInMs < logoutInMs) { this.stopWarningTimer = startTimer(this.showWarning, showWarningInMs); } // Refresh session info before showing warning if (fetchSessionInMs > 0 && fetchSessionInMs < logoutInMs) { this.stopRefreshTimer = startTimer(this.fetchSessionInfo, fetchSessionInMs); } } }); (0, _defineProperty2.default)(this, "toggleEventHandlers", ({ expiresInMs, canBeExtended }) => { if (expiresInMs !== null) { // Monitor activity if session can be extended if (canBeExtended && !this.stopActivityMonitor) { this.stopActivityMonitor = monitorActivity(this.handleUserActivity); } // Intercept HTTP requests if session can expire if (!this.removeHttpInterceptor) { this.removeHttpInterceptor = this.http.intercept({ request: this.handleHttpRequest }); } if (!this.stopVisibilityMonitor) { this.stopVisibilityMonitor = monitorVisibility(this.handleVisibilityChange); } } else { var _this$removeHttpInter, _this$stopActivityMon, _this$stopVisibilityM; this.removeHttpInterceptor = (_this$removeHttpInter = this.removeHttpInterceptor) === null || _this$removeHttpInter === void 0 ? void 0 : _this$removeHttpInter.call(this); this.stopActivityMonitor = (_this$stopActivityMon = this.stopActivityMonitor) === null || _this$stopActivityMon === void 0 ? void 0 : _this$stopActivityMon.call(this); this.stopVisibilityMonitor = (_this$stopVisibilityM = this.stopVisibilityMonitor) === null || _this$stopVisibilityM === void 0 ? void 0 : _this$stopVisibilityM.call(this); } }); (0, _defineProperty2.default)(this, "fetchSessionInfo", async (extend = false) => { this.isFetchingSessionInfo = true; try { const sessionInfo = await this.http.fetch(_constants.SESSION_ROUTE, { method: extend ? 'POST' : 'GET', asSystemRequest: !extend }); this.consecutiveErrorCount = 0; if (sessionInfo) { const { expiresInMs, canBeExtended } = sessionInfo; const nextState = { lastExtensionTime: Date.now(), expiresInMs, canBeExtended }; this.sessionState$.next(nextState); if (this.channel) { this.channel.postMessage(nextState); } return nextState; } } catch (error) { this.consecutiveErrorCount++; } finally { this.isFetchingSessionInfo = false; } }); (0, _defineProperty2.default)(this, "showWarning", () => { if (!this.warningToast) { const onExtend = async () => { const { canBeExtended } = this.sessionState$.getValue(); if (canBeExtended) { await this.fetchSessionInfo(true); } }; const onClose = () => { this.hideWarning(true); return onExtend(); }; const toast = (0, _session_expiration_toast.createSessionExpirationToast)(this.sessionState$, onExtend, onClose); this.warningToast = this.notifications.toasts.add(toast); } }); (0, _defineProperty2.default)(this, "hideWarning", (snooze = false) => { if (this.warningToast) { this.notifications.toasts.remove(this.warningToast); this.warningToast = undefined; if (snooze) { this.snoozedWarningState = this.sessionState$.getValue(); } } }); this.notifications = notifications; this.sessionExpired = sessionExpired; this.http = http; this.tenant = tenant; } async start() { if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } this.subscription = this.sessionState$.pipe((0, _operators.skip)(1), (0, _operators.throttleTime)(1000), (0, _operators.tap)(this.toggleEventHandlers)).subscribe(this.resetTimers); // Subscribe to a broadcast channel for session timeout messages. // This allows us to synchronize the UX across tabs and avoid repetitive API calls. try { this.channel = new BroadcastChannel(`${this.tenant}/session_timeout`); this.channel.onmessage = event => this.handleChannelMessage(event); } catch (error) { // eslint-disable-next-line no-console console.warn(`Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded.`, error); } return this.fetchSessionInfo(); } stop() { var _this$subscription, _this$channel; const nextState = { lastExtensionTime: 0, expiresInMs: null, canBeExtended: false }; this.toggleEventHandlers(nextState); this.resetTimers(nextState); (_this$subscription = this.subscription) === null || _this$subscription === void 0 ? void 0 : _this$subscription.unsubscribe(); (_this$channel = this.channel) === null || _this$channel === void 0 ? void 0 : _this$channel.close(); } shouldExtend() { const { lastExtensionTime } = this.sessionState$.getValue(); return !this.isFetchingSessionInfo && !this.warningToast && Date.now() > lastExtensionTime + _constants.SESSION_EXTENSION_THROTTLE_MS * Math.exp(this.consecutiveErrorCount); } } /** * Starts a timer that uses a native `setTimeout` under the hood. When `timeout` is larger * than the maximum supported one then method calls itself recursively as many times as needed. * @param callback A function to be executed after the timer expires. * @param timeout The time, in milliseconds the timer should wait before the specified function is * executed. * @returns Function to stop the timer. */ exports.SessionTimeout = SessionTimeout; function startTimer(callback, timeout, updater) { var _updater; // Max timeout is the largest possible 32-bit signed integer or 2,147,483,647 or 0x7fffffff. const maxTimeout = 0x7fffffff; let timeoutID; updater = (_updater = updater) !== null && _updater !== void 0 ? _updater : id => timeoutID = id; updater(timeout > maxTimeout ? window.setTimeout(() => startTimer(callback, timeout - maxTimeout, updater), maxTimeout) : window.setTimeout(callback, timeout)); return () => clearTimeout(timeoutID); } /** * Adds event handlers to the window object that track user activity. * @param callback Function to be executed when user activity is detected. * @returns Function to remove all event handlers from window. */ function monitorActivity(callback) { const eventTypes = ['mousemove', 'mousedown', 'wheel', 'touchstart', 'keydown']; for (const eventType of eventTypes) { window.addEventListener(eventType, callback); } return () => { for (const eventType of eventTypes) { window.removeEventListener(eventType, callback); } }; } /** * Adds event handlers to the document object that track page visibility. * @param callback Function to be executed when page visibility changes. * @returns Function to remove all event handlers from document. */ function monitorVisibility(callback) { const handleVisibilityChange = () => callback(document.visibilityState !== 'hidden'); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }