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
22 changes: 14 additions & 8 deletions tools/site-admin/helpers/api-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,25 +145,31 @@ export const createSecret = async (orgValue, siteName, logFn = null) => {
};

/**
* Create a named secret with a specific value
* Create a named secret with an optional value
*/
export const createNamedSecret = async (
orgValue,
siteName,
secretName,
secretValue,
secretValue = null,
logFn = null,
) => {
const options = { method: 'POST' };
if (secretValue) {
options.headers = { 'content-type': 'application/json' };
options.body = JSON.stringify({ value: secretValue });
}
const resp = await adminFetch(
`${getSitesPath(orgValue)}/${siteName}/secrets/${encodeURIComponent(secretName)}.json`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ value: secretValue }),
},
options,
logFn,
);
return resp.ok;
if (!resp.ok) return null;
try {
return await resp.json();
} catch {
return { id: secretName };
}
};

/**
Expand Down
91 changes: 80 additions & 11 deletions tools/site-admin/helpers/modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,7 @@ export const openAuthModal = async (siteName, orgValue) => {
</div>
`);

const [secrets, access] = await Promise.all([
fetchSecrets(orgValue, siteName),
fetchSiteAccess(orgValue, siteName),
]);
const access = await fetchSiteAccess(orgValue, siteName);

let currentScope = 'none';
if (access.site) currentScope = 'site';
Expand Down Expand Up @@ -315,6 +312,35 @@ export const openAuthModal = async (siteName, orgValue) => {
</div>
`;

const authTokenResult = document.createElement('div');
authTokenResult.className = 'auth-token-result';
authTokenResult.setAttribute('aria-hidden', 'true');
authTokenResult.innerHTML = `
<h4>Site Token Created</h4>
<p class="field-hint">Pass this token as an <code>Authorization</code> header from your CDN to access your protected site:<br/>
<code>authorization: token &lt;value&gt;</code></p>
<p class="field-hint"><a href="https://www.aem.live/docs/authentication-setup-site#make-your-cdn-pass-the-right-authorization-header" target="_blank" rel="noopener noreferrer">Learn more about CDN authorization setup</a></p>
<div class="token-copy-row">
<input type="password" class="auth-token-value" aria-label="Site Token" readonly />
<button type="button" class="button outline copy-token-btn">${icon('copy')} Copy</button>
</div>
<p class="field-hint"><strong>Save this value!</strong> It will not be shown again.</p>
<div class="form-actions">
<button type="button" class="button close-auth-btn">Done</button>
</div>
`;
container.querySelector('.auth-content').appendChild(authTokenResult);

authTokenResult.querySelector('.copy-token-btn').addEventListener('click', (e) => {
navigator.clipboard.writeText(authTokenResult.querySelector('.auth-token-value').value);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unhandled clipboard.writeText Promise

navigator.clipboard.writeText() returns a Promise that can reject (permissions denied, insecure context, or browser not focused). The button immediately shows Copied! regardless of whether the write succeeded. Since the UI explicitly warns 'It will not be shown again', a silent clipboard failure permanently locks the user out of their token.

Add async/await and a catch handler so the user knows if clipboard write fails.

e.currentTarget.innerHTML = `${icon('check')} Copied!`;
});

authTokenResult.querySelector('.close-auth-btn').addEventListener('click', () => {
dialog.close();
refreshSites(orgValue, 'update', siteName);
});

container.querySelector('.auth-loading').setAttribute('aria-hidden', 'true');
container.querySelector('.auth-content').classList.add('visible');

Expand Down Expand Up @@ -355,10 +381,14 @@ export const openAuthModal = async (siteName, orgValue) => {

const newAccess = {};
let tokenId = currentAccess.secretId?.[0] || '';
let newTokenValue = null;

if (scope !== 'none' && !tokenId && (!secrets || secrets.length === 0)) {
if (scope !== 'none' && !tokenId) {
const result = await createSecret(orgValue, siteName);
if (result) tokenId = result.id;
if (result) {
tokenId = result.id;
newTokenValue = result.value;
}
}

if (scope !== 'none') {
Expand All @@ -374,10 +404,17 @@ export const openAuthModal = async (siteName, orgValue) => {
btn.disabled = false;
btn.textContent = success ? 'Saved!' : 'Save';
if (success) {
setTimeout(() => {
dialog.close();
refreshSites(orgValue, 'update', siteName);
}, 1000);
if (newTokenValue) {
authTokenResult.querySelector('.auth-token-value').value = newTokenValue;
authStatusCard.setAttribute('aria-hidden', 'true');
authForm.setAttribute('aria-hidden', 'true');
authTokenResult.removeAttribute('aria-hidden');
} else {
setTimeout(() => {
dialog.close();
refreshSites(orgValue, 'update', siteName);
}, 1000);
}
}
});

Expand Down Expand Up @@ -491,6 +528,11 @@ const openManageItemsModal = async (siteName, orgValue, config) => {
container.querySelector('.new-item-result').classList.add('visible');
btn.setAttribute('aria-hidden', 'true');
loadItems();
} else if (result) {
loadItems();
btn.disabled = false;
btn.textContent = `Create ${itemName}`;
showToast(`${itemName} created successfully`, 'success');
} else {
btn.disabled = false;
btn.textContent = 'Failed - Try Again';
Expand All @@ -511,9 +553,36 @@ export const openSecretModal = (siteName, orgValue) => openManageItemsModal(site
itemNamePlural: 'Secrets',
iconName: 'lock',
fetchFn: fetchSecrets,
createFn: createSecret,
createFn: async (org, site, body) => {
if (body?.name) {
const result = await createNamedSecret(org, site, body.name, body.value || null);
if (result && body.value) {
delete result.value;
}
return result;
}
return createSecret(org, site);
},
deleteFn: deleteSecret,
showExpiration: false,
formHtml: `
<div class="form-field">
<label for="secret-name">Name (optional)</label>
<input type="text" id="secret-name" placeholder="e.g. my-secret-name" />
</div>
<div class="form-field">
<input type="password" id="secret-value" placeholder="e.g. secret from external service" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessibility: Missing label on password input (WCAG 2.1 AA)

The #secret-value password input has no associated label element. The adjacent #secret-name field correctly uses a label, but #secret-value only has a placeholder attribute. Placeholder text is not a valid substitute for a programmatic label - it disappears on input and is not reliably announced by screen readers.

This violates AGENTS.md which requires 'Follow WCAG 2.1 AA guidelines' (WCAG SC 1.3.1 and SC 3.3.2).

Add aria-label or a visible label element to fix this:

<p class="field-hint">See <a href="https://www.aem.live/docs/admin.html#tag/siteConfig/operation/createSiteSecret">here</a> for more details. Remember this value, it won't be shown again.</p>
</div>
Comment thread
usman-khalid marked this conversation as resolved.
Comment on lines +573 to +576
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secret-value password input is missing a <label> element, unlike secret-name directly above it which has one. Placeholder text alone does not satisfy WCAG 2.1 AA Success Criteria 1.3.1 or 3.3.2, violating AGENTS.md:

Ensure accessibility standards (ARIA labels, proper heading hierarchy)

Suggested change
<div class="form-field">
<input type="password" id="secret-value" placeholder="e.g. secret from external service" />
<p class="field-hint">See <a href="https://www.aem.live/docs/admin.html#tag/siteConfig/operation/createSiteSecret">here</a> for more details. Remember this value, it won't be shown again.</p>
</div>
<div class="form-field">
<label for="secret-value">Value (optional)</label>
<input type="password" id="secret-value" placeholder="e.g. secret from external service" />
<p class="field-hint">See <a href="https://www.aem.live/docs/admin.html#tag/siteConfig/operation/createSiteSecret">here</a> for more details. The value will not be shown again.</p>
</div>

`,
getCreateBody: (el) => {
const name = el.querySelector('#secret-name').value.trim();
const value = el.querySelector('#secret-value').value.trim();
const body = {};
if (name) body.name = name;
if (value) body.value = value;
return Object.keys(body).length ? body : undefined;
},
});

const API_KEY_ROLES = [
Expand Down
74 changes: 54 additions & 20 deletions tools/site-admin/site-admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,9 @@
align-items: center;
gap: 4px;
font-size: var(--body-size-xs);
color: var(--green-800);
color: light-dark(var(--green-800), var(--green-500));
padding: 4px 8px;
background: var(--green-100);
background: light-dark(var(--green-100), var(--green-1200));
border-radius: 4px;
font-family: monospace;
text-decoration: none;
Expand All @@ -346,7 +346,7 @@

&:hover {
text-decoration: underline;
background: var(--green-200);
background: light-dark(var(--green-200), var(--green-1100));
}

&.visible {
Expand Down Expand Up @@ -560,10 +560,10 @@
}

&.danger {
color: var(--red-600);
color: light-dark(var(--red-600), var(--red-500));

&:hover:not(:disabled) {
background: var(--red-50);
background: light-dark(var(--red-50), var(--red-1200));
}
}

Expand Down Expand Up @@ -993,8 +993,8 @@
}

.manage-modal .icon-btn:hover {
color: var(--red-600);
background: var(--red-50);
color: light-dark(var(--red-600), var(--red-500));
background: light-dark(var(--red-50), var(--red-1200));
}

.manage-modal .icon-btn svg {
Expand All @@ -1004,8 +1004,8 @@

.manage-modal .expired-label {
font-size: var(--body-size-xs);
color: var(--red-600);
background: var(--red-50);
color: light-dark(var(--red-600), var(--red-500));
background: light-dark(var(--red-50), var(--red-1200));
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
Expand All @@ -1014,15 +1014,15 @@
.manage-modal .new-item-result {
margin-top: var(--spacing-m);
padding: var(--spacing-m);
background: var(--green-100);
border: 1px solid var(--green-300);
background: light-dark(var(--green-100), var(--green-1200));
border: 1px solid light-dark(var(--green-300), var(--green-1100));
border-radius: 8px;
}

.manage-modal .new-item-result label {
display: block;
font-weight: 600;
color: var(--green-800);
color: light-dark(var(--green-800), var(--green-500));
margin-bottom: var(--spacing-xs);
}

Expand Down Expand Up @@ -1077,23 +1077,23 @@
}

.auth-modal .status-icon.status-green {
background: var(--green-100);
color: var(--green-600);
background: light-dark(var(--green-100), var(--green-1200));
color: light-dark(var(--green-600), var(--green-500));
}

.auth-modal .status-icon.status-blue {
background: var(--blue-100);
color: var(--blue-600);
background: light-dark(var(--blue-100), var(--blue-1200));
color: light-dark(var(--blue-600), var(--blue-500));
}

.auth-modal .status-icon.status-orange {
background: var(--orange-100);
color: var(--orange-600);
background: light-dark(var(--orange-100), var(--orange-1200));
color: light-dark(var(--orange-600), var(--orange-500));
}

.auth-modal .status-icon.status-gray {
background: var(--gray-100);
color: var(--gray-600);
background: light-dark(var(--gray-100), var(--gray-700));
color: light-dark(var(--gray-600), var(--gray-400));
}

.auth-modal .status-info {
Expand Down Expand Up @@ -1136,6 +1136,40 @@
border-top: 1px solid var(--color-border);
}

.auth-modal .auth-token-result {
margin-top: var(--spacing-m);
padding: var(--spacing-m);
background: light-dark(var(--green-100), var(--green-1200));
border: 1px solid light-dark(var(--green-300), var(--green-1100));
border-radius: 8px;
}

.auth-modal .auth-token-result h4 {
margin: 0 0 var(--spacing-s);
font-weight: 600;
color: light-dark(var(--green-800), var(--green-500));
text-transform: none;
letter-spacing: normal;
font-size: var(--body-size-m);
}

.auth-modal .auth-token-result .token-copy-row {
margin-top: var(--spacing-s);
}

.auth-modal .auth-token-result .copy-token-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: var(--spacing-xs) var(--spacing-s);
white-space: nowrap;
}

.auth-modal .auth-token-result .copy-token-btn svg {
width: 14px;
height: 14px;
}

.auth-modal .auth-form h4 {
margin: 0 0 var(--spacing-s);
font-size: var(--body-size-s);
Expand Down
Loading