- POST /api/admin/api-keys/scanned/recheck-all: prüft alle nicht-adoptierten Scanned-Keys sequenziell - Button mit Loading-State, Ergebnis-Toast und Auto-Reload - EasyMDE/CodeMirror refresh() nach initMoreButton() Einblenden (behebt leeren Editor-Bug)
470 lines
16 KiB
C#
470 lines
16 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.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);
|
|
|
|
// 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;
|
|
}
|
|
|
|
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<object>();
|
|
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 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");
|
|
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
|
|
});
|
|
}
|
|
|
|
}
|