PageSourceSearch

https://cloud.aiforia.com/hub/Scripts/services/reauthRedirect.js?v=AtNYWfUCD4nuHF_xXSzXRqAkg-AU8fRvuDyCVt8MXw8

js aiforia.com collected 2026-09-24 09:20:33 UTC 6,363 bytes, 164 lines download raw bytes

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.