Lançamento da versão 1.0.10
This commit is contained in:
@@ -59,7 +59,6 @@ async function installSpaFrameHeaderRules() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
condition: {
|
condition: {
|
||||||
initiatorDomains: ["eeho.fa.us2.oraclecloud.com"],
|
|
||||||
requestDomains: ["spa.oracle.com"],
|
requestDomains: ["spa.oracle.com"],
|
||||||
resourceTypes: ["sub_frame"],
|
resourceTypes: ["sub_frame"],
|
||||||
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",
|
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
TARGET_PARAM_VALUE,
|
TARGET_PARAM_VALUE,
|
||||||
TARGET_APP_URL,
|
TARGET_APP_URL,
|
||||||
HCM_ORIGIN,
|
HCM_ORIGIN,
|
||||||
HCM_WELCOME_PATHNAME,
|
HCM_WELCOME_PATHNAMES,
|
||||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||||
WORKLIST_ORIGIN,
|
WORKLIST_ORIGIN,
|
||||||
WORKLIST_SAASUI_PATHNAME,
|
WORKLIST_SAASUI_PATHNAME,
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
getOperationalWeekInfo,
|
getOperationalWeekInfo,
|
||||||
getCalendarRowWeekDate,
|
getCalendarRowWeekDate,
|
||||||
parseDatePreservingDateOnly,
|
parseDatePreservingDateOnly,
|
||||||
|
decodeJwtPayload,
|
||||||
|
isJwtExpired,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
cssEscape,
|
cssEscape,
|
||||||
@@ -134,6 +136,7 @@
|
|||||||
storageService,
|
storageService,
|
||||||
normalizeCachedExportPayload,
|
normalizeCachedExportPayload,
|
||||||
cleanString,
|
cleanString,
|
||||||
|
isJwtExpired,
|
||||||
});
|
});
|
||||||
const hcmPage = globalThis.ArchPanelHcmPage.create({
|
const hcmPage = globalThis.ArchPanelHcmPage.create({
|
||||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||||
@@ -141,6 +144,7 @@
|
|||||||
WORKLIST_SAASUI_PATHNAME,
|
WORKLIST_SAASUI_PATHNAME,
|
||||||
IDS,
|
IDS,
|
||||||
cleanString,
|
cleanString,
|
||||||
|
isJwtExpired,
|
||||||
hcmTileTemplate: globalThis.ArchPanelTemplates.hcmTile,
|
hcmTileTemplate: globalThis.ArchPanelTemplates.hcmTile,
|
||||||
onOpenArchPanel: openModal,
|
onOpenArchPanel: openModal,
|
||||||
writeCachedWorklistToken,
|
writeCachedWorklistToken,
|
||||||
@@ -247,11 +251,15 @@
|
|||||||
const currentUrl = new URL(window.location.href);
|
const currentUrl = new URL(window.location.href);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
currentUrl.origin === HCM_ORIGIN &&
|
isAllowedHcmOrigin(currentUrl) &&
|
||||||
currentUrl.pathname === HCM_WELCOME_PATHNAME
|
HCM_WELCOME_PATHNAMES.includes(currentUrl.pathname)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllowedHcmOrigin(url) {
|
||||||
|
return url?.origin === HCM_ORIGIN;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleSync() {
|
function scheduleSync() {
|
||||||
if (syncFrame) {
|
if (syncFrame) {
|
||||||
return;
|
return;
|
||||||
@@ -642,6 +650,7 @@
|
|||||||
cleanString,
|
cleanString,
|
||||||
firstNonEmptyString,
|
firstNonEmptyString,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
|
decodeJwtPayload,
|
||||||
toNumber,
|
toNumber,
|
||||||
normalizeWorkload,
|
normalizeWorkload,
|
||||||
isWonWorkload,
|
isWonWorkload,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Arch Panel",
|
"name": "Arch Panel",
|
||||||
"version": "1.0.7",
|
"version": "1.0.10",
|
||||||
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"declarativeNetRequestWithHostAccess",
|
"declarativeNetRequestWithHostAccess",
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*",
|
||||||
|
"https://*.oraclecloud.com/*",
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
"https://spa.oracle.com/*",
|
"https://spa.oracle.com/*",
|
||||||
"https://worklist.oracle.com/*"
|
"https://worklist.oracle.com/*"
|
||||||
@@ -31,8 +32,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matches": [
|
"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": [
|
"css": [
|
||||||
"src/content/styles.css"
|
"src/content/styles.css"
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"page-bridge.js"
|
"page-bridge.js"
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
|
"https://*.oraclecloud.com/*",
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
"https://spa.oracle.com/*"
|
"https://spa.oracle.com/*"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const origin of candidates) {
|
for (const origin of candidates) {
|
||||||
if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
|
if (isAllowedTopMessageOrigin(origin)) {
|
||||||
return origin;
|
return origin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,25 @@
|
|||||||
return "";
|
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) {
|
function safePostToTop(payload) {
|
||||||
const targetOrigin = getAllowedTopMessageOrigin();
|
const targetOrigin = getAllowedTopMessageOrigin();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
WORKLIST_SAASUI_PATHNAME,
|
WORKLIST_SAASUI_PATHNAME,
|
||||||
IDS,
|
IDS,
|
||||||
cleanString,
|
cleanString,
|
||||||
|
isJwtExpired,
|
||||||
hcmTileTemplate,
|
hcmTileTemplate,
|
||||||
onOpenArchPanel,
|
onOpenArchPanel,
|
||||||
writeCachedWorklistToken,
|
writeCachedWorklistToken,
|
||||||
@@ -12,12 +13,9 @@
|
|||||||
let lastPersistedWorklistIframeUrl = "";
|
let lastPersistedWorklistIframeUrl = "";
|
||||||
|
|
||||||
function syncHcmTile() {
|
function syncHcmTile() {
|
||||||
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
|
const insertion = resolveHcmTileInsertion();
|
||||||
const addTile = appsGroup?.querySelector(
|
|
||||||
".flat-grid-cell.flat-grid-cell-addicon"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!addTile?.parentElement) {
|
if (!insertion?.container) {
|
||||||
document.getElementById(IDS.hcmTile)?.remove();
|
document.getElementById(IDS.hcmTile)?.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,22 +23,48 @@
|
|||||||
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
|
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
tile.parentElement !== addTile.parentElement ||
|
tile.parentElement !== insertion.container ||
|
||||||
tile.nextElementSibling !== addTile
|
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 = {}) {
|
function syncWorklistIframeToken(options = {}) {
|
||||||
for (const frame of document.querySelectorAll("iframe")) {
|
for (const frame of document.querySelectorAll("iframe")) {
|
||||||
const tokenPayload = extractWorklistIframeToken(frame);
|
const tokenPayload = extractWorklistIframeToken(frame);
|
||||||
|
|
||||||
if (tokenPayload?.token) {
|
if (!tokenPayload?.token || isWorklistTokenExpired(tokenPayload.token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
persistWorklistIframeToken(tokenPayload, options);
|
persistWorklistIframeToken(tokenPayload, options);
|
||||||
return tokenPayload;
|
return tokenPayload;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -73,6 +97,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWorklistTokenExpired(token) {
|
||||||
|
return typeof isJwtExpired === "function" && isJwtExpired(token, 120);
|
||||||
|
}
|
||||||
|
|
||||||
function persistWorklistIframeToken(tokenPayload, options = {}) {
|
function persistWorklistIframeToken(tokenPayload, options = {}) {
|
||||||
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
|
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
appState.errorMessage = "";
|
appState.errorMessage = "";
|
||||||
updateLoadingProgress("Requesting current user", 4);
|
updateLoadingProgress("Requesting current user", 4);
|
||||||
|
|
||||||
const user = await fetchCurrentUser();
|
let user = await fetchCurrentUser();
|
||||||
|
user = await preserveCachedUserAvatar(user);
|
||||||
|
|
||||||
updateLoadingProgress("Loading current resource user", 8);
|
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) {
|
function updateLoadingProgress(message, progress) {
|
||||||
appState.loadingMessage = message;
|
appState.loadingMessage = message;
|
||||||
appState.loadingProgress = Math.max(
|
appState.loadingProgress = Math.max(
|
||||||
@@ -312,14 +355,10 @@
|
|||||||
|
|
||||||
const worklistToken = currentFrameToken || (await readCachedWorklistToken());
|
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 = {};
|
appState.sessionAuthHeaders = {};
|
||||||
|
|
||||||
|
if (worklistToken) {
|
||||||
|
try {
|
||||||
const user = await workbenchRepository.fetchWorklistCurrentUser(
|
const user = await workbenchRepository.fetchWorklistCurrentUser(
|
||||||
worklistToken
|
worklistToken
|
||||||
);
|
);
|
||||||
@@ -327,6 +366,38 @@
|
|||||||
appState.sessionAuthHeaders = {};
|
appState.sessionAuthHeaders = {};
|
||||||
|
|
||||||
return user;
|
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) {
|
function findWorkloadById(workloadId) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
storageService,
|
storageService,
|
||||||
normalizeCachedExportPayload,
|
normalizeCachedExportPayload,
|
||||||
cleanString,
|
cleanString,
|
||||||
|
isJwtExpired,
|
||||||
}) {
|
}) {
|
||||||
async function readCachedDataset() {
|
async function readCachedDataset() {
|
||||||
const record = await storageService.readRecord(
|
const record = await storageService.readRecord(
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
async function writeCachedWorklistToken(tokenPayload) {
|
async function writeCachedWorklistToken(tokenPayload) {
|
||||||
const token = cleanString(tokenPayload?.token);
|
const token = cleanString(tokenPayload?.token);
|
||||||
|
|
||||||
if (!token) {
|
if (!token || isCachedWorklistTokenExpired(token)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +83,13 @@
|
|||||||
CACHE.worklistTokenKey,
|
CACHE.worklistTokenKey,
|
||||||
"Unable to read the cached Worklist token."
|
"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({
|
return Object.freeze({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
cleanString,
|
cleanString,
|
||||||
firstNonEmptyString,
|
firstNonEmptyString,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
|
decodeJwtPayload,
|
||||||
toNumber,
|
toNumber,
|
||||||
normalizeWorkload,
|
normalizeWorkload,
|
||||||
isWonWorkload,
|
isWonWorkload,
|
||||||
@@ -31,28 +32,45 @@
|
|||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
const payload = await fetchJson(url.toString(), { forceExtension: true });
|
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(
|
const firstName = cleanString(
|
||||||
getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
|
|
||||||
);
|
|
||||||
const lastName = cleanString(
|
|
||||||
getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
|
|
||||||
);
|
|
||||||
const userEmail = cleanString(
|
|
||||||
getFieldValue(userPayload, [
|
getFieldValue(userPayload, [
|
||||||
"Email",
|
"FirstName",
|
||||||
"email",
|
"firstName",
|
||||||
"EmailAddress",
|
"GivenName",
|
||||||
"emailAddress",
|
"givenName",
|
||||||
"User.Email",
|
"User.FirstName",
|
||||||
"User.email",
|
|
||||||
"User.EmailAddress",
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
const lastName = cleanString(
|
||||||
|
getFieldValue(userPayload, [
|
||||||
|
"LastName",
|
||||||
|
"lastName",
|
||||||
|
"FamilyName",
|
||||||
|
"familyName",
|
||||||
|
"User.LastName",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const userEmail = resolveUserEmail(payload, userPayload);
|
||||||
const displayName = firstNonEmptyString([
|
const displayName = firstNonEmptyString([
|
||||||
`${firstName} ${lastName}`.trim(),
|
`${firstName} ${lastName}`.trim(),
|
||||||
userPayload?.DisplayName,
|
userPayload?.DisplayName,
|
||||||
userPayload?.displayName,
|
userPayload?.displayName,
|
||||||
|
userPayload?.FullName,
|
||||||
|
userPayload?.fullName,
|
||||||
|
userPayload?.Name,
|
||||||
|
userPayload?.name,
|
||||||
userEmail,
|
userEmail,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -62,7 +80,7 @@
|
|||||||
? Object.keys(userPayload).join(", ")
|
? Object.keys(userPayload).join(", ")
|
||||||
: "";
|
: "";
|
||||||
throw new Error(
|
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 || {}
|
payload || {}
|
||||||
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
|
).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
|
||||||
);
|
);
|
||||||
@@ -71,9 +89,7 @@
|
|||||||
return {
|
return {
|
||||||
userEmail,
|
userEmail,
|
||||||
name: displayName || userEmail,
|
name: displayName || userEmail,
|
||||||
avatar: cleanString(
|
avatar: resolveUserAvatar(payload, userPayload),
|
||||||
getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])
|
|
||||||
),
|
|
||||||
administrator: Boolean(
|
administrator: Boolean(
|
||||||
getFieldValue(userPayload, [
|
getFieldValue(userPayload, [
|
||||||
"Administrator",
|
"Administrator",
|
||||||
@@ -84,20 +100,178 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWorklistUserPayload(payload) {
|
function normalizeCurrentUserCandidate(payload) {
|
||||||
const candidate = payload?.User ?? payload?.user ?? 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)) {
|
if (Array.isArray(candidate)) {
|
||||||
return candidate[0] || {};
|
return candidate[0] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate?.User) {
|
const nestedUser =
|
||||||
return normalizeWorklistUserPayload(candidate);
|
getObjectCandidate(candidate?.User) ?? getObjectCandidate(candidate?.user);
|
||||||
|
|
||||||
|
if (nestedUser) {
|
||||||
|
return normalizeCurrentUserCandidate(nestedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate || {};
|
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) {
|
async function fetchAllCustomers(userEmail) {
|
||||||
const limit = 49;
|
const limit = 49;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@@ -277,6 +451,7 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
fetchWorklistCurrentUser,
|
fetchWorklistCurrentUser,
|
||||||
|
fetchSpaCurrentUser,
|
||||||
fetchAllCustomers,
|
fetchAllCustomers,
|
||||||
fetchCustomerWorkloads,
|
fetchCustomerWorkloads,
|
||||||
fetchWorkloadActions,
|
fetchWorkloadActions,
|
||||||
|
|||||||
@@ -172,7 +172,9 @@
|
|||||||
const response = await new Promise((resolve, reject) => {
|
const response = await new Promise((resolve, reject) => {
|
||||||
const timeout = window.setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
pendingBridgeRequests.delete(requestId);
|
pendingBridgeRequests.delete(requestId);
|
||||||
reject(new Error(`Timed out while requesting ${requestUrl}`));
|
reject(
|
||||||
|
new Error(`Timed out while requesting ${redactUrlForError(requestUrl)}`)
|
||||||
|
);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
pendingBridgeRequests.set(requestId, {
|
pendingBridgeRequests.set(requestId, {
|
||||||
@@ -229,7 +231,9 @@
|
|||||||
`${
|
`${
|
||||||
payloadMessage ||
|
payloadMessage ||
|
||||||
response.error ||
|
response.error ||
|
||||||
`Request failed (${response.status}) for ${requestUrl}`
|
`Request failed (${response.status}) for ${redactUrlForError(
|
||||||
|
requestUrl
|
||||||
|
)}`
|
||||||
}${debugSuffix}`
|
}${debugSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -297,7 +301,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response || typeof response !== "object") {
|
if (!response || typeof response !== "object") {
|
||||||
throw new Error(`No response received for ${requestUrl}`);
|
throw new Error(
|
||||||
|
`No response received for ${redactUrlForError(requestUrl)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -328,8 +334,8 @@
|
|||||||
const debugFrameUrl = cleanString(response.debug?.frameUrl);
|
const debugFrameUrl = cleanString(response.debug?.frameUrl);
|
||||||
const debugUrl = cleanString(response.debug?.url);
|
const debugUrl = cleanString(response.debug?.url);
|
||||||
const debugParts = [
|
const debugParts = [
|
||||||
debugUrl ? `url: ${debugUrl}` : "",
|
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
|
||||||
debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
|
debugFrameUrl ? `frame: ${redactUrlForError(debugFrameUrl)}` : "",
|
||||||
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const debugSuffix =
|
const debugSuffix =
|
||||||
@@ -339,7 +345,9 @@
|
|||||||
`${
|
`${
|
||||||
payloadMessage ||
|
payloadMessage ||
|
||||||
response.error ||
|
response.error ||
|
||||||
`Request failed (${response.status}) for ${requestUrl}`
|
`Request failed (${response.status}) for ${redactUrlForError(
|
||||||
|
requestUrl
|
||||||
|
)}`
|
||||||
}${debugSuffix}`
|
}${debugSuffix}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -493,7 +501,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response || typeof response !== "object") {
|
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) {
|
if (!response.ok) {
|
||||||
@@ -511,7 +521,7 @@
|
|||||||
: "";
|
: "";
|
||||||
const debugUrl = cleanString(response.debug?.url);
|
const debugUrl = cleanString(response.debug?.url);
|
||||||
const debugParts = [
|
const debugParts = [
|
||||||
debugUrl ? `url: ${debugUrl}` : "",
|
debugUrl ? `url: ${redactUrlForError(debugUrl)}` : "",
|
||||||
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const debugSuffix =
|
const debugSuffix =
|
||||||
@@ -521,7 +531,9 @@
|
|||||||
`${
|
`${
|
||||||
payloadMessage ||
|
payloadMessage ||
|
||||||
response.error ||
|
response.error ||
|
||||||
`Request failed (${response.status}) for ${requestUrl}`
|
`Request failed (${response.status}) for ${redactUrlForError(
|
||||||
|
requestUrl
|
||||||
|
)}`
|
||||||
}${debugSuffix}`
|
}${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) {
|
function handleBridgeMessage(event) {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
const TARGET_APP_URL =
|
const TARGET_APP_URL =
|
||||||
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
|
"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_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 HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
|
||||||
const WORKLIST_ORIGIN = "https://worklist.oracle.com";
|
const WORKLIST_ORIGIN = "https://worklist.oracle.com";
|
||||||
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
|
const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
|
||||||
@@ -167,7 +170,7 @@
|
|||||||
TARGET_PARAM_VALUE,
|
TARGET_PARAM_VALUE,
|
||||||
TARGET_APP_URL,
|
TARGET_APP_URL,
|
||||||
HCM_ORIGIN,
|
HCM_ORIGIN,
|
||||||
HCM_WELCOME_PATHNAME,
|
HCM_WELCOME_PATHNAMES,
|
||||||
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
HCM_MY_INFORMATION_APPS_GROUP_ID,
|
||||||
WORKLIST_ORIGIN,
|
WORKLIST_ORIGIN,
|
||||||
WORKLIST_SAASUI_PATHNAME,
|
WORKLIST_SAASUI_PATHNAME,
|
||||||
|
|||||||
@@ -402,6 +402,54 @@
|
|||||||
return new Date(value);
|
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) {
|
function getErrorMessage(error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return error.message;
|
return error.message;
|
||||||
@@ -456,6 +504,9 @@
|
|||||||
getOperationalWeekInfo,
|
getOperationalWeekInfo,
|
||||||
getCalendarRowWeekDate,
|
getCalendarRowWeekDate,
|
||||||
parseDatePreservingDateOnly,
|
parseDatePreservingDateOnly,
|
||||||
|
decodeJwtPayload,
|
||||||
|
getJwtExpiresAt,
|
||||||
|
isJwtExpired,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
cssEscape,
|
cssEscape,
|
||||||
|
|||||||
Reference in New Issue
Block a user