1/* Shared 401 re-auth redirect helper. 2 * 3 * Exposes `window.aiforiaReauth` so legacy AngularJS code, the Redux/RTK Query 4 * layer, and the React bootstrap can all share a single implementation. 5 * 6 * Loaded via <script> in each layout before any bundle that may produce a 401 7 * (httpInterceptor.js, oauthService.js). Also import-safe â Vitest tests 8 * import this file directly; the IIFE side-effect populates window.aiforiaReauth. 9 * 10 * Must stay in a folder that `dotnet publish` ships: it is referenced by raw 11 * path, not through a bundle or the Vite dist output. Aiforia.Web.UI.csproj 12 * strips wwwroot\Scripts\components\** (and hooks, React, redux, store) from 13 * Content. 14 */ 15/* global Aiforia */ 16(() => { 17 const PRE_AUTH_STORAGE_KEY = 'aiforia.preAuthUrl'; 18 // Sized to cover idle-logout flows where the user may be away for hours 19 // before returning to re-authenticate. The entry is cleared on first read 20 // and is per-tab, so a long window doesn't accumulate stale URLs. 21 const PRE_AUTH_TTL_MS = 4 * 60 * 60 * 1000; 22 23 let challengeInProgress = false; 24 25 const getCurrentSubscriptionId = () => 26 typeof Aiforia !== 'undefined' && Aiforia && Aiforia.subscriptionId 27 ? Aiforia.subscriptionId 28 : null; 29 30 // Razor renders a null ViewBag value as an empty string, so treat falsy as absent. 31 const getCurrentUserId = () => 32 typeof Aiforia !== 'undefined' && Aiforia && Aiforia.userId ? Aiforia.userId : null; 33 34 const buildSelectSubscriptionUri = (origin, pathbase, subscriptionId) => 35 `${origin}${pathbase}/account/selectsubscription?subscriptionId=${subscriptionId}`; 36 37 const buildChallengeUri = (origin, pathbase, redirectUri) => 38 `${origin}${pathbase}/account/challenge?redirect_uri=${btoa(encodeURIComponent(redirectUri))}`; 39 40 const writePreAuthEntry = (subscriptionId) => { 41 try { 42 const entry = { 43 url: window.location.pathname + window.location.search + window.location.hash, 44 subscriptionId, 45 userId: getCurrentUserId(), 46 ts: Date.now(), 47 }; 48 sessionStorage.setItem(PRE_AUTH_STORAGE_KEY, JSON.stringify(entry)); 49 } catch (e) { 50 // sessionStorage unavailable (private mode, quota exceeded) â proceed without restoration 51 } 52 }; 53 54 const savePreAuthUrl = () => { 55 const subscriptionId = getCurrentSubscriptionId(); 56 if (!subscriptionId) return; 57 writePreAuthEntry(subscriptionId); 58 }; 59 60 const redirectToReauth = () => { 61 if (challengeInProgress) return; 62 63 const subscriptionId = getCurrentSubscriptionId(); 64 const { origin } = window.location; 65 const pathbase = (window.aiforia && window.aiforia.pathbase) || ''; 66 67 if (!subscriptionId) { 68 // No subscription context â we can't safely round-trip the URL because 69 // restore relies on a subscriptionId match. In practice UserData.cshtml 70 // sets Aiforia.subscriptionId inline before any XHR can issue, so this 71 // path is the early-bootstrap / login-page fallback. Accept URL loss. 72 challengeInProgress = true; 73 window.location.href = '/'; 74 return; 75 } 76 77 writePreAuthEntry(subscriptionId); 78 challengeInProgress = true; 79 const selectSubUri = buildSelectSubscriptionUri(origin, pathbase, subscriptionId); 80 window.location.href = buildChallengeUri(origin, pathbase, selectSubUri); 81 }; 82 83 const readAndClearPreAuthEntry = () => { 84 let raw; 85 try { 86 raw = sessionStorage.getItem(PRE_AUTH_STORAGE_KEY); 87 if (raw === null) return null; 88 sessionStorage.removeItem(PRE_AUTH_STORAGE_KEY); 89 } catch (e) { 90 return null; 91 } 92 try { 93 return JSON.parse(raw); 94 } catch (e) { 95 return null; 96 } 97 }; 98 99 const isSafeRelativeUrl = (url) => { 100 if (typeof url !== 'string') return false; 101 if (!url.startsWith('/')) return false; 102 // Reject protocol-relative URLs ("//host/...") and backslash variants 103 // ("/\\host/...") that some browsers normalize to "//host/..." and would 104 // navigate off-origin. 105 if (url.startsWith('//') || url.startsWith('/\\') || url.startsWith('/%2f')) return false; 106 return true; 107 }; 108 109 const restorePreAuthUrl = (currentSubscriptionId) => { 110 const entry = readAndClearPreAuthEntry(); 111 if ( 112 !entry || 113 typeof entry.url !== 'string' || 114 typeof entry.ts !== 'number' || 115 typeof entry.subscriptionId !== 'string' || 116 !entry.subscriptionId || 117 typeof entry.userId !== 'string' || 118 !entry.userId 119 ) { 120 return false;
121 } 122 // The entry must belong to the user who is now signed in. sessionStorage is 123 // per-tab and survives sign-out, so without this the route saved for user A 124 // gets replayed for whoever logs in next in that tab -- and because the save 125 // is driven by a 401, it can land *after* a deliberate logout has already 126 // started (observed 14ms after the LogOff response). Binding to the identity 127 // rather than to the logout sequence makes the write's timing irrelevant. 128 const currentUserId = getCurrentUserId(); 129 if (!currentUserId || entry.userId !== currentUserId) return false; 130 if (!currentSubscriptionId) return false; 131 if (Date.now() - entry.ts > PRE_AUTH_TTL_MS) return false; 132 if (entry.subscriptionId !== currentSubscriptionId) return false; 133 if (!isSafeRelativeUrl(entry.url)) return false; 134 135 const currentRelative = 136 window.location.pathname + window.location.search + window.location.hash; 137 if (entry.url === currentRelative) return false; 138 139 const target = new URL(entry.url, window.location.origin); 140 const sameDocument = 141 target.pathname === window.location.pathname && target.search === window.location.search; 142 143 if (sameDocument) { 144 // Hash-only change: replace() wouldn't reload â it would only fire 145 // hashchange, which lets AngularJS swap the hash route mid-bootstrap 146 // and leaves React panels (e.g. AiModelView) half-mounted. Force a 147 // reload so the app boots cleanly at the target route. hashchange is 148 // queued async, so reload() preempts before any handler can run. 149 window.location.hash = target.hash; 150 window.location.reload(); 151 } else { 152 window.location.replace(entry.url); 153 } 154 return true; 155 }; 156 157 window.aiforiaReauth = { 158 redirectToReauth, 159 savePreAuthUrl, 160 restorePreAuthUrl, 161 PRE_AUTH_STORAGE_KEY, 162 PRE_AUTH_TTL_MS, 163 }; 164})();
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.