Files
gott/Endpoints/ApiKeysEndpoint.cs
MelekiandClaude Opus 4.6 3e19da4d48 refactor: Backend-Deduplizierung – Parameter-Objekte, Shared Helpers, KeyCheckDispatcher
- ToolDefinitionOptions + ToolExecutionContext ersetzen lange Parameterlisten in ToolService
- AppendToLastMessage-Helper in ChatService eliminiert 5x duplizierte Logik
- OpenAiService: ResolveProviderConfig + BuildApiMessages + ConvertToOpenAiTools statt doppeltem Code
- Tool-Definitionen aus einer Quelle (Anthropic→OpenAI Konvertierung statt hardcodierter Liste)
- KeyCheckDispatcher zentralisiert Key-Check-Dispatch (ApiKeysEndpoint, KeyScanner, GitHubEventScanner)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:01:49 +01:00

589 lines
21 KiB
C#

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<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 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<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);
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;
}
var check = await KeyCheckDispatcher.CheckByProvider(httpFactory, existing.Provider, existing.ApiKey);
if (check != null)
{
await db.UpdateApiKeyMeta(keyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
// Provider-spezifische Response-Details
if (check.ProviderResult is KeyCheckResult anthropic)
{
await ctx.Response.WriteAsJsonAsync(new
{
status = check.DbStatus,
message = check.Message,
plan = check.Plan,
anthropic.IsSubscription,
anthropic.Usage7d,
anthropic.Usage5h,
anthropic.Overage,
status7d = anthropic.Status7d,
status5h = anthropic.Status5h,
statusOverage = anthropic.StatusOverage
});
}
else if (check.ProviderResult is StripeCheckResult stripe)
{
await ctx.Response.WriteAsJsonAsync(new
{
status = check.DbStatus,
message = check.Message,
plan = check.Plan,
stripe.AccountId,
stripe.AccountName,
stripe.AccountEmail,
stripe.AccountCountry,
stripe.Currency,
stripe.BalanceAvailable,
stripe.BalancePending,
stripe.ChargesEnabled,
stripe.PayoutsEnabled
});
}
else
{
await ctx.Response.WriteAsJsonAsync(new { status = check.DbStatus, message = check.Message, plan = check.Plan });
}
}
else
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = $"Check für Provider '{existing.Provider}' nicht unterstützt" });
}
}
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<object>();
foreach (var key in allKeys)
{
var check = await KeyCheckDispatcher.CheckByProvider(httpFactory, key.Provider, key.ApiKey);
if (check == null) continue;
await db.UpdateApiKeyMeta(key.KeyId, check.DbStatus, check.Plan, check.Usage7d, check.Usage5h);
results.Add(new { key.KeyId, key.Name, status = check.DbStatus, check.Plan, check.Usage5h, check.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; }
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 agentId = ctx.Request.Query["agentId"].FirstOrDefault();
if (string.IsNullOrEmpty(agentId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "agentId required" });
return;
}
// "none" = Key-Zuweisung entfernen
if (keyId == "none")
{
await db.SetAgentApiKey(userId, agentId, null);
await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId = (string)null, agentId, name = "Kein Key" });
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.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 });
}
}