- Quick-Scan: nur GitHub mit Dotenv, Text, Markdown + .env - Web-Scan: nur Serper.dev (Google Dorks) - Full-Scan: GitHub + Serper komplett (wie bisher) - Backend: TriggerScan(mode), ScanGitHubQuick(), mode-basierte Quellsteuerung - Endpoint: ?mode=quick|web|full Query-Parameter - Frontend: 3 Buttons (⚡ Quick, 🌐 Web, 🔍 Full) mit gemeinsamer Scan-Logik - Status-Anzeige zeigt Scan-Modus im Ergebnis
511 lines
18 KiB
JavaScript
511 lines
18 KiB
JavaScript
// ── API Keys Page (Admin) ──
|
||
|
||
function formatKeyStatus(status) {
|
||
var map = {
|
||
'valid': '✅ Gültig',
|
||
'invalid': '❌ Ungültig',
|
||
'rate_limited': '⚠️ Rate Limited',
|
||
'quota_exceeded': '⚠️ Quota Exceeded',
|
||
'error': '⚠️ Fehler',
|
||
'unchecked': '⏳ Ungeprüft'
|
||
};
|
||
return map[status] || status;
|
||
}
|
||
|
||
function createUsageBadge(usage5h, usage7d) {
|
||
if (usage5h == null && usage7d == null) return null;
|
||
var badge = document.createElement('div');
|
||
badge.className = 'usage-badge';
|
||
|
||
function addRow(label, pct) {
|
||
if (pct == null) return;
|
||
var row = document.createElement('div');
|
||
row.className = 'usage-row';
|
||
|
||
var lbl = document.createElement('span');
|
||
lbl.className = 'usage-label';
|
||
lbl.textContent = label;
|
||
|
||
var track = document.createElement('div');
|
||
track.className = 'usage-track';
|
||
|
||
var fill = document.createElement('div');
|
||
fill.className = 'usage-fill';
|
||
if (pct >= 90) fill.className += ' danger';
|
||
else if (pct >= 70) fill.className += ' warn';
|
||
fill.style.width = Math.min(pct, 100) + '%';
|
||
track.appendChild(fill);
|
||
|
||
var val = document.createElement('span');
|
||
val.className = 'usage-pct';
|
||
val.textContent = pct + '%';
|
||
|
||
row.appendChild(lbl);
|
||
row.appendChild(track);
|
||
row.appendChild(val);
|
||
badge.appendChild(row);
|
||
}
|
||
|
||
addRow('Sitzung', usage5h);
|
||
addRow('Woche', usage7d);
|
||
return badge;
|
||
}
|
||
|
||
registerPage('api-keys', {
|
||
async init() {
|
||
if (!currentUser || !currentUser.isAdmin) {
|
||
navigateTo('dashboard');
|
||
return;
|
||
}
|
||
|
||
var keys = [];
|
||
var editingKeyId = null;
|
||
|
||
var list = document.getElementById('apiKeysList');
|
||
var newBtn = document.getElementById('apiKeysNewBtn');
|
||
var form = document.getElementById('apiKeysForm');
|
||
var formTitle = document.getElementById('apiKeysFormTitle');
|
||
var providerSelect = document.getElementById('apiKeysProvider');
|
||
var nameInput = document.getElementById('apiKeysName');
|
||
var keyInput = document.getElementById('apiKeysKey');
|
||
var saveBtn = document.getElementById('apiKeysFormSaveBtn');
|
||
|
||
function renderKeys() {
|
||
list.innerHTML = '';
|
||
|
||
if (keys.length === 0) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'admin-user-meta';
|
||
empty.style.padding = '1rem 0';
|
||
empty.textContent = 'Keine API-Keys konfiguriert.';
|
||
list.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
keys.forEach(function(key) {
|
||
var wrapper = document.createElement('div');
|
||
|
||
var row = document.createElement('div');
|
||
row.className = 'admin-user-row';
|
||
|
||
var info = document.createElement('div');
|
||
info.className = 'admin-user-info';
|
||
|
||
var nameSpan = document.createElement('span');
|
||
nameSpan.className = 'admin-user-name';
|
||
nameSpan.textContent = key.name;
|
||
|
||
var metaSpan = document.createElement('span');
|
||
metaSpan.className = 'admin-user-meta';
|
||
var parts = [key.provider];
|
||
parts.push(formatKeyStatus(key.status));
|
||
if (key.plan) parts.push(key.plan);
|
||
if (key.checkedAt) parts.push(new Date(key.checkedAt).toLocaleString());
|
||
metaSpan.textContent = parts.join(' · ');
|
||
|
||
info.appendChild(nameSpan);
|
||
if (key.sourceUrl) {
|
||
var urlSpan = document.createElement('a');
|
||
urlSpan.className = 'admin-user-meta';
|
||
urlSpan.href = key.sourceUrl;
|
||
urlSpan.target = '_blank';
|
||
urlSpan.rel = 'noopener';
|
||
urlSpan.textContent = key.sourceUrl.replace('https://github.com/', '');
|
||
info.appendChild(urlSpan);
|
||
}
|
||
info.appendChild(metaSpan);
|
||
|
||
// Toggle + Usage-Badge nebeneinander
|
||
var badgeRow = document.createElement('div');
|
||
badgeRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:2px';
|
||
|
||
if (key.isActive) {
|
||
var deactivateBtn = document.createElement('button');
|
||
deactivateBtn.className = 'icon-btn';
|
||
deactivateBtn.title = 'Deaktivieren';
|
||
deactivateBtn.innerHTML = '<i class="fa fa-toggle-on" style="color:var(--accent)"></i>';
|
||
deactivateBtn.addEventListener('click', function() { deactivateKey(key); });
|
||
badgeRow.appendChild(deactivateBtn);
|
||
} else {
|
||
var activateBtn = document.createElement('button');
|
||
activateBtn.className = 'icon-btn';
|
||
activateBtn.title = 'Aktivieren';
|
||
activateBtn.innerHTML = '<i class="fa fa-toggle-off"></i>';
|
||
activateBtn.addEventListener('click', function() { activateKey(key); });
|
||
badgeRow.appendChild(activateBtn);
|
||
}
|
||
|
||
var usageBadge = createUsageBadge(key.usage5h, key.usage7d);
|
||
if (usageBadge) badgeRow.appendChild(usageBadge);
|
||
|
||
// Status-Zeile (für Check-Ergebnis)
|
||
var statusLine = document.createElement('div');
|
||
statusLine.className = 'admin-user-meta';
|
||
statusLine.style.cssText = 'padding:0 var(--box-padding) .5rem;display:none;font-size:.85em';
|
||
|
||
// Check (nur Anthropic) – rechts neben Usage-Badge
|
||
if (key.provider === 'anthropic') {
|
||
var checkBtn = document.createElement('button');
|
||
checkBtn.className = 'icon-btn';
|
||
checkBtn.title = 'Key prüfen';
|
||
checkBtn.innerHTML = '<i class="fa fa-refresh"></i>';
|
||
checkBtn.addEventListener('click', function() { checkKey(key, checkBtn, statusLine); });
|
||
badgeRow.appendChild(checkBtn);
|
||
}
|
||
|
||
info.appendChild(badgeRow);
|
||
|
||
var actions = document.createElement('div');
|
||
actions.className = 'admin-user-actions';
|
||
|
||
// Bearbeiten
|
||
var editBtn = document.createElement('button');
|
||
editBtn.className = 'icon-btn';
|
||
editBtn.title = 'Bearbeiten';
|
||
editBtn.innerHTML = '<i class="fa fa-pencil"></i>';
|
||
editBtn.addEventListener('click', function() { startEdit(key); });
|
||
actions.appendChild(editBtn);
|
||
|
||
// Löschen
|
||
var deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'icon-btn';
|
||
deleteBtn.title = 'Löschen';
|
||
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
||
deleteBtn.addEventListener('click', function() { deleteKey(key); });
|
||
actions.appendChild(deleteBtn);
|
||
|
||
row.appendChild(info);
|
||
row.appendChild(actions);
|
||
wrapper.appendChild(row);
|
||
wrapper.appendChild(statusLine);
|
||
list.appendChild(wrapper);
|
||
});
|
||
}
|
||
|
||
async function loadKeys() {
|
||
try {
|
||
keys = await apiGetApiKeys();
|
||
renderKeys();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function checkKey(key, btn, statusLine) {
|
||
var origHtml = btn.innerHTML;
|
||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||
btn.disabled = true;
|
||
statusLine.style.display = 'none';
|
||
statusLine.innerHTML = '';
|
||
try {
|
||
var result = await apiCheckApiKey(key.keyId);
|
||
statusLine.style.opacity = '1';
|
||
statusLine.innerHTML = '';
|
||
if (result.status === 'valid') {
|
||
var text = document.createElement('span');
|
||
var parts = [formatKeyStatus('valid'), result.plan];
|
||
if (result.overage != null && result.overage > 0) parts.push('Overage: ' + result.overage + '%');
|
||
text.textContent = parts.join(' · ');
|
||
statusLine.appendChild(text);
|
||
var badge = createUsageBadge(result.usage5h, result.usage7d);
|
||
if (badge) { badge.style.marginTop = '4px'; statusLine.appendChild(badge); }
|
||
} else {
|
||
statusLine.textContent = '❌ ' + (result.message || 'Unbekannter Fehler');
|
||
}
|
||
await loadKeys();
|
||
} catch (err) {
|
||
statusLine.style.opacity = '1';
|
||
statusLine.textContent = '❌ ' + err.message;
|
||
} finally {
|
||
btn.innerHTML = origHtml;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function startEdit(key) {
|
||
editingKeyId = key.keyId;
|
||
formTitle.textContent = 'API-Key bearbeiten';
|
||
providerSelect.value = key.provider;
|
||
providerSelect.disabled = true;
|
||
nameInput.value = key.name;
|
||
keyInput.value = key.apiKey;
|
||
saveBtn.textContent = 'Speichern';
|
||
form.hidden = false;
|
||
nameInput.focus();
|
||
}
|
||
|
||
async function activateKey(key) {
|
||
try {
|
||
await apiActivateApiKey(key.keyId);
|
||
showToast(key.name + ' aktiviert');
|
||
await loadKeys();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deactivateKey(key) {
|
||
try {
|
||
await apiDeactivateApiKey(key.keyId);
|
||
showToast(key.name + ' deaktiviert');
|
||
await loadKeys();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteKey(key) {
|
||
if (!confirm('API-Key "' + key.name + '" wirklich löschen?')) return;
|
||
try {
|
||
await apiDeleteApiKey(key.keyId);
|
||
showToast('API-Key gelöscht');
|
||
await loadKeys();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Neuer Key
|
||
newBtn.addEventListener('click', function() {
|
||
editingKeyId = null;
|
||
formTitle.textContent = 'Neuer API-Key';
|
||
providerSelect.value = 'anthropic';
|
||
providerSelect.disabled = false;
|
||
nameInput.value = '';
|
||
keyInput.value = '';
|
||
saveBtn.textContent = 'Erstellen';
|
||
form.hidden = false;
|
||
nameInput.focus();
|
||
});
|
||
|
||
// Speichern (Erstellen oder Bearbeiten)
|
||
saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, saveBtn.textContent, async () => {
|
||
var name = nameInput.value.trim();
|
||
|
||
if (!name) {
|
||
showToast('Name erforderlich', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (editingKeyId) {
|
||
// Bearbeiten
|
||
var data = { name: name };
|
||
var newKey = keyInput.value.trim();
|
||
if (newKey) data.apiKey = newKey;
|
||
await apiUpdateApiKey(editingKeyId, data);
|
||
showToast('API-Key aktualisiert');
|
||
} else {
|
||
// Erstellen
|
||
var provider = providerSelect.value;
|
||
var apiKey = keyInput.value.trim();
|
||
if (!apiKey) {
|
||
showToast('API-Key erforderlich', 'error');
|
||
return;
|
||
}
|
||
await apiCreateApiKey({ provider: provider, name: name, apiKey: apiKey });
|
||
showToast('API-Key erstellt');
|
||
}
|
||
form.hidden = true;
|
||
editingKeyId = null;
|
||
await loadKeys();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
}
|
||
}));
|
||
|
||
await loadKeys();
|
||
|
||
// ── Scanner UI ──
|
||
|
||
var scanQuickBtn = document.getElementById('scanQuickBtn');
|
||
var scanWebBtn = document.getElementById('scanWebBtn');
|
||
var scanFullBtn = document.getElementById('scanFullBtn');
|
||
var scanBtns = [scanQuickBtn, scanWebBtn, scanFullBtn];
|
||
var scanStatusEl = document.getElementById('scanStatus');
|
||
var scannedList = document.getElementById('scannedKeysList');
|
||
var scannedKeys = [];
|
||
var pollTimer = null;
|
||
|
||
function renderScannedKeys() {
|
||
scannedList.innerHTML = '';
|
||
var visibleKeys = scannedKeys.filter(function(sk) { return sk.adoptedApiKeyId == null; });
|
||
if (visibleKeys.length === 0) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'admin-user-meta';
|
||
empty.style.padding = '1rem 0';
|
||
empty.textContent = 'Keine gescannten Keys vorhanden.';
|
||
scannedList.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
visibleKeys.forEach(function(sk) {
|
||
|
||
var row = document.createElement('div');
|
||
row.className = 'admin-user-row';
|
||
|
||
var info = document.createElement('div');
|
||
info.className = 'admin-user-info';
|
||
info.style.minWidth = '0';
|
||
|
||
if (sk.sourceUrl) {
|
||
var nameSpan = document.createElement('a');
|
||
nameSpan.className = 'admin-user-name';
|
||
nameSpan.href = sk.sourceUrl;
|
||
nameSpan.target = '_blank';
|
||
nameSpan.rel = 'noopener';
|
||
var displayUrl = sk.sourceUrl.replace('https://github.com/', '').replace('https://www.', '').replace('https://', '');
|
||
if (displayUrl.length > 80) displayUrl = displayUrl.substring(0, 77) + '…';
|
||
nameSpan.textContent = displayUrl;
|
||
info.appendChild(nameSpan);
|
||
}
|
||
|
||
var metaSpan = document.createElement('span');
|
||
metaSpan.className = 'admin-user-meta';
|
||
var metaParts = [];
|
||
var sourceIcons = { github: '🐙 GitHub', serper: '🔍 Google', gist: '📋 Gist', pastebin: '📄 Pastebin' };
|
||
metaParts.push(sourceIcons[sk.source] || sk.source || 'github');
|
||
var typeLabels = { anthropic_oauth: 'OAuth', anthropic_api: 'API', anthropic_unknown: 'Anthropic', unknown: '?' };
|
||
metaParts.push(typeLabels[sk.type] || sk.type || '?');
|
||
metaParts.push(formatKeyStatus(sk.status));
|
||
if (sk.plan && sk.plan !== 'Unknown') metaParts.push(sk.plan);
|
||
if (sk.checkedAt) metaParts.push(new Date(sk.checkedAt).toLocaleString());
|
||
metaSpan.textContent = metaParts.join(' · ');
|
||
|
||
info.appendChild(metaSpan);
|
||
var skUsageBadge = createUsageBadge(sk.usage5h, sk.usage7d);
|
||
if (skUsageBadge) info.appendChild(skUsageBadge);
|
||
|
||
var actions = document.createElement('div');
|
||
actions.className = 'admin-user-actions';
|
||
|
||
// Recheck
|
||
var recheckBtn = document.createElement('button');
|
||
recheckBtn.className = 'icon-btn';
|
||
recheckBtn.title = 'Erneut prüfen';
|
||
recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>';
|
||
recheckBtn.addEventListener('click', async function() {
|
||
recheckBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||
recheckBtn.disabled = true;
|
||
try {
|
||
await apiRecheckScannedKey(sk.scannedKeyId);
|
||
await loadScannedKeys();
|
||
} catch (err) { showToast(err.message, 'error'); }
|
||
finally { recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>'; recheckBtn.disabled = false; }
|
||
});
|
||
actions.appendChild(recheckBtn);
|
||
|
||
// Übernehmen (bei valid oder quota_exceeded)
|
||
if (sk.status === 'valid' || sk.status === 'quota_exceeded') {
|
||
var adoptBtn = document.createElement('button');
|
||
adoptBtn.className = 'icon-btn';
|
||
adoptBtn.title = 'Als API-Key übernehmen';
|
||
adoptBtn.innerHTML = '<i class="fa fa-plus-circle" style="color:var(--accent)"></i>';
|
||
adoptBtn.addEventListener('click', async function() {
|
||
try {
|
||
await apiAdoptScannedKey(sk.scannedKeyId);
|
||
showToast('Key übernommen');
|
||
await loadScannedKeys();
|
||
await loadKeys();
|
||
} catch (err) { showToast(err.message, 'error'); }
|
||
});
|
||
actions.appendChild(adoptBtn);
|
||
}
|
||
|
||
// Löschen
|
||
var delBtn = document.createElement('button');
|
||
delBtn.className = 'icon-btn';
|
||
delBtn.title = 'Ignorieren';
|
||
delBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
||
delBtn.addEventListener('click', async function() {
|
||
try {
|
||
await apiDeleteScannedKey(sk.scannedKeyId);
|
||
await loadScannedKeys();
|
||
} catch (err) { showToast(err.message, 'error'); }
|
||
});
|
||
actions.appendChild(delBtn);
|
||
|
||
row.appendChild(info);
|
||
row.appendChild(actions);
|
||
scannedList.appendChild(row);
|
||
});
|
||
}
|
||
|
||
async function loadScannedKeys() {
|
||
try {
|
||
scannedKeys = await apiGetScannedKeys();
|
||
renderScannedKeys();
|
||
} catch (err) { showToast(err.message, 'error'); }
|
||
}
|
||
|
||
var scanBtnDefaults = [
|
||
{ btn: scanQuickBtn, icon: 'fa-bolt', label: 'Quick' },
|
||
{ btn: scanWebBtn, icon: 'fa-globe', label: 'Web' },
|
||
{ btn: scanFullBtn, icon: 'fa-search', label: 'Full' }
|
||
];
|
||
|
||
function setScanButtonsDisabled(disabled) {
|
||
scanBtnDefaults.forEach(function(d) { d.btn.disabled = disabled; });
|
||
}
|
||
|
||
function resetScanButtons() {
|
||
scanBtnDefaults.forEach(function(d) {
|
||
d.btn.innerHTML = '<i class="fa ' + d.icon + '"></i> ' + d.label;
|
||
d.btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
async function updateScanStatus() {
|
||
try {
|
||
var s = await apiScanStatus();
|
||
if (s.running) {
|
||
scanStatusEl.textContent = '⏳ Scan läuft…';
|
||
setScanButtonsDisabled(true);
|
||
if (!pollTimer) {
|
||
pollTimer = setInterval(async function() {
|
||
var st = await apiScanStatus();
|
||
await loadScannedKeys();
|
||
if (!st.running) {
|
||
clearInterval(pollTimer);
|
||
pollTimer = null;
|
||
resetScanButtons();
|
||
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
||
}
|
||
}, 3000);
|
||
}
|
||
} else {
|
||
resetScanButtons();
|
||
if (s.lastResult) {
|
||
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
|
||
scanStatusEl.textContent = timeStr + s.lastResult;
|
||
}
|
||
}
|
||
} catch (err) { /* ignore */ }
|
||
}
|
||
|
||
async function triggerScan(mode, btn) {
|
||
try {
|
||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft…';
|
||
setScanButtonsDisabled(true);
|
||
await apiTriggerScan(mode);
|
||
showToast(mode.charAt(0).toUpperCase() + mode.slice(1) + '-Scan gestartet');
|
||
await updateScanStatus();
|
||
} catch (err) {
|
||
showToast(err.message, 'error');
|
||
resetScanButtons();
|
||
}
|
||
}
|
||
|
||
scanQuickBtn.addEventListener('click', function() { triggerScan('quick', scanQuickBtn); });
|
||
scanWebBtn.addEventListener('click', function() { triggerScan('web', scanWebBtn); });
|
||
scanFullBtn.addEventListener('click', function() { triggerScan('full', scanFullBtn); });
|
||
|
||
await loadScannedKeys();
|
||
await updateScanStatus();
|
||
},
|
||
|
||
destroy() {
|
||
// Polling stoppen wenn Seite verlassen wird
|
||
}
|
||
});
|