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:
1 parent
1228dc9d58
commit
fa8deb15b6
11 files changed
+649
-4
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user