Files
crank/apps/ui/js/api-keys.js
T
2026-03-30 00:15:02 +03:00

242 lines
9.5 KiB
JavaScript

/* ═══════════════════════════════════════════════════
Crank — API Keys page
Data: GET /api/workspaces/{slug}/keys
═══════════════════════════════════════════════════ */
var KEYS = [];
var currentWorkspaceSlug = '';
var SCOPES = ['read', 'write', 'deploy'];
var SCOPE_DESC = {
read: 'Read operations, logs, and usage',
write: 'Create and update operations',
deploy: 'Publish and rollback deployments',
};
var selectedScopes = new Set(['read']);
var search = '';
// ── API helpers ──────────────────────────────────────────────────────────────
function apiBase() {
return '/api/workspaces/' + currentWorkspaceSlug + '/keys';
}
// In production: replace fetch(mockUrl()) with fetch(apiBase())
function mockUrl() {
return (window.DATA_URL || 'data/') + 'keys.json';
}
// ── Load keys for current workspace ─────────────────────────────────────────
function loadKeys() {
currentWorkspaceSlug = getCurrentWs().slug;
setTableLoading(true);
// Production: fetch(apiBase())
fetch(mockUrl())
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
// Mock JSON is keyed by workspace slug; real API returns array directly
KEYS = Array.isArray(data) ? data : (data[currentWorkspaceSlug] || []);
setTableLoading(false);
renderTable();
})
.catch(function() {
KEYS = [];
setTableLoading(false);
renderTable();
});
}
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);
}
// ── Mutations (in production: real fetch calls) ──────────────────────────────
function revokeKey(id) {
if (!confirm('Revoke this key? All requests using it will fail immediately.')) return;
// Production: fetch(apiBase() + '/' + id, { method: 'PATCH', body: JSON.stringify({ status: 'revoked' }) })
var k = KEYS.find(function(k) { return k.id === id; });
if (k) { k.status = 'revoked'; renderTable(); }
}
function deleteKey(id) {
// Production: fetch(apiBase() + '/' + id, { method: 'DELETE' })
KEYS.splice(KEYS.findIndex(function(k) { return k.id === id; }), 1);
renderTable();
}
function createKey(name, scopes) {
// Production: fetch(apiBase(), { method: 'POST', body: JSON.stringify({ name, scopes }) })
// → response: { key: 'mcp_...', prefix: 'mcp_...', id: '...' }
var rand = Array.from({length: 32}, function() { return Math.random().toString(36)[2]; }).join('');
var rawKey = 'mcp_' + name.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4) + '_' + rand;
var prefix = rawKey.slice(0, 12) + '…';
var newKey = {
id: 'key_' + Date.now(),
name: name,
prefix: prefix,
scopes: scopes,
created: new Date().toISOString().slice(0, 10),
lastUsed: 'Never',
status: 'active',
};
KEYS.unshift(newKey);
return rawKey;
}
// ── Render ───────────────────────────────────────────────────────────────────
function renderScopes() {
var el = document.getElementById('scope-checkboxes');
var tmpl = document.getElementById('tmpl-scope-checkbox');
el.innerHTML = '';
SCOPES.forEach(function(s) {
var node = tmpl.content.cloneNode(true);
var input = node.querySelector('input');
input.dataset.scope = s;
if (selectedScopes.has(s)) input.checked = true;
node.querySelector('.scope-checkbox-name').textContent = s;
node.querySelector('.scope-checkbox-desc').textContent = SCOPE_DESC[s];
input.addEventListener('change', function() {
if (this.checked) selectedScopes.add(this.dataset.scope);
else selectedScopes.delete(this.dataset.scope);
});
el.appendChild(node);
});
}
function renderTable() {
var q = search.toLowerCase();
var tbody = document.getElementById('keys-tbody');
var rows = KEYS.filter(function(k) {
return !q || k.name.toLowerCase().includes(q) || k.prefix.includes(q);
});
tbody.innerHTML = '';
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(k) {
var node = tmpl.content.cloneNode(true);
node.querySelector('.col-name').textContent = k.name;
node.querySelector('.col-mono').textContent = k.prefix;
node.querySelector('.col-created').textContent = k.created;
node.querySelector('.col-last-used').textContent = k.lastUsed;
var scopesEl = node.querySelector('.col-scopes');
k.scopes.forEach(function(s) {
var span = document.createElement('span');
span.className = 'badge badge-scope';
span.textContent = s;
scopesEl.appendChild(span);
});
var badge = document.createElement('span');
badge.className = k.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
badge.textContent = k.status === 'active' ? 'Active' : 'Revoked';
node.querySelector('.col-status').appendChild(badge);
var actionsActive = node.querySelector('.actions-active');
var actionsRevoked = node.querySelector('.actions-revoked');
if (k.status === 'active') {
actionsActive.querySelector('[title="Copy key prefix"]')
.addEventListener('click', function() { copyPrefix(k.prefix); });
actionsActive.querySelector('[title="Revoke key"]')
.addEventListener('click', function() { revokeKey(k.id); });
} else {
actionsActive.style.display = 'none';
actionsRevoked.style.display = '';
actionsRevoked.querySelector('[title="Delete"]')
.addEventListener('click', function() { deleteKey(k.id); });
}
tbody.appendChild(node);
});
}
// ── Modal ────────────────────────────────────────────────────────────────────
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(e) { if (e.target === modal) closeModal(); });
document.getElementById('modal-confirm-btn').addEventListener('click', 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; }
var rawKey = createKey(name, Array.from(selectedScopes));
document.getElementById('reveal-key-value').textContent = 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();
});
document.getElementById('modal-done-btn').addEventListener('click', closeModal);
document.getElementById('copy-key-btn').addEventListener('click', function() {
var val = document.getElementById('reveal-key-value').textContent;
navigator.clipboard && navigator.clipboard.writeText(val);
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();
});
// ── Init ─────────────────────────────────────────────────────────────────────
function copyPrefix(prefix) {
navigator.clipboard && navigator.clipboard.writeText(prefix);
}
document.addEventListener('DOMContentLoaded', loadKeys);