using System; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; 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); } 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(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; } try { var client = httpFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(30); var payload = JsonSerializer.Serialize(new { model = "claude-sonnet-4-20250514", max_tokens = 1, messages = new[] { new { role = "user", content = "hi" } } }); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages"); request.Headers.Add("x-api-key", existing.ApiKey); request.Headers.Add("anthropic-version", "2023-06-01"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var resp = await client.SendAsync(request); var headers = resp.Headers; string GetHeader(string name) { if (headers.TryGetValues("anthropic-ratelimit-" + name, out var v1)) return string.Join("", v1); if (headers.TryGetValues(name, out var v2)) return string.Join("", v2); return null; } if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Ungültig oder abgelaufen (401)" }); return; } if (resp.StatusCode == (System.Net.HttpStatusCode)429) { await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Rate Limited (429)" }); return; } if (resp.StatusCode == System.Net.HttpStatusCode.Forbidden) { await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Insufficient Quota (403)" }); return; } if ((int)resp.StatusCode >= 400) { await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "API-Fehler (" + (int)resp.StatusCode + ")" }); return; } // Key ist gültig – Header auslesen var d7Util = GetHeader("unified-7d-utilization"); var d5hUtil = GetHeader("unified-5h-utilization"); var overageUtil = GetHeader("unified-overage-utilization"); var d7Status = GetHeader("unified-7d-status"); var d5hStatus = GetHeader("unified-5h-status"); var overageStatus = GetHeader("unified-overage-status"); // Subscription-Keys haben unified-* Headers, API-Keys nicht var isSubscription = d7Util != null || d5hUtil != null; var fallbackPct = GetHeader("unified-fallback-percentage"); // Plan-Erkennung: // - overage > 0 → definitiv MAX (Extra Usage aktiv, nur bei Max sinnvoll nutzbar) // - kein fallback-percentage Header → PRO // - sonst → Pro/Max (nicht unterscheidbar) string keyType; if (!isSubscription) keyType = "API Console"; else if (overageUtil != null && double.TryParse(overageUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var ovCheck) && ovCheck > 0) keyType = "Max"; else if (fallbackPct == null) keyType = "Pro"; else keyType = "Pro/Max"; double? usage7d = null, usage5h = null, overage = null; if (d7Util != null && double.TryParse(d7Util, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var d7)) usage7d = Math.Round(d7 * 100, 1); if (d5hUtil != null && double.TryParse(d5hUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var d5)) usage5h = Math.Round(d5 * 100, 1); if (overageUtil != null && double.TryParse(overageUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var ov)) overage = Math.Round(ov * 100, 1); await ctx.Response.WriteAsJsonAsync(new { status = "ok", keyType, isSubscription, usage7d, usage5h, overage, status7d = d7Status, status5h = d5hStatus, statusOverage = overageStatus }); } catch (Exception ex) { await ctx.Response.WriteAsJsonAsync(new { status = "error", message = ex.Message }); } } }