using System; using System.Collections.Generic; 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) { // Admin-Endpoints 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}/public", HandleTogglePublic); app.MapDelete("/api/admin/api-keys/{keyId}", HandleDelete); app.MapPost("/api/admin/api-keys/{keyId}/check", HandleCheck); app.MapPost("/api/admin/api-keys/check-all", HandleCheckAll); // 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.MapGet("/api/admin/api-keys/scanned/summary", HandleGetScannedSummary); app.MapPost("/api/admin/api-keys/scanned/recheck-all", HandleRecheckAll); 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); // Event Scanner Status & Log app.MapGet("/api/admin/api-keys/event-scanner/status", HandleEventScannerStatus); app.MapGet("/api/admin/api-keys/event-scanner/log", HandleEventScannerLog); // Scanner Logs (persistiert) app.MapGet("/api/admin/scanner/logs", HandleGetScannerLogs); // User-Endpoints (für alle authentifizierten User) app.MapGet("/api/api-keys/available", HandleGetAvailable); app.MapPut("/api/api-keys/select/{keyId}", HandleSelectKey); app.MapGet("/api/api-keys/selected", HandleGetSelected); } 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.IsPublic, 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 isPublic = body.TryGetProperty("isPublic", out var pub) && pub.GetBoolean(); var record = new ApiKeyRecord { KeyId = Guid.NewGuid().ToString("N"), Provider = provider, Name = name, ApiKey = apiKey, IsPublic = isPublic, CreatedAt = now, UpdatedAt = now }; await db.CreateApiKey(record); await ctx.Response.WriteAsJsonAsync(new { record.KeyId, record.Provider, record.Name, record.ApiKey, record.IsPublic, record.CreatedAt }); } private static async Task HandleUpdate(HttpContext ctx, 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; } 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); await ctx.Response.WriteAsJsonAsync(new { ok = true }); } private static async Task HandleTogglePublic(HttpContext ctx, 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; } var newPublic = !existing.IsPublic; await db.SetApiKeyPublic(keyId, newPublic); await ctx.Response.WriteAsJsonAsync(new { ok = true, isPublic = newPublic }); } private static async Task HandleDelete(HttpContext ctx, 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; } 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; } switch (existing.Provider) { case "anthropic": { var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; 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 }); break; } case "openai": { var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message }); break; } case "google": { var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message }); break; } case "stripe": { var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null); await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.IsLiveKey ? "Live" : "Test", result.AccountId, result.AccountName, result.AccountEmail, result.AccountCountry, result.Currency, result.BalanceAvailable, result.BalancePending, result.ChargesEnabled, result.PayoutsEnabled }); break; } default: { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = $"Check für Provider '{existing.Provider}' nicht unterstützt" }); break; } } } private static async Task HandleCheckAll(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var allKeys = await db.GetApiKeys(); var results = new List(); foreach (var key in allKeys) { switch (key.Provider) { case "anthropic": { var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey); var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h); results.Add(new { key.KeyId, key.Name, status = dbStatus, result.Plan, result.Usage5h, result.Usage7d }); break; } case "openai": { var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null); results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null }); break; } case "google": { var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null); results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null }); break; } case "stripe": { var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey); var dbStatus = result.Status == "ok" ? "valid" : result.Status; await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null); results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.IsLiveKey ? "Live" : "Test", Usage5h = (double?)null, Usage7d = (double?)null }); break; } } } await ctx.Response.WriteAsJsonAsync(new { checked_ = results.Count, results }); } // ── 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" }); } var mode = ctx.Request.Query["mode"].FirstOrDefault() ?? "full"; var validModes = new[] { "quick", "web", "full" }; if (!validModes.Contains(mode)) mode = "full"; scanner.TriggerScan(mode); return ctx.Response.WriteAsJsonAsync(new { ok = true, message = $"{mode}-Scan gestartet" }); } private static Task HandleScanStatus(HttpContext ctx, KeyScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; } var progress = scanner.Progress; return ctx.Response.WriteAsJsonAsync(new { running = scanner.IsRunning, lastRun = scanner.LastRun, lastResult = scanner.LastResult, phase = progress.Phase, detail = progress.Detail, current = progress.Current, total = progress.Total, keysFound = progress.KeysFound }); } private static async Task HandleGetScanned(HttpContext ctx, DatabaseService db) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var type = ctx.Request.Query["type"].FirstOrDefault(); var keys = string.IsNullOrEmpty(type) ? await db.GetScannedKeys() : await db.GetScannedKeysByType(type); 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 HandleGetScannedSummary(HttpContext ctx, DatabaseService db) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var summary = await db.GetScannedKeysSummary(); await ctx.Response.WriteAsJsonAsync(summary); } private static async Task HandleRecheckAll(HttpContext ctx, DatabaseService db, KeyScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var keys = await db.GetScannedKeys(); var notAdopted = keys.Where(k => k.AdoptedApiKeyId == null).ToList(); int success = 0, errors = 0; foreach (var sk in notAdopted) { try { await scanner.CheckAndUpdateKey(sk); success++; } catch { errors++; } } await ctx.Response.WriteAsJsonAsync(new { ok = true, total = notAdopted.Count, success, errors }); } 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"); // Provider anhand des Key-Typs bestimmen var keyType = KeyPatterns.DetectType(sk.ApiKey); string provider; if (keyType.StartsWith("openai")) provider = "openai"; else if (keyType == "google_api") provider = "google"; else if (keyType.StartsWith("stripe")) provider = "stripe"; else provider = "anthropic"; var record = new ApiKeyRecord { KeyId = Guid.NewGuid().ToString("N"), Provider = provider, Name = title, ApiKey = sk.ApiKey, IsPublic = 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 }); } // ── User-Endpoints ── private static async Task HandleGetAvailable(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); if (userId == null) { ctx.Response.StatusCode = 401; return; } var isAdmin = ctx.IsAdmin(); var keys = isAdmin ? await db.GetApiKeys() : await db.GetPublicApiKeys(); var result = keys.Select(k => new { k.KeyId, k.Provider, k.Name, k.IsPublic, k.Status, k.Plan, k.Usage7d, k.Usage5h, k.CheckedAt }); await ctx.Response.WriteAsJsonAsync(result); } private static async Task HandleSelectKey(HttpContext ctx, DatabaseService db, string keyId) { var userId = ctx.GetUserId(); if (userId == null) { ctx.Response.StatusCode = 401; return; } var key = await db.GetApiKey(keyId); if (key == null) { ctx.Response.StatusCode = 404; await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" }); return; } // Nicht-Admins dürfen nur public Keys auswählen if (!ctx.IsAdmin() && !key.IsPublic) { ctx.Response.StatusCode = 403; await ctx.Response.WriteAsJsonAsync(new { error = "Key ist nicht öffentlich" }); return; } var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "agentId required" }); return; } await db.SetAgentApiKey(userId, agentId, keyId); await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, agentId, name = key.Name }); } private static async Task HandleGetSelected(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); if (userId == null) { ctx.Response.StatusCode = 401; return; } var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); if (string.IsNullOrEmpty(agentId)) { await ctx.Response.WriteAsJsonAsync(new { selected = false }); return; } var key = await db.GetAgentApiKeyRecord(userId, agentId); if (key == null) { await ctx.Response.WriteAsJsonAsync(new { selected = false }); return; } await ctx.Response.WriteAsJsonAsync(new { selected = true, keyId = key.KeyId, provider = key.Provider, name = key.Name, isPublic = key.IsPublic, status = key.Status, plan = key.Plan, usage5h = key.Usage5h, usage7d = key.Usage7d }); } private static async Task HandleEventScannerStatus(HttpContext ctx, GitHubEventScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } await ctx.Response.WriteAsJsonAsync(new { enabled = scanner.Enabled, running = scanner.IsRunning, lastPoll = scanner.LastPoll, eventsPerHour = scanner.EventsPerHour, keysFoundTotal = scanner.KeysFoundTotal, pollCount = scanner.PollCount, lastError = scanner.LastError }); } private static async Task HandleEventScannerLog(HttpContext ctx, GitHubEventScannerService scanner) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var log = scanner.GetLog(); await ctx.Response.WriteAsJsonAsync(new { entries = log.Select(e => new { e.Time, e.Type, e.Message }) }); } private static async Task HandleGetScannerLogs(HttpContext ctx, DatabaseService db) { if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } var limit = int.TryParse(ctx.Request.Query["limit"], out var l) ? Math.Min(l, 1000) : 200; var offset = int.TryParse(ctx.Request.Query["offset"], out var o) ? o : 0; var type = ctx.Request.Query["type"].FirstOrDefault(); var scanner = ctx.Request.Query["scanner"].FirstOrDefault(); var logs = await db.GetScannerLogs(limit, offset, type, scanner); var total = await db.GetScannerLogCount(type, scanner); await ctx.Response.WriteAsJsonAsync(new { logs, total }); } }