-
Notifications
You must be signed in to change notification settings - Fork 5
chore: surface token when applying auth, allow named secret creation #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'; | ||||||||||||||||||||
|
|
@@ -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 <value></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); | ||||||||||||||||||||
| 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'); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -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') { | ||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -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'; | ||||||||||||||||||||
|
|
@@ -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" /> | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||||||||||||||||||||
|
usman-khalid marked this conversation as resolved.
Comment on lines
+573
to
+576
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||
| `, | ||||||||||||||||||||
| 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 = [ | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
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.