User profile controls that hide non-sales features from users outside sales

This commit is contained in:
2026-09-24 17:23:50 -03:00
parent f74071e39e
commit 451c27e9d7
22 changed files with 391 additions and 36 deletions

View File

@@ -19,6 +19,14 @@ Browser extension that adds the "Opportunities Extension" shortcut to the permit
## CRM Service Request Badges ## CRM Service Request Badges
On the first Opportunities modal opening in a page session, `requestCurrentUser`
fetches SPA identity before tokenrelay or opportunity requests. Concurrent calls,
Reference changes and modal reopenings reuse that result. Users with `SALES_REP`
do not trigger CRM Service Request or Workload Actions queries or see their badges.
If identity or roles cannot be validated, only these two features stay disabled;
other opportunity features continue. Reload the page to refresh identity/roles.
The unused identity token is discarded and redacted from debug responses.
Once per opening of the opportunities modal, `resquestCrmServiceRequest` fetches all pages of Once per opening of the opportunities modal, `resquestCrmServiceRequest` fetches all pages of
`CRMServiceRequest` records with `SVC_PENDING_DELIVERY` and `ORA_SVC_MY_SR_TEAM_SRS`, `CRMServiceRequest` records with `SVC_PENDING_DELIVERY` and `ORA_SVC_MY_SR_TEAM_SRS`,
using the existing general tokenrelay Bearer. It uses the Fusion `searchResources/latest` using the existing general tokenrelay Bearer. It uses the Fusion `searchResources/latest`

View File

@@ -14,6 +14,7 @@ function createMessageTypes() {
requestInstallBase: "opportunitiesExtension.requestInstallBase", requestInstallBase: "opportunitiesExtension.requestInstallBase",
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
requestWlActions: "opportunitiesExtension.requestWlActions", requestWlActions: "opportunitiesExtension.requestWlActions",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail",
requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan"
@@ -643,6 +644,35 @@ function createSpaWorkloadActions({ fetchJson }) {
return { requestWlActions }; return { requestWlActions };
} }
// Source: src/services/spa/current-user.js
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
const debugRequests = [];
let user = null;
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.");
}
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 {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };
}
// Source: src/services/spa/consumption.js // Source: src/services/spa/consumption.js
function createSpaConsumption({ function createSpaConsumption({
CONSUMPTION_TREND_REQUESTS, CONSUMPTION_TREND_REQUESTS,
@@ -1696,6 +1726,7 @@ function registerBackgroundMessages({
requestInstallBase, requestInstallBase,
requestCustomerServiceRequest, requestCustomerServiceRequest,
requestWlActions, requestWlActions,
requestCurrentUser,
requestConsumerTokenService, requestConsumerTokenService,
requestConsumptionTrend, requestConsumptionTrend,
requestConsumptionPlanDetail, requestConsumptionPlanDetail,
@@ -1777,6 +1808,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; return true;
} }
if (message.type === messageTypes.requestCurrentUser) {
requestCurrentUser().then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load current user.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;
}
if (message.type === messageTypes.requestWlActions) { if (message.type === messageTypes.requestWlActions) {
requestWlActions(message).then(sendResponse).catch(error => sendResponse({ requestWlActions(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load workload actions.", ok: false, error: error.message || "Unable to load workload actions.",
@@ -1859,6 +1898,8 @@ function createBackgroundApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const spaCurrentUser = createSpaCurrentUser({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
const requestCurrentUser = () => spaCurrentUser.requestCurrentUser();
const contracts = createBackgroundContracts(); const contracts = createBackgroundContracts();
const productsCacheState = createProductsCacheState(); const productsCacheState = createProductsCacheState();
const authConsumerSession = createAuthConsumerSession({ const authConsumerSession = createAuthConsumerSession({
@@ -1973,7 +2014,7 @@ function createBackgroundApplication(overrides = {}) {
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
function start() { function start() {
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -15,6 +15,7 @@ function createMessageTypes() {
requestInstallBase: "opportunitiesExtension.requestInstallBase", requestInstallBase: "opportunitiesExtension.requestInstallBase",
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
requestWlActions: "opportunitiesExtension.requestWlActions", requestWlActions: "opportunitiesExtension.requestWlActions",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail",
requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan"
@@ -470,6 +471,7 @@ function createContentModal({
resetWorkloadActions, resetWorkloadActions,
reopenWorkloadActions, reopenWorkloadActions,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod, resetCustomerAndOwnerFiltersForPeriod,
resetOpportunitiesTable, resetOpportunitiesTable,
restoreFilterPreferences, restoreFilterPreferences,
@@ -496,11 +498,16 @@ function createContentModal({
if (existingModal) { if (existingModal) {
existingModal.hidden = false; existingModal.hidden = false;
reopenWorkloadActions();
existingModal.querySelector("select").focus(); existingModal.querySelector("select").focus();
document.documentElement.classList.add("opportunities-extension-scroll-lock"); document.documentElement.classList.add("opportunities-extension-scroll-lock");
ensureEscapeKeyHandler(); ensureEscapeKeyHandler();
requestTokenRelayOnce().then(token => loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion)) ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
})
.then(() => renderOpportunitiesTable()).catch(() => {}); .then(() => renderOpportunitiesTable()).catch(() => {});
return; return;
} }
@@ -1186,6 +1193,25 @@ function createAuthContentSession({
return { requestTokenRelayOnce, requestConsumerTokenServiceOnce }; return { requestTokenRelayOnce, requestConsumerTokenServiceOnce };
} }
// Source: src/auth/current-user.js
function createCurrentUser({ sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) {
let pending = null;
let allowed = false;
function ensure() {
if (!pending) {
pending = sendRuntimeMessage({ type: messageTypes.requestCurrentUser }).then(response => {
appendBackgroundRequestLog(response?.debugRequests);
const roles = response?.user?.roles;
allowed = response?.ok === true && Array.isArray(roles)
&& roles.every(role => typeof role === "string") && !roles.includes("SALES_REP");
return allowed;
}).catch(() => { allowed = false; return false; });
}
return pending;
}
return { ensure, allowsCrmActivities: () => allowed };
}
// Source: src/auth/sales-common.js // Source: src/auth/sales-common.js
function createAuthSalesCommon({ function createAuthSalesCommon({
TOKEN_RELAY_URL, TOKEN_RELAY_URL,
@@ -1568,11 +1594,14 @@ function createOpportunitiesController({
resetWorkloadActions, resetWorkloadActions,
requestOpptyProductsInBackground, requestOpptyProductsInBackground,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
setOpportunitiesTableState setOpportunitiesTableState
}) { }) {
async function requestOpportunitiesForPeriod(period) { async function requestOpportunitiesForPeriod(period) {
const dateRange = getFiscalPeriodRange(period, new Date()); const dateRange = getFiscalPeriodRange(period, new Date());
const requestVersion = ++opportunitiesQueryState.periodRequestVersion; const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
await ensureCurrentUser();
if (!isCurrentPeriodRequest(requestVersion)) return;
if (preferencesState.selectedOpportunityTypeValues.size === 0) { if (preferencesState.selectedOpportunityTypeValues.size === 0) {
setOpportunitiesTableState({ setOpportunitiesTableState({
@@ -1933,14 +1962,14 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
} }
// Source: src/features/opportunities/crm-service-requests.js // Source: src/features/opportunities/crm-service-requests.js
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, environment }) { function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, allowsCrmActivities, environment }) {
let status = "idle"; let status = "idle";
let byOpportunity = new Map(); let byOpportunity = new Map();
let pending = null; let pending = null;
let generation = 0; let generation = 0;
function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); } function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); }
function load(token, version) { function load(token, version) {
if (!isCurrentPeriodRequest(version)) return; if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return;
if (!pending) pending = run(token, version, generation); if (!pending) pending = run(token, version, generation);
return pending; return pending;
} }
@@ -1965,7 +1994,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!optyNumber || status === "idle" || status === "loading") return null; if (!allowsCrmActivities() || !optyNumber || status === "idle" || status === "loading") return null;
const items = byOpportunity.get(String(optyNumber)) || []; const items = byOpportunity.get(String(optyNumber)) || [];
const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span"); const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span");
badge.className = "opportunities-extension-crm-sr-badge"; badge.className = "opportunities-extension-crm-sr-badge";
@@ -2123,7 +2152,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, envir
} }
// Source: src/features/opportunities/workload-actions.js // Source: src/features/opportunities/workload-actions.js
function createOpportunityWorkloadActions({ authSessionState, requestConsumerTokenServiceOnce, sendRuntimeMessage, function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) { appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) {
let session = newSession(); let session = newSession();
function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; } function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; }
@@ -2137,7 +2166,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
} }
function loadProducts(products) { function loadProducts(products) {
if (!session.enabled) return; if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) { for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue; if (session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false }; const record = { status: "loading", actions: [], hasNext: false };
@@ -2161,9 +2190,9 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
async function run(current, { revnId, record }) { async function run(current, { revnId, record }) {
try { try {
const authorizations = await requestConsumerTokenServiceOnce(); const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions }); const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests); appendBackgroundRequestLog(result?.debugRequests);
if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions."); if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions.");
record.actions = result.actions; record.actions = result.actions;
@@ -2178,7 +2207,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!session.enabled || !optyNumber) return null; if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null;
const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); const products = productsContentState.opptyProductsByNumber.get(String(optyNumber));
if (!products) return null; if (!products) return null;
const ids = idsFor(products); const ids = idsFor(products);
@@ -6463,7 +6492,7 @@ function createDebugContentView({
let returnFocus = null; let returnFocus = null;
function getRequestCategory(entry) { function getRequestCategory(entry) {
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
if (entry.name === "requestOpportunities") return "opportunities"; if (entry.name === "requestOpportunities") return "opportunities";
if (entry.name === "requestOpptyProducts") return "products"; if (entry.name === "requestOpptyProducts") return "products";
if (entry.name === "requestWlActions") return "actions"; if (entry.name === "requestWlActions") return "actions";
@@ -6826,6 +6855,11 @@ function createContentApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const currentUser = createCurrentUser({
messageTypes,
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args)
});
const contracts = createContentContracts(); const contracts = createContentContracts();
const authSessionState = createAuthSessionState(contracts); const authSessionState = createAuthSessionState(contracts);
const authSalesCommonState = createAuthSalesCommonState(); const authSalesCommonState = createAuthSalesCommonState();
@@ -6857,6 +6891,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const contentModal = createContentModal({ const contentModal = createContentModal({
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS, AUTH_STATUS: contracts.AUTH_STATUS,
CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID,
@@ -6975,6 +7010,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunitiesController = createOpportunitiesController({ const opportunitiesController = createOpportunitiesController({
ensureCurrentUser: () => currentUser.ensure(),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args), resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
STAGE_OPTIONS: contracts.STAGE_OPTIONS, STAGE_OPTIONS: contracts.STAGE_OPTIONS,
STATUS_OPTIONS: contracts.STATUS_OPTIONS, STATUS_OPTIONS: contracts.STATUS_OPTIONS,
@@ -7023,6 +7059,7 @@ function createContentApplication(overrides = {}) {
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment
}); });
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
bindPopover: (...args) => crmSrPopover.bind(...args), bindPopover: (...args) => crmSrPopover.bind(...args),
closePopover: (...args) => crmSrPopover.close(...args), closePopover: (...args) => crmSrPopover.close(...args),
requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args), requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args),
@@ -7143,6 +7180,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunityWorkloadActions = createOpportunityWorkloadActions({ const opportunityWorkloadActions = createOpportunityWorkloadActions({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
authSessionState, authSessionState,
requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args),
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
@@ -7335,6 +7373,7 @@ function createContentApplication(overrides = {}) {
contentIcons, contentIcons,
contentStyles, contentStyles,
authContentSession, authContentSession,
currentUser,
authSalesCommon, authSalesCommon,
authTokenRelay, authTokenRelay,
authXsrfContent, authXsrfContent,

View File

@@ -14,6 +14,7 @@ function createMessageTypes() {
requestInstallBase: "opportunitiesExtension.requestInstallBase", requestInstallBase: "opportunitiesExtension.requestInstallBase",
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
requestWlActions: "opportunitiesExtension.requestWlActions", requestWlActions: "opportunitiesExtension.requestWlActions",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail",
requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan"
@@ -643,6 +644,35 @@ function createSpaWorkloadActions({ fetchJson }) {
return { requestWlActions }; return { requestWlActions };
} }
// Source: src/services/spa/current-user.js
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
const debugRequests = [];
let user = null;
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.");
}
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 {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };
}
// Source: src/services/spa/consumption.js // Source: src/services/spa/consumption.js
function createSpaConsumption({ function createSpaConsumption({
CONSUMPTION_TREND_REQUESTS, CONSUMPTION_TREND_REQUESTS,
@@ -1696,6 +1726,7 @@ function registerBackgroundMessages({
requestInstallBase, requestInstallBase,
requestCustomerServiceRequest, requestCustomerServiceRequest,
requestWlActions, requestWlActions,
requestCurrentUser,
requestConsumerTokenService, requestConsumerTokenService,
requestConsumptionTrend, requestConsumptionTrend,
requestConsumptionPlanDetail, requestConsumptionPlanDetail,
@@ -1777,6 +1808,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; return true;
} }
if (message.type === messageTypes.requestCurrentUser) {
requestCurrentUser().then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load current user.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;
}
if (message.type === messageTypes.requestWlActions) { if (message.type === messageTypes.requestWlActions) {
requestWlActions(message).then(sendResponse).catch(error => sendResponse({ requestWlActions(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load workload actions.", ok: false, error: error.message || "Unable to load workload actions.",
@@ -1859,6 +1898,8 @@ function createBackgroundApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const spaCurrentUser = createSpaCurrentUser({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
const requestCurrentUser = () => spaCurrentUser.requestCurrentUser();
const contracts = createBackgroundContracts(); const contracts = createBackgroundContracts();
const productsCacheState = createProductsCacheState(); const productsCacheState = createProductsCacheState();
const authConsumerSession = createAuthConsumerSession({ const authConsumerSession = createAuthConsumerSession({
@@ -1973,7 +2014,7 @@ function createBackgroundApplication(overrides = {}) {
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
function start() { function start() {
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -15,6 +15,7 @@ function createMessageTypes() {
requestInstallBase: "opportunitiesExtension.requestInstallBase", requestInstallBase: "opportunitiesExtension.requestInstallBase",
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
requestWlActions: "opportunitiesExtension.requestWlActions", requestWlActions: "opportunitiesExtension.requestWlActions",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail",
requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan"
@@ -470,6 +471,7 @@ function createContentModal({
resetWorkloadActions, resetWorkloadActions,
reopenWorkloadActions, reopenWorkloadActions,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod, resetCustomerAndOwnerFiltersForPeriod,
resetOpportunitiesTable, resetOpportunitiesTable,
restoreFilterPreferences, restoreFilterPreferences,
@@ -496,11 +498,16 @@ function createContentModal({
if (existingModal) { if (existingModal) {
existingModal.hidden = false; existingModal.hidden = false;
reopenWorkloadActions();
existingModal.querySelector("select").focus(); existingModal.querySelector("select").focus();
document.documentElement.classList.add("opportunities-extension-scroll-lock"); document.documentElement.classList.add("opportunities-extension-scroll-lock");
ensureEscapeKeyHandler(); ensureEscapeKeyHandler();
requestTokenRelayOnce().then(token => loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion)) ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
})
.then(() => renderOpportunitiesTable()).catch(() => {}); .then(() => renderOpportunitiesTable()).catch(() => {});
return; return;
} }
@@ -1186,6 +1193,25 @@ function createAuthContentSession({
return { requestTokenRelayOnce, requestConsumerTokenServiceOnce }; return { requestTokenRelayOnce, requestConsumerTokenServiceOnce };
} }
// Source: src/auth/current-user.js
function createCurrentUser({ sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) {
let pending = null;
let allowed = false;
function ensure() {
if (!pending) {
pending = sendRuntimeMessage({ type: messageTypes.requestCurrentUser }).then(response => {
appendBackgroundRequestLog(response?.debugRequests);
const roles = response?.user?.roles;
allowed = response?.ok === true && Array.isArray(roles)
&& roles.every(role => typeof role === "string") && !roles.includes("SALES_REP");
return allowed;
}).catch(() => { allowed = false; return false; });
}
return pending;
}
return { ensure, allowsCrmActivities: () => allowed };
}
// Source: src/auth/sales-common.js // Source: src/auth/sales-common.js
function createAuthSalesCommon({ function createAuthSalesCommon({
TOKEN_RELAY_URL, TOKEN_RELAY_URL,
@@ -1568,11 +1594,14 @@ function createOpportunitiesController({
resetWorkloadActions, resetWorkloadActions,
requestOpptyProductsInBackground, requestOpptyProductsInBackground,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
setOpportunitiesTableState setOpportunitiesTableState
}) { }) {
async function requestOpportunitiesForPeriod(period) { async function requestOpportunitiesForPeriod(period) {
const dateRange = getFiscalPeriodRange(period, new Date()); const dateRange = getFiscalPeriodRange(period, new Date());
const requestVersion = ++opportunitiesQueryState.periodRequestVersion; const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
await ensureCurrentUser();
if (!isCurrentPeriodRequest(requestVersion)) return;
if (preferencesState.selectedOpportunityTypeValues.size === 0) { if (preferencesState.selectedOpportunityTypeValues.size === 0) {
setOpportunitiesTableState({ setOpportunitiesTableState({
@@ -1933,14 +1962,14 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
} }
// Source: src/features/opportunities/crm-service-requests.js // Source: src/features/opportunities/crm-service-requests.js
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, environment }) { function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, allowsCrmActivities, environment }) {
let status = "idle"; let status = "idle";
let byOpportunity = new Map(); let byOpportunity = new Map();
let pending = null; let pending = null;
let generation = 0; let generation = 0;
function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); } function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); }
function load(token, version) { function load(token, version) {
if (!isCurrentPeriodRequest(version)) return; if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return;
if (!pending) pending = run(token, version, generation); if (!pending) pending = run(token, version, generation);
return pending; return pending;
} }
@@ -1965,7 +1994,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!optyNumber || status === "idle" || status === "loading") return null; if (!allowsCrmActivities() || !optyNumber || status === "idle" || status === "loading") return null;
const items = byOpportunity.get(String(optyNumber)) || []; const items = byOpportunity.get(String(optyNumber)) || [];
const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span"); const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span");
badge.className = "opportunities-extension-crm-sr-badge"; badge.className = "opportunities-extension-crm-sr-badge";
@@ -2123,7 +2152,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, envir
} }
// Source: src/features/opportunities/workload-actions.js // Source: src/features/opportunities/workload-actions.js
function createOpportunityWorkloadActions({ authSessionState, requestConsumerTokenServiceOnce, sendRuntimeMessage, function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) { appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) {
let session = newSession(); let session = newSession();
function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; } function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; }
@@ -2137,7 +2166,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
} }
function loadProducts(products) { function loadProducts(products) {
if (!session.enabled) return; if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) { for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue; if (session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false }; const record = { status: "loading", actions: [], hasNext: false };
@@ -2161,9 +2190,9 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
async function run(current, { revnId, record }) { async function run(current, { revnId, record }) {
try { try {
const authorizations = await requestConsumerTokenServiceOnce(); const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions }); const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests); appendBackgroundRequestLog(result?.debugRequests);
if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions."); if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions.");
record.actions = result.actions; record.actions = result.actions;
@@ -2178,7 +2207,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!session.enabled || !optyNumber) return null; if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null;
const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); const products = productsContentState.opptyProductsByNumber.get(String(optyNumber));
if (!products) return null; if (!products) return null;
const ids = idsFor(products); const ids = idsFor(products);
@@ -6463,7 +6492,7 @@ function createDebugContentView({
let returnFocus = null; let returnFocus = null;
function getRequestCategory(entry) { function getRequestCategory(entry) {
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
if (entry.name === "requestOpportunities") return "opportunities"; if (entry.name === "requestOpportunities") return "opportunities";
if (entry.name === "requestOpptyProducts") return "products"; if (entry.name === "requestOpptyProducts") return "products";
if (entry.name === "requestWlActions") return "actions"; if (entry.name === "requestWlActions") return "actions";
@@ -6826,6 +6855,11 @@ function createContentApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const currentUser = createCurrentUser({
messageTypes,
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args)
});
const contracts = createContentContracts(); const contracts = createContentContracts();
const authSessionState = createAuthSessionState(contracts); const authSessionState = createAuthSessionState(contracts);
const authSalesCommonState = createAuthSalesCommonState(); const authSalesCommonState = createAuthSalesCommonState();
@@ -6857,6 +6891,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const contentModal = createContentModal({ const contentModal = createContentModal({
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS, AUTH_STATUS: contracts.AUTH_STATUS,
CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID,
@@ -6975,6 +7010,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunitiesController = createOpportunitiesController({ const opportunitiesController = createOpportunitiesController({
ensureCurrentUser: () => currentUser.ensure(),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args), resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
STAGE_OPTIONS: contracts.STAGE_OPTIONS, STAGE_OPTIONS: contracts.STAGE_OPTIONS,
STATUS_OPTIONS: contracts.STATUS_OPTIONS, STATUS_OPTIONS: contracts.STATUS_OPTIONS,
@@ -7023,6 +7059,7 @@ function createContentApplication(overrides = {}) {
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment
}); });
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
bindPopover: (...args) => crmSrPopover.bind(...args), bindPopover: (...args) => crmSrPopover.bind(...args),
closePopover: (...args) => crmSrPopover.close(...args), closePopover: (...args) => crmSrPopover.close(...args),
requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args), requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args),
@@ -7143,6 +7180,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunityWorkloadActions = createOpportunityWorkloadActions({ const opportunityWorkloadActions = createOpportunityWorkloadActions({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
authSessionState, authSessionState,
requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args),
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
@@ -7335,6 +7373,7 @@ function createContentApplication(overrides = {}) {
contentIcons, contentIcons,
contentStyles, contentStyles,
authContentSession, authContentSession,
currentUser,
authSalesCommon, authSalesCommon,
authTokenRelay, authTokenRelay,
authXsrfContent, authXsrfContent,

View File

@@ -23,6 +23,7 @@ module.exports = {
"src/content/svg.js", "src/content/svg.js",
"src/content/styles.js", "src/content/styles.js",
"src/auth/content-session.js", "src/auth/content-session.js",
"src/auth/current-user.js",
"src/auth/sales-common.js", "src/auth/sales-common.js",
"src/auth/token-relay.js", "src/auth/token-relay.js",
"src/auth/xsrf-content.js", "src/auth/xsrf-content.js",
@@ -76,6 +77,7 @@ module.exports = {
"src/services/spa/install-base.js", "src/services/spa/install-base.js",
"src/services/spa/customer-service-requests.js", "src/services/spa/customer-service-requests.js",
"src/services/spa/workload-actions.js", "src/services/spa/workload-actions.js",
"src/services/spa/current-user.js",
"src/services/spa/consumption.js", "src/services/spa/consumption.js",
"src/services/spa/response-shapes.js", "src/services/spa/response-shapes.js",
"src/services/http/background-json.js", "src/services/http/background-json.js",

17
src/auth/current-user.js Normal file
View File

@@ -0,0 +1,17 @@
function createCurrentUser({ sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) {
let pending = null;
let allowed = false;
function ensure() {
if (!pending) {
pending = sendRuntimeMessage({ type: messageTypes.requestCurrentUser }).then(response => {
appendBackgroundRequestLog(response?.debugRequests);
const roles = response?.user?.roles;
allowed = response?.ok === true && Array.isArray(roles)
&& roles.every(role => typeof role === "string") && !roles.includes("SALES_REP");
return allowed;
}).catch(() => { allowed = false; return false; });
}
return pending;
}
return { ensure, allowsCrmActivities: () => allowed };
}

View File

@@ -7,6 +7,8 @@ function createBackgroundApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const spaCurrentUser = createSpaCurrentUser({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
const requestCurrentUser = () => spaCurrentUser.requestCurrentUser();
const contracts = createBackgroundContracts(); const contracts = createBackgroundContracts();
const productsCacheState = createProductsCacheState(); const productsCacheState = createProductsCacheState();
const authConsumerSession = createAuthConsumerSession({ const authConsumerSession = createAuthConsumerSession({
@@ -121,7 +123,7 @@ function createBackgroundApplication(overrides = {}) {
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args); const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome; const runtimeApi = environment.browser || environment.chrome;
function start() { function start() {
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi }); startBackgroundApplication({ messageTypes, requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
} }
return { return {
start, contracts, start, contracts,

View File

@@ -7,6 +7,7 @@ function registerBackgroundMessages({
requestInstallBase, requestInstallBase,
requestCustomerServiceRequest, requestCustomerServiceRequest,
requestWlActions, requestWlActions,
requestCurrentUser,
requestConsumerTokenService, requestConsumerTokenService,
requestConsumptionTrend, requestConsumptionTrend,
requestConsumptionPlanDetail, requestConsumptionPlanDetail,
@@ -88,6 +89,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; return true;
} }
if (message.type === messageTypes.requestCurrentUser) {
requestCurrentUser().then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load current user.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;
}
if (message.type === messageTypes.requestWlActions) { if (message.type === messageTypes.requestWlActions) {
requestWlActions(message).then(sendResponse).catch(error => sendResponse({ requestWlActions(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load workload actions.", ok: false, error: error.message || "Unable to load workload actions.",

View File

@@ -18,6 +18,11 @@ function createContentApplication(overrides = {}) {
...overrides ...overrides
}; };
const messageTypes = createMessageTypes(); const messageTypes = createMessageTypes();
const currentUser = createCurrentUser({
messageTypes,
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args)
});
const contracts = createContentContracts(); const contracts = createContentContracts();
const authSessionState = createAuthSessionState(contracts); const authSessionState = createAuthSessionState(contracts);
const authSalesCommonState = createAuthSalesCommonState(); const authSalesCommonState = createAuthSalesCommonState();
@@ -49,6 +54,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const contentModal = createContentModal({ const contentModal = createContentModal({
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS, AUTH_STATUS: contracts.AUTH_STATUS,
CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID,
@@ -167,6 +173,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunitiesController = createOpportunitiesController({ const opportunitiesController = createOpportunitiesController({
ensureCurrentUser: () => currentUser.ensure(),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args), resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
STAGE_OPTIONS: contracts.STAGE_OPTIONS, STAGE_OPTIONS: contracts.STAGE_OPTIONS,
STATUS_OPTIONS: contracts.STATUS_OPTIONS, STATUS_OPTIONS: contracts.STATUS_OPTIONS,
@@ -215,6 +222,7 @@ function createContentApplication(overrides = {}) {
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment
}); });
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
bindPopover: (...args) => crmSrPopover.bind(...args), bindPopover: (...args) => crmSrPopover.bind(...args),
closePopover: (...args) => crmSrPopover.close(...args), closePopover: (...args) => crmSrPopover.close(...args),
requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args), requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args),
@@ -335,6 +343,7 @@ function createContentApplication(overrides = {}) {
environment environment
}); });
const opportunityWorkloadActions = createOpportunityWorkloadActions({ const opportunityWorkloadActions = createOpportunityWorkloadActions({
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
authSessionState, authSessionState,
requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args),
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
@@ -527,6 +536,7 @@ function createContentApplication(overrides = {}) {
contentIcons, contentIcons,
contentStyles, contentStyles,
authContentSession, authContentSession,
currentUser,
authSalesCommon, authSalesCommon,
authTokenRelay, authTokenRelay,
authXsrfContent, authXsrfContent,

View File

@@ -56,6 +56,7 @@ function createContentModal({
resetWorkloadActions, resetWorkloadActions,
reopenWorkloadActions, reopenWorkloadActions,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod, resetCustomerAndOwnerFiltersForPeriod,
resetOpportunitiesTable, resetOpportunitiesTable,
restoreFilterPreferences, restoreFilterPreferences,
@@ -82,11 +83,16 @@ function createContentModal({
if (existingModal) { if (existingModal) {
existingModal.hidden = false; existingModal.hidden = false;
reopenWorkloadActions();
existingModal.querySelector("select").focus(); existingModal.querySelector("select").focus();
document.documentElement.classList.add("opportunities-extension-scroll-lock"); document.documentElement.classList.add("opportunities-extension-scroll-lock");
ensureEscapeKeyHandler(); ensureEscapeKeyHandler();
requestTokenRelayOnce().then(token => loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion)) ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
})
.then(() => renderOpportunitiesTable()).catch(() => {}); .then(() => renderOpportunitiesTable()).catch(() => {});
return; return;
} }

View File

@@ -20,7 +20,7 @@ function createDebugContentView({
let returnFocus = null; let returnFocus = null;
function getRequestCategory(entry) { function getRequestCategory(entry) {
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
if (entry.name === "requestOpportunities") return "opportunities"; if (entry.name === "requestOpportunities") return "opportunities";
if (entry.name === "requestOpptyProducts") return "products"; if (entry.name === "requestOpptyProducts") return "products";
if (entry.name === "requestWlActions") return "actions"; if (entry.name === "requestWlActions") return "actions";

View File

@@ -18,11 +18,14 @@ function createOpportunitiesController({
resetWorkloadActions, resetWorkloadActions,
requestOpptyProductsInBackground, requestOpptyProductsInBackground,
requestTokenRelayOnce, requestTokenRelayOnce,
ensureCurrentUser,
setOpportunitiesTableState setOpportunitiesTableState
}) { }) {
async function requestOpportunitiesForPeriod(period) { async function requestOpportunitiesForPeriod(period) {
const dateRange = getFiscalPeriodRange(period, new Date()); const dateRange = getFiscalPeriodRange(period, new Date());
const requestVersion = ++opportunitiesQueryState.periodRequestVersion; const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
await ensureCurrentUser();
if (!isCurrentPeriodRequest(requestVersion)) return;
if (preferencesState.selectedOpportunityTypeValues.size === 0) { if (preferencesState.selectedOpportunityTypeValues.size === 0) {
setOpportunitiesTableState({ setOpportunitiesTableState({

View File

@@ -1,11 +1,11 @@
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, environment }) { function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, allowsCrmActivities, environment }) {
let status = "idle"; let status = "idle";
let byOpportunity = new Map(); let byOpportunity = new Map();
let pending = null; let pending = null;
let generation = 0; let generation = 0;
function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); } function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); }
function load(token, version) { function load(token, version) {
if (!isCurrentPeriodRequest(version)) return; if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return;
if (!pending) pending = run(token, version, generation); if (!pending) pending = run(token, version, generation);
return pending; return pending;
} }
@@ -30,7 +30,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!optyNumber || status === "idle" || status === "loading") return null; if (!allowsCrmActivities() || !optyNumber || status === "idle" || status === "loading") return null;
const items = byOpportunity.get(String(optyNumber)) || []; const items = byOpportunity.get(String(optyNumber)) || [];
const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span"); const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span");
badge.className = "opportunities-extension-crm-sr-badge"; badge.className = "opportunities-extension-crm-sr-badge";

View File

@@ -1,4 +1,4 @@
function createOpportunityWorkloadActions({ authSessionState, requestConsumerTokenServiceOnce, sendRuntimeMessage, function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) { appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, environment }) {
let session = newSession(); let session = newSession();
function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; } function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0 }; }
@@ -12,7 +12,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
} }
function loadProducts(products) { function loadProducts(products) {
if (!session.enabled) return; if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) { for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue; if (session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false }; const record = { status: "loading", actions: [], hasNext: false };
@@ -36,9 +36,9 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
async function run(current, { revnId, record }) { async function run(current, { revnId, record }) {
try { try {
const authorizations = await requestConsumerTokenServiceOnce(); const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions }); const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests); appendBackgroundRequestLog(result?.debugRequests);
if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions."); if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions.");
record.actions = result.actions; record.actions = result.actions;
@@ -53,7 +53,7 @@ function createOpportunityWorkloadActions({ authSessionState, requestConsumerTok
} }
} }
function createBadge(optyNumber) { function createBadge(optyNumber) {
if (!session.enabled || !optyNumber) return null; if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null;
const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); const products = productsContentState.opptyProductsByNumber.get(String(optyNumber));
if (!products) return null; if (!products) return null;
const ids = idsFor(products); const ids = idsFor(products);

View File

@@ -0,0 +1,27 @@
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
const debugRequests = [];
let user = null;
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.");
}
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 {
// The identity token is not needed by content scripts or debug, including on failures.
for (const entry of debugRequests) {
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
entry.responseBodyTruncated = false;
}
}
}
return { requestCurrentUser };
}

View File

@@ -11,6 +11,7 @@ function createMessageTypes() {
requestInstallBase: "opportunitiesExtension.requestInstallBase", requestInstallBase: "opportunitiesExtension.requestInstallBase",
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
requestWlActions: "opportunitiesExtension.requestWlActions", requestWlActions: "opportunitiesExtension.requestWlActions",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail",
requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan"

View File

@@ -5,6 +5,7 @@ const response = data => new Response(JSON.stringify(data), { status: 200 });
function setup(fetch) { function setup(fetch) {
const document = { createElement: () => ({ attributes: {}, addEventListener() {}, setAttribute(key, value) { this.attributes[key] = value; } }) }; const document = { createElement: () => ({ attributes: {}, addEventListener() {}, setAttribute(key, value) { this.attributes[key] = value; } }) };
const app = loadApplication("content", { fetch, document, Intl, Date }); const app = loadApplication("content", { fetch, document, Intl, Date });
app.currentUser.allowsCrmActivities = () => true;
app.debugContentView.renderDebugPanel = () => {}; app.debugContentView.renderDebugPanel = () => {};
app.opportunitiesQueryState.periodRequestVersion = 1; app.opportunitiesQueryState.periodRequestVersion = 1;
return app; return app;

View File

@@ -32,6 +32,7 @@ function setup() {
const copied = []; const copied = [];
const app = loadApplication("content", { document, window, setTimeout, clearTimeout, const app = loadApplication("content", { document, window, setTimeout, clearTimeout,
navigator: { clipboard: { writeText: async value => copied.push(value) } } }); navigator: { clipboard: { writeText: async value => copied.push(value) } } });
app.currentUser.allowsCrmActivities = () => true;
// Keep the copy test independent of the toast's own DOM/timer implementation. // Keep the copy test independent of the toast's own DOM/timer implementation.
modal.querySelector = () => ({ classList: { toggle() {} } }); modal.querySelector = () => ({ classList: { toggle() {} } });
window.setTimeout = () => 0; window.setTimeout = () => 0;

105
tests/currentUser.test.cjs Normal file
View File

@@ -0,0 +1,105 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const { loadApplication } = require("./support/application.cjs");
const type = "opportunitiesExtension.requestCurrentUser";
for (const family of ["chrome", "firefox"]) {
test(`${family}: current user GET uses SPA cookies and discards identity token before messaging/debug`, async () => {
const calls = [];
const app = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
return response({ user: { id: "synthetic@example.invalid", roles: ["SALES_REP", "SIMPLE_USER"] }, token: "synthetic-secret" });
} });
const result = await app.dispatch({ type });
assert.equal(result.ok, true);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, "https://spa.oracle.com/oalcrm/web/api/identity-management/users/current");
assert.equal(calls[0].options.method, "GET");
assert.equal(calls[0].options.credentials, "include");
assert.equal(calls[0].options.cache, "no-store");
assert.equal(result.debugRequests[0].name, "requestCurrentUser");
assert.equal(result.debugRequests[0].httpStatus, 200);
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
assert.equal(result.token, undefined);
});
}
test("identity failures and malformed roles remain explicit and never expose response tokens", async () => {
for (const fetch of [
async () => new Response("synthetic-secret<html>fallback</html>"),
async () => response({ token: "synthetic-secret", user: {} }),
async () => response({ token: "synthetic-secret", user: { roles: [null] } }),
async () => response({ token: "synthetic-secret" }, 401),
async () => { throw new Error("Synthetic network failure"); }
]) {
const app = loadBackground({ fetch });
const result = await app.dispatch({ type });
assert.equal(result.ok, false);
assert.equal(result.debugRequests[0].status, "failed");
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
}
});
function content(identity) {
const app = loadApplication("content", { document: { createElement: () => ({ setAttribute() {} }) } });
app.debugContentView.renderDebugPanel = () => {};
const calls = [];
app.platformContentMessages.sendRuntimeMessage = async message => {
calls.push(message.type);
if (message.type === type) return identity();
return { ok: true, actions: [], hasNext: false };
};
app.authContentSession.requestTokenRelayOnce = async () => { calls.push("tokenrelay"); return "synthetic"; };
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { calls.push("crm"); return []; };
app.fusionOpportunities.requestOpportunitiesPage = async () => { calls.push("opportunities"); return []; };
app.productsContentJobs.renderProductsProgress = () => {};
app.productsContentJobs.requestOpptyProductsInBackground = () => {};
app.productsContentJobs.queueProductsTableRender = () => {};
app.opportunitiesTable.setOpportunitiesTableState = () => {};
app.preferencesState.selectedOpportunityTypeValues = new Set(["ORA_ALLOPTIES"]);
return { app, calls };
}
for (const [roles, allowed] of [[ ["SALES_REP", "SIMPLE_USER"], false ], [ ["SOLUTION_ENG_NOTERR"], true ], [ [], true ]]) {
test(`identity runs first, once; CRM/Actions enabled=${allowed} for ${JSON.stringify(roles)}`, async () => {
let finish;
const { app, calls } = content(() => new Promise(resolve => { finish = resolve; }));
const first = app.opportunitiesController.requestOpportunitiesForPeriod("Current Quarter");
assert.deepEqual(calls, [type]);
assert.equal(app.currentUser.allowsCrmActivities(), false);
const same = app.currentUser.ensure();
assert.equal(same, app.currentUser.ensure());
finish({ ok: true, user: { roles } });
await first;
assert.deepEqual(calls, allowed ? [type, "tokenrelay", "crm", "opportunities"] : [type, "tokenrelay", "opportunities"]);
const products = { items: [{ RevnId: "123" }] };
app.productsContentState.opptyProductsByNumber.set("OP", products);
app.opportunityWorkloadActions.loadProducts(products);
await new Promise(resolve => setImmediate(resolve));
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), allowed);
if (!allowed) {
assert.equal(app.opportunityCrmServiceRequests.createBadge("OP"), null);
assert.equal(app.opportunityWorkloadActions.createBadge("OP"), null);
}
await app.opportunitiesController.requestOpportunitiesForPeriod("Next Quarter");
app.opportunityCrmServiceRequests.reset();
app.opportunityWorkloadActions.reopen();
await app.currentUser.ensure();
assert.equal(calls.filter(value => value === type).length, 1);
});
}
test("identity failure disables only CRM/Actions and does not retry on Reference changes", async () => {
for (const identity of [async () => ({ ok: false }), async () => ({ ok: true, user: {} }), async () => { throw new Error("Offline"); }]) {
const { app, calls } = content(identity);
await app.opportunitiesController.requestOpportunitiesForPeriod("Current Quarter");
await app.opportunitiesController.requestOpportunitiesForPeriod("Next Quarter");
assert.equal(calls.filter(value => value === type).length, 1);
assert.equal(calls.filter(value => value === "opportunities").length, 2);
assert.equal(calls.includes("crm"), false);
app.opportunityWorkloadActions.loadProducts({ items: [{ RevnId: "123" }] });
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), false);
}
});

View File

@@ -36,6 +36,7 @@ function setup(items = [synthetic()]) {
const document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag), const document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag),
getElementById: id => id === app.contracts.OPPORTUNITIES_TABLE_ID ? table : null }; getElementById: id => id === app.contracts.OPPORTUNITIES_TABLE_ID ? table : null };
const app = loadApplication("content", { document, Intl, Date }); const app = loadApplication("content", { document, Intl, Date });
app.currentUser.allowsCrmActivities = () => true;
const summaries = []; const summaries = [];
app.aiSummaryContent.requestGenerateAiSummary = number => summaries.push(number); app.aiSummaryContent.requestGenerateAiSummary = number => summaries.push(number);
app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items }); app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items });

View File

@@ -53,6 +53,7 @@ test("workload actions reject missing credentials, invalid ids, HTTP/network/JSO
function contentFixture(send) { function contentFixture(send) {
const app = loadApplication("content", { document: { createElement: () => ({ attributes: {}, setAttribute(k, v) { this.attributes[k] = v; } }) } }); const app = loadApplication("content", { document: { createElement: () => ({ attributes: {}, setAttribute(k, v) { this.attributes[k] = v; } }) } });
app.currentUser.allowsCrmActivities = () => true;
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } }); app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } });
app.authSessionState.principal = "test@example.invalid"; app.authSessionState.principal = "test@example.invalid";
app.platformContentMessages.sendRuntimeMessage = send; app.platformContentMessages.sendRuntimeMessage = send;
@@ -165,6 +166,7 @@ test("product progress triggers Actions for cached products and ignores stale pr
window: { requestAnimationFrame() {} }, window: { requestAnimationFrame() {} },
chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } } chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } }
}); });
app.currentUser.allowsCrmActivities = () => true;
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } }); app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
app.platformContentMessages.sendRuntimeMessage = async message => { app.platformContentMessages.sendRuntimeMessage = async message => {
calls.push(message); calls.push(message);