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();