Files
crank/apps/ui/js/api-keys.js
T
2026-03-30 01:36:42 +03:00

280 lines
9.1 KiB
JavaScript

var KEYS = [];
var currentWorkspaceId = null;
var selectedScopes = new Set(['read']);
var search = '';
var SCOPES = ['read', 'write', 'deploy'];
var SCOPE_DESC = {
read: 'Read operations, logs, and usage',
write: 'Create and update operations',
deploy: 'Publish agents and operations',
};
function mapKeyRecord(record) {
var apiKey = record.api_key || {};
return {
id: apiKey.id,
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) : 'Never',
status: apiKey.status || 'active',
};
}
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) {
KEYS = [];
setTableLoading(false);
renderTable('Workspace or API is unavailable');
return;
}
try {
var response = await window.CrankApi.listPlatformApiKeys(currentWorkspaceId);
KEYS = ((response && response.items) || []).map(mapKeyRecord);
setTableLoading(false);
renderTable();
} catch (error) {
KEYS = [];
setTableLoading(false);
renderTable(error.message || 'Failed to load API keys');
}
}
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 = 'Loading…';
tr.appendChild(td);
tbody.appendChild(tr);
}
async function revokeKey(id) {
if (!confirm('Revoke this key? All requests using it will fail immediately.')) return;
try {
await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id);
await loadKeys();
} catch (error) {
alert(error.message || 'Failed to revoke key');
}
}
async function deleteKey(id) {
if (!confirm('Delete this key? This cannot be undone.')) return;
try {
await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id);
await loadKeys();
} catch (error) {
alert(error.message || 'Failed to delete key');
}
}
async function createKey(name, scopes) {
var created = await window.CrankApi.createPlatformApiKey(currentWorkspaceId, {
name: name,
scopes: scopes,
});
return {
rawKey: created.secret,
record: mapKeyRecord(created.api_key),
};
}
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 = 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 rows = KEYS.filter(function(key) {
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
});
var subtitle = document.querySelector('.section-card-subtitle');
tbody.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 = active + ' active · ' + revoked + ' revoked';
}
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);
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 = KEYS.length ? 'No keys match your search' : 'No API keys yet';
empty.appendChild(td);
tbody.appendChild(empty);
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' ? 'Active' : '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.style.display = 'none';
actionsRevoked.style.display = '';
actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() {
deleteKey(key.id);
});
}
tbody.appendChild(node);
});
}
var modal = document.getElementById('modal-create');
function openModal() {
document.getElementById('modal-form-body').style.display = '';
document.getElementById('modal-reveal-body').style.display = 'none';
document.getElementById('modal-footer-create').style.display = '';
document.getElementById('modal-footer-done').style.display = 'none';
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 name = document.getElementById('new-key-name').value.trim();
if (!name) {
document.getElementById('new-key-name').focus();
return;
}
if (!selectedScopes.size) {
alert('Select at least one scope.');
return;
}
try {
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').style.display = 'none';
document.getElementById('modal-reveal-body').style.display = '';
document.getElementById('modal-footer-create').style.display = 'none';
document.getElementById('modal-footer-done').style.display = '';
renderTable();
} catch (error) {
alert(error.message || 'Failed to create key');
}
});
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.innerHTML = '<svg width="13" height="13"><use href="' + (window.APP_BASE || '') + 'icons/general/check.svg#icon"/></svg>';
});
document.getElementById('key-search').addEventListener('input', function() {
search = this.value;
renderTable();
});
function copyPrefix(prefix) {
if (navigator.clipboard) {
navigator.clipboard.writeText(prefix).catch(function() {});
}
}
document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
loadKeys();
});
});