PageSourceSearch

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

js phenomenex.com collected 2026-10-03 23:17:18 UTC 19,442 bytes, 512 lines download raw bytes

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.