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.