using System; using System.Linq; using System.Net.Http; 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); app.MapPost("/api/admin/api-keys/{keyId}/check", HandleCheck); // Scan-Endpoints app.MapPost("/api/admin/api-keys/scan", HandleScanTrigger); app.MapGet("/api/admin/api-keys/scan/status", HandleScanStatus); app.MapGet("/api/admin/api-keys/scanned", HandleGetScanned); app.MapPost("/api/admin/api-keys/scanned/{id}/recheck", HandleRecheck); app.MapPost("/api/admin/api-keys/scanned/{id}/adopt", HandleAdopt); app.MapDelete("/api/admin/api-keys/scanned/{id}", HandleDeleteScanned); } 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.Status, k.Plan, k.Usage7d, k.Usage5h, k.CheckedAt, k.SourceUrl, 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(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(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 }); } private static async Task HandleCheck(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db, 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; } if (existing.Provider != "anthropic") { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Check nur für Anthropic-Keys verfügbar" }); return; } var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; // Metadaten in DB speichern await db.UpdateApiKeyMeta(keyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h); await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Plan, result.IsSubscription, result.Usage7d, result.Usage5h, result.Overage, status7d = result.Status7d, status5h = result.Status5h, statusOverage = result.StatusOverage }); } // ── Scan-Endpoints ── private static Task HandleScanTrigger(HttpContext ctx, KeyScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; } if (scanner.IsRunning) { ctx.Response.StatusCode = 409; return ctx.Response.WriteAsJsonAsync(new { error = "Scan läuft bereits" }); } scanner.TriggerScan(); return ctx.Response.WriteAsJsonAsync(new { ok = true, message = "Scan gestartet" }); } private static Task HandleScanStatus(HttpContext ctx, KeyScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; } return ctx.Response.WriteAsJsonAsync(new { running = scanner.IsRunning, lastRun = scanner.LastRun, lastResult = scanner.LastResult }); } private static async Task HandleGetScanned(HttpContext ctx, DatabaseService db) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var keys = await db.GetScannedKeys(); var result = keys.Select(k => new { k.ScannedKeyId, k.Source, k.Type, k.SourceUrl, k.Status, k.Plan, k.Usage7d, k.Usage5h, k.ScannedAt, k.CheckedAt, k.AdoptedApiKeyId }); await ctx.Response.WriteAsJsonAsync(result); } private static async Task HandleRecheck(HttpContext ctx, DatabaseService db, KeyScannerService scanner, string id) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var sk = await db.GetScannedKey(id); if (sk == null) { ctx.Response.StatusCode = 404; await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" }); return; } await scanner.CheckAndUpdateKey(sk); var updated = await db.GetScannedKey(id); await ctx.Response.WriteAsJsonAsync(new { ok = true, updated.Status, updated.Plan, updated.Usage7d, updated.Usage5h, updated.CheckedAt }); } private static async Task HandleAdopt(HttpContext ctx, DatabaseService db, string id) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var sk = await db.GetScannedKey(id); if (sk == null) { ctx.Response.StatusCode = 404; await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" }); return; } if (sk.AdoptedApiKeyId != null) { ctx.Response.StatusCode = 409; await ctx.Response.WriteAsJsonAsync(new { error = "Key wurde bereits übernommen" }); return; } // Prüfen ob Key schon als API-Key existiert if (await db.ApiKeyExistsInKeys(sk.ApiKey)) { ctx.Response.StatusCode = 409; await ctx.Response.WriteAsJsonAsync(new { error = "Key existiert bereits als API-Key" }); return; } // Name: user/repo aus URL extrahieren, SourceUrl: vollständige URL var sourceUrl = !string.IsNullOrWhiteSpace(sk.SourceUrl) ? sk.SourceUrl : null; var title = $"Scan: {sk.Plan ?? "Unknown"}"; if (sourceUrl != null) { var path = sourceUrl.Replace("https://github.com/", ""); var parts = path.Split('/'); if (parts.Length >= 2) title = parts[0] + "/" + parts[1]; } var now = DateTime.UtcNow.ToString("o"); var record = new ApiKeyRecord { KeyId = Guid.NewGuid().ToString("N"), Provider = "anthropic", Name = title, ApiKey = sk.ApiKey, IsActive = false, Status = sk.Status, Plan = sk.Plan, Usage7d = sk.Usage7d, Usage5h = sk.Usage5h, CheckedAt = sk.CheckedAt, SourceUrl = sourceUrl, CreatedAt = now, UpdatedAt = now }; var apiKeyId = await db.CreateApiKey(record); await db.SetScannedKeyAdopted(id, apiKeyId); await ctx.Response.WriteAsJsonAsync(new { ok = true, record.KeyId, record.Name }); } private static async Task HandleDeleteScanned(HttpContext ctx, DatabaseService db, string id) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } await db.DeleteScannedKey(id); await ctx.Response.WriteAsJsonAsync(new { ok = true }); } }