feat: API-Keys-Verwaltung in DB statt appsettings

- Neue Tabelle api_keys (Provider, Name, ApiKey, IsActive)
- Admin-CRUD: GET/POST/PUT/DELETE /api/admin/api-keys
- Aktivieren/Deaktivieren pro Provider (nur einer aktiv)
- Live-Token-Update im AnthropicService ohne Neustart
- Migration: AnthropicToken aus appsettings.Production.json → DB
- AnthropicToken aus ServerSettings-Definitionen + appsettings entfernt
- Frontend: API-Keys-Seite mit Erstellen/Bearbeiten/Löschen/Toggle
- Dashboard: API-Keys-Link für Admins
This commit is contained in:
Meleki committed 2026-03-12 12:07:22 +01:00
1 parent 1228dc9d58
commit fa8deb15b6
11 files changed
+649 -4

No files matched your search

+35
View File
@@ -57,6 +57,7 @@
<div class="dashboard-links">
<a id="dashboardSettingsLink" class="dashboard-link">Einstellungen</a>
<a id="dashboardAdminLink" class="dashboard-link" hidden>Benutzerverwaltung</a>
<a id="dashboardApiKeysLink" class="dashboard-link" hidden>API-Keys</a>
<a id="dashboardServerSettingsLink" class="dashboard-link" hidden>Servereinstellungen</a>
<a id="dashboardProfileLink" class="dashboard-link">Mein Profil</a>
</div>
@@ -539,6 +540,39 @@
</div>
</template>
<template id="tpl-api-keys">
<div class="page form-page">
<header>
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
<h1>API-Keys</h1>
</header>
<div class="page-content">
<div class="form">
<div class="section-header">
<h2>API-Key</h2>
<button id="apiKeysNewBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Key</button>
</div>
<div id="apiKeysList" class="admin-user-list"></div>
</div>
<div id="apiKeysForm" class="form" hidden>
<h2 id="apiKeysFormTitle">Neuer API-Key</h2>
<label>Provider</label>
<select id="apiKeysProvider">
<option value="anthropic">Anthropic</option>
</select>
<label>Name</label>
<input type="text" id="apiKeysName" placeholder="z.B. Mein Pro Key" autocomplete="off">
<label>API-Key</label>
<input type="text" id="apiKeysKey" placeholder="sk-ant-..." autocomplete="off">
<div class="form-actions">
<button id="apiKeysFormSaveBtn">Erstellen</button>
</div>
</div>
</div>
</div>
</template>
<template id="tpl-admin">
<div class="page form-page">
<header>
@@ -642,6 +676,7 @@
<script src="js/pages/settings-page.js"></script>
<script src="js/pages/profile-page.js"></script>
<script src="js/pages/admin-page.js"></script>
<script src="js/pages/api-keys-page.js"></script>
<script src="js/pages/server-settings-page.js"></script>
<script src="js/pages/shared-page.js"></script>
<script src="js/app.js"></script>
+61
View File
@@ -584,6 +584,67 @@ async function apiSaveServerSettings(data) {
return resp.json();
}
// ── API Keys (Admin) ──
async function apiGetApiKeys() {
const resp = await fetch('/api/admin/api-keys');
if (!resp.ok) throw new Error('Fehler beim Laden der API-Keys');
return resp.json();
}
async function apiCreateApiKey(data) {
const resp = await fetch('/api/admin/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Erstellen');
}
return resp.json();
}
async function apiUpdateApiKey(keyId, data) {
const resp = await fetch('/api/admin/api-keys/' + keyId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Aktualisieren');
}
return resp.json();
}
async function apiActivateApiKey(keyId) {
const resp = await fetch('/api/admin/api-keys/' + keyId + '/activate', { method: 'PUT' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Aktivieren');
}
return resp.json();
}
async function apiDeactivateApiKey(keyId) {
const resp = await fetch('/api/admin/api-keys/' + keyId + '/deactivate', { method: 'PUT' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Deaktivieren');
}
return resp.json();
}
async function apiDeleteApiKey(keyId) {
const resp = await fetch('/api/admin/api-keys/' + keyId, { method: 'DELETE' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Löschen');
}
return resp.json();
}
// ── Krea Image API ──
async function apiKreaStatus() {
+201
View File
@@ -0,0 +1,201 @@
// ── API Keys Page (Admin) ──
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 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];
if (key.isActive) parts.push('✅ Aktiv');
metaSpan.textContent = parts.join(' · ');
info.appendChild(nameSpan);
info.appendChild(metaSpan);
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);
// Aktivieren/Deaktivieren
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); });
actions.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); });
actions.appendChild(activateBtn);
}
// 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);
list.appendChild(row);
});
}
async function loadKeys() {
try {
keys = await apiGetApiKeys();
renderKeys();
} catch (err) {
showToast(err.message, 'error');
}
}
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();
},
destroy() {}
});
+6
View File
@@ -17,12 +17,18 @@ registerPage('dashboard', {
// Admin-Links (nur für Admins sichtbar)
var adminLink = document.getElementById('dashboardAdminLink');
var serverSettingsLink = document.getElementById('dashboardServerSettingsLink');
var apiKeysLink = document.getElementById('dashboardApiKeysLink');
if (currentUser && currentUser.isAdmin) {
adminLink.hidden = false;
adminLink.addEventListener('click', (e) => {
e.preventDefault();
navigateTo('admin');
});
apiKeysLink.hidden = false;
apiKeysLink.addEventListener('click', (e) => {
e.preventDefault();
navigateTo('api-keys');
});
serverSettingsLink.hidden = false;
serverSettingsLink.addEventListener('click', (e) => {
e.preventDefault();