Files
gott/Endpoints/ApiKeysEndpoint.cs
T
Meleki 608118a823 Key-Scanner: Auth-Header fix, 401-mit-Usage-Handling, breitere Suche, Live-Refresh
- AnthropicKeyChecker: Bearer-Auth + CLI-Header statt x-api-key (identisch mit AnthropicService)
- 401 mit Usage-Headern → 'usage_limited' statt 'invalid' (Key gültig, Limit erreicht)
- Haiku-Phase entfernt, Streaming-Logik entfernt → ein einfacher Request reicht
- KeyScannerService: SearchLanguages 1→13, PathExtensions 24 neue Pfad-Filter
- ApiKeysEndpoint: usage_limited als 'ok' ans Frontend
- Frontend: Adopt-Button auch bei quota_exceeded, Live-Refresh alle 3s während Scan
- CSS: Overflow-Ellipsis für lange URLs in Admin-Listen
2026-03-12 17:11:06 +01:00

363 lines
12 KiB
C#

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.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 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<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);
// 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);
// Metadaten in DB speichern
await db.UpdateApiKeyMeta(keyId, result.Status, result.Plan, result.Usage7d, result.Usage5h);
await ctx.Response.WriteAsJsonAsync(new
{
status = (result.Status == "ok" || result.Status == "usage_limited") ? "ok" : "error",
message = result.Message,
keyType = 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.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;
}
// URL als Titel verwenden, Metadaten übertragen
var title = !string.IsNullOrWhiteSpace(sk.SourceUrl)
? sk.SourceUrl.Replace("https://github.com/", "")
: $"Scan: {sk.Plan}";
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,
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 });
}
}