Fixed the SPA authentication flow

This commit is contained in:
2026-10-21 15:55:29 -03:00
parent 5ef4c22c59
commit ba61f91c53
33 changed files with 1924 additions and 117 deletions

View File

@@ -1,20 +1,87 @@
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
function createSpaCurrentUser({ fetchJson, fetchInTab, openSessionTab, waitForSession, closeSessionTab, sessionUrl }) {
const pending = new Map();
function requestCurrentUser(sender) {
const key = JSON.stringify([sender?.tab?.windowId ?? null, sender?.tab?.cookieStoreId ?? "", Boolean(sender?.tab?.incognito)]);
if (!pending.has(key)) {
pending.set(key, loadCurrentUser(sender).finally(() => pending.delete(key)));
}
return pending.get(key);
}
async function loadCurrentUser(sender) {
const debugRequests = [];
let user = null;
let sessionTab = null;
let receivedValidJson = false;
try {
const result = await fetchJson("https://spa.oracle.com/oalcrm/web/api/identity-management/users/current", {
method: "GET", credentials: "include", cache: "no-store", headers: { Accept: "application/json" }
}, "requestCurrentUser", debugRequests);
if (!result?.user || !Array.isArray(result.user.roles) || result.user.roles.some(role => typeof role !== "string")) {
throw new Error("requestCurrentUser returned invalid user roles.");
// Bound each bootstrap cycle so a failed sign-in never leaves an orphan tab.
// The content-side gate can start another cycle without releasing dependent requests.
for (let attempt = 1; attempt <= 6; attempt++) {
try {
const result = sessionTab ? await fetchInTab(sessionTab, debugRequests) : await fetchJson("https://spa.oracle.com/oalcrm/web/api/identity-management/users/current", {
method: "GET", credentials: "include", cache: "no-store", headers: { Accept: "application/json" }
}, "requestCurrentUser", debugRequests);
const validText = value => typeof value === "string" && value.trim().length > 0;
if (!result || typeof result !== "object" || Array.isArray(result)
|| !result.user || typeof result.user !== "object" || Array.isArray(result.user)
|| !validText(result.user.id) || !validText(result.user.name) || !validText(result.token)
|| !Array.isArray(result.user.roles) || result.user.roles.some(role => !validText(role))
|| ["onWhiteList", "provisioned", "administrator"].some(key => key in result.user && typeof result.user[key] !== "boolean")) {
const error = new Error("requestCurrentUser returned an invalid user/token response.");
error.code = "INVALID_CURRENT_USER_RESPONSE";
throw error;
}
const entry = debugRequests[debugRequests.length - 1];
if (entry) entry.metadata.attempt = String(attempt);
receivedValidJson = true;
const user = { id: result.user.id, name: result.user.name, roles: result.user.roles };
return { ok: true, user, debugRequests };
} catch (error) {
const entry = debugRequests[debugRequests.length - 1];
if (entry) {
entry.status = "failed";
entry.error = error.message;
entry.metadata.attempt = String(attempt);
}
const needsSession = error.code === "INVALID_JSON_RESPONSE" || error.code === "INVALID_CURRENT_USER_RESPONSE"
|| error.httpStatus === 401 || error.httpStatus === 403 || !error.httpStatus;
if (!needsSession || attempt === 6) {
if (entry && needsSession) entry.metadata.retryLimitReached = "true";
throw error;
}
if (entry) {
entry.metadata.sessionBootstrapUrl = sessionUrl;
entry.metadata.retryReason = "Current user requires a valid SPA session";
}
if (!sessionTab) {
try {
const tab = await openSessionTab(sender);
if (!Number.isInteger(tab?.id)) throw new Error("SPA session tab was not created.");
sessionTab = tab;
if (entry) entry.metadata.sessionBootstrapState = "opened-inactive-tab";
} catch (sessionError) {
if (entry) entry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
throw sessionError;
}
} else if (entry) {
entry.metadata.sessionBootstrapState = "waiting-for-valid-session";
}
await waitForSession(sessionTab, entry);
}
}
user = { id: result.user.id, name: result.user.name, roles: result.user.roles };
return { ok: true, user, debugRequests };
} catch (error) {
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
error.debugRequests = debugRequests;
throw error;
} finally {
if (sessionTab) {
const entry = debugRequests[debugRequests.length - 1];
try {
await closeSessionTab(sessionTab.id);
if (entry) entry.metadata.sessionBootstrapState = receivedValidJson
? "closed-after-valid-json" : "closed-after-retry-finished";
} catch (error) {
if (entry) entry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
}
}
}
}
return { requestCurrentUser };

View File

@@ -0,0 +1,34 @@
function createSpaPageClient({ sendMessage, getHttp }) {
const options = { frameId: 0 };
const inspect = tab => sendMessage(tab.id, { type: "opportunitiesExtension.inspectSpaPage" }, options);
async function requestIdentity(tab, debugRequests) {
const http = getHttp();
const url = "https://spa.oracle.com/oalcrm/web/api/identity-management/users/current";
const entry = http.createBackgroundDebugEntry("requestCurrentUser", url,
{ method: "GET", headers: { Accept: "application/json" } }, new Date());
entry.metadata.executionContext = "spa-tab";
try {
const result = await sendMessage(tab.id, { type: "opportunitiesExtension.readSpaIdentity", documentId: tab.spaDocumentId }, options);
if (!result || result.error || !Number.isInteger(result.status) || typeof result.body !== "string") {
throw new Error(result?.error || "Invalid SPA identity response.");
}
const response = { status: result.status, statusText: result.statusText || "", ok: result.status >= 200 && result.status < 300,
headers: { forEach: callback => Object.entries(result.headers || { "content-type": result.contentType || "" }).forEach(([key, value]) => callback(value, key)) } };
http.completeBackgroundDebugEntry(entry, response, http.createDebugBody(result.body), "");
entry.metadata.responseContentType = result.contentType || "(not provided)";
entry.metadata.responseLength = String(result.body.length);
if (result.status !== 200) {
const error = new Error(`requestCurrentUser failed with status ${result.status}.`);
error.httpStatus = result.status;
throw error;
}
return http.parseJsonResponse(result.body, "requestCurrentUser");
} catch (error) {
if (!entry.completedAt) http.completeBackgroundDebugEntry(entry, null, { value: "", truncated: false }, error.message);
entry.status = "failed";
entry.error = error.message;
throw error;
} finally { debugRequests.push(entry); }
}
return { inspect, requestIdentity };
}