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

+9 -1
View File
@@ -16,7 +16,7 @@ namespace Gott.Services;
public class AnthropicService
{
private readonly IHttpClientFactory _httpFactory;
private readonly string _token;
private string _token;
private readonly ToolService _toolService;
private readonly DatabaseService _db;
private readonly MemoryService _memoryService;
@@ -54,6 +54,14 @@ public class AnthropicService
_telegramService = telegramService;
}
/// <summary>
/// Aktualisiert den API-Token zur Laufzeit (z.B. wenn ein anderer Key aktiviert wird).
/// </summary>
public void UpdateToken(string token)
{
_token = token ?? "";
}
public async Task StreamChat(
HttpResponse response,
string modelId,
+141
View File
@@ -289,6 +289,23 @@ public class DatabaseService
CREATE INDEX IF NOT EXISTS idx_shared_messages_message ON shared_messages(message_id);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_id TEXT NOT NULL UNIQUE,
provider TEXT NOT NULL,
name TEXT NOT NULL,
api_key TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_api_keys_provider ON api_keys(provider);
");
// Migration: AnthropicToken aus appsettings.Production.json → api_keys Tabelle
MigrateAnthropicTokenFromConfig(conn);
TryAddColumn(conn, "agents", "avatar_small", "TEXT");
TryAddColumn(conn, "agents", "is_public", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "image_small", "TEXT");
@@ -1399,4 +1416,128 @@ public class DatabaseService
WHERE c.is_enabled = 1 AND c.next_run_at IS NOT NULL AND c.next_run_at <= @now",
new { now });
}
// ── API Keys ──
private void MigrateAnthropicTokenFromConfig(SqliteConnection conn)
{
// Nur migrieren wenn noch keine Anthropic-Keys existieren
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM api_keys WHERE provider = 'anthropic'");
if (count > 0) return;
// Token aus appsettings.Production.json lesen
var configPath = Path.Combine(Path.GetDirectoryName(_connectionString.Replace("Data Source=", "")) ?? "", "..", "appsettings.Production.json");
// Normaler Pfad: neben der DB-Datei im übergeordneten Ordner
var basePath = Path.GetDirectoryName(_connectionString.Replace("Data Source=", ""));
if (basePath != null)
{
var prodPath = Path.Combine(basePath, "..", "appsettings.Production.json");
if (File.Exists(prodPath))
{
try
{
var json = File.ReadAllText(prodPath);
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("AnthropicToken", out var tokenEl))
{
var token = tokenEl.GetString();
if (!string.IsNullOrWhiteSpace(token))
{
var now = DateTime.UtcNow.ToString("o");
conn.Execute(@"
INSERT INTO api_keys (key_id, provider, name, api_key, is_active, created_at, updated_at)
VALUES (@keyId, 'anthropic', 'Migriert aus Config', @token, 1, @now, @now)",
new { keyId = Guid.NewGuid().ToString("N"), token, now });
Console.WriteLine("[Migration] AnthropicToken aus appsettings.Production.json → api_keys migriert");
}
}
}
catch { /* Config nicht lesbar – ignorieren */ }
}
}
}
public async Task<IEnumerable<ApiKeyRecord>> GetApiKeys()
{
using var conn = CreateConnection();
return await conn.QueryAsync<ApiKeyRecord>(
"SELECT * FROM api_keys ORDER BY provider, name");
}
public async Task<IEnumerable<ApiKeyRecord>> GetApiKeysByProvider(string provider)
{
using var conn = CreateConnection();
return await conn.QueryAsync<ApiKeyRecord>(
"SELECT * FROM api_keys WHERE provider = @provider ORDER BY name",
new { provider });
}
public async Task<ApiKeyRecord> GetActiveApiKey(string provider)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<ApiKeyRecord>(
"SELECT * FROM api_keys WHERE provider = @provider AND is_active = 1 LIMIT 1",
new { provider });
}
public async Task<ApiKeyRecord> GetApiKey(string keyId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<ApiKeyRecord>(
"SELECT * FROM api_keys WHERE key_id = @keyId",
new { keyId });
}
public async Task CreateApiKey(ApiKeyRecord record)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
INSERT INTO api_keys (key_id, provider, name, api_key, is_active, created_at, updated_at)
VALUES (@KeyId, @Provider, @Name, @ApiKey, @IsActive, @CreatedAt, @UpdatedAt)",
record);
}
public async Task UpdateApiKey(string keyId, string name, string apiKey = null)
{
using var conn = CreateConnection();
if (!string.IsNullOrWhiteSpace(apiKey))
{
await conn.ExecuteAsync(
"UPDATE api_keys SET name = @name, api_key = @apiKey, updated_at = @now WHERE key_id = @keyId",
new { keyId, name, apiKey, now = DateTime.UtcNow.ToString("o") });
}
else
{
await conn.ExecuteAsync(
"UPDATE api_keys SET name = @name, updated_at = @now WHERE key_id = @keyId",
new { keyId, name, now = DateTime.UtcNow.ToString("o") });
}
}
public async Task SetActiveApiKey(string keyId, string provider)
{
using var conn = CreateConnection();
// Erst alle deaktivieren
await conn.ExecuteAsync(
"UPDATE api_keys SET is_active = 0, updated_at = @now WHERE provider = @provider",
new { provider, now = DateTime.UtcNow.ToString("o") });
// Dann den gewählten aktivieren
await conn.ExecuteAsync(
"UPDATE api_keys SET is_active = 1, updated_at = @now WHERE key_id = @keyId",
new { keyId, now = DateTime.UtcNow.ToString("o") });
}
public async Task DeactivateApiKey(string keyId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE api_keys SET is_active = 0, updated_at = @now WHERE key_id = @keyId",
new { keyId, now = DateTime.UtcNow.ToString("o") });
}
public async Task DeleteApiKey(string keyId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync("DELETE FROM api_keys WHERE key_id = @keyId", new { keyId });
}
}