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
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Gott.Extensions;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
public static class DatabaseExplorerEndpoint
|
||||
{
|
||||
public static void MapDatabaseExplorerEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/admin/db/tables", HandleGetTables);
|
||||
app.MapGet("/api/admin/db/tables/{name}", HandleGetTableData);
|
||||
}
|
||||
|
||||
private static async Task HandleGetTables(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin())
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||
return;
|
||||
}
|
||||
|
||||
using var conn = db.GetConnection();
|
||||
var tables = conn.Query<TableInfo>(
|
||||
"SELECT name, (SELECT COUNT(*) FROM pragma_table_info(m.name)) as column_count " +
|
||||
"FROM sqlite_master m WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var t in tables)
|
||||
{
|
||||
var rowCount = conn.ExecuteScalar<long>($"SELECT COUNT(*) FROM \"{t.Name}\"");
|
||||
result.Add(new { name = t.Name, columns = t.ColumnCount, rows = rowCount });
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { tables = result });
|
||||
}
|
||||
|
||||
private static async Task HandleGetTableData(HttpContext ctx, DatabaseService db, string name)
|
||||
{
|
||||
if (!ctx.IsAdmin())
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Tabellennamen validieren (SQL-Injection verhindern)
|
||||
using var conn = db.GetConnection();
|
||||
var exists = conn.ExecuteScalar<int>(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = @name",
|
||||
new { name });
|
||||
|
||||
if (exists == 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Table not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
var page = 1;
|
||||
var pageSize = 100;
|
||||
if (ctx.Request.Query.TryGetValue("page", out var pageStr) && int.TryParse(pageStr, out var p) && p > 0)
|
||||
page = p;
|
||||
if (ctx.Request.Query.TryGetValue("pageSize", out var psStr) && int.TryParse(psStr, out var ps) && ps > 0 && ps <= 500)
|
||||
pageSize = ps;
|
||||
|
||||
var offset = (page - 1) * pageSize;
|
||||
|
||||
// Spalten-Info
|
||||
var columns = conn.Query<ColumnInfo>($"PRAGMA table_info(\"{name}\")");
|
||||
|
||||
// Daten
|
||||
var totalRows = conn.ExecuteScalar<long>($"SELECT COUNT(*) FROM \"{name}\"");
|
||||
var rows = conn.Query($"SELECT * FROM \"{name}\" LIMIT @limit OFFSET @offset",
|
||||
new { limit = pageSize, offset });
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
table = name,
|
||||
columns,
|
||||
rows,
|
||||
pagination = new
|
||||
{
|
||||
page,
|
||||
pageSize,
|
||||
totalRows,
|
||||
totalPages = (int)Math.Ceiling((double)totalRows / pageSize)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class TableInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int ColumnCount { get; set; }
|
||||
}
|
||||
|
||||
private class ColumnInfo
|
||||
{
|
||||
public int Cid { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public int Notnull { get; set; }
|
||||
public string DfltValue { get; set; }
|
||||
public int Pk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,23 @@ public static class ServerSettingsEndpoint
|
||||
case "bool":
|
||||
settings.SetBool(def.Key, prop.ValueKind == JsonValueKind.True);
|
||||
break;
|
||||
case "string_array":
|
||||
if (prop.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var arr = new List<string>();
|
||||
foreach (var el in prop.EnumerateArray())
|
||||
{
|
||||
var s = el.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(s)) arr.Add(s);
|
||||
}
|
||||
settings.SetStringArray(def.Key, arr.ToArray());
|
||||
}
|
||||
else if (prop.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
// Fallback: CSV-String
|
||||
settings.Set(def.Key, prop.GetString() ?? "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user