let apiKeys = []; let branches = []; async function loadKeys() { const [kRes, bRes] = await Promise.all([ fetch(`${API}/api/api-keys`, { headers: hdr() }), fetch(`${API}/api/branches`, { headers: hdr() }), ]); if (!kRes.ok) { showToast('Ошибка загрузки ключей'); return; } if (bRes.ok) branches = await bRes.json(); apiKeys = await kRes.json(); renderKeys(); } function selectedBranches() { return [...document.getElementById('kBranches').selectedOptions].map(o => Number(o.value)); } function keyStatus(k) { if (k.revoked_at) return 'Отозван'; if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) return 'Истёк'; return 'Активен'; } function renderKeys() { const tbody = document.querySelector('#keysTable tbody'); const empty = document.getElementById('keysEmpty'); tbody.innerHTML = ''; empty.style.display = apiKeys.length ? 'none' : 'block'; const branchById = {}; branches.forEach(b => { branchById[b.id] = b.name; }); apiKeys.forEach(k => { const tr = document.createElement('tr'); const scopeNames = { read: 'чтение', write: 'запись' }; const scopes = (k.scopes || []).map(s => scopeNames[s] || s).join(', '); const branchNames = (k.branch_ids || []).map(id => branchById[id] || `#${id}`).join(', ') || 'все'; const expires = k.expires_at ? fmtDateOnlyIso(k.expires_at) : 'бессрочный'; tr.innerHTML = ` ${k.id} ${esc(k.name)} ${esc(k.prefix || '')}… ${esc(scopes)} ${esc(branchNames)} ${fmtDateTimeY(k.created_at)} ${k.last_used_at ? fmtDateTimeY(k.last_used_at) : '—'} ${keyStatus(k)} `; tbody.appendChild(tr); }); } function openModal() { const sel = document.getElementById('kBranches'); sel.innerHTML = branches.map(b => ``).join(''); document.getElementById('kName').value = ''; document.getElementById('kRead').checked = true; document.getElementById('kWrite').checked = false; document.getElementById('kRate').value = ''; document.getElementById('kExpires').value = ''; document.getElementById('keyModal').classList.add('open'); setTimeout(() => document.getElementById('kName').focus(), 100); } function closeModal() { document.getElementById('keyModal').classList.remove('open'); } function showSecret(secret) { document.getElementById('secretValue').value = secret; document.getElementById('secretModal').classList.add('open'); } async function saveKey() { const name = document.getElementById('kName').value.trim(); if (!name) { showToast('Введите название', true); return; } const scopes = []; if (document.getElementById('kRead').checked) scopes.push('read'); if (document.getElementById('kWrite').checked) scopes.push('write'); if (!scopes.length) { showToast('Выберите хотя бы одно право', true); return; } const expires = document.getElementById('kExpires').value; const res = await fetch(`${API}/api/api-keys`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ name, scopes, branch_ids: selectedBranches(), rate_limit_per_min: document.getElementById('kRate').value || null, expires_at: expires || null, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { showToast(data.error || 'Ошибка создания ключа', true); return; } closeModal(); showSecret(data.key); await loadKeys(); } async function rotateKey(id) { if (!window.confirm('Перевыпустить ключ? Старый ключ перестанет работать.')) return; const res = await fetch(`${API}/api/api-keys/${id}/rotate`, { method: 'POST', headers: hdr() }); const data = await res.json().catch(() => ({})); if (!res.ok) { showToast(data.error || 'Ошибка ротации', true); return; } showSecret(data.key); await loadKeys(); } async function deleteKey(id) { if (!window.confirm('Удалить API-ключ? Он перестанет работать немедленно.')) return; const res = await fetch(`${API}/api/api-keys/${id}`, { method: 'DELETE', headers: hdr() }); const data = await res.json().catch(() => ({})); if (!res.ok) { showToast(data.error || 'Ошибка удаления', true); return; } showToast('Ключ удалён'); await loadKeys(); } async function init() { if (!(await requireAdminPage())) return; buildSidebar('apikeys'); document.getElementById('addKeyBtn').addEventListener('click', openModal); document.getElementById('kCancel').addEventListener('click', closeModal); document.getElementById('kSave').addEventListener('click', saveKey); document.getElementById('secretClose').addEventListener('click', () => document.getElementById('secretModal').classList.remove('open')); document.getElementById('secretCopy').addEventListener('click', async () => { const field = document.getElementById('secretValue'); try { await navigator.clipboard.writeText(field.value); showToast('Ключ скопирован'); } catch { field.select(); showToast('Скопируйте ключ вручную', true); } }); document.querySelector('#keysTable tbody').addEventListener('click', (e) => { const rot = e.target.closest('[data-rot]'); if (rot) return rotateKey(rot.dataset.rot); const del = e.target.closest('[data-del]'); if (del) return deleteKey(del.dataset.del); }); await loadKeys(); } init();