fix: make credential lookup and blocked rotation guidance actionable

This commit is contained in:
2026-09-27 12:45:46 +08:00
parent e3d8b16844
commit 0782b0488b
5 changed files with 49 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
(() => {
const $ = id => document.getElementById(id);
const ui = { after: '', next: '', selected: '', detail: null, loading: false, busy: false, pending: null, rotating: null, result: null, saved: true, mode: 'CREATE' };
const ui = { after: '', next: '', selected: '', detail: null, loading: false, busy: false, pending: null, rotating: null, result: null, saved: true, mode: 'CREATE', helpCredential: '' };
const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const time = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '长期有效';
function message(error) { return error.status === 403 ? '此账号未启用应用接入管理。请在服务端配置 ORBIT_CONSOLE_APPLICATION_ADMIN 为管理员登录名。' : error.message; }
@@ -49,17 +49,30 @@
const rotationHelp = transitioning ? `<aside class="access-rotation-notice" id="${escape(helpID)}"><strong>旧 Token 仍在过渡期</strong><p>${escape(reason)}</p><p>过渡截止:<time datetime="${escape(c.previous_valid_until)}">${escape(time(c.previous_valid_until))}</time>。到期后,换证按钮随页面刷新恢复可用。</p><p>请用已保存的新 Token 更新服务、Worker 和结果消费者配置。确认全部切换、在途任务收尾后,也可提前点击「结束过渡,停用旧 Token」。</p></aside>` : c.revoked_at ? `<p id="${escape(helpID)}">${escape(reason)}</p>` : c.previous_valid_until ? `<p>旧 Token 已失效于:${escape(time(c.previous_valid_until))},可再次换证。</p>` : '';
const groups = [['tasks:submit', '任务'], ['workers:consume', 'Worker 执行'], ['events:consume', '结果订阅'], ['schedules:manage', '定时调度'], ['workflows:start', '流程编排'], ['tasks:review', '人工恢复'], ['application:resources:manage', '应用额度']].filter(([action]) => c.permissions.actions.includes(action)).map(([,name]) => name);
return `<section class="access-credential">
<div class="access-credential-header"><b>${escape(c.name)} · ${status}</b><button class="text-button" data-access-rotate="${escape(c.id)}" ${blocked ? `disabled title="${escape(reason)}" aria-describedby="${escape(helpID)}"` : ''}>${c.revoked_at ? '已撤销 · 不可换证' : transitioning ? '过渡中 · 暂不可换证' : '平滑换证 ↗'}</button></div>
<p>凭据 ID:${escape(c.id)}<br>有效期:${escape(time(c.expires_at))} · 版本 ${escape(c.revision)}<br>Token 原文不保存在数据库;请妥善保存接入配置。</p>
<div class="access-credential-header"><b>${escape(c.name)} · ${status}</b><button class="text-button ${blocked ? 'access-rotation-blocked' : ''}" ${blocked ? `data-access-help="${escape(c.id)}" title="${escape(reason)}" aria-describedby="${escape(helpID)}"` : `data-access-rotate="${escape(c.id)}"`} aria-haspopup="dialog">${c.revoked_at ? '已撤销 · 查看原因 ⓘ' : transitioning ? '暂不可换证 · 查看原因 ⓘ' : '平滑换证 ↗'}</button></div>
<p>凭据 ID:${escape(c.id)}<br>有效期:${escape(time(c.expires_at))} · 凭据版本 ${escape(c.revision)}</p>
<div class="access-token-summary"><div><strong>当前 Token</strong><span>原文仅在生成时显示,无法回看</span></div><button type="button" class="text-button" data-access-help="${escape(c.id)}" aria-haspopup="dialog">Token 在哪里查看?</button></div>
${rotationHelp}
<div class="access-form-actions"><button class="text-button" data-access-expiry="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>调整有效期</button>${c.previous_valid_until && !c.revoked_at ? `<button class="text-button" data-access-finalize="${escape(c.id)}">结束过渡,停用旧 Token</button>` : ''}</div>
<div class="access-permissions">${groups.map(a => `<span>${escape(a)}</span>`).join('')}</div><details class="access-permission-details"><summary>查看具体权限</summary><p>${c.permissions.actions.map(escape).join(' · ')}<br>授权队列:${escape(c.permissions.queues.join(', '))}</p></details>
</section>`;
}).join('') || '<p class="panel-description">该环境暂无凭据。</p>';
if ($('access-help-dialog').open) renderCredentialHelp();
document.querySelectorAll('[data-access-id]').forEach(el => el.classList.toggle('selected', el.dataset.accessId === namespace)); renderConfig();
} catch (error) { $('access-error').textContent = message(error); }
}
function renderConfig() { try { $('access-config').textContent = config(); } catch (error) { $('access-config').textContent = error.message; } }
function renderCredentialHelp() {
const credential = ui.detail?.credentials.find(c => c.id === ui.helpCredential);
if (!credential) { $('access-help-dialog').close(); return; }
const transitioning = !credential.revoked_at && Date.parse(credential.previous_valid_until) > Date.now();
$('access-help-scope').textContent = `${ui.detail.entry.application} · ${ui.detail.entry.environment} / ${ui.detail.entry.namespace} · ${credential.name} · 凭据版本 ${credential.revision}`;
$('access-help-state').textContent = credential.revoked_at ? '凭据已撤销,不能再用于接入或换证。' : transitioning ? `上一轮换证已生效。旧 Token 过渡截止:${time(credential.previous_valid_until)}。期间暂不能再次换证,以免中断还在用旧 Token 的服务。` : '当前没有有效的旧 Token 过渡期,可从凭据卡片发起换证。';
$('access-help-transition').hidden = !transitioning;
}
function showCredentialHelp(credential) {
ui.helpCredential = credential.id; renderCredentialHelp(); $('access-help-dialog').showModal();
}
function open(credential = null, mode = 'ROTATE') {
if (ui.busy) return;
ui.rotating = credential; ui.mode = credential ? mode : 'CREATE'; ui.pending = null; ui.result = null; ui.saved = true;
@@ -77,12 +90,13 @@
SET_EXPIRY: '只调整当前 Token 的有效期,Token 和业务配置保持不变。已有旧 Token 的过渡截止不会延长。',
FINALIZE: '此操作立即停用旧 Token,新 Token 继续有效。请先确认全部服务、Worker 和结果消费者已切换,在途执行已收尾。'
}[ui.mode];
$('access-token-once-note').hidden = !['CREATE', 'ROTATE'].includes(ui.mode);
$('access-submit').textContent = ui.mode === 'CREATE' ? '创建并生成凭据 →' : `确认${titles[ui.mode]} →`;
$('access-dialog').showModal(); (credential ? $('access-reason') : $('access-name')).focus();
}
function close() {
if (ui.busy) return;
if ((ui.pending || !ui.saved) && !window.confirm(ui.pending ? '操作结果尚未确认,请先保存待核对操作。仍要关闭吗?' : '请确认已保存新 Token。关闭后无法再次查看原文。')) return;
if ((ui.pending || !ui.saved) && !window.confirm(ui.pending ? '操作结果尚未确认,请先保存待核对操作。仍要关闭吗?' : '请确认已保存完整 SDK 配置。关闭后无法再次查看 Token;如果还在换证过渡期,也不能立即再次换证。仍要关闭吗?')) return;
$('access-dialog').close(); ui.pending = null; ui.result = null; ui.saved = true; $('access-result-config').value = ''; load();
}
function download(name, content, type = 'text/plain') {
@@ -118,6 +132,7 @@
const preparedConfig = op.token ? config(op.token, queue, op.namespace_id) : '';
ui.result = op; ui.pending = null;
$('access-form').hidden = true; $('access-result').hidden = false;
$('access-token-once-note').hidden = true;
$('access-result-title').textContent = { CREATE: '应用已创建,可以接入', ROTATE: '新 Token 已生效', SET_EXPIRY: '有效期已更新,原 Token 继续使用', FINALIZE: '过渡已结束,旧 Token 已停用' }[op.input.kind];
$('access-result-note').hidden = !op.token; $('access-result-config-field').hidden = !op.token; $('access-result-actions').hidden = !op.token;
$('access-result-config').value = preparedConfig;
@@ -133,11 +148,14 @@
$('access-url').value = location.origin;
$('access-list').addEventListener('click', e => { const el = e.target.closest('[data-access-id]'); if (el) select(el.dataset.accessId); });
$('access-credentials').addEventListener('click', e => {
const el = e.target.closest('[data-access-rotate],[data-access-expiry],[data-access-finalize]'); if (!el || el.disabled) return;
const id = el.dataset.accessRotate || el.dataset.accessExpiry || el.dataset.accessFinalize;
const el = e.target.closest('[data-access-help],[data-access-rotate],[data-access-expiry],[data-access-finalize]'); if (!el || el.disabled) return;
const id = el.dataset.accessHelp || el.dataset.accessRotate || el.dataset.accessExpiry || el.dataset.accessFinalize;
const credential = ui.detail?.credentials.find(c => c.id === id);
if (credential && el.dataset.accessHelp) { showCredentialHelp(credential); return; }
if (credential && !credential.revoked_at) open(credential, el.dataset.accessExpiry ? 'SET_EXPIRY' : el.dataset.accessFinalize ? 'FINALIZE' : 'ROTATE');
});
$('access-help-close').addEventListener('click', () => $('access-help-dialog').close());
$('access-help-done').addEventListener('click', () => $('access-help-dialog').close());
$('access-create').addEventListener('click', () => open()); $('access-close').addEventListener('click', close);
$('access-dialog').addEventListener('cancel', e => { e.preventDefault(); close(); });
$('access-refresh').addEventListener('click', load); $('access-next').addEventListener('click', () => { ui.after = ui.next; load(); }); $('access-first').addEventListener('click', () => { ui.after = ''; load(); });