289 lines
30 KiB
JavaScript
289 lines
30 KiB
JavaScript
(() => {
|
||
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', helpCredential: '' };
|
||
const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
const time = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '长期有效';
|
||
let tokenRequest = 0, tokenTarget = null, selectionSequence = 0;
|
||
const resourceMessages = {
|
||
'resource is retired; history remains available':'资源已退役,历史仍可查询。',
|
||
'queue has unfinished tasks, including tasks needing review':'队列还有未完成或待核查任务,请先结清。',
|
||
'queue has unfinished or uncertain tasks':'队列还有未完成或结果不确定的任务,请先核查。',
|
||
'queue has active execution attempts':'队列仍有在途执行,请等待结束。',
|
||
'queue has unreleased execution permits':'队列仍有未释放的执行许可,请先核查在途工作。',
|
||
'queue has enabled schedules; pause them first':'队列还有启用中的定时计划,请先暂停计划。',
|
||
'queue is referenced by unfinished workflows':'队列仍被未结束的流程引用,请先结清流程。',
|
||
'queue has subscriptions; retire them first':'队列还有未退役订阅,请先结清并退役订阅。',
|
||
'subscription has unacknowledged deliveries; drain or resolve expired deliveries first':'订阅还有未确认结果,请先消费并确认;过期交付需要核查后重试。',
|
||
'subscription has unacknowledged deliveries':'订阅还有未确认结果,请先结清。',
|
||
'subscription has unconsumed results; resume consumption and acknowledge them before retirement':'还有尚未投递的结果,请恢复消费并全部确认后再退役。'
|
||
};
|
||
window.orbitResourceMessage = text => resourceMessages[text] || text;
|
||
function message(error) { return error.status === 403 ? '此账号未启用应用接入管理。请在服务端配置 ORBIT_CONSOLE_APPLICATION_ADMIN 为管理员登录名。' : window.orbitResourceMessage(error.message); }
|
||
async function api(path = '', body) {
|
||
const response = await fetch(`/console/applications${path}`, { method: body ? 'POST' : 'GET', credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json', 'X-Orbit-Console': '1' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
||
const value = await response.json().catch(() => ({}));
|
||
if (response.status === 401) location.replace(`/console/login?return=${encodeURIComponent('/console/#/applications')}`);
|
||
if (!response.ok) { const error = new Error(value.error?.message || `请求失败 (${response.status})`); error.status = response.status; throw error; }
|
||
return value.data;
|
||
}
|
||
const queueMemory = new Map();
|
||
function filterApplications() { const term=$('access-search').value.trim().toLocaleLowerCase(); const items=[...document.querySelectorAll('#access-list [data-access-id]')]; items.forEach(item=>{item.hidden=!!term&&!item.textContent.toLocaleLowerCase().includes(term);}); $('access-search-empty').hidden=!term||items.some(item=>!item.hidden); }
|
||
$('access-search').addEventListener('input',filterApplications);
|
||
function rememberedQueue(namespace) { try { return localStorage.getItem(`orbit.access.queue.${namespace}`) || queueMemory.get(namespace) || ''; } catch { return queueMemory.get(namespace) || ''; } }
|
||
function rememberQueue() { const value = $('access-queue').value; queueMemory.set(ui.selected, value); try { localStorage.setItem(`orbit.access.queue.${ui.selected}`, value); } catch {} renderConfig(); }
|
||
function config(token = '••••••••', queue = $('access-queue').value, namespace = ui.selected) {
|
||
if (!queue) throw new Error('请先选择接入队列');
|
||
const url = $('access-url').value.trim() || location.origin;
|
||
let parsed; try { parsed = new URL(url); } catch { throw new Error('请输入完整的 HTTP / HTTPS 服务地址'); }
|
||
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash || /[\s'"`$\\;&|<>(){}]/.test(url)) throw new Error('服务地址必须是不含账号、查询参数或特殊字符的 HTTP / HTTPS 地址');
|
||
return `ORBIT_URL=${url.replace(/\/$/, '')}\nORBIT_APPLICATION_ID=${ui.detail?.entry.application_id || ''}\nORBIT_NAMESPACE_ID=${namespace}\nORBIT_QUEUE_ID=${queue}\nORBIT_TOKEN=${token}\n`;
|
||
}
|
||
let lastScrollAt = -Infinity, detailSignature = '', previousListHTML = '';
|
||
document.addEventListener('scroll', () => { lastScrollAt = performance.now(); }, true);
|
||
function editingOrScrolling() {
|
||
return performance.now() - lastScrollAt < 1500 || document.activeElement?.matches('input,select,textarea') ||
|
||
!!document.querySelector('dialog[open], #access-detail details[open], #access-retire-form:not([hidden])');
|
||
}
|
||
async function load({ background = false } = {}) {
|
||
if (ui.loading || ui.busy) return;
|
||
if (background && editingOrScrolling()) return;
|
||
ui.loading = true; $('access-error').textContent = '';
|
||
try {
|
||
const page = await api(ui.after ? `?after=${encodeURIComponent(ui.after)}` : '');
|
||
if (background && editingOrScrolling()) return;
|
||
ui.next = page.next || ''; $('access-next').disabled = !ui.next;
|
||
const listHTML = page.items.length ? page.items.map(entry => `<button class="access-item ${entry.namespace_id === ui.selected ? 'selected' : ''}" data-access-id="${escape(entry.namespace_id)}"><span class="access-avatar">${escape(entry.application.slice(0, 1).toUpperCase())}</span><span><strong>${escape(entry.application)}</strong><small>${escape(entry.environment)} / ${escape(entry.namespace)} · ${entry.enabled ? '已启用' : '已停用'}</small></span></button>`).join('') : '<p class="panel-description">还没有接入应用,点击右上角新建。</p>';
|
||
if (previousListHTML !== listHTML) { $('access-list').innerHTML = listHTML; previousListHTML = listHTML; }
|
||
filterApplications();
|
||
if (!ui.selected && page.items.length) await select(page.items[0].namespace_id);
|
||
else if (ui.selected && !$('access-dialog').open && !$('access-token-dialog').open) await select(ui.selected, { background });
|
||
} catch (error) { $('access-error').textContent = message(error); }
|
||
finally { ui.loading = false; }
|
||
}
|
||
async function select(namespace, { background = false } = {}) {
|
||
if (namespace !== ui.selected) {
|
||
detailSignature = '';
|
||
retirement = null; $('access-retire-form').hidden = true;
|
||
$('access-detail').hidden = true; $('access-empty').hidden = false;
|
||
$('access-empty').textContent = '正在读取应用配置…';
|
||
}
|
||
const requested = namespace, ticket = ++selectionSequence; ui.selected = namespace;
|
||
try {
|
||
const detail = await api(`/${encodeURIComponent(namespace)}`); if (ui.selected !== requested || ticket !== selectionSequence) return;
|
||
if (background && editingOrScrolling()) return;
|
||
const now = Date.now();
|
||
// 相同快照不重建详情,保留滚动锚点、焦点和折叠状态;到期状态仍按当前时间更新。
|
||
const signature = JSON.stringify([detail, detail.credentials.map(c => [!!c.expires_at && Date.parse(c.expires_at) <= now, Date.parse(c.previous_valid_until) > now])]);
|
||
ui.detail = detail;
|
||
if (signature === detailSignature && !$('access-detail').hidden) return;
|
||
$('access-empty').hidden = true; $('access-detail').hidden = false;
|
||
$('access-title').textContent = detail.entry.application; $('access-status').textContent = detail.entry.enabled ? '已启用' : '已停用';
|
||
$('access-app-id').textContent = detail.entry.application_id; $('access-ns-id').textContent = namespace; $('access-scope').textContent = `${detail.entry.environment} / ${detail.entry.namespace}`;
|
||
const previous = rememberedQueue(namespace);
|
||
const usable = detail.queues.filter(q => !q.retired_at);
|
||
$('access-queue').innerHTML = '<option value="">请选择接入队列</option>' + detail.queues.map(q => `<option value="${escape(q.id)}" ${q.retired_at ? 'disabled' : ''}>${escape(q.name)} · ${q.retired_at ? '已退役' : q.paused ? '已暂停' : '可用'} · ${escape(q.id.slice(0, 8))}</option>`).join('');
|
||
$('access-queue').value = usable.some(q => q.id === previous) ? previous : (!detail.truncated && usable.length === 1 ? usable[0].id : '');
|
||
$('access-capacity').textContent = `订阅名额 ${detail.active_subscriptions ?? '—'} / ${detail.subscription_limit ?? '—'} · 暂停仍占名额,结清后退役释放名额。`;
|
||
const availableCredential = detail.credentials.find(c => !c.revoked_at);
|
||
$('access-copy-template').textContent = availableCredential?.token_stored ? '查看完整接入配置 ↗' : '补存当前 Token ↗';
|
||
$('access-config-note').textContent = availableCredential?.token_stored ? '选择正确队列后,可查看、复制含当前 Token 的完整配置。' : '当前 Token 尚未补存。请从已保存的新配置导入当前 Token,再查看或复制;过渡期旧 Token 不能用于补存。';
|
||
$('access-truncated').hidden = !detail.truncated;
|
||
$('access-credentials').innerHTML = detail.credentials.map(c => {
|
||
const status = c.revoked_at ? '已撤销' : c.expires_at && Date.parse(c.expires_at) <= now ? '已到期' : '有效';
|
||
const transitioning = !c.revoked_at && Date.parse(c.previous_valid_until) > now;
|
||
const blocked = !!c.revoked_at || transitioning;
|
||
const helpID = `access-rotation-help-${c.id}`;
|
||
const reason = c.revoked_at ? '凭据已撤销,不能再次换证。' : '旧 Token 仍在过渡期,暂不能再次换证,以免中断尚未切换的服务。';
|
||
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>${c.token_stored ? '请在上方复制当前接入配置,更新服务、Worker 和结果消费者。' : '当前新 Token 尚未补存,请先从已保存的新配置导入;过渡期旧 Token 不能替代当前新 Token。'}确认全部切换、在途任务收尾后,也可提前点击「结束过渡,停用旧 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 ${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>${c.revoked_at ? '凭据已撤销' : c.token_stored ? '•••••••••••••••• · 已加密保管' : '历史凭据 · 导入当前 Token 后可随时查看'}</span></div></div>
|
||
<div class="access-form-actions"><button type="button" class="primary-button compact" data-access-token="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>${c.token_stored ? '查看接入信息' : '导入当前 Token'}</button><button type="button" class="secondary-button compact" data-access-copy-config="${escape(c.id)}" ${c.revoked_at || !c.token_stored ? 'disabled' : ''}>复制接入配置 ↗</button></div>
|
||
${rotationHelp}
|
||
<details class="credential-controls"><summary>凭据管理与权限</summary><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></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();
|
||
detailSignature = signature;
|
||
} catch (error) {
|
||
if (ticket !== selectionSequence) return;
|
||
$('access-error').textContent = message(error);
|
||
$('access-detail').hidden = true; $('access-empty').hidden = false;
|
||
$('access-empty').textContent = '应用配置读取失败,请刷新重试。';
|
||
}
|
||
}
|
||
function renderConfig() { const q = ui.detail?.queues.find(q => q.id === $('access-queue').value); $('access-queue-note').textContent = !q ? '请明确选择接入队列,避免复制到错误的目标。' : `当前目标:${q.name} · ${q.paused ? '已暂停,任务暂不执行' : '可用'}。选择只影响复制配置,不修改服务端路由。`; $('access-retire-queue').disabled = !q || !!q.retired_at; try { $('access-config').textContent = config(); } catch (error) { $('access-config').textContent = error.message; } }
|
||
function closeToken() {
|
||
tokenRequest++; tokenTarget = null;
|
||
$('access-token-import-submit').disabled = false;
|
||
$('access-token-config').value = ''; $('access-token-input').value = '';
|
||
$('access-token-dialog').close();
|
||
}
|
||
async function showToken(credential, copyImmediately = false) {
|
||
closeToken();
|
||
tokenTarget = { namespace: ui.selected, credential, queue: $('access-queue').value };
|
||
// 只导出该凭据已授权的队列,不能拼出无法使用的配置。
|
||
if (!tokenTarget.queue) { $('access-error').textContent = '请先选择要接入的队列。'; return; }
|
||
if (!credential.permissions.queues.includes('*') && !credential.permissions.queues.includes(tokenTarget.queue)) { $('access-error').textContent = '当前凭据没有所选队列权限,请选择已授权队列。'; return; }
|
||
$('access-token-scope').textContent = `${ui.detail.entry.application} · ${ui.detail.entry.environment} / ${ui.detail.entry.namespace} · 凭据版本 ${credential.revision}`;
|
||
$('access-token-error').textContent = '';
|
||
$('access-token-import').hidden = credential.token_stored;
|
||
$('access-token-output').hidden = true;
|
||
$('access-token-dialog').showModal();
|
||
if (!ui.detail.token_vault_enabled) { $('access-token-error').textContent = '服务端尚未配置 Token 保管密钥,请联系部署管理员。'; return; }
|
||
if (credential.token_stored) await readToken(false, copyImmediately);
|
||
else $('access-token-input').focus();
|
||
}
|
||
async function readToken(importCurrent, copyImmediately = false) {
|
||
const target = tokenTarget; if (!target) return;
|
||
const request = ++tokenRequest;
|
||
$('access-token-error').textContent = '正在读取接入配置…'; $('access-token-import-submit').disabled = true;
|
||
try {
|
||
if (!target.queue) throw new Error('没有可用的授权队列,请先配置队列权限。');
|
||
const value = await api(`/${encodeURIComponent(target.namespace)}/credentials/${encodeURIComponent(target.credential.id)}/token/${importCurrent ? 'import' : 'reveal'}`, { expected_revision: String(target.credential.revision), ...(importCurrent ? { token: $('access-token-input').value.trim() } : {}) });
|
||
if (request !== tokenRequest || ui.selected !== target.namespace || !$('access-token-dialog').open) return;
|
||
if (value.namespace_id !== target.namespace || value.credential_id !== target.credential.id || String(value.revision) !== String(target.credential.revision)) throw new Error('凭据已更新,请刷新后重试。');
|
||
$('access-token-config').value = config(value.token, target.queue, target.namespace);
|
||
$('access-token-import').hidden = true; $('access-token-input').value = '';
|
||
$('access-token-output').hidden = false; $('access-token-error').textContent = '';
|
||
if (importCurrent) target.credential.token_stored = true;
|
||
if (copyImmediately) await copy($('access-token-config').value, $('access-token-copy'));
|
||
} catch (error) { if (request === tokenRequest) $('access-token-error').textContent = message(error); }
|
||
finally { if (request === tokenRequest) $('access-token-import-submit').disabled = false; }
|
||
}
|
||
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;
|
||
$('access-form').reset(); $('access-form').querySelectorAll('input,select,button').forEach(el => { el.disabled = false; }); $('access-form').hidden = false; $('access-result').hidden = true; $('access-backup').hidden = true; $('access-form-error').textContent = '';
|
||
$('access-result-config').value = ''; $('access-clipboard-status').textContent = '';
|
||
$('access-create-fields').hidden = !!credential;
|
||
$('access-create-fields').querySelectorAll('input,select').forEach(el => { el.disabled = !!credential; });
|
||
const titles = { CREATE: '新建应用', ROTATE: '平滑换证', SET_EXPIRY: '调整有效期', FINALIZE: '结束过渡' };
|
||
$('access-form-title').textContent = titles[ui.mode];
|
||
$('access-valid-field').hidden = ui.mode === 'FINALIZE';
|
||
$('access-grace-field').hidden = ui.mode !== 'ROTATE';
|
||
$('access-form-note').textContent = {
|
||
CREATE: '为应用创建独立的环境、队列与调用凭据,默认长期有效。',
|
||
ROTATE: '生成新 Token 后,旧 Token 在过渡期内继续可用。逐台更新服务配置并正常收尾在途任务;确认全部切换后,再结束过渡。',
|
||
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 && !window.confirm('操作结果尚未确认,请先保存待核对操作。仍要关闭吗?')) 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') {
|
||
const url = URL.createObjectURL(new Blob([content], { type })); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||
}
|
||
async function copy(value, button) {
|
||
try { await navigator.clipboard.writeText(value); const old = button.textContent; button.textContent = '已复制 ✓'; setTimeout(() => { button.textContent = old; }, 2000); return true; }
|
||
catch { ($('access-token-dialog').open ? $('access-token-error') : $('access-dialog').open ? $('access-clipboard-status') : $('access-error')).textContent = '浏览器未允许复制,请选中配置后手动复制,或下载 .env 文件。'; return false; }
|
||
}
|
||
async function submit(event) {
|
||
event.preventDefault(); if (ui.busy) return; ui.busy = true; $('access-form-error').textContent = '';
|
||
const form = $('access-form'); const disabled = [...form.elements].map(el => [el, el.disabled]);
|
||
try {
|
||
if (!ui.pending) {
|
||
const input = { kind: ui.mode, valid_days: ui.mode === 'FINALIZE' ? 0 : Number($('access-valid-days').value), reason: $('access-reason').value.trim(), expected_revision: ui.rotating ? String(ui.rotating.revision) : '0' };
|
||
if (ui.mode === 'ROTATE') input.grace_days = Number($('access-grace-days').value);
|
||
if (ui.rotating) Object.assign(input, { namespace_id: ui.selected, credential_id: ui.rotating.id });
|
||
else Object.assign(input, { application: $('access-name').value.trim(), namespace: $('access-namespace').value.trim(), environment: $('access-environment').value, queue: $('access-queue-name').value.trim(), capabilities: [...document.querySelectorAll('.access-capabilities input:checked')].map(el => el.value) });
|
||
form.querySelectorAll('input,select,button').forEach(el => { el.disabled = true; });
|
||
ui.pending = await api('/prepare', input);
|
||
}
|
||
form.querySelectorAll('input,select,button').forEach(el => { el.disabled = true; });
|
||
const op = ui.pending;
|
||
await api('/apply', op);
|
||
ui.saved = true;
|
||
let queue = op.queue_id || $('access-queue').value;
|
||
// 先确认当前凭据版本,旧操作回执不能冒充仍有效的最新 Token。
|
||
await select(op.namespace_id);
|
||
const credential = ui.detail?.entry.namespace_id === op.namespace_id && ui.detail.credentials.find(c => c.id === op.credential_id);
|
||
const expected = op.input.kind === 'CREATE' ? '1' : String(BigInt(op.input.expected_revision) + 1n);
|
||
if (!credential || String(credential.revision) !== expected || credential.revoked_at) throw new Error('操作已提交,但凭据已被更新或暂时无法核对,请刷新应用详情;不要使用旧操作中的 Token。');
|
||
if (!credential.permissions.queues.includes('*') && !credential.permissions.queues.includes(queue)) queue = ui.detail.queues.find(q => credential.permissions.queues.includes(q.id))?.id || '';
|
||
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;
|
||
$('access-result-expiry').textContent = `当前 Token:${time(credential.expires_at)}。${credential.previous_valid_until ? `旧 Token 截止:${time(credential.previous_valid_until)},请在此之前完成切换。` : '无旧 Token 过渡。'}`;
|
||
} catch (error) {
|
||
$('access-form-error').textContent = `${message(error)}${ui.pending ? '。保留当前窗口,点击“重试确认”会使用原操作核对结果。' : ''}`;
|
||
} finally {
|
||
ui.busy = false; disabled.forEach(([el, value]) => { el.disabled = value; });
|
||
$('access-backup').hidden = !ui.pending;
|
||
if (ui.pending) { form.querySelectorAll('input,select').forEach(el => { el.disabled = true; }); $('access-submit').textContent = '重试确认 →'; }
|
||
}
|
||
}
|
||
$('access-url').value = location.origin;
|
||
$('access-list').addEventListener('click', e => { const el = e.target.closest('[data-access-id]'); if (el) { closeToken(); select(el.dataset.accessId); } });
|
||
$('access-credentials').addEventListener('click', e => {
|
||
const el = e.target.closest('[data-access-help],[data-access-rotate],[data-access-expiry],[data-access-finalize],[data-access-token],[data-access-copy-config]'); if (!el || el.disabled) return;
|
||
const id = el.dataset.accessHelp || el.dataset.accessRotate || el.dataset.accessExpiry || el.dataset.accessFinalize || el.dataset.accessToken || el.dataset.accessCopyConfig;
|
||
const credential = ui.detail?.credentials.find(c => c.id === id);
|
||
if (credential && (el.dataset.accessToken || el.dataset.accessCopyConfig)) { showToken(credential, !!el.dataset.accessCopyConfig); return; }
|
||
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-token-close').addEventListener('click', () => { closeToken(); load(); });
|
||
$('access-token-dialog').addEventListener('cancel', e => { e.preventDefault(); closeToken(); load(); });
|
||
$('access-token-import').addEventListener('submit', e => { e.preventDefault(); readToken(true); });
|
||
$('access-token-copy').addEventListener('click', () => copy($('access-token-config').value, $('access-token-copy')));
|
||
$('access-token-download').addEventListener('click', () => download('orbit.env', $('access-token-config').value));
|
||
window.addEventListener('hashchange', closeToken);
|
||
window.addEventListener('pagehide', closeToken);
|
||
$('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(); });
|
||
$('access-url').addEventListener('input', renderConfig); $('access-queue').addEventListener('change', rememberQueue);
|
||
$('access-copy-template').addEventListener('click', () => { const credential = ui.detail?.credentials.find(c => !c.revoked_at); if (credential) showToken(credential); });
|
||
$('access-form').addEventListener('submit', submit);
|
||
$('access-copy').addEventListener('click', async () => { if (await copy($('access-result-config').value, $('access-copy'))) ui.saved = true; });
|
||
$('access-download').addEventListener('click', () => { download('orbit.env', $('access-result-config').value); ui.saved = true; });
|
||
$('access-backup').addEventListener('click', () => { if (ui.pending) download(`orbit-operation-${ui.pending.operation_id}.json`, JSON.stringify(ui.pending, null, 2), 'application/json'); });
|
||
window.addEventListener('beforeunload', e => { if (ui.pending || !ui.saved) { e.preventDefault(); e.returnValue = ''; } });
|
||
let retirement = null;
|
||
async function queueAPI(path, body, key, namespace = ui.selected) {
|
||
const response = await fetch(path, {method: body ? 'POST' : 'GET', credentials:'same-origin', cache:'no-store', headers:{'Content-Type':'application/json', 'X-Orbit-Console':'1','X-Orbit-Namespace':namespace,...(key ? {'Idempotency-Key':key} : {})}, ...(body ? {body:JSON.stringify(body)} : {})});
|
||
const value = await response.json(); if (!response.ok) throw new Error(value.error?.message || '操作失败'); return value.data;
|
||
}
|
||
$('access-retire-queue').addEventListener('click', async () => {
|
||
const id = $('access-queue').value, namespace = ui.selected; if (!id) return;
|
||
$('access-retire-form').hidden=false; $('access-retire-error').textContent='正在核对队列…'; retirement=null;
|
||
try { const q=await queueAPI(`/v1/queues/${id}`,null,null,namespace); if (namespace!==ui.selected || id!==$('access-queue').value) return; retirement={id,namespace,revision:q.revision,key:crypto.randomUUID(),body:null}; $('access-retire-reason').value=''; $('access-retire-error').textContent=''; } catch(error) { $('access-retire-error').textContent=message(error); }
|
||
});
|
||
$('access-retire-cancel').addEventListener('click',()=>{retirement=null;$('access-retire-form').hidden=true;});
|
||
$('access-retire-form').addEventListener('submit', async event=>{
|
||
event.preventDefault(); const target=retirement; if (!target || target.namespace!==ui.selected || target.id!==$('access-queue').value) { $('access-retire-error').textContent='队列已变化,请重新打开退役操作。'; return; }
|
||
target.body ||= {expected_revision:String(target.revision),reason:$('access-retire-reason').value.trim()};
|
||
$('access-retire-submit').disabled=true;
|
||
try { await queueAPI(`/v1/queues/${target.id}:retire`,target.body,target.key,target.namespace); $('access-retire-form').hidden=true; retirement=null; await load(); } catch(error) { $('access-retire-error').textContent=message(error); } finally { $('access-retire-submit').disabled=false; }
|
||
});
|
||
window.orbitApplications = { load };
|
||
})();
|