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
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Extensions;
|
||||
using Gott.Models;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
public static class ApiKeysEndpoint
|
||||
{
|
||||
public static void MapApiKeysEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/admin/api-keys", HandleGetAll);
|
||||
app.MapPost("/api/admin/api-keys", HandleCreate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}", HandleUpdate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}/activate", HandleActivate);
|
||||
app.MapPut("/api/admin/api-keys/{keyId}/deactivate", HandleDeactivate);
|
||||
app.MapDelete("/api/admin/api-keys/{keyId}", HandleDelete);
|
||||
}
|
||||
|
||||
private static async Task HandleGetAll(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var keys = await db.GetApiKeys();
|
||||
var result = keys.Select(k => new
|
||||
{
|
||||
k.KeyId,
|
||||
k.Provider,
|
||||
k.Name,
|
||||
k.ApiKey,
|
||||
k.IsActive,
|
||||
k.CreatedAt,
|
||||
k.UpdatedAt
|
||||
});
|
||||
await ctx.Response.WriteAsJsonAsync(result);
|
||||
}
|
||||
|
||||
private static async Task HandleCreate(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
var provider = body.GetProperty("provider").GetString()?.Trim().ToLower() ?? "";
|
||||
var name = body.GetProperty("name").GetString()?.Trim() ?? "";
|
||||
var apiKey = body.GetProperty("apiKey").GetString()?.Trim() ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Provider, Name und API-Key sind erforderlich" });
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
var record = new ApiKeyRecord
|
||||
{
|
||||
KeyId = Guid.NewGuid().ToString("N"),
|
||||
Provider = provider,
|
||||
Name = name,
|
||||
ApiKey = apiKey,
|
||||
IsActive = false,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
await db.CreateApiKey(record);
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
record.KeyId,
|
||||
record.Provider,
|
||||
record.Name,
|
||||
record.ApiKey,
|
||||
record.IsActive,
|
||||
record.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var existing = await db.GetApiKey(keyId);
|
||||
if (existing == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
var name = body.TryGetProperty("name", out var n) ? n.GetString()?.Trim() : null;
|
||||
var apiKey = body.TryGetProperty("apiKey", out var k) ? k.GetString()?.Trim() : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
name = existing.Name;
|
||||
|
||||
await db.UpdateApiKey(keyId, name, apiKey);
|
||||
|
||||
// Wenn aktiver Key geändert wird → Token im Service aktualisieren
|
||||
if (existing.IsActive && existing.Provider == "anthropic" && !string.IsNullOrWhiteSpace(apiKey))
|
||||
anthropic.UpdateToken(apiKey);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task HandleActivate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var existing = await db.GetApiKey(keyId);
|
||||
if (existing == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
await db.SetActiveApiKey(keyId, existing.Provider);
|
||||
|
||||
// Token im laufenden Service aktualisieren
|
||||
if (existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken(existing.ApiKey);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task HandleDeactivate(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var existing = await db.GetApiKey(keyId);
|
||||
if (existing == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
await db.DeactivateApiKey(keyId);
|
||||
|
||||
// Token im laufenden Service leeren
|
||||
if (existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken("");
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task HandleDelete(HttpContext ctx, DatabaseService db, AnthropicService anthropic, string keyId)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var existing = await db.GetApiKey(keyId);
|
||||
if (existing == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn aktiver Key gelöscht wird → Token leeren
|
||||
if (existing.IsActive && existing.Provider == "anthropic")
|
||||
anthropic.UpdateToken("");
|
||||
|
||||
await db.DeleteApiKey(keyId);
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user