Lançamento da versão 1.0.10
This commit is contained in:
@@ -59,7 +59,6 @@ async function installSpaFrameHeaderRules() {
|
||||
],
|
||||
},
|
||||
condition: {
|
||||
initiatorDomains: ["eeho.fa.us2.oraclecloud.com"],
|
||||
requestDomains: ["spa.oracle.com"],
|
||||
resourceTypes: ["sub_frame"],
|
||||
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
TARGET_PARAM_VALUE,
|
||||
TARGET_APP_URL,
|
||||
HCM_ORIGIN,
|
||||
HCM_WELCOME_PATHNAME,
|
||||
HCM_WELCOME_PATHNAMES,
|
||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||
WORKLIST_ORIGIN,
|
||||
WORKLIST_SAASUI_PATHNAME,
|
||||
@@ -57,6 +57,8 @@
|
||||
getOperationalWeekInfo,
|
||||
getCalendarRowWeekDate,
|
||||
parseDatePreservingDateOnly,
|
||||
decodeJwtPayload,
|
||||
isJwtExpired,
|
||||
getErrorMessage,
|
||||
escapeHtml,
|
||||
cssEscape,
|
||||
@@ -134,6 +136,7 @@
|
||||
storageService,
|
||||
normalizeCachedExportPayload,
|
||||
cleanString,
|
||||
isJwtExpired,
|
||||
});
|
||||
const hcmPage = globalThis.ArchPanelHcmPage.create({
|
||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||
@@ -141,6 +144,7 @@
|
||||
WORKLIST_SAASUI_PATHNAME,
|
||||
IDS,
|
||||
cleanString,
|
||||
isJwtExpired,
|
||||
hcmTileTemplate: globalThis.ArchPanelTemplates.hcmTile,
|
||||
onOpenArchPanel: openModal,
|
||||
writeCachedWorklistToken,
|
||||
@@ -247,11 +251,15 @@
|
||||
const currentUrl = new URL(window.location.href);
|
||||
|
||||
return (
|
||||
currentUrl.origin === HCM_ORIGIN &&
|
||||
currentUrl.pathname === HCM_WELCOME_PATHNAME
|
||||
isAllowedHcmOrigin(currentUrl) &&
|
||||
HCM_WELCOME_PATHNAMES.includes(currentUrl.pathname)
|
||||
);
|
||||
}
|
||||
|
||||
function isAllowedHcmOrigin(url) {
|
||||
return url?.origin === HCM_ORIGIN;
|
||||
}
|
||||
|
||||
function scheduleSync() {
|
||||
if (syncFrame) {
|
||||
return;
|
||||
@@ -642,6 +650,7 @@
|
||||
cleanString,
|
||||
firstNonEmptyString,
|
||||
getFieldValue,
|
||||
decodeJwtPayload,
|
||||
toNumber,
|
||||
normalizeWorkload,
|
||||
isWonWorkload,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Arch Panel",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.10",
|
||||
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
||||
"permissions": [
|
||||
"declarativeNetRequestWithHostAccess",
|
||||
@@ -10,6 +10,7 @@
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
|
||||
"https://*.oraclecloud.com/*",
|
||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||
"https://spa.oracle.com/*",
|
||||
"https://worklist.oracle.com/*"
|
||||
@@ -31,8 +32,8 @@
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*",
|
||||
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*"
|
||||
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*",
|
||||
"https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"
|
||||
],
|
||||
"css": [
|
||||
"src/content/styles.css"
|
||||
@@ -74,6 +75,7 @@
|
||||
"page-bridge.js"
|
||||
],
|
||||
"matches": [
|
||||
"https://*.oraclecloud.com/*",
|
||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||
"https://spa.oracle.com/*"
|
||||
]
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
for (const origin of candidates) {
|
||||
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
|
||||
if (isAllowedTopMessageOrigin(origin)) {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,25 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
function isAllowedTopMessageOrigin(origin) {
|
||||
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
|
||||
return (
|
||||
url.protocol === "https:" &&
|
||||
hostname.endsWith(".oraclecloud.com") &&
|
||||
hostname.includes(".fa.")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function safePostToTop(payload) {
|
||||
const targetOrigin = getAllowedTopMessageOrigin();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
WORKLIST_SAASUI_PATHNAME,
|
||||
IDS,
|
||||
cleanString,
|
||||
isJwtExpired,
|
||||
hcmTileTemplate,
|
||||
onOpenArchPanel,
|
||||
writeCachedWorklistToken,
|
||||
@@ -12,12 +13,9 @@
|
||||
let lastPersistedWorklistIframeUrl = "";
|
||||
|
||||
function syncHcmTile() {
|
||||
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
|
||||
const addTile = appsGroup?.querySelector(
|
||||
".flat-grid-cell.flat-grid-cell-addicon"
|
||||
);
|
||||
const insertion = resolveHcmTileInsertion();
|
||||
|
||||
if (!addTile?.parentElement) {
|
||||
if (!insertion?.container) {
|
||||
document.getElementById(IDS.hcmTile)?.remove();
|
||||
return;
|
||||
}
|
||||
@@ -25,22 +23,48 @@
|
||||
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
|
||||
|
||||
if (
|
||||
tile.parentElement !== addTile.parentElement ||
|
||||
tile.nextElementSibling !== addTile
|
||||
tile.parentElement !== insertion.container ||
|
||||
tile.nextElementSibling !== insertion.before
|
||||
) {
|
||||
addTile.parentElement.insertBefore(tile, addTile);
|
||||
insertion.container.insertBefore(tile, insertion.before || null);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHcmTileInsertion() {
|
||||
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
|
||||
|
||||
if (!appsGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addTile = appsGroup.querySelector(
|
||||
".flat-grid-cell.flat-grid-cell-addicon"
|
||||
);
|
||||
|
||||
if (addTile?.parentElement && appsGroup.contains(addTile.parentElement)) {
|
||||
return {
|
||||
container: addTile.parentElement,
|
||||
before: addTile,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
container: appsGroup,
|
||||
before: null,
|
||||
};
|
||||
}
|
||||
|
||||
function syncWorklistIframeToken(options = {}) {
|
||||
for (const frame of document.querySelectorAll("iframe")) {
|
||||
const tokenPayload = extractWorklistIframeToken(frame);
|
||||
|
||||
if (tokenPayload?.token) {
|
||||
if (!tokenPayload?.token || isWorklistTokenExpired(tokenPayload.token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
persistWorklistIframeToken(tokenPayload, options);
|
||||
return tokenPayload;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -73,6 +97,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isWorklistTokenExpired(token) {
|
||||
return typeof isJwtExpired === "function" && isJwtExpired(token, 120);
|
||||
}
|
||||
|
||||
function persistWorklistIframeToken(tokenPayload, options = {}) {
|
||||
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
|
||||
return;
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
appState.errorMessage = "";
|
||||
updateLoadingProgress("Requesting current user", 4);
|
||||
|
||||
const user = await fetchCurrentUser();
|
||||
let user = await fetchCurrentUser();
|
||||
user = await preserveCachedUserAvatar(user);
|
||||
|
||||
updateLoadingProgress("Loading current resource user", 8);
|
||||
|
||||
@@ -206,6 +207,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function preserveCachedUserAvatar(user) {
|
||||
if (cleanString(user?.avatar)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
const currentDatasetAvatar =
|
||||
getCachedUserAvatar(user, appState.dataset?.user) ||
|
||||
getCachedUserAvatar(user, appState.dataset?.exportPayload?.user);
|
||||
|
||||
if (currentDatasetAvatar) {
|
||||
return {
|
||||
...user,
|
||||
avatar: currentDatasetAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
const cachedPayload = await readCachedDataset().catch(() => null);
|
||||
const persistedAvatar = getCachedUserAvatar(user, cachedPayload?.user);
|
||||
|
||||
return persistedAvatar
|
||||
? {
|
||||
...user,
|
||||
avatar: persistedAvatar,
|
||||
}
|
||||
: user;
|
||||
}
|
||||
|
||||
function getCachedUserAvatar(user, cachedUser) {
|
||||
if (!cachedUser) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const userEmail = normalizeString(user?.userEmail);
|
||||
const cachedUserEmail = normalizeString(cachedUser?.userEmail);
|
||||
|
||||
if (!userEmail || userEmail !== cachedUserEmail) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return cleanString(cachedUser?.avatar || cachedUser?.imageUrl);
|
||||
}
|
||||
|
||||
function updateLoadingProgress(message, progress) {
|
||||
appState.loadingMessage = message;
|
||||
appState.loadingProgress = Math.max(
|
||||
@@ -312,14 +355,10 @@
|
||||
|
||||
const worklistToken = currentFrameToken || (await readCachedWorklistToken());
|
||||
|
||||
if (!worklistToken) {
|
||||
throw new Error(
|
||||
"Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
|
||||
);
|
||||
}
|
||||
|
||||
appState.sessionAuthHeaders = {};
|
||||
|
||||
if (worklistToken) {
|
||||
try {
|
||||
const user = await workbenchRepository.fetchWorklistCurrentUser(
|
||||
worklistToken
|
||||
);
|
||||
@@ -327,6 +366,38 @@
|
||||
appState.sessionAuthHeaders = {};
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
appState.sessionAuthHeaders = {};
|
||||
|
||||
if (!isWorklistAuthError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await workbenchRepository.fetchSpaCurrentUser();
|
||||
|
||||
appState.sessionAuthHeaders = {};
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
appState.sessionAuthHeaders = {};
|
||||
|
||||
throw new Error(
|
||||
worklistToken
|
||||
? `Unable to refresh current user. The Worklist token was rejected and the Workload Workbench current-user fallback also failed: ${getErrorMessage(
|
||||
error
|
||||
)}. Reload the FuseWelcome page and try Refresh data again.`
|
||||
: `Unable to resolve the current user. Reload the FuseWelcome page and try Refresh data again. ${getErrorMessage(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isWorklistAuthError(error) {
|
||||
return /\b(401|403)\b/.test(getErrorMessage(error));
|
||||
}
|
||||
|
||||
function findWorkloadById(workloadId) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
storageService,
|
||||
normalizeCachedExportPayload,
|
||||
cleanString,
|
||||
isJwtExpired,
|
||||
}) {
|
||||
async function readCachedDataset() {
|
||||
const record = await storageService.readRecord(
|
||||
@@ -60,7 +61,7 @@
|
||||
async function writeCachedWorklistToken(tokenPayload) {
|
||||
const token = cleanString(tokenPayload?.token);
|
||||
|
||||
if (!token) {
|
||||
if (!token || isCachedWorklistTokenExpired(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,8 +83,13 @@
|
||||
CACHE.worklistTokenKey,
|
||||
"Unable to read the cached Worklist token."
|
||||
);
|
||||
const token = cleanString(record?.token);
|
||||
|
||||
return cleanString(record?.token);
|
||||
return isCachedWorklistTokenExpired(token) ? "" : token;
|
||||
}
|
||||
|
||||
function isCachedWorklistTokenExpired(token) {
|
||||
return typeof isJwtExpired === "function" && isJwtExpired(token, 120);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
cleanString,
|
||||
firstNonEmptyString,
|
||||
getFieldValue,
|
||||
decodeJwtPayload,
|
||||
toNumber,
|
||||
normalizeWorkload,
|
||||
isWonWorkload,
|
||||
@@ -31,28 +32,45 @@
|
||||
}).toString();
|
||||
|
||||
const payload = await fetchJson(url.toString(), { forceExtension: true });
|
||||
const userPayload = normalizeWorklistUserPayload(payload);
|
||||
|
||||
return normalizeCurrentUserPayload(payload, "Worklist user");
|
||||
}
|
||||
|
||||
async function fetchSpaCurrentUser() {
|
||||
const payload = await fetchJson(API_ENDPOINTS.currentUser);
|
||||
|
||||
return normalizeCurrentUserPayload(payload, "SPA current user");
|
||||
}
|
||||
|
||||
function normalizeCurrentUserPayload(payload, sourceLabel) {
|
||||
const userPayload = normalizeCurrentUserCandidate(payload);
|
||||
const firstName = cleanString(
|
||||
getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
|
||||
);
|
||||
const lastName = cleanString(
|
||||
getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
|
||||
);
|
||||
const userEmail = cleanString(
|
||||
getFieldValue(userPayload, [
|
||||
"Email",
|
||||
"email",
|
||||
"EmailAddress",
|
||||
"emailAddress",
|
||||
"User.Email",
|
||||
"User.email",
|
||||
"User.EmailAddress",
|
||||
"FirstName",
|
||||
"firstName",
|
||||
"GivenName",
|
||||
"givenName",
|
||||
"User.FirstName",
|
||||
])
|
||||
);
|
||||
const lastName = cleanString(
|
||||
getFieldValue(userPayload, [
|
||||
"LastName",
|
||||
"lastName",
|
||||
"FamilyName",
|
||||
"familyName",
|
||||
"User.LastName",
|
||||
])
|
||||
);
|
||||
const userEmail = resolveUserEmail(payload, userPayload);
|
||||
const displayName = firstNonEmptyString([
|
||||
`${firstName} ${lastName}`.trim(),
|
||||
userPayload?.DisplayName,
|
||||
userPayload?.displayName,
|
||||
userPayload?.FullName,
|
||||
userPayload?.fullName,
|
||||
userPayload?.Name,
|
||||
userPayload?.name,
|
||||
userEmail,
|
||||
]);
|
||||
|
||||
@@ -62,7 +80,7 @@
|
||||
? Object.keys(userPayload).join(", ")
|
||||
: "";
|
||||
throw new Error(
|
||||
`Unable to resolve userEmail from Worklist user response. Available top-level keys: ${Object.keys(
|
||||
`Unable to resolve userEmail from ${sourceLabel} response. Available top-level keys: ${Object.keys(
|
||||
payload || {}
|
||||
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
|
||||
);
|
||||
@@ -71,9 +89,7 @@
|
||||
return {
|
||||
userEmail,
|
||||
name: displayName || userEmail,
|
||||
avatar: cleanString(
|
||||
getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])
|
||||
),
|
||||
avatar: resolveUserAvatar(payload, userPayload),
|
||||
administrator: Boolean(
|
||||
getFieldValue(userPayload, [
|
||||
"Administrator",
|
||||
@@ -84,20 +100,178 @@
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWorklistUserPayload(payload) {
|
||||
const candidate = payload?.User ?? payload?.user ?? payload;
|
||||
function normalizeCurrentUserCandidate(payload) {
|
||||
const candidate =
|
||||
getObjectCandidate(payload?.User) ??
|
||||
getObjectCandidate(payload?.user) ??
|
||||
getObjectCandidate(payload?.currentUser) ??
|
||||
getObjectCandidate(payload?.CurrentUser) ??
|
||||
getObjectCandidate(payload?.data) ??
|
||||
getObjectCandidate(payload?.result) ??
|
||||
getObjectCandidate(payload?.items?.[0]) ??
|
||||
payload;
|
||||
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate[0] || {};
|
||||
}
|
||||
|
||||
if (candidate?.User) {
|
||||
return normalizeWorklistUserPayload(candidate);
|
||||
const nestedUser =
|
||||
getObjectCandidate(candidate?.User) ?? getObjectCandidate(candidate?.user);
|
||||
|
||||
if (nestedUser) {
|
||||
return normalizeCurrentUserCandidate(nestedUser);
|
||||
}
|
||||
|
||||
return candidate || {};
|
||||
}
|
||||
|
||||
function getObjectCandidate(value) {
|
||||
return value && typeof value === "object" ? value : null;
|
||||
}
|
||||
|
||||
function resolveUserEmail(payload, userPayload) {
|
||||
return firstEmailLikeString([
|
||||
getFieldValue(userPayload, [
|
||||
"Email",
|
||||
"email",
|
||||
"EmailAddress",
|
||||
"emailAddress",
|
||||
"User.Email",
|
||||
"User.email",
|
||||
"User.EmailAddress",
|
||||
"UserName",
|
||||
"userName",
|
||||
"username",
|
||||
"mail",
|
||||
"Mail",
|
||||
"userEmail",
|
||||
"UserEmail",
|
||||
"PrimaryEmail.EmailAddress",
|
||||
"primaryEmail.emailAddress",
|
||||
"ResourceEmailAddress",
|
||||
"resourceEmailAddress",
|
||||
"id",
|
||||
"Id",
|
||||
]),
|
||||
...getJwtEmailCandidates(payload?.token),
|
||||
...getJwtEmailCandidates(payload?.accessToken),
|
||||
...getJwtEmailCandidates(payload?.idToken),
|
||||
...getJwtEmailCandidates(userPayload?.token),
|
||||
]);
|
||||
}
|
||||
|
||||
function resolveUserAvatar(payload, userPayload) {
|
||||
return firstNonEmptyString([
|
||||
getFieldValue(userPayload, [
|
||||
"ImageURL",
|
||||
"imageUrl",
|
||||
"imageURL",
|
||||
"User.ImageURL",
|
||||
"Picture",
|
||||
"picture",
|
||||
"avatar",
|
||||
"Avatar",
|
||||
"avatarUrl",
|
||||
"avatarURL",
|
||||
"photo",
|
||||
"photoUrl",
|
||||
"photoURL",
|
||||
"profileImageUrl",
|
||||
"profileImageURL",
|
||||
"thumbnailUrl",
|
||||
"thumbnailURL",
|
||||
"image",
|
||||
"userImage",
|
||||
"userImageUrl",
|
||||
"profile.picture",
|
||||
"profile.avatar",
|
||||
"profile.photoUrl",
|
||||
]),
|
||||
...getJwtAvatarCandidates(payload?.token),
|
||||
...getJwtAvatarCandidates(payload?.accessToken),
|
||||
...getJwtAvatarCandidates(payload?.idToken),
|
||||
...getJwtAvatarCandidates(userPayload?.token),
|
||||
]);
|
||||
}
|
||||
|
||||
function getJwtAvatarCandidates(token) {
|
||||
const tokenPayload = decodeTokenPayload(token);
|
||||
|
||||
if (!tokenPayload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
getFieldValue(tokenPayload, [
|
||||
"picture",
|
||||
"avatar",
|
||||
"avatar_url",
|
||||
"image",
|
||||
"photo",
|
||||
"photo_url",
|
||||
"thumbnail",
|
||||
"thumbnail_url",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function getJwtEmailCandidates(token) {
|
||||
const tokenPayload = decodeTokenPayload(token);
|
||||
|
||||
if (!tokenPayload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
getFieldValue(tokenPayload, [
|
||||
"email",
|
||||
"Email",
|
||||
"sub",
|
||||
"prn",
|
||||
"upn",
|
||||
"user_name",
|
||||
"username",
|
||||
"preferred_username",
|
||||
"unique_name",
|
||||
"mail",
|
||||
"userEmail",
|
||||
"UserEmail",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function decodeTokenPayload(token) {
|
||||
const normalizedToken = cleanTokenValue(token);
|
||||
|
||||
if (!normalizedToken || typeof decodeJwtPayload !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decodeJwtPayload(normalizedToken);
|
||||
}
|
||||
|
||||
function cleanTokenValue(value) {
|
||||
const normalized = cleanString(value);
|
||||
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalized.replace(/^Bearer\s+/i, "");
|
||||
}
|
||||
|
||||
function firstEmailLikeString(values) {
|
||||
for (const value of values) {
|
||||
const normalized = cleanString(value);
|
||||
|
||||
if (/@/.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function fetchAllCustomers(userEmail) {
|
||||
const limit = 49;
|
||||
let offset = 0;
|
||||
@@ -277,6 +451,7 @@
|
||||
|
||||
return {
|
||||
fetchWorklistCurrentUser,
|
||||
fetchSpaCurrentUser,
|
||||
fetchAllCustomers,
|
||||
fetchCustomerWorkloads,
|
||||
fetchWorkloadActions,
|
||||
|
||||
@@ -172,7 +172,9 @@
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
pendingBridgeRequests.delete(requestId);
|
||||
reject(new Error(`Timed out while requesting ${requestUrl}`));
|
||||
reject(
|
||||
new Error(`Timed out while requesting ${redactUrlForError(requestUrl)}`)
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
pendingBridgeRequests.set(requestId, {
|
||||
@@ -229,7 +231,9 @@
|
||||
`${
|
||||
payloadMessage ||
|
||||
response.error ||
|
||||
`Request failed (${response.status}) for ${requestUrl}`
|
||||
`Request failed (${response.status}) for ${redactUrlForError(
|
||||
requestUrl
|
||||
)}`
|
||||
}${debugSuffix}`
|
||||
);
|
||||
}
|
||||
@@ -297,7 +301,9 @@
|
||||
});
|
||||
|
||||
if (!response || typeof response !== "object") {
|
||||
throw new Error(`No response received for ${requestUrl}`);
|
||||
throw new Error(
|
||||
`No response received for ${redactUrlForError(requestUrl)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -328,8 +334,8 @@
|
||||
const debugFrameUrl = cleanString(response.debug?.frameUrl);
|
||||
const debugUrl = cleanString(response.debug?.url);
|
||||
const debugParts = [
|
||||
debugUrl ? `url: ${debugUrl}` : "",
|
||||
debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
|
||||
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
|
||||
debugFrameUrl ? `frame: ${redactUrlForError(debugFrameUrl)}` : "",
|
||||
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
||||
].filter(Boolean);
|
||||
const debugSuffix =
|
||||
@@ -339,7 +345,9 @@
|
||||
`${
|
||||
payloadMessage ||
|
||||
response.error ||
|
||||
`Request failed (${response.status}) for ${requestUrl}`
|
||||
`Request failed (${response.status}) for ${redactUrlForError(
|
||||
requestUrl
|
||||
)}`
|
||||
}${debugSuffix}`
|
||||
);
|
||||
}
|
||||
@@ -493,7 +501,9 @@
|
||||
});
|
||||
|
||||
if (!response || typeof response !== "object") {
|
||||
throw new Error(`No response received for ${requestUrl}`);
|
||||
throw new Error(
|
||||
`No response received for ${redactUrlForError(requestUrl)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -511,7 +521,7 @@
|
||||
: "";
|
||||
const debugUrl = cleanString(response.debug?.url);
|
||||
const debugParts = [
|
||||
debugUrl ? `url: ${debugUrl}` : "",
|
||||
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
|
||||
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
||||
].filter(Boolean);
|
||||
const debugSuffix =
|
||||
@@ -521,7 +531,9 @@
|
||||
`${
|
||||
payloadMessage ||
|
||||
response.error ||
|
||||
`Request failed (${response.status}) for ${requestUrl}`
|
||||
`Request failed (${response.status}) for ${redactUrlForError(
|
||||
requestUrl
|
||||
)}`
|
||||
}${debugSuffix}`
|
||||
);
|
||||
}
|
||||
@@ -631,6 +643,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
function redactUrlForError(value) {
|
||||
const normalized = cleanString(value);
|
||||
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(normalized, window.location.origin);
|
||||
const sensitiveQueryPattern =
|
||||
/(token|authorization|auth|jwt|id_token|access_token)/i;
|
||||
|
||||
for (const key of Array.from(parsedUrl.searchParams.keys())) {
|
||||
if (sensitiveQueryPattern.test(key)) {
|
||||
parsedUrl.searchParams.set(key, "[redacted]");
|
||||
}
|
||||
}
|
||||
|
||||
const redactedUrl = parsedUrl.toString();
|
||||
|
||||
if (
|
||||
parsedUrl.origin === window.location.origin &&
|
||||
!/^[a-z][a-z0-9+.-]*:/i.test(normalized)
|
||||
) {
|
||||
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
||||
}
|
||||
|
||||
return redactedUrl;
|
||||
} catch {
|
||||
return normalized.replace(
|
||||
/([?&][^=\s|]*(?:token|authorization|auth|jwt|id_token|access_token)[^=\s|]*=)[^&\s|]+/gi,
|
||||
"$1[redacted]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBridgeMessage(event) {
|
||||
const message = event.data;
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
const TARGET_APP_URL =
|
||||
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
|
||||
const HCM_ORIGIN = "https://eeho.fa.us2.oraclecloud.com";
|
||||
const HCM_WELCOME_PATHNAME = "/hcmUI/faces/FuseWelcome";
|
||||
const HCM_WELCOME_PATHNAMES = Object.freeze([
|
||||
"/hcmUI/faces/FuseWelcome",
|
||||
"/fscmUI/faces/FuseWelcome",
|
||||
]);
|
||||
const HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
|
||||
const WORKLIST_ORIGIN = "https://worklist.oracle.com";
|
||||
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
|
||||
@@ -167,7 +170,7 @@
|
||||
TARGET_PARAM_VALUE,
|
||||
TARGET_APP_URL,
|
||||
HCM_ORIGIN,
|
||||
HCM_WELCOME_PATHNAME,
|
||||
HCM_WELCOME_PATHNAMES,
|
||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||
WORKLIST_ORIGIN,
|
||||
WORKLIST_SAASUI_PATHNAME,
|
||||
|
||||
@@ -402,6 +402,54 @@
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token) {
|
||||
const normalizedToken = cleanString(token);
|
||||
|
||||
if (
|
||||
!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
|
||||
normalizedToken
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payloadPart = normalizedToken.split(".")[1];
|
||||
const base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const json = decodeURIComponent(
|
||||
Array.from(atob(paddedBase64), (char) => {
|
||||
return `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
||||
}).join("")
|
||||
);
|
||||
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getJwtExpiresAt(token) {
|
||||
const payload = decodeJwtPayload(token);
|
||||
const expiresAtSeconds = Number(payload?.exp);
|
||||
|
||||
return Number.isFinite(expiresAtSeconds) && expiresAtSeconds > 0
|
||||
? expiresAtSeconds * 1000
|
||||
: 0;
|
||||
}
|
||||
|
||||
function isJwtExpired(token, skewSeconds = 60) {
|
||||
const expiresAt = getJwtExpiresAt(token);
|
||||
|
||||
if (!expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const skewMilliseconds = Math.max(Number(skewSeconds) || 0, 0) * 1000;
|
||||
|
||||
return expiresAt <= Date.now() + skewMilliseconds;
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
@@ -456,6 +504,9 @@
|
||||
getOperationalWeekInfo,
|
||||
getCalendarRowWeekDate,
|
||||
parseDatePreservingDateOnly,
|
||||
decodeJwtPayload,
|
||||
getJwtExpiresAt,
|
||||
isJwtExpired,
|
||||
getErrorMessage,
|
||||
escapeHtml,
|
||||
cssEscape,
|
||||
|
||||
Reference in New Issue
Block a user