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.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); // 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; } 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 }); } 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 anthropicKeys = allKeys.Where(k => k.Provider == "anthropic").ToList(); var results = new List(); foreach (var key in anthropicKeys) { 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 }); } 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; } 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, 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; } await db.SetUserSelectedApiKey(userId, keyId); await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, 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 key = await db.GetUserSelectedApiKey(userId); 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 }); } }