621 lines
19 KiB
JavaScript
621 lines
19 KiB
JavaScript
var KEYS = [];
|
|
var AGENTS = [];
|
|
var capabilities = null;
|
|
var currentWorkspaceId = null;
|
|
var currentAgentId = null;
|
|
var selectedScopes = new Set(['read']);
|
|
var search = '';
|
|
|
|
var SCOPES = ['read', 'write', 'deploy'];
|
|
var SCOPE_DESC = {
|
|
read: 'apikeys.scope.read',
|
|
write: 'apikeys.scope.write',
|
|
deploy: 'apikeys.scope.deploy',
|
|
};
|
|
|
|
function tKey(key) {
|
|
return window.t ? t(key) : key;
|
|
}
|
|
|
|
function tfKey(key, vars) {
|
|
return window.tf ? tf(key, vars) : tKey(key);
|
|
}
|
|
|
|
function capabilityLabel(key) {
|
|
return tKey('settings.capability.' + key);
|
|
}
|
|
|
|
function buildIconSvg(href, width, height) {
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', String(width));
|
|
svg.setAttribute('height', String(height));
|
|
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
use.setAttribute('href', href);
|
|
svg.appendChild(use);
|
|
return svg;
|
|
}
|
|
|
|
function mapKeyRecord(record) {
|
|
var apiKey = record.api_key || {};
|
|
return {
|
|
id: apiKey.id,
|
|
agentId: apiKey.agent_id || null,
|
|
name: apiKey.name,
|
|
prefix: apiKey.prefix || '',
|
|
scopes: apiKey.scopes || [],
|
|
created: apiKey.created_at ? apiKey.created_at.slice(0, 10) : '—',
|
|
lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : tKey('apikeys.last_used.never'),
|
|
status: apiKey.status || 'active',
|
|
};
|
|
}
|
|
|
|
function mapAgentRecord(record) {
|
|
return {
|
|
id: record.id,
|
|
slug: record.slug,
|
|
displayName: record.display_name || record.slug || record.id,
|
|
mcpEndpoint: record.mcp_endpoint || '',
|
|
};
|
|
}
|
|
|
|
function currentWorkspace() {
|
|
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
|
}
|
|
|
|
async function loadKeys() {
|
|
var workspace = currentWorkspace();
|
|
currentWorkspaceId = workspace ? workspace.id : null;
|
|
|
|
setTableLoading(true);
|
|
|
|
if (!currentWorkspaceId || !window.CrankApi) {
|
|
AGENTS = [];
|
|
KEYS = [];
|
|
currentAgentId = null;
|
|
renderAgentPicker();
|
|
setCreateButtonState();
|
|
setTableLoading(false);
|
|
renderTable(tKey('apikeys.error.api'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var response = await window.CrankApi.listAgents(currentWorkspaceId);
|
|
AGENTS = ((response && response.items) || []).map(mapAgentRecord);
|
|
if (!AGENTS.length) {
|
|
currentAgentId = null;
|
|
KEYS = [];
|
|
renderAgentPicker();
|
|
setCreateButtonState();
|
|
setTableLoading(false);
|
|
renderTable();
|
|
return;
|
|
}
|
|
if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
|
|
currentAgentId = AGENTS[0].id;
|
|
}
|
|
renderAgentPicker();
|
|
var keysResponse = await window.CrankApi.listAgentPlatformApiKeys(
|
|
currentWorkspaceId,
|
|
currentAgentId
|
|
);
|
|
KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord);
|
|
setCreateButtonState();
|
|
setTableLoading(false);
|
|
renderTable();
|
|
} catch (error) {
|
|
AGENTS = [];
|
|
KEYS = [];
|
|
currentAgentId = null;
|
|
renderAgentPicker();
|
|
setCreateButtonState();
|
|
setTableLoading(false);
|
|
renderTable(error.message || tKey('apikeys.error.load'));
|
|
}
|
|
}
|
|
|
|
async function loadCapabilities() {
|
|
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
capabilities = await window.CrankApi.getCapabilities();
|
|
} catch (_error) {
|
|
capabilities = null;
|
|
}
|
|
|
|
renderMachineAccessSummary();
|
|
}
|
|
|
|
function renderMachineAccessSummary() {
|
|
var title = document.getElementById('machine-access-title');
|
|
var subtitle = document.getElementById('machine-access-subtitle');
|
|
var summary = document.getElementById('machine-access-summary');
|
|
var note = document.getElementById('machine-access-note');
|
|
if (!title || !subtitle || !summary || !note) {
|
|
return;
|
|
}
|
|
|
|
if (!capabilities) {
|
|
summary.textContent = tKey('apikeys.machine_access.summary_default');
|
|
note.textContent = tKey('apikeys.machine_access.note');
|
|
return;
|
|
}
|
|
|
|
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
|
|
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
|
|
|
|
title.textContent = tKey('apikeys.machine_access.title_' + capabilities.edition);
|
|
subtitle.textContent = tKey('apikeys.machine_access.subtitle_' + capabilities.edition);
|
|
summary.textContent = tfKey('apikeys.machine_access.summary', {
|
|
access_modes: accessModes || capabilityLabel('none'),
|
|
security_levels: securityLevels || capabilityLabel('none'),
|
|
});
|
|
note.textContent = tKey('apikeys.machine_access.note_' + capabilities.edition);
|
|
}
|
|
|
|
function setTableLoading(on) {
|
|
var tbody = document.getElementById('keys-tbody');
|
|
if (!on) return;
|
|
tbody.innerHTML = '';
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.colSpan = 7;
|
|
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
|
td.textContent = tKey('apikeys.loading');
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
async function revokeKey(id) {
|
|
if (!confirm(tKey('apikeys.confirm.revoke'))) return;
|
|
if (!currentAgentId) return;
|
|
|
|
try {
|
|
var key = KEYS.find(function(item) { return item.id === id; });
|
|
await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
|
await loadKeys();
|
|
if (window.CrankUi) {
|
|
window.CrankUi.success(
|
|
tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }),
|
|
tKey('apikeys.toast.revoke_title')
|
|
);
|
|
}
|
|
} catch (error) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(
|
|
error.message || tKey('apikeys.toast.revoke_error_message'),
|
|
tKey('apikeys.toast.revoke_error_title')
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteKey(id) {
|
|
if (!confirm(tKey('apikeys.confirm.delete'))) return;
|
|
if (!currentAgentId) return;
|
|
|
|
try {
|
|
var key = KEYS.find(function(item) { return item.id === id; });
|
|
await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
|
await loadKeys();
|
|
if (window.CrankUi) {
|
|
window.CrankUi.success(
|
|
tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }),
|
|
tKey('apikeys.toast.delete_title')
|
|
);
|
|
}
|
|
} catch (error) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(
|
|
error.message || tKey('apikeys.toast.delete_error_message'),
|
|
tKey('apikeys.toast.delete_error_title')
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createKey(name, scopes) {
|
|
var created = await window.CrankApi.createAgentPlatformApiKey(currentWorkspaceId, currentAgentId, {
|
|
name: name,
|
|
scopes: scopes,
|
|
});
|
|
return {
|
|
rawKey: created.secret,
|
|
record: mapKeyRecord(created.api_key),
|
|
};
|
|
}
|
|
|
|
function currentAgent() {
|
|
return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null;
|
|
}
|
|
|
|
function renderAgentPicker() {
|
|
var select = document.getElementById('agent-select');
|
|
var hint = document.getElementById('agent-endpoint-hint');
|
|
if (!select || !hint) return;
|
|
|
|
select.innerHTML = '';
|
|
|
|
if (!AGENTS.length) {
|
|
var emptyOption = document.createElement('option');
|
|
emptyOption.value = '';
|
|
emptyOption.textContent = tKey('apikeys.agent.empty');
|
|
select.appendChild(emptyOption);
|
|
select.disabled = true;
|
|
hint.textContent = tKey('apikeys.agent.empty_hint');
|
|
return;
|
|
}
|
|
|
|
AGENTS.forEach(function(agent) {
|
|
var option = document.createElement('option');
|
|
option.value = agent.id;
|
|
option.textContent = agent.displayName;
|
|
if (agent.id === currentAgentId) option.selected = true;
|
|
select.appendChild(option);
|
|
});
|
|
select.disabled = false;
|
|
|
|
var agent = currentAgent();
|
|
hint.textContent = agent && agent.mcpEndpoint
|
|
? tfKey('apikeys.agent.endpoint', { endpoint: agent.mcpEndpoint })
|
|
: tKey('apikeys.agent.endpoint_missing');
|
|
}
|
|
|
|
function setCreateButtonState() {
|
|
var button = document.getElementById('btn-create-key');
|
|
if (!button) return;
|
|
button.disabled = !currentAgentId;
|
|
}
|
|
|
|
function renderScopes() {
|
|
var el = document.getElementById('scope-checkboxes');
|
|
var tmpl = document.getElementById('tmpl-scope-checkbox');
|
|
el.innerHTML = '';
|
|
SCOPES.forEach(function(scope) {
|
|
var node = tmpl.content.cloneNode(true);
|
|
var input = node.querySelector('input');
|
|
input.dataset.scope = scope;
|
|
if (selectedScopes.has(scope)) input.checked = true;
|
|
node.querySelector('.scope-checkbox-name').textContent = scope;
|
|
node.querySelector('.scope-checkbox-desc').textContent = tKey(SCOPE_DESC[scope]);
|
|
input.addEventListener('change', function() {
|
|
if (this.checked) selectedScopes.add(this.dataset.scope);
|
|
else selectedScopes.delete(this.dataset.scope);
|
|
});
|
|
el.appendChild(node);
|
|
});
|
|
}
|
|
|
|
function renderTable(errorMessage) {
|
|
var q = search.toLowerCase();
|
|
var tbody = document.getElementById('keys-tbody');
|
|
var cardList = document.getElementById('keys-card-list');
|
|
var rows = KEYS.filter(function(key) {
|
|
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
|
|
});
|
|
var subtitle = document.getElementById('keys-summary-subtitle');
|
|
|
|
tbody.innerHTML = '';
|
|
if (cardList) {
|
|
cardList.innerHTML = '';
|
|
}
|
|
|
|
if (subtitle) {
|
|
var active = KEYS.filter(function(key) { return key.status === 'active'; }).length;
|
|
var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length;
|
|
subtitle.textContent = currentAgentId
|
|
? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked })
|
|
: tKey('apikeys.agent.empty_hint');
|
|
}
|
|
|
|
if (errorMessage) {
|
|
var errorRow = document.createElement('tr');
|
|
var errorCell = document.createElement('td');
|
|
errorCell.colSpan = 7;
|
|
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
|
|
errorCell.textContent = errorMessage;
|
|
errorRow.appendChild(errorCell);
|
|
tbody.appendChild(errorRow);
|
|
renderKeyCards([], errorMessage);
|
|
return;
|
|
}
|
|
|
|
if (!rows.length) {
|
|
var empty = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.colSpan = 7;
|
|
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
|
td.textContent = currentAgentId
|
|
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
|
|
: tKey('apikeys.agent.empty_hint');
|
|
empty.appendChild(td);
|
|
tbody.appendChild(empty);
|
|
renderKeyCards(rows);
|
|
return;
|
|
}
|
|
|
|
var tmpl = document.getElementById('tmpl-key-row');
|
|
rows.forEach(function(key) {
|
|
var node = tmpl.content.cloneNode(true);
|
|
node.querySelector('.col-name').textContent = key.name;
|
|
node.querySelector('.col-mono').textContent = key.prefix;
|
|
node.querySelector('.col-created').textContent = key.created;
|
|
node.querySelector('.col-last-used').textContent = key.lastUsed;
|
|
|
|
var scopesEl = node.querySelector('.col-scopes');
|
|
key.scopes.forEach(function(scope) {
|
|
var span = document.createElement('span');
|
|
span.className = 'badge badge-scope';
|
|
span.textContent = scope;
|
|
scopesEl.appendChild(span);
|
|
});
|
|
|
|
var badge = document.createElement('span');
|
|
badge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
|
|
badge.textContent = key.status === 'active' ? tKey('apikeys.status.active') : tKey('apikeys.status.revoked');
|
|
node.querySelector('.col-status').appendChild(badge);
|
|
|
|
var actionsActive = node.querySelector('.actions-active');
|
|
var actionsRevoked = node.querySelector('.actions-revoked');
|
|
if (key.status === 'active') {
|
|
actionsActive.querySelector('[title=\"Copy key prefix\"]').addEventListener('click', function() {
|
|
copyPrefix(key.prefix);
|
|
});
|
|
actionsActive.querySelector('[title=\"Revoke key\"]').addEventListener('click', function() {
|
|
revokeKey(key.id);
|
|
});
|
|
} else {
|
|
actionsActive.hidden = true;
|
|
actionsRevoked.hidden = false;
|
|
actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() {
|
|
deleteKey(key.id);
|
|
});
|
|
}
|
|
|
|
tbody.appendChild(node);
|
|
});
|
|
|
|
renderKeyCards(rows);
|
|
}
|
|
|
|
function renderKeyCards(rows, errorMessage) {
|
|
var cardList = document.getElementById('keys-card-list');
|
|
if (!cardList) return;
|
|
|
|
cardList.innerHTML = '';
|
|
|
|
if (errorMessage) {
|
|
cardList.appendChild(buildKeyCardMessage(errorMessage, true));
|
|
return;
|
|
}
|
|
|
|
if (!rows.length) {
|
|
cardList.appendChild(
|
|
buildKeyCardMessage(
|
|
currentAgentId
|
|
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
|
|
: tKey('apikeys.agent.empty_hint'),
|
|
false
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
rows.forEach(function(key) {
|
|
var card = document.createElement('div');
|
|
card.className = 'resource-card';
|
|
|
|
var header = document.createElement('div');
|
|
header.className = 'resource-card-header';
|
|
var headerMain = document.createElement('div');
|
|
var title = document.createElement('div');
|
|
title.className = 'resource-card-title';
|
|
title.textContent = key.name;
|
|
var subtitle = document.createElement('div');
|
|
subtitle.className = 'resource-card-subtitle';
|
|
subtitle.textContent = key.prefix;
|
|
headerMain.appendChild(title);
|
|
headerMain.appendChild(subtitle);
|
|
|
|
var actions = document.createElement('div');
|
|
actions.className = 'resource-card-actions';
|
|
|
|
var statusBadge = document.createElement('span');
|
|
statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
|
|
statusBadge.textContent = key.status === 'active'
|
|
? tKey('apikeys.status.active')
|
|
: tKey('apikeys.status.revoked');
|
|
actions.appendChild(statusBadge);
|
|
header.appendChild(headerMain);
|
|
header.appendChild(actions);
|
|
card.appendChild(header);
|
|
|
|
var metaGrid = document.createElement('div');
|
|
metaGrid.className = 'resource-meta-grid';
|
|
metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—'));
|
|
metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created));
|
|
metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed));
|
|
card.appendChild(metaGrid);
|
|
|
|
var actionRow = document.createElement('div');
|
|
actionRow.className = 'resource-card-actions';
|
|
if (key.status === 'active') {
|
|
actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() {
|
|
copyPrefix(key.prefix);
|
|
}));
|
|
actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() {
|
|
revokeKey(key.id);
|
|
}, true));
|
|
} else {
|
|
actionRow.appendChild(buildCardAction('apikeys.action.delete', function() {
|
|
deleteKey(key.id);
|
|
}, true));
|
|
}
|
|
card.appendChild(actionRow);
|
|
|
|
cardList.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function buildKeyCardMessage(text, isError) {
|
|
var card = document.createElement('div');
|
|
card.className = 'resource-card';
|
|
var value = document.createElement('div');
|
|
value.className = 'resource-meta-value';
|
|
value.textContent = text;
|
|
value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)';
|
|
card.appendChild(value);
|
|
return card;
|
|
}
|
|
|
|
function buildMetaItem(labelKey, valueText) {
|
|
var item = document.createElement('div');
|
|
item.className = 'resource-meta-item';
|
|
var label = document.createElement('div');
|
|
label.className = 'resource-meta-label';
|
|
label.textContent = tKey(labelKey);
|
|
var value = document.createElement('div');
|
|
value.className = 'resource-meta-value';
|
|
value.textContent = valueText;
|
|
item.appendChild(label);
|
|
item.appendChild(value);
|
|
return item;
|
|
}
|
|
|
|
function buildCardAction(labelKey, onClick, isDanger) {
|
|
var button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = isDanger ? 'btn-secondary' : 'btn-secondary';
|
|
button.textContent = tKey(labelKey);
|
|
button.addEventListener('click', onClick);
|
|
return button;
|
|
}
|
|
|
|
var modal = document.getElementById('modal-create');
|
|
|
|
function openModal() {
|
|
if (!currentAgentId) return;
|
|
document.getElementById('modal-form-body').hidden = false;
|
|
document.getElementById('modal-reveal-body').hidden = true;
|
|
document.getElementById('modal-footer-create').hidden = false;
|
|
document.getElementById('modal-footer-done').hidden = true;
|
|
document.getElementById('new-key-name').value = '';
|
|
selectedScopes = new Set(['read']);
|
|
renderScopes();
|
|
modal.classList.add('open');
|
|
setTimeout(function() {
|
|
document.getElementById('new-key-name').focus();
|
|
}, 50);
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.classList.remove('open');
|
|
}
|
|
|
|
document.getElementById('btn-create-key').addEventListener('click', openModal);
|
|
document.getElementById('modal-close-btn').addEventListener('click', closeModal);
|
|
document.getElementById('modal-cancel-btn').addEventListener('click', closeModal);
|
|
modal.addEventListener('click', function(event) {
|
|
if (event.target === modal) closeModal();
|
|
});
|
|
|
|
document.getElementById('modal-confirm-btn').addEventListener('click', async function() {
|
|
var button = this;
|
|
var name = document.getElementById('new-key-name').value.trim();
|
|
if (!name) {
|
|
document.getElementById('new-key-name').focus();
|
|
return;
|
|
}
|
|
if (!selectedScopes.size) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.info(
|
|
tKey('apikeys.toast.scope_missing_message'),
|
|
tKey('apikeys.toast.scope_missing_title')
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
button.disabled = true;
|
|
button.textContent = tKey('apikeys.creating');
|
|
var created = await createKey(name, Array.from(selectedScopes));
|
|
KEYS.unshift(created.record);
|
|
document.getElementById('reveal-key-value').textContent = created.rawKey;
|
|
document.getElementById('modal-form-body').hidden = true;
|
|
document.getElementById('modal-reveal-body').hidden = false;
|
|
document.getElementById('modal-footer-create').hidden = true;
|
|
document.getElementById('modal-footer-done').hidden = false;
|
|
renderTable();
|
|
if (window.CrankUi) {
|
|
window.CrankUi.success(
|
|
tKey('apikeys.toast.create_message'),
|
|
tKey('apikeys.toast.create_title')
|
|
);
|
|
}
|
|
} catch (error) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(
|
|
error.message || tKey('apikeys.toast.create_error_message'),
|
|
tKey('apikeys.toast.create_error_title')
|
|
);
|
|
}
|
|
} finally {
|
|
button.disabled = false;
|
|
button.textContent = tKey('apikeys.modal.create');
|
|
}
|
|
});
|
|
|
|
document.getElementById('modal-done-btn').addEventListener('click', closeModal);
|
|
|
|
document.getElementById('copy-key-btn').addEventListener('click', function() {
|
|
var value = document.getElementById('reveal-key-value').textContent;
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(value).catch(function() {});
|
|
}
|
|
this.replaceChildren(
|
|
buildIconSvg(
|
|
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
|
|
13,
|
|
13
|
|
)
|
|
);
|
|
if (window.CrankUi) {
|
|
window.CrankUi.info(
|
|
tKey('apikeys.toast.copy_message'),
|
|
tKey('apikeys.toast.copy_title')
|
|
);
|
|
}
|
|
});
|
|
|
|
document.getElementById('key-search').addEventListener('input', function() {
|
|
search = this.value;
|
|
renderTable();
|
|
});
|
|
|
|
document.getElementById('agent-select').addEventListener('change', async function() {
|
|
currentAgentId = this.value || null;
|
|
await loadKeys();
|
|
});
|
|
|
|
function copyPrefix(prefix) {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(prefix).catch(function() {});
|
|
}
|
|
if (window.CrankUi) {
|
|
window.CrankUi.info(prefix, tKey('apikeys.toast.prefix_title'));
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
|
await loadCapabilities();
|
|
await loadKeys();
|
|
window.addEventListener('crank:workspacechange', function() {
|
|
loadCapabilities();
|
|
loadKeys();
|
|
});
|
|
});
|