PageSourceSearch

https://www.phenomenex.com/scripts/idleTimeout.js

js phenomenex.com collected 2026-10-03 23:17:32 UTC 15,127 bytes, 453 lines download raw bytes

1/* eslint-disable no-console */
2// eslint-disable-next-line import/no-cycle
3import {
4  refreshToken, logout, login, loginAfterSessionExpiry,
5} from './auth.js';
6import { div, p, span } from './dom-builder.js';
7import { pauseTokenExpiryMonitor, resumeTokenExpiryMonitor, resetIdleRefreshingFlag } from './tokenExpiryMonitor.js';
8
9// Configuration
10// Two-tier idle detection (simple approach):
11// 1. 15 seconds idle → Pause token auto-refresh
12// 2. 13 minutes idle → Show modal with 2 minute countdown
13// Note: If token expires before user clicks "Continue", they'll need to re-login
14const IDLE_DETECTION_TIME = 15 * 1000; // 15 seconds - pause auto-refresh
15const IDLE_TIMEOUT = 13 * 60 * 1000; // 13 minutes - show modal
16const AUTO_LOGOUT_COUNTDOWN = 2 * 60 * 1000; // 2 minutes countdown before auto logout
17const EVENTS_TO_MONITOR = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
18
19let idleDetectionTimer = null; // Timer for 15-second idle detection
20let idleTimer = null; // Timer for showing modal at 13 min
21let isModalOpen = false;
22let countdownInterval = null;
23let autoLogoutTimer = null;
24let isUserIdle = false; // Track if user is currently idle (15+ seconds)
25
26// Forward declaration for resetIdleTimer
27let resetIdleTimer;
28
29/**
30 * Creates and shows the idle timeout warning modal
31 */
32function showIdleTimeoutModal() {
33  if (isModalOpen) return; // Prevent multiple modals
34  isModalOpen = true;
35
36  // Token auto-refresh was already paused when user became idle (15 sec ago)
37  console.log('Showing idle timeout modal (auto-refresh already paused at 15 sec idle)');
38
39  // Create modal overlay
40  const overlay = div({
41    id: 'idle-timeout-modal-overlay',
42    class: 'fixed inset-0 bg-grey-heading-2/50 backdrop-blur-[0.5px] flex items-center justify-center z-[9999]',
43    style: 'z-index: 99999;',
44  });
45
46  // Create modal content
47  const modal = div(
48    { class: 'bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6' },
49    // Title (hidden when countdown ends — only Session Expired box remains)
50    p(
51      { class: 'idle-timeout-modal-title text-xl font-semibold text-grey-heading-2 mb-4' },
52      'Session Timeout Warning',
53    ),
54    // Message with countdown placeholder
55    p(
56      {
57        class: 'text-base text-grey-text mb-4 countdown-display idle-timeout-countdown-msg',
58      },
59      'Due to inactivity, your session will expire in 2 minute(s) 0 second(s). If additional time is required, please click "Continue Session".',
60    ),
61    // Buttons container
62    div(
63      { class: 'idle-timeout-actions-row flex gap-4 justify-end' },
64      // Logout button
65      span(
66        {
67          class: 'px-6 py-2 border border-grey-dim text-grey-heading-2 rounded hover:bg-gray-50 transition-colors cursor-pointer',
68        },
69        'Logout',
70      ),
71      // Continue button
72      span(
73        {
74          class: 'px-6 py-2 bg-blue-primary text-white rounded hover:bg-blue-600 transition-colors cursor-pointer',
75        },
76        'Continue Session',
77      ),
78    ),
79  );
80
81  overlay.appendChild(modal);
82
83  // Get elements
84  const buttons = modal.querySelectorAll('span');
85  const logoutBtn = buttons[0];
86  const continueBtn = buttons[1];
87  const countdownDisplay = modal.querySelector('.countdown-display');
88
89  // Countdown logic
90  let secondsRemaining = AUTO_LOGOUT_COUNTDOWN / 1000; // 120 seconds (2 minutes)
91
92  // Update countdown display with mm:ss format
93  const updateCountdown = () => {
94    const minutes = Math.floor(secondsRemaining / 60);
95    const seconds = secondsRemaining % 60;
96    countdownDisplay.innerHTML = `Due to inactivity, your session will expire in <b>${minutes} minute(s) ${seconds} second(s)</b>. If additional time is required, please click "Continue Session".`;
97  };
98
99  // Initial display
100  updateCountdown();
101
102  // Start countdown interval (updates every second)
103  countdownInterval = setInterval(() => {
104    secondsRemaining -= 1;
105    updateCountdown();
106
107    if (secondsRemaining <= 0) {
108      clearInterval(countdownInterval);
109    }
110  }, 1000);
111
112  // Auto logout after countdown
113  autoLogoutTimer = setTimeout(async () => {
114    clearInterval(countdownInterval);
115    console.log('Auto-logout triggered after countdown...');
116
117    // Store flags in localStorage (NOT sessionStorage) so they persist across tabs
118    localStorage.setItem('sessionExpiredModal', 'true');
119    localStorage.setItem('sessionExpiredFrom', window.location.pathname);
120    localStorage.setItem('sessionExpiredTime', Date.now().toString());
121
122    // Logout the user - this will redirect to home page
123    try {
124      console.log('Clearing Auth0 session...');
125      await logout();
126      // logout() handles redirect, so code below won't execute
127    } catch (error) {
128      console.error('Error during logout:', error);
129      // If logout fails, force redirect to home
130      window.location.href = '/';
131    }
132  }, AUTO_LOGOUT_COUNTDOWN);
133
134  // Logout button handler
135  logoutBtn.addEventListener('click', async () => {
136    // Clear timers
137    clearInterval(countdownInterval);
138    clearTimeout(autoLogoutTimer);
139
140    try {
141      await logout();
142    } catch (error) {
143      console.error('Error during logout:', error);
144      // Force logout anyway
145      window.location.href = '/';
146    }
147  });
148
149  // Continue button handler
150  const disableContinueBtn = () => {
151    continueBtn.classList.add('opacity-50', 'cursor-not-allowed', 'pointer-events-none');
152    continueBtn.classList.remove('hover:bg-blue-600', 'cursor-pointer');
153  };
154
155  const handleContinueClick = async () => { // eslint-disable-line no-use-before-define
156    // Disable button immediately to prevent multiple clicks
157    disableContinueBtn();
158    continueBtn.removeEventListener('click', handleContinueClick);
159
160    // Disable button immediately on click
161    continueBtn.textContent = 'Refreshing...';
162    continueBtn.classList.add('opacity-50', 'cursor-not-allowed');
163    continueBtn.classList.remove('hover:bg-blue-600');
164
165    // Clear timers
166    clearInterval(countdownInterval);
167    clearTimeout(autoLogoutTimer);
168
169    try {
170      // Reset the idle-refresh guard so the next idle cycle's silent refresh
171      // is never skipped even if a background refresh was still in-flight
172      // when the user clicked "Continue Session".
173      resetIdleRefreshingFlag();
174
175      // Directly attempt token refresh — refreshToken() already throws with
176      // requiresLogin=true if the token is expired, so the separate
177      // canRefreshToken() pre-check is redundant and can race against the
178      // in-flight silent idle refresh that just updated the token cache.
179      const newToken = await refreshToken();
180      console.log('Token refreshed successfully:', newToken ? 'New token received' : 'No token received');
181
182      // Reset idle state and resume token auto-refresh monitor
183      isUserIdle = false;
184      resumeTokenExpiryMonitor();
185
186      // Close modal and reset idle timer
187      document.body.removeChild(overlay);
188      isModalOpen = false;
189      resetIdleTimer();
190    } catch (error) {
191      disableContinueBtn();
192      console.error('Failed to refresh token:', error);
193
194      // Check if error requires re-authentication
195      const requiresLogin = error.requiresLogin === true
196        || error.error === 'invalid_grant'
197        || error.error === 'login_required'
198        || error.message?.includes('expired')
199        || error.message?.includes('re-authentication required');
200
201      // Check if it's a network/connection error
202      const isNetworkError = !requiresLogin
203        && (error.message?.includes('403')
204          || error.message?.includes('Forbidden')
205          || error.message?.includes('Network')
206          || error.message?.includes('timeout'));
207
208      if (requiresLogin) {
209        // Token expired or invalid - redirect to login
210        continueBtn.textContent = 'Session Expired';
211        continueBtn.classList.remove('bg-blue-primary', 'hover:bg-blue-600');
212        continueBtn.classList.add('bg-yellow-600', 'cursor-not-allowed');
213
214        const errorMsg = div(
215          { class: 'error-message mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded' },
216          p(
217            { class: 'text-yellow-800 font-semibold mb-2' },
218            'Session Expired',
219          ),
220          p(
221            { class: 'text-yellow-700 text-sm mb-3' },
222            'Your session has expired. Redirecting to login page...',
223          ),
224          p(
225            { class: 'text-yellow-600 text-xs' },
226            'Redirecting in 3 seconds...',
227          ),
228        );
229
230        modal.appendChild(errorMsg);
231
232        setTimeout(() => {
233          login();
234        }, 3000);
235      } else {
236        // Network or other error - offer logout
237        continueBtn.textContent = 'Connection Error';
238        continueBtn.classList.remove('bg-blue-primary', 'hover:bg-blue-600');
239        continueBtn.classList.add('bg-red-600', 'cursor-not-allowed');
240
241        const errorMsg = div(
242          { class: 'error-message mt-4 p-4 bg-red-50 border border-red-200 rounded' },
243          p(
244            { class: 'text-red-800 font-semibold mb-2' },
245            'Connection Error',
246          ),
247          p(
248            { class: 'text-red-700 text-sm mb-3' },
249            isNetworkError
250              ? 'Unable to connect to the server. Please check your internet connection and try again.'
251              : 'Unable to refresh your session. Logging out...',
252          ),
253          p(
254            { class: 'text-red-600 text-xs' },
255            'Redirecting in 5 seconds...',
256          ),
257        );
258
259        modal.appendChild(errorMsg);
260
261        setTimeout(async () => {
262          console.log('Logging out due to token refresh failure...');
263          await logout();
264        }, 5000);
265      }
266    }
267  };
268
269  continueBtn.addEventListener('click', handleContinueClick);
270
271  // Append to body
272  document.body.appendChild(overlay);
273}
274
275/**
276 * Shows the session expired modal after auto-logout
277 * This is called on page load if sessionStorage flag is set
278 */
279export function showSessionExpiredModal() {
280  if (isModalOpen) return;
281  isModalOpen = true;
282
283  console.log('Showing session expired modal...');
284
285  // Create modal overlay
286  const overlay = div({
287    id: 'session-expired-modal-overlay',
288    class: 'fixed inset-0 bg-grey-heading-2/50 backdrop-blur-[0.5px] flex items-center justify-center z-[9999]',
289    style: 'z-index: 99999;',
290  });
291
292  // Create modal content (only Session Expired box — no duplicate header text)
293  const modal = div(
294    { class: 'bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6' },
295    div(
296      { class: 'p-4 bg-white rounded' },
297      p(
298        { class: 'text-black font-semibold mb-4 text-center' },
299        'Session Expired',
300      ),
301      p(
302        { class: 'text-black text-sm mb-4 text-center' },
303        'You have been logged out due to inactivity. Please sign in again to continue.',
304      ),
305      // Buttons container
306      div(
307        { class: 'flex gap-4 justify-center' },
308        // Cancel button
309        span(
310          {
311            class: 'px-6 py-2 border border-grey-dim text-grey-heading-2 rounded hover:bg-gray-50 transition-colors cursor-pointer cancel-btn',
312          },
313          'Cancel',
314        ),
315        // Sign In button
316        span(
317          {
318            class: 'px-6 py-2 bg-blue-primary text-white rounded hover:bg-blue-600 transition-colors cursor-pointer signin-btn',
319          },
320          'Sign In',
321        ),
322      ),
323    ),
324  );
325
326  overlay.appendChild(modal);
327
328  // Get buttons
329  const cancelBtn = modal.querySelector('.cancel-btn');
330  const signinBtn = modal.querySelector('.signin-btn');
331
332  // Cancel button handler - close modal
333  cancelBtn.addEventListener('click', () => {
334    document.body.removeChild(overlay);
335    isModalOpen = false;
336    // Clear the localStorage flags
337    localStorage.removeItem('sessionExpiredModal');
338    localStorage.removeItem('sessionExpiredFrom');
339    localStorage.removeItem('sessionExpiredTime');
340  });
341
342  // Sign In button handler - redirect to login with forced authentication
343  signinBtn.addEventListener('click', () => {
344    // Clear the localStorage flags
345    localStorage.removeItem('sessionExpiredModal');
346    localStorage.removeItem('sessionExpiredFrom');
347    localStorage.removeItem('sessionExpiredTime');
348    // Force login prompt (don't auto-sign in with existing Auth0 session)
349    loginAfterSessionExpiry();
350  });
351
352  // Append to body
353  document.body.appendChild(overlay);
354}
355
356/**
357 * Resets the idle timer
358 */
359resetIdleTimer = async function resetIdleTimerFn() {
360  // If user was idle and now active, resume token monitoring ONLY if modal is not open
361  if (isUserIdle && !isModalOpen) {
362    console.log('User is active again, resuming token auto-refresh...');
363    isUserIdle = false;
364    resumeTokenExpiryMonitor();
365  }
366
367  // Clear all existing timers
368  if (idleDetectionTimer) {
369    clearTimeout(idleDetectionTimer);
370  }
371  if (idleTimer) {
372    clearTimeout(idleTimer);
373  }
374
375  // TIER 1: After 15 seconds of idle, pause token auto-refresh
376  idleDetectionTimer = setTimeout(() => {
377    console.log('User idle for 15 seconds, pausing token auto-refresh...');
378    isUserIdle = true;
379    pauseTokenExpiryMonitor();
380  }, IDLE_DETECTION_TIME);
381
382  // TIER 2: After 13 minutes of idle, show modal with 2-minute countdown
383  idleTimer = setTimeout(() => {
384    console.log('DEBUG idleTimeout: ✅ 13-min timer FIRED — calling showIdleTimeoutModal()');
385    showIdleTimeoutModal();
386  }, IDLE_TIMEOUT);
387};
388
389/**
390 * Initializes the idle timeout monitor
391 * Call this function when the user logs in or on app initialization
392 */
393export function initIdleTimeoutMonitor() {
394  console.log('Initializing idle timeout monitor (15sec → pause auto-refresh | 13min → show modal)...');
395
396  // Add event listeners for user activity
397  EVENTS_TO_MONITOR.forEach((event) => {
398    window.addEventListener(event, resetIdleTimer, { passive: true });
399  });
400
401  // Start the initial timer
402  resetIdleTimer();
403}
404
405/**
406 * Stops the idle timeout monitor
407 * Call this function when the user logs out
408 */
409export function stopIdleTimeoutMonitor() {
410  console.log('Stopping idle timeout monitor...');
411
412  // Clear idle detection timer (15 sec)
413  if (idleDetectionTimer) {
414    clearTimeout(idleDetectionTimer);
415    idleDetectionTimer = null;
416  }
417
418  // Clear idle timer (13 min)
419  if (idleTimer) {
420    clearTimeout(idleTimer);
421    idleTimer = null;
422  }
423
424  // Clear countdown interval
425  if (countdownInterval) {
426    clearInterval(countdownInterval);
427    countdownInterval = null;
428  }
429
430  // Clear auto logout timer
431  if (autoLogoutTimer) {
432    clearTimeout(autoLogoutTimer);
433    autoLogoutTimer = null;
434  }
435
436  // Reset idle state
437  isUserIdle = false;
438
439  // Remove event listeners
440  EVENTS_TO_MONITOR.forEach((event) => {
441    window.removeEventListener(event, resetIdleTimer);
442  });
443
444  // Close modal if open — use id selector for reliable lookup
445  if (isModalOpen) {
446    const overlay = document.getElementById('idle-timeout-modal-overlay')
447      || document.getElementById('session-expired-modal-overlay');
448    if (overlay && overlay.parentNode) {
449      overlay.parentNode.removeChild(overlay);
450    }
451    isModalOpen = false;
452  }
453}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.