Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions nx/blocks/media-library/core/export.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { etcFetch } from './urls.js';
import { getMediaType, getSubtype } from './media.js';
import { decodeDisplayName, getFileName } from './files.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import { isMediaLibraryPluginMode, withTimeout } from './utils.js';

function escapeCsvCell(value) {
Expand Down Expand Up @@ -187,8 +187,8 @@ export async function copyMediaToClipboard(media) {
try {
await copyNonImageLinkRichClipboard(media);
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_LINK_MSG'),
};
} catch {
/* fall through to plain writeText */
Expand All @@ -202,23 +202,23 @@ export async function copyMediaToClipboard(media) {
await copyImageToClipboard(mediaUrl);
if (plugin) {
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_IMAGE_MSG'),
};
}
return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_IMAGE') };
return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_IMAGE') };
}
await navigator.clipboard.writeText(mediaUrl);
if (plugin) {
return {
heading: t('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: t('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'),
heading: getMessage('NOTIFY_PLUGIN_FALLBACK_HEADING'),
message: getMessage('NOTIFY_PLUGIN_FALLBACK_PLAIN_MSG'),
};
}
return { heading: t('NOTIFY_COPIED'), message: t('NOTIFY_COPIED_URL') };
return { heading: getMessage('NOTIFY_COPIED'), message: getMessage('NOTIFY_COPIED_URL') };
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to copy to clipboard:', error);
return { heading: t('NOTIFY_ERROR'), message: t('NOTIFY_COPY_ERROR') };
return { heading: getMessage('NOTIFY_ERROR'), message: getMessage('NOTIFY_COPY_ERROR') };
}
}
104 changes: 104 additions & 0 deletions nx/blocks/media-library/core/ims-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* IMS adapter for media-library
* Supports plugin mode token injection and falls back to nx2/utils/ims.js for app mode
*/

let cachedImsDetails;
let imsLoadPromise;
let isRedirectingToSignIn = false;

/**
* Inject IMS token for plugin mode
* @param {string} token - Bearer token from plugin host
*/
export function setImsDetails(token) {
cachedImsDetails = { accessToken: { token } };
imsLoadPromise = Promise.resolve(cachedImsDetails);
}

/**
* Initialize IMS authentication
* @returns {Promise<Object|null>} IMS details with accessToken
*/
export async function initIms() {
if (cachedImsDetails && !cachedImsDetails.anonymous) return cachedImsDetails;
if (imsLoadPromise) return imsLoadPromise;

imsLoadPromise = (async () => {
const { loadIms } = await import('../../../../nx2/utils/ims.js');
try {
const imsDetails = await loadIms();

// Only cache authenticated sessions
if (imsDetails && !imsDetails.anonymous) {
cachedImsDetails = imsDetails;
}
return imsDetails;
} catch (_) {
// Fallback: if IMS timeout but token exists in window, use it
if (window.adobeIMS?.getAccessToken) {
const activeToken = window.adobeIMS.getAccessToken();
if (activeToken) {
const accessToken = typeof activeToken === 'string' ? { token: activeToken } : activeToken;
const fallbackDetails = { accessToken, anonymous: false };
cachedImsDetails = fallbackDetails;
return fallbackDetails;
}
}

imsLoadPromise = null;
return null;
}
})();

return imsLoadPromise;
}

/**
* Authenticated fetch with IMS token
* @param {string} url - URL to fetch
* @param {Object} opts - Fetch options
* @returns {Promise<Response>} Fetch response
*/
export const daFetch = async (url, opts = {}) => {
opts.headers ||= {};

// Add auth header if user is authenticated
const hasAuthSession = !!localStorage.getItem('nx-ims');
if (hasAuthSession || cachedImsDetails) {
const imsDetails = await initIms();
if (imsDetails && !imsDetails.anonymous && imsDetails.accessToken) {
opts.headers.Authorization = `Bearer ${imsDetails.accessToken.token}`;
}
}

let response;
try {
response = await fetch(url, opts);
} catch (err) {
response = new Response(null, { status: 500, statusText: err.message });
}

// Smart 401 handling: auto-redirect for anonymous users, show error for authenticated users
if (response.status === 401) {
const returningFromSignIn = new URLSearchParams(window.location.search).get('from_ims') === 'true'
|| window.location.hash.includes('from_ims=true');

if (!returningFromSignIn) {
const userHasAuthSession = !!localStorage.getItem('nx-ims');
const hasActiveToken = !!window.adobeIMS?.getAccessToken?.();

if (!userHasAuthSession && !hasActiveToken && !isRedirectingToSignIn) {
// Anonymous user - auto-redirect to sign-in
isRedirectingToSignIn = true;
const { handleSignIn } = await import('../../../../nx2/utils/ims.js');
handleSignIn();
}
}
}

// Add DA permissions metadata
response.permissions = response.headers.get('x-da-actions')?.split('=').pop().split(',');

return response;
};
22 changes: 11 additions & 11 deletions nx/blocks/media-library/core/indexing-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { updateAppState, getAppState, showNotification } from './state.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import { clearProcessDataCache } from '../ui/filters.js';
import { getDedupeKey } from './urls.js';
import { getCanonicalMediaTimestamp } from './utils.js';
Expand Down Expand Up @@ -82,22 +82,22 @@ function getErrorMessage(errorCode, technicalMessage) {
switch (errorCode) {
case IndexingErrorCode.AUTH_REQUIRED:
case IndexingErrorCode.AUTH_FAILED:
return t('NOTIFY_SIGN_IN');
return getMessage('NOTIFY_SIGN_IN');
case IndexingErrorCode.DA_READ_DENIED:
return t('DA_READ_DENIED');
return getMessage('DA_READ_DENIED');
case IndexingErrorCode.DA_WRITE_DENIED:
case IndexingErrorCode.DA_SAVE_FAILED:
return t('DA_SAVE_FAILED');
return getMessage('DA_SAVE_FAILED');
case IndexingErrorCode.INDEX_PARSE_ERROR:
return t('INDEX_PARSE_ERROR');
return getMessage('INDEX_PARSE_ERROR');
case IndexingErrorCode.LOCK_CREATE_FAILED:
return t('LOCK_CREATE_FAILED_GENERIC');
return getMessage('LOCK_CREATE_FAILED_GENERIC');
case IndexingErrorCode.LOCK_REMOVE_FAILED:
return t('LOCK_REMOVE_FAILED');
return getMessage('LOCK_REMOVE_FAILED');
case IndexingErrorCode.NETWORK_TIMEOUT:
case IndexingErrorCode.BUILD_FAILED:
default:
return technicalMessage || t('NOTIFY_DISCOVERY_FAILED');
return technicalMessage || getMessage('NOTIFY_DISCOVERY_FAILED');
}
}

Expand Down Expand Up @@ -147,7 +147,7 @@ export function handleIndexingEvent(event, onMediaDataUpdated) {
clearProcessDataCache();

if (event.lockRemoveFailed) {
showNotification(t('NOTIFY_WARNING'), t('LOCK_REMOVE_FAILED'), 'danger');
showNotification(getMessage('NOTIFY_WARNING'), getMessage('LOCK_REMOVE_FAILED'), 'danger');
}

if (event.hasChanges && event.data && event.data.length > 0) {
Expand Down Expand Up @@ -214,9 +214,9 @@ export function handleIndexingEvent(event, onMediaDataUpdated) {
// Show notification for non-persistent errors
if (!event.isPersistent && event.code !== IndexingErrorCode.AUTH_REQUIRED) {
const userMessage = getErrorMessage(event.code, event.message);
showNotification(t('NOTIFY_ERROR'), userMessage, 'danger');
showNotification(getMessage('NOTIFY_ERROR'), userMessage, 'danger');
} else if (event.code === IndexingErrorCode.AUTH_REQUIRED) {
showNotification(t('NOTIFY_ERROR'), t('NOTIFY_SIGN_IN'), 'danger');
showNotification(getMessage('NOTIFY_ERROR'), getMessage('NOTIFY_SIGN_IN'), 'danger');
}

resetProgressiveState();
Expand Down
9 changes: 7 additions & 2 deletions nx/blocks/media-library/core/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ const MESSAGES = {
VALIDATION_PATH_EMPTY: 'Parent path not found or empty: {path}',
VALIDATION_SITE_PATH_FILE: 'Site path cannot point to a file',
VALIDATION_ENTER_SITE_URL: 'Enter a site URL to start. Example: https://main--site--org.aem.page',
VALIDATION_SERVER_ERROR: 'Server error occurred. Please try again or sign in if you\'re not authenticated.',
VALIDATION_NETWORK_ERROR: 'Network error. Check your connection and try again.',
DA_WRITE_DENIED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin for write access.",
DA_SAVE_FAILED: "Couldn't save your media results. You can still browse current results, but your changes weren't saved. Try again, refresh the page, or contact your admin.",
PARTIAL_SAVE: "Some save steps didn't complete. Media results may be incomplete or out of date. Try refreshing the page.",
EDS_LOG_DENIED: 'You need Author or higher permissions on EDS to see the latest media data. You can still browse existing media.',
EDS_LOG_DENIED: 'You need one of these roles in the EDS project: author, publish, or admin. Contact your project admin to add you to one of these roles. You can still browse existing media.',
EDS_LOG_DENIED_HINT: 'The author role (or higher: publish, admin) is required for log:read permission. Note: basic_author and basic_publish do NOT have log:read access.',
EDS_AUTH_EXPIRED: 'Session expired. Sign in again.',
PROTECTED_SITE_IMAGES_WARNING: 'Protected site - some images may not load, refresh or login into site in a new tab using sidekick',
DATA_LOAD_FAILED: 'Failed to load media data. Please ensure you are signed in.',

NOTIFY_SIGN_IN: 'Sign in to run discovery.',
NOTIFY_VERIFY_AUTH: 'Failed to verify authentication.',
Expand Down Expand Up @@ -89,7 +94,7 @@ const MESSAGES = {
UI_OPEN_IN_NEW_TAB: 'Open in new tab',
};

export function t(key, params = {}) {
export function getMessage(key, params = {}) {
const str = MESSAGES[key];
if (str == null) return key;

Expand Down
38 changes: 23 additions & 15 deletions nx/blocks/media-library/core/paths.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { daFetch } from '../../../utils/daFetch.js';
import { daFetch } from './ims-adapter.js';
import { DA_ADMIN } from '../../../utils/utils.js';
import { Paths, Domains, DA_LIVE_EDIT_BASE } from './constants.js';
import { ErrorCodes, logMediaLibraryError } from './errors.js';
import { t } from './messages.js';
import { getMessage } from './messages.js';
import {
normalizeSitePath as _normalizeSitePath,
getContentPathFromSitePath as _getContentPathFromSitePath,
Expand Down Expand Up @@ -172,7 +172,7 @@ export function resolveAbsolutePath(path, isFolder = false) {
export async function validateSitePath(sitePath) {
if (!sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {});
return { valid: false, error: t('VALIDATION_ENTER_SITE_URL') };
return { valid: false, error: getMessage('VALIDATION_ENTER_SITE_URL') };
}

const parts = sitePath.split('/').filter(Boolean);
Expand All @@ -181,7 +181,7 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_PATH_MISSING, {});
return {
valid: false,
error: t('VALIDATION_ENTER_SITE_URL'),
error: getMessage('VALIDATION_ENTER_SITE_URL'),
};
}

Expand All @@ -200,7 +200,7 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 });
return {
valid: false,
error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }),
error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }),
};
}

Expand All @@ -209,18 +209,22 @@ export async function validateSitePath(sitePath) {

if (resp.status === 404) {
logMediaLibraryError(ErrorCodes.VALIDATION_SITE_NOT_FOUND, { path: `/${org}/${repo}`, status: 404 });
return { valid: false, error: t('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) };
return { valid: false, error: getMessage('VALIDATION_SITE_NOT_FOUND', { path: `/${org}/${repo}` }) };
}

if (resp.status === 401 || resp.status === 403) {
logMediaLibraryError(ErrorCodes.DA_READ_DENIED, { path: `/${org}/${repo}`, status: resp.status });
return {
valid: false,
error: t('VALIDATION_SITE_403'),
suggestion: t('VALIDATION_SITE_403_SUGGESTION'),
error: getMessage('VALIDATION_SITE_403'),
suggestion: getMessage('VALIDATION_SITE_403_SUGGESTION'),
};
}

if (resp.status >= 500) {
return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') };
}

return { valid: false, error: `Validation failed: ${resp.status}` };
} catch (error) {
return { valid: false, error: error.message };
Expand All @@ -246,7 +250,7 @@ export async function validateSitePath(sitePath) {
});
return {
valid: false,
error: t('VALIDATION_PATH_NOT_FOUND', { path: parentPath }),
error: getMessage('VALIDATION_PATH_NOT_FOUND', { path: parentPath }),
};
}

Expand All @@ -257,11 +261,15 @@ export async function validateSitePath(sitePath) {
);
return {
valid: false,
error: t('VALIDATION_PATH_403'),
suggestion: t('VALIDATION_PATH_403_SUGGESTION'),
error: getMessage('VALIDATION_PATH_403'),
suggestion: getMessage('VALIDATION_PATH_403_SUGGESTION'),
};
}

if (resp.status >= 500) {
return { valid: false, error: getMessage('VALIDATION_SERVER_ERROR') };
}

return { valid: false, error: `Validation failed: ${resp.status}` };
}

Expand All @@ -271,7 +279,7 @@ export async function validateSitePath(sitePath) {
if (!items || items.length === 0) {
return {
valid: false,
error: t('VALIDATION_PATH_EMPTY', { path: parentPath }),
error: getMessage('VALIDATION_PATH_EMPTY', { path: parentPath }),
};
}

Expand All @@ -284,15 +292,15 @@ export async function validateSitePath(sitePath) {
logMediaLibraryError(ErrorCodes.VALIDATION_PATH_NOT_FOUND, { path: `${parentPath}/${lastSegment}`, status: 404 });
return {
valid: false,
error: t('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }),
suggestion: t('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }),
error: getMessage('VALIDATION_PATH_NOT_FOUND_CHILD', { path: `/${[org, repo, ...restPath].join('/')}` }),
suggestion: getMessage('VALIDATION_PATH_NOT_FOUND_SUGGESTION', { segment: lastSegment, parentPath }),
};
}

if (targetEntry.ext) {
return {
valid: false,
error: t('VALIDATION_SITE_PATH_FILE'),
error: getMessage('VALIDATION_SITE_PATH_FILE'),
suggestion: parentPath,
isFile: true,
fileType: targetEntry.ext,
Expand Down
7 changes: 4 additions & 3 deletions nx/blocks/media-library/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export async function checkSiteAuthRequired(org, repo) {

try {
const response = await etcFetch(indexUrl, 'cors', { method: 'HEAD' });
const requiresAuth = response.status === 401 || response.status === 403;
const requiresAuth = response.status === 401
|| response.status === 403;
const result = { requiresAuth, status: response.status };

debugLog('Site auth check result', result);
Expand All @@ -145,8 +146,8 @@ export async function checkSiteAuthRequired(org, repo) {

export async function livePreviewLogin(owner, repo) {
try {
const { loadIms } = await import('../../../../nx2/utils/ims.js');
const { accessToken } = await loadIms() || {};
const { initIms } = await import('./ims-adapter.js');
const { accessToken } = (await initIms()) || {};
const url = `${getLivePreviewUrl(owner, repo)}/gimme_cookie`;

debugLog('Setting preview.da.live cookie', { owner, repo, url });
Expand Down
Loading
Loading