Fixed the SPA authentication flow
This commit is contained in:
@@ -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 };
|
||||
|
||||
34
src/services/spa/page-client.js
Normal file
34
src/services/spa/page-client.js
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user