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
142 lines
4.9 KiB
C#
142 lines
4.9 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Nodes;
|
||
using System.Threading.Tasks;
|
||
using Gott.Extensions;
|
||
using Gott.Services;
|
||
using Microsoft.AspNetCore.Builder;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.Extensions.Configuration;
|
||
|
||
namespace Gott.Endpoints;
|
||
|
||
public static class ServerSettingsEndpoint
|
||
{
|
||
public static void MapServerSettingsEndpoints(this WebApplication app)
|
||
{
|
||
app.MapGet("/api/admin/server-settings", HandleGet);
|
||
app.MapPut("/api/admin/server-settings", HandleUpdate);
|
||
}
|
||
|
||
private static async Task HandleGet(HttpContext ctx, ServerSettingsService settings, IConfiguration config)
|
||
{
|
||
if (!ctx.IsAdmin())
|
||
{
|
||
ctx.Response.StatusCode = 403;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||
return;
|
||
}
|
||
|
||
// Port aus appsettings (nicht DB) – als erstes Feld
|
||
var port = config.GetValue<int>("Port", 19000);
|
||
var allSettings = new List<object>
|
||
{
|
||
new
|
||
{
|
||
key = "Port",
|
||
type = "int",
|
||
value = (object)port,
|
||
label = "Server-Port (erfordert Neustart)",
|
||
inputType = "number",
|
||
}
|
||
};
|
||
allSettings.AddRange(settings.GetAll());
|
||
|
||
await ctx.Response.WriteAsJsonAsync(new { settings = allSettings });
|
||
}
|
||
|
||
private static async Task HandleUpdate(HttpContext ctx, ServerSettingsService settings, IConfiguration config)
|
||
{
|
||
if (!ctx.IsAdmin())
|
||
{
|
||
ctx.Response.StatusCode = 403;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||
return;
|
||
}
|
||
|
||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||
var hint = "";
|
||
|
||
// Port separat behandeln – in appsettings.Production.json speichern
|
||
if (body.TryGetProperty("Port", out var portProp) && portProp.TryGetInt32(out var newPort))
|
||
{
|
||
var currentPort = config.GetValue<int>("Port", 19000);
|
||
if (newPort != currentPort && newPort > 0 && newPort <= 65535)
|
||
{
|
||
SavePortToAppSettings(ctx, newPort);
|
||
hint = "Port geändert – Neustart erforderlich!";
|
||
}
|
||
}
|
||
|
||
// Alle anderen Settings in DB speichern
|
||
foreach (var def in ServerSettingsService.Definitions)
|
||
{
|
||
if (!body.TryGetProperty(def.Key, out var prop)) continue;
|
||
|
||
switch (def.Type)
|
||
{
|
||
case "string":
|
||
settings.Set(def.Key, prop.GetString() ?? "");
|
||
break;
|
||
case "int":
|
||
settings.SetInt(def.Key, prop.TryGetInt32(out var i) ? i : 0);
|
||
break;
|
||
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;
|
||
}
|
||
}
|
||
|
||
await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = string.IsNullOrEmpty(hint) ? "Einstellungen gespeichert" : hint });
|
||
}
|
||
|
||
private static void SavePortToAppSettings(HttpContext ctx, int port)
|
||
{
|
||
// appsettings.Production.json im ContentRootPath (= publish-Verzeichnis)
|
||
var contentRoot = ctx.RequestServices.GetService(typeof(Microsoft.AspNetCore.Hosting.IWebHostEnvironment)) as Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
|
||
var path = Path.Combine(contentRoot?.ContentRootPath ?? AppContext.BaseDirectory, "appsettings.Production.json");
|
||
|
||
JsonObject obj;
|
||
if (File.Exists(path))
|
||
{
|
||
try
|
||
{
|
||
var existing = File.ReadAllText(path);
|
||
obj = JsonNode.Parse(existing) as JsonObject ?? new JsonObject();
|
||
}
|
||
catch
|
||
{
|
||
obj = new JsonObject();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
obj = new JsonObject();
|
||
}
|
||
|
||
obj["Port"] = port;
|
||
|
||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||
File.WriteAllText(path, obj.ToJsonString(options));
|
||
}
|
||
}
|