1/* eslint-disable no-console */ 2/* eslint-disable import/no-cycle */ 3/** 4 * Session Management Utility 5 * Handles comprehensive session cleanup, validation, and environment isolation 6 */ 7 8import { getEnvironment } from './scripts.js'; 9import { getCookie } from './utils/getCookie.js'; 10 11/** 12 * Get environment-specific prefix for storage keys 13 */ 14export function getEnvPrefix() { 15 const env = getEnvironment(); 16 return env === 'PROD' ? '' : `${env.toLowerCase()}_`; 17} 18 19/** 20 * Get environment-specific cookie name 21 */ 22export function getEnvCookieName(baseName) { 23 const env = getEnvironment(); 24 return env === 'PROD' ? baseName : `${env.toLowerCase()}_${baseName}`; 25} 26 27// âââ Session Storage Registry ââââââââââââââââââââââââââââââââââââââââââââââââ 28// 29// The registry key holds a JSON array of every sessionStorage key that was 30// written via setUserSessionItem(). On logout ONLY those keys are removed. 31// No hardcoding, no pattern matching â the data self-documents its own owner. 32// 33// Usage in any file: 34// import { setUserSessionItem, setAppSessionItem, 35// getUserSessionItem, removeUserSessionItem } from './sessionManager.js'; 36// 37// setUserSessionItem('checkoutData', data); // cleared on logout â 38// setAppSessionItem('sgs_by_phase_state', s); // never cleared â 39// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 40 41// âââ Session Storage Registry ââââââââââââââââââââââââââââââââââââââââââââââââ 42// 43// The registry key holds a JSON array of every sessionStorage key written via 44// setUserSessionItem(). On logout ONLY those keys are removed. 45// 46// Cross-environment safety: 47// - The registry key itself is env-prefixed (e.g. dev___phx_user_keys__) 48// - Every user key is also env-prefixed (e.g. dev_checkoutData) 49// - App keys (setAppSessionItem) are never prefixed and never cleared 50// 51// Usage in any file: 52// setUserSessionItem('checkoutData', data); // stored as dev_checkoutData â 53// setAppSessionItem('sgs_by_phase_state', s); // stored as-is, never cleared â 54// 55// getUserSessionItem('checkoutData'); // reads dev_checkoutData â 56// removeUserSessionItem('checkoutData'); // removes dev_checkoutData â 57// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 58 59/** Registry key is env-prefixed so DEV/QA/PROD never share the same registry. */ 60function getRegistryKey() { 61 return `${getEnvPrefix()}__phx_user_keys__`; 62} 63 64/** Read the current env registry (array of already-prefixed key strings). */ 65function getRegistry() { 66 try { 67 return JSON.parse(sessionStorage.getItem(getRegistryKey()) || '[]'); 68 } catch { 69 return []; 70 } 71} 72 73/** Persist the registry. */ 74function saveRegistry(keys) { 75 sessionStorage.setItem(getRegistryKey(), JSON.stringify([...new Set(keys)])); 76} 77 78/** Register a (already-prefixed) key as user-owned. */ 79function registerUserKey(prefixedKey) { 80 const keys = getRegistry(); 81 if (!keys.includes(prefixedKey)) { 82 keys.push(prefixedKey); 83 saveRegistry(keys); 84 } 85} 86 87/** Deregister a (already-prefixed) key when it is explicitly removed. */ 88function deregisterUserKey(prefixedKey) { 89 saveRegistry(getRegistry().filter((k) => k !== prefixedKey)); 90} 91 92/** 93 * Write a USER-SESSION value to sessionStorage. 94 * The key is automatically env-prefixed and registered for logout cleanup. 95 * 96 * Use this for: cart, checkout, webUser, orders, favourites, billing, etc. 97 * 98 * Example: setUserSessionItem('checkoutData', val) 99 * â stored as 'checkoutData' on PROD 100 * â stored as 'dev_checkoutData' on DEV 101 * â stored as 'qa_checkoutData' on QA 102 */ 103export function setUserSessionItem(key, value) { 104 const prefixedKey = `${getEnvPrefix()}${key}`; 105 registerUserKey(prefixedKey); 106 sessionStorage.setItem(prefixedKey, value); 107} 108 109/** 110 * Read a user-session value (env-prefix applied automatically). 111 */ 112export function getUserSessionItem(key) { 113 return sessionStorage.getItem(`${getEnvPrefix()}${key}`); 114} 115 116/** 117 * Remove a user-session value and deregister the key. 118 */ 119export function removeUserSessionItem(key) { 120 const prefixedKey = `${getEnvPrefix()}${key}`; 121 deregisterUserKey(prefixedKey); 122 sessionStorage.removeItem(prefixedKey); 123} 124 125/** 126 * Write an APPLICATION-STATE value to sessionStorage. 127 * The key is NOT prefixed and NOT registered â it survives logout and is 128 * shared across environments (intentional: tool state is not user-specific). 129 * 130 * Use this for: tool state, UI flags, font-loaded, spgid, etc. 131 */ 132export function setAppSessionItem(key, value) { 133 // Intentionally no env prefix and no registry â this key survives logout. 134 sessionStorage.setItem(key, value); 135} 136 137// âââ Environment-aware helpers (keep existing callers working) ââââââââââââââââ 138 139/** 140 * Set environment-aware sessionStorage item (user-session, cleared on logout).
141 * Identical to setUserSessionItem â env-prefix is applied inside. 142 */ 143export function setEnvSessionStorage(key, value) { 144 setUserSessionItem(key, value); 145} 146 147/** 148 * Get environment-aware sessionStorage item. 149 */ 150export function getEnvSessionStorage(key) { 151 return getUserSessionItem(key); 152} 153 154/** 155 * Remove environment-aware sessionStorage item. 156 */ 157export function removeEnvSessionStorage(key) { 158 removeUserSessionItem(key); 159} 160 161/** 162 * Clear ALL storage for the current environment 163 * This includes: sessionStorage, localStorage, and cookies 164 * BUT preserves country/currency preferences and non-user-related sessionStorage 165 */ 166export function clearAllStorage() { 167 console.log('ð§¹ Clearing all storage for logout...'); 168 169 // Set a document-level logout flag in localStorage BEFORE any storage is cleared. 170 // localStorage is NOT frozen by the bfcache â unlike the JS heap and DOM, it always 171 // reflects real-world state. When the user presses Back after logout and the browser 172 // restores a bfcache snapshot, the pageshow handler reads this flag and forces a reload 173 // before the stale logged-in UI ever becomes visible. The flag is cleared after the 174 // reload so it does not persist across future navigations. 175 try { 176 localStorage.setItem('phx_logged_out', 'true'); 177 } catch { /* private browsing storage full â not fatal */ } 178 179 // STEP 1: Save country/currency preferences before clearing 180 const cookieName = getEnvCookieName('userinfo'); 181 const userInfoCookie = getCookie(cookieName); 182 let countryPreferences = null; 183 184 if (userInfoCookie) { 185 try { 186 const userInfo = JSON.parse(userInfoCookie); 187 // Extract only country/currency related data 188 // NOTE: CountryName MUST be included so that getUserInfo() returns a name 189 // and the header's loadCountries() does NOT fall into geo-detection, 190 // which would silently overwrite the user's selected country on logout. 191 countryPreferences = { 192 CountryCode: userInfo.CountryCode, 193 CountryId: userInfo.CountryId, 194 CountryName: userInfo.CountryName, 195 CurrencyCode: userInfo.CurrencyCode, 196 CurrencySymbol: userInfo.CurrencySymbol, 197 EcommerceEnabled: userInfo.EcommerceEnabled, 198 showPrice: userInfo.showPrice, 199 showAddToQuote: userInfo.showAddToQuote, 200 }; 201 console.log('ð¾ Saved country/currency preferences:', countryPreferences); 202 } catch (error) { 203 console.error('Error parsing userinfo cookie:', error); 204 } 205 } 206 207 // STEP 2: Clear ONLY user-registered sessionStorage keys (from registry) 208 try { 209 const userSessionKeys = getRegistry(); // only keys written via setUserSessionItem() 210 const totalKeys = Object.keys(sessionStorage).length; 211 212 console.log(`ð¦ Clearing ${userSessionKeys.length} user session items (preserving ${totalKeys - userSessionKeys.length} application items)`); 213 userSessionKeys.forEach((key) => sessionStorage.removeItem(key)); 214 215 // Remove the registry itself 216 sessionStorage.removeItem(getRegistryKey()); 217 } catch (error) { 218 console.error('Error clearing sessionStorage:', error); 219 } 220 221 // STEP 3: Clear Auth0 data from localStorage 222 try { 223 const localStorageKeys = Object.keys(localStorage); 224 const auth0Keys = localStorageKeys.filter( 225 (key) => key.includes('@@auth0spajs@@') 226 || key.includes('auth0') 227 || key === 'sessionExpiredModal' 228 || key === 'sessionExpiredFrom' 229 || key === 'sessionExpiredTime', 230 ); 231 console.log(`ð Clearing ${auth0Keys.length} Auth0/session localStorage items`); 232 auth0Keys.forEach((key) => localStorage.removeItem(key)); 233 } catch (error) { 234 console.error('Error clearing localStorage:', error); 235 } 236 237 // STEP 4: Clear environment-specific cookies 238 // Each cookie must be deleted at EVERY path it could have been set on. 239 // A cookie set at path=/en is a DIFFERENT entry from path=/ â failing to 240 // delete both leaves a stale duplicate that getCookie() may return on next read. 241 try { 242 const cookiesToClear = [ 243 getEnvCookieName('userinfo'), 244 'phxUserHash', 245 'featureFlags', 246 ]; 247 const pathsToEvict = ['/', '/en', '/de', '/fr', '/ja', '/zh', '/ko', '/pt', '/es']; 248 const isHttps = window.location.protocol === 'https:'; 249 const expiredDate = 'Thu, 01 Jan 1970 00:00:00 UTC'; 250 251 console.log('ðª Clearing cookies:', cookiesToClear, 'across paths:', pathsToEvict); 252 cookiesToClear.forEach((cookieNameToClear) => { 253 pathsToEvict.forEach((path) => { 254 document.cookie = `${cookieNameToClear}=; expires=${expiredDate}; path=${path}; SameSite=Lax`; 255 if (isHttps) { 256 document.cookie = `${cookieNameToClear}=; expires=${expiredDate}; path=${path}; SameSite=Lax; Secure`; 257 } 258 }); 259 }); 260 } catch (error) { 261 console.error('Error clearing cookies:', error); 262 } 263 264 // STEP 5: Restore country/currency preferences
265 // Write via the same path-eviction pattern as setOrUpdateCookie() in header.js 266 // to ensure no stale path-scoped duplicate survives the restore. 267 if (countryPreferences) { 268 try { 269 const expires = new Date(); 270 expires.setFullYear(expires.getFullYear() + 1); // 1 year from now 271 const expiredDate = 'Thu, 01 Jan 1970 00:00:00 UTC'; 272 const isHttps = window.location.protocol === 'https:'; 273 const cookieValue = JSON.stringify(countryPreferences); 274 const pathsToEvict = ['/en', '/de', '/fr', '/ja', '/zh', '/ko', '/pt', '/es']; 275 276 // Evict any sub-path copies before writing the canonical path=/ entry 277 pathsToEvict.forEach((path) => { 278 document.cookie = `${cookieName}=; expires=${expiredDate}; path=${path}; SameSite=Lax`; 279 if (isHttps) { 280 document.cookie = `${cookieName}=; expires=${expiredDate}; path=${path}; SameSite=Lax; Secure`; 281 } 282 }); 283 284 // Write single canonical entry at path=/ 285 document.cookie = `${cookieName}=${encodeURIComponent(cookieValue)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; 286 if (isHttps) { 287 document.cookie = `${cookieName}=${encodeURIComponent(cookieValue)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax; Secure`; 288 } 289 console.log('â Restored country/currency preferences'); 290 } catch (error) { 291 console.error('Error restoring country/currency preferences:', error); 292 } 293 } 294 295 console.log('â Storage cleared successfully'); 296} 297 298/** 299 * Validate if user session is still valid 300 * Checks if cookies/sessionStorage exist - if cleared, session is invalid 301 */ 302export function validateSession() { 303 const cookieName = getEnvCookieName('userinfo'); 304 const userInfoCookie = getCookie(cookieName); 305 const webUser = getEnvSessionStorage('webUser'); 306 307 // If user cleared cache (both cookie and sessionStorage gone), allow it 308 if (!userInfoCookie && !webUser) { 309 return true; // Valid logged-out state 310 } 311 312 // If cookie exists but webUser doesn't, check if it's just country/currency data 313 if (userInfoCookie && !webUser) { 314 try { 315 const userInfo = JSON.parse(userInfoCookie); 316 // Check if this is ONLY country/currency data (no user authentication data) 317 const hasOnlyCountryData = userInfo.CountryCode 318 && !userInfo.email 319 && !userInfo.name 320 && !userInfo.sub 321 && !userInfo.userId; 322 323 if (hasOnlyCountryData) { 324 // This is valid - just country/currency preferences after logout 325 return true; 326 } 327 } catch (error) { 328 console.error('Error parsing userinfo cookie:', error); 329 } 330 331 // Cookie has user data but no sessionStorage - invalid 332 console.warn('â ï¸ Session invalid: User data in cookie but no sessionStorage'); 333 return false; 334 } 335 336 // If only sessionStorage exists but no cookie, session is invalid 337 if (!userInfoCookie && webUser) { 338 console.warn('â ï¸ Session invalid: SessionStorage exists but cookie missing'); 339 return false; 340 } 341 342 // Both exist - valid logged-in state 343 return true; 344} 345 346/** 347 * Check if user is logged in based on current session state 348 */ 349export function isUserLoggedIn() { 350 const cookieName = getEnvCookieName('userinfo'); 351 const userInfoCookie = getCookie(cookieName); 352 const webUser = getEnvSessionStorage('webUser'); 353 354 return !!(userInfoCookie && webUser); 355} 356 357// Flag to prevent multiple logout attempts 358let isLoggingOut = false;
359 360// Flag to prevent multiple initializations 361let isInitialized = false; 362 363// Flag to indicate if page was loaded via back/forward button 364let isBackForwardNavigation = false; 365 366/** 367 * Check if a page requires authentication by checking if it has a data-auth-required attribute 368 * This allows pages to self-declare if they need authentication 369 * Default behavior: If no attribute is found, page is considered PUBLIC (doesn't require auth) 370 */ 371function pageRequiresAuth() { 372 // Check if the main element has auth-required attribute 373 const main = document.querySelector('main'); 374 if (main?.hasAttribute('data-auth-required')) { 375 return main.getAttribute('data-auth-required') === 'true'; 376 } 377 378 // Check if any block has auth-required attribute 379 const authRequiredBlock = document.querySelector('[data-auth-required="true"]'); 380 if (authRequiredBlock) { 381 return true; 382 } 383 384 // Default: Page is PUBLIC - does not require authentication 385 // Protected pages MUST explicitly set data-auth-required="true" 386 return false; 387} 388 389/** 390 * Force logout if session is invalid 391 * Called on page load/refresh to detect cache clearing 392 * Validates session if ANY session data exists (cookie OR sessionStorage) 393 */ 394export function forceLogoutIfInvalid() { 395 // Prevent multiple logout attempts in same session 396 if (isLoggingOut) { 397 console.log('âï¸ Skipping validation - logout already in progress'); 398 return false; 399 } 400 401 const cookieName = getEnvCookieName('userinfo'); 402 const userInfoCookie = getCookie(cookieName); 403 const webUser = getEnvSessionStorage('webUser'); 404 405 console.log('ð Session check:', { 406 hasCookie: !!userInfoCookie, 407 hasSessionStorage: !!webUser, 408 path: window.location.pathname, 409 }); 410 411 // If no session data at all, user is logged out - skip validation 412 if (!userInfoCookie && !webUser) { 413 console.log('â No session data - user is logged out'); 414 // If this is back/forward navigation, allow the page to display 415 if (isBackForwardNavigation) { 416 console.log('â¬ ï¸ Back/forward navigation detected - showing page in logged-out state'); 417 } 418 return false; 419 } 420 421 // User has SOME session data (either cookie or sessionStorage or both) 422 // Validate if it's complete and consistent 423 if (!validateSession()) { 424 console.warn('ð« Invalid session detected - forcing logout'); 425 isLoggingOut = true; // Set flag to prevent re-execution 426 427 // Clear all storage 428 clearAllStorage(); 429 430 // For back/forward navigation, the bfcache has frozen the logged-in DOM. 431 // Simply clearing storage is not enough â the stale UI is still visible. 432 // Reload to get a fresh logged-out render (same fix as the pageshow handler). 433 if (isBackForwardNavigation) { 434 console.log('â¬ ï¸ Back/forward navigation with invalid session â reloading for fresh logged-out state'); 435 window.location.reload(); 436 return true; 437 } 438 439 // Check if current page requires authentication 440 const requiresAuth = pageRequiresAuth(); 441 const currentPath = window.location.pathname; 442 const isHomePage = currentPath === '/' || currentPath === '' || currentPath.includes('/homepage'); 443 444 // Only redirect if page requires auth AND we're not already on homepage 445 if (requiresAuth && !isHomePage) { 446 console.log(`ð Protected page (${currentPath}) requires auth - redirecting to homepage...`); 447 window.location.replace('/'); 448 } else { 449 console.log(`â Page does not require auth (${currentPath}) - allowing access in logged-out state`); 450 } 451 return true; 452 } 453 454 console.log('â Session valid'); 455 return false; 456} 457 458/** 459 * Initialize session validation on page load 460 * Validates session when page loads, becomes visible, or user navigates back 461 */ 462export function initSessionValidation() { 463 // Skip if auth is in progress (just completed Auth0 callback) 464 if (sessionStorage.getItem('auth_in_progress') === 'true') { 465 console.log('âï¸ Skipping session validation - auth in progress'); 466 // Clear the flag after a delay to allow session to fully establish 467 setTimeout(() => { 468 sessionStorage.removeItem('auth_in_progress'); 469 console.log('â Auth flag cleared - session validation will run on next page load'); 470 }, 5000); 471 return; 472 } 473 474 // Skip initialization on auth-callback page 475 const currentPath = window.location.pathname; 476 if (currentPath.includes('/auth-callback')) { 477 console.log('âï¸ Skipping session validation on auth-callback page'); 478 return; 479 } 480 481 // Prevent multiple initializations 482 if (isInitialized) { 483 console.log('â ï¸ Session validation already initialized - skipping'); 484 return; 485 } 486 487 isInitialized = true; 488 console.log('ð Initializing session validation...'); 489 490 // Detect if page was loaded via back/forward button (before validating) 491 const perfEntries = performance.getEntriesByType('navigation'); 492 if (perfEntries.length > 0 && perfEntries[0].type === 'back_forward') { 493 isBackForwardNavigation = true; 494 console.log('â¬ ï¸ Detected back/forward navigation'); 495 } 496 497 // 1. Validate immediately on page load 498 forceLogoutIfInvalid(); 499 500 // 2. Validate when page becomes visible (tab switching) 501 document.addEventListener('visibilitychange', () => { 502 if (document.visibilityState === 'visible') { 503 console.log('ðï¸ Page visible - validating session'); 504 forceLogoutIfInvalid(); 505 } 506 }); 507 508 // 3. The pageshow/bfcache guard is registered early in scripts.js (runs
509 // immediately on page load) as a plain window.addEventListener('pageshow'). 510 // It cannot live here because sessionManager.js imports from scripts.js â 511 // a circular import would make initBfcacheGuard undefined at call time. 512}
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.