WebUI gibt keine Geheimnisse mehr heraus und aendert keine Zugangseinstellungen
Behebt #562. Entscheidung Richard, 13.09.2026: Geheimnisse verwalten wir ausschliesslich im Desktop. Bis dahin lieferte GET /api/settings/webui das Zugangs-Token im Klartext. Ueber das WebUI liessen sich ausserdem ein Token setzen, ein neues erzeugen und LocalOnly abschalten. Wer das Token hatte, konnte das WebUI damit ueber unverschluesseltes HTTP ins Netz oeffnen, sich den Zugang verlegen oder sich selbst aussperren. Jetzt: - GET /api/settings/webui liefert Enabled, Port, LocalOnly und vom Token nur hasToken. - POST /api/settings/webui und /webui/generate-token antworten mit 403 und protokollieren den Versuch. Sie bleiben als ausdrueckliche Ablehnung stehen: Ohne eigene Route faengt die Fallback-Route den POST ab und antwortet mit 200, ein altes Skript hielte das fuer einen Erfolg. - Die WebUI-Einstellungsansicht zeigt den Zugang nur noch an. Gepflegt wird er im Desktop unter Einstellungen -> WebUI. WebUiSecretsTests zaehlt alle GET-Routen selbst auf und prueft jede Antwort gegen Kanarienwerte. Geprueft werden ApiKey, ApiSecret, ApiPassphrase und PrivateKey eines Kontos, Mullvad-Account, Alchemy-Key, Watchdog-Token, Lizenzschluessel und das WebUI-Token. Eine neue Route, die ein Konto oder die Server-Einstellungen serialisiert, faellt damit auf, ohne dass jemand an diesen Test denken muss. Gegen die alte Route schlug er fehl ("GET /api/settings/webui liefert ein Geheimnis aus"), jetzt ist er gruen. Ausserdem festgehalten: /api/accounts kennt nur GET. Polymarket-Konten bleiben im WebUI lesbar und nicht aenderbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,22 @@ using PolyTraderSharp.Services;
|
|||||||
|
|
||||||
namespace PolyTrader.Web.Endpoints
|
namespace PolyTrader.Web.Endpoints
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Zugangseinstellungen des WebUI - nur lesend (#562).
|
||||||
|
///
|
||||||
|
/// <para>Entscheidung Richard, 13.09.2026: Geheimnisse verwalten wir ausschliesslich im Desktop. Bis
|
||||||
|
/// dahin lieferte GET /api/settings/webui das Zugangs-Token im Klartext aus, und ueber das WebUI
|
||||||
|
/// liessen sich Token setzen, ein neues erzeugen und LocalOnly abschalten. Wer das Token hatte,
|
||||||
|
/// konnte damit das WebUI ins Netz oeffnen (unverschluesseltes HTTP), sich den Zugang verlegen oder
|
||||||
|
/// sich selbst aussperren.</para>
|
||||||
|
///
|
||||||
|
/// <para>Port, LocalOnly, Enabled und das Token werden jetzt nur noch im Desktop unter
|
||||||
|
/// Einstellungen -> WebUI gepflegt. Das WebUI zeigt die Werte an, vom Token nur, ob es gesetzt ist.</para>
|
||||||
|
/// </summary>
|
||||||
public static class SettingsEndpoints
|
public static class SettingsEndpoints
|
||||||
{
|
{
|
||||||
public record UpdateWebUiSettingsRequest(bool Enabled, int Port, bool LocalOnly, string? AuthToken);
|
internal const string DesktopOnlyMessage =
|
||||||
|
"Zugang, Port und Token des WebUI werden ausschließlich im Desktop verwaltet (Einstellungen → WebUI).";
|
||||||
|
|
||||||
public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder routes)
|
public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder routes)
|
||||||
{
|
{
|
||||||
@@ -24,51 +37,23 @@ namespace PolyTrader.Web.Endpoints
|
|||||||
port = opt.Port,
|
port = opt.Port,
|
||||||
localOnly = opt.LocalOnly,
|
localOnly = opt.LocalOnly,
|
||||||
hasToken = !string.IsNullOrWhiteSpace(opt.AuthToken),
|
hasToken = !string.IsNullOrWhiteSpace(opt.AuthToken),
|
||||||
token = opt.AuthToken
|
managedIn = "desktop"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("/webui", (UpdateWebUiSettingsRequest req, IOptions<WebUiOptions> optionsProvider, TerminalLogger logger) =>
|
// Die frueheren Schreibwege bleiben als ausdrueckliche Ablehnung stehen, statt zu verschwinden:
|
||||||
{
|
// Ohne eigene Route faengt die Fallback-Route (index.html) den POST ab und antwortet mit 200 -
|
||||||
var opt = optionsProvider.Value;
|
// ein altes Skript hielte das fuer einen Erfolg.
|
||||||
opt.Enabled = req.Enabled;
|
group.MapPost("/webui", RejectDesktopOnly);
|
||||||
opt.Port = req.Port > 0 && req.Port <= 65535 ? req.Port : 6001;
|
group.MapPost("/webui/generate-token", RejectDesktopOnly);
|
||||||
opt.LocalOnly = req.LocalOnly;
|
|
||||||
if (req.AuthToken != null)
|
|
||||||
{
|
|
||||||
opt.AuthToken = req.AuthToken.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
WebUiConfigManager.SaveToLocalSettings(opt);
|
|
||||||
logger.Info($"🌐 WebUI: Einstellungen gespeichert (Port {opt.Port}, LocalOnly: {opt.LocalOnly}).");
|
|
||||||
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
success = true,
|
|
||||||
enabled = opt.Enabled,
|
|
||||||
port = opt.Port,
|
|
||||||
localOnly = opt.LocalOnly,
|
|
||||||
hasToken = !string.IsNullOrWhiteSpace(opt.AuthToken)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.MapPost("/webui/generate-token", (IOptions<WebUiOptions> optionsProvider, TerminalLogger logger) =>
|
|
||||||
{
|
|
||||||
var opt = optionsProvider.Value;
|
|
||||||
string newToken = WebUiConfigManager.GenerateSecureToken();
|
|
||||||
opt.AuthToken = newToken;
|
|
||||||
|
|
||||||
WebUiConfigManager.SaveToLocalSettings(opt);
|
|
||||||
logger.Info("🔐 WebUI: Neues Zugangs-Token generiert und in appsettings.Local.json gespeichert.");
|
|
||||||
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
success = true,
|
|
||||||
token = newToken
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IResult RejectDesktopOnly(HttpContext context, TerminalLogger logger)
|
||||||
|
{
|
||||||
|
logger.Warning($"🌐 WebUI: Änderung der Zugangseinstellungen über {context.Request.Path} abgelehnt – nur im Desktop möglich.");
|
||||||
|
return Results.Json(new { error = DesktopOnlyMessage }, statusCode: StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1162,3 +1162,22 @@ tr:hover td {
|
|||||||
content: "⚠️";
|
content: "⚠️";
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nur lesend angezeigte Einstellungen (#562) */
|
||||||
|
.settings-readonly {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 10px 28px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-readonly dt {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-readonly dd {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1151,46 +1151,27 @@ Wähle ein Profil und stelle mir eine Frage zu deinen Positionen, Risiken oder d
|
|||||||
|
|
||||||
<!-- VIEW: SETTINGS -->
|
<!-- VIEW: SETTINGS -->
|
||||||
<section id="view-settings" class="view-panel">
|
<section id="view-settings" class="view-panel">
|
||||||
<h2 style="margin-bottom: 16px; font-size: 18px;">WebGUI Anwendungseinstellungen</h2>
|
<h2 style="margin-bottom: 16px; font-size: 18px;">Einstellungen</h2>
|
||||||
|
|
||||||
|
<!-- Zugang zum WebUI (#562): nur lesend. Port, Netzfreigabe und Token werden ausschliesslich
|
||||||
|
im Desktop gepflegt. Wer das Token hat, soll sich damit weder den Zugang ins Netz oeffnen
|
||||||
|
noch ihn verlegen oder sich selbst aussperren koennen. -->
|
||||||
<div class="card settings-form">
|
<div class="card settings-form">
|
||||||
<form id="settingsForm" onsubmit="saveSettings(event)">
|
<div class="card-title"><span>Zugang zum WebUI</span></div>
|
||||||
<div class="form-row">
|
<div class="form-desc" style="margin-bottom: 14px;">
|
||||||
<label class="checkbox-label">
|
Zugang, Port und Token werden ausschließlich im Desktop verwaltet
|
||||||
<input type="checkbox" id="cfgEnabled">
|
(<b>Einstellungen → WebUI</b>). Hier stehen sie nur zur Ansicht.
|
||||||
WebGUI Server aktiv
|
|
||||||
</label>
|
|
||||||
<div class="form-desc">Steuert, ob Kestrel beim Programmstart hochfährt.</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<dl class="settings-readonly">
|
||||||
<div class="form-row">
|
<dt>WebUI-Server</dt>
|
||||||
<label for="cfgPort">Server-Port</label>
|
<dd id="cfgEnabled">…</dd>
|
||||||
<input type="number" id="cfgPort" class="form-input" min="1024" max="65535" required>
|
<dt>Port</dt>
|
||||||
<div class="form-desc">Standard ist 6001. Änderung wird nach dem nächsten Neustart aktiv.</div>
|
<dd id="cfgPort">…</dd>
|
||||||
</div>
|
<dt>Erreichbar</dt>
|
||||||
|
<dd id="cfgBind">…</dd>
|
||||||
<div class="form-row">
|
<dt>Zugangs-Token</dt>
|
||||||
<label class="checkbox-label">
|
<dd id="cfgTokenStatus">…</dd>
|
||||||
<input type="checkbox" id="cfgLocalOnly">
|
</dl>
|
||||||
Nur lokaler Zugriff (127.0.0.1)
|
|
||||||
</label>
|
|
||||||
<div class="form-desc">Wenn deaktiviert, ist das WebUI über alle Netzwerkschnittstellen (LAN/VPN) erreichbar.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="cfgToken">Zugangs-Token</label>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<input type="text" id="cfgToken" class="form-input" style="font-family: var(--font-mono); font-size: 12px;" readonly>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="copyToken()">Kopieren</button>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="generateNewToken()">Neu generieren</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-desc">Wird in appsettings.Local.json gespeichert. Bei leerem Token ist das WebUI gesperrt.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; align-items: center; margin-top: 24px;">
|
|
||||||
<button type="submit" class="btn btn-primary" id="btnSaveSettings">Einstellungen speichern</button>
|
|
||||||
<span id="settingsStatusMsg" style="font-size: 13px; color: var(--positive); display: none;">✓ Gespeichert</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -585,54 +585,17 @@ async function loadSettings() {
|
|||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const opt = await res.json();
|
const opt = await res.json();
|
||||||
|
|
||||||
document.getElementById('cfgEnabled').checked = opt.enabled;
|
document.getElementById('cfgEnabled').textContent = opt.enabled ? 'aktiv' : 'deaktiviert';
|
||||||
document.getElementById('cfgPort').value = opt.port;
|
document.getElementById('cfgPort').textContent = opt.port;
|
||||||
document.getElementById('cfgLocalOnly').checked = opt.localOnly;
|
document.getElementById('cfgBind').textContent = opt.localOnly
|
||||||
document.getElementById('cfgToken').value = opt.token || '';
|
? 'nur lokal (127.0.0.1)'
|
||||||
|
: 'im Netzwerk – alle Schnittstellen, unverschlüsseltes HTTP';
|
||||||
|
document.getElementById('cfgTokenStatus').textContent = opt.hasToken
|
||||||
|
? 'gesetzt'
|
||||||
|
: 'nicht gesetzt – das WebUI ist gesperrt';
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const enabled = document.getElementById('cfgEnabled').checked;
|
|
||||||
const port = parseInt(document.getElementById('cfgPort').value, 10);
|
|
||||||
const localOnly = document.getElementById('cfgLocalOnly').checked;
|
|
||||||
const token = document.getElementById('cfgToken').value.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/settings/webui', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ enabled, port, localOnly, authToken: token })
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
const msg = document.getElementById('settingsStatusMsg');
|
|
||||||
msg.style.display = 'inline';
|
|
||||||
setTimeout(() => { msg.style.display = 'none'; }, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateNewToken() {
|
|
||||||
if (!confirm('Neues Zugangs-Token generieren? Alle bestehenden Sessions müssen sich danach neu anmelden.')) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/settings/webui/generate-token', { method: 'POST' });
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById('cfgToken').value = data.token;
|
|
||||||
alert('Neues Token generiert und in appsettings.Local.json gespeichert:\n\n' + data.token);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToken() {
|
|
||||||
const input = document.getElementById('cfgToken');
|
|
||||||
if (!input.value) return;
|
|
||||||
navigator.clipboard.writeText(input.value).then(() => {
|
|
||||||
alert('Token in Zwischenablage kopiert!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests.WebUi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Geheimnisse verlassen den Server nie ueber das WebUI (#562).
|
||||||
|
///
|
||||||
|
/// <para>Entscheidung Richard, 13.09.2026: Geheimnisse werden ausschliesslich im Desktop verwaltet.
|
||||||
|
/// Dieser Test haelt das fest, ohne sich auf eine Liste von Routen zu verlassen: Er zaehlt alle
|
||||||
|
/// GET-Routen selbst auf und prueft jede Antwort gegen eindeutige Kanarienwerte. Eine neue Route,
|
||||||
|
/// die versehentlich ein Konto oder die Server-Einstellungen serialisiert, faellt damit auf, ohne
|
||||||
|
/// dass jemand an diesen Test denken muss.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class WebUiSecretsTests
|
||||||
|
{
|
||||||
|
private const string ApiKeyCanary = "CANARY-APIKEY-4b1f";
|
||||||
|
private const string ApiSecretCanary = "CANARY-APISECRET-8c2e";
|
||||||
|
private const string ApiPassphraseCanary = "CANARY-PASSPHRASE-1d7a";
|
||||||
|
private const string PrivateKeyCanary = "CANARY-PRIVATEKEY-0x9f1e2d3c";
|
||||||
|
private const string MullvadCanary = "CANARY-MULLVAD-5566";
|
||||||
|
private const string AlchemyCanary = "CANARY-ALCHEMYKEY-7a7a";
|
||||||
|
private const string WatchdogCanary = "CANARY-WATCHDOGTOKEN-3e3e";
|
||||||
|
private const string LicenseCanary = "CANARY-LICENSE-2f2f";
|
||||||
|
|
||||||
|
private static readonly string[] Secrets =
|
||||||
|
{
|
||||||
|
ApiKeyCanary, ApiSecretCanary, ApiPassphraseCanary, PrivateKeyCanary,
|
||||||
|
MullvadCanary, AlchemyCanary, WatchdogCanary, LicenseCanary,
|
||||||
|
WebUiTestHost.Token
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ein Live-Konto mit allen Schluesseln und Server-Einstellungen mit allen Geheimnissen. Die
|
||||||
|
/// Modul-Ablagen sind registriert, damit die Routen echten Code durchlaufen statt frueh
|
||||||
|
/// mit "nicht verfuegbar" zu antworten.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<WebUiTestHost> StartWithSecretsAsync()
|
||||||
|
{
|
||||||
|
var settings = new ServerSettings
|
||||||
|
{
|
||||||
|
MullvadAccount = MullvadCanary,
|
||||||
|
PolygonRpcUrl = "wss://polygon-mainnet.g.alchemy.com/v2/" + AlchemyCanary,
|
||||||
|
WatchdogToken = WatchdogCanary,
|
||||||
|
LicenseKey = LicenseCanary
|
||||||
|
};
|
||||||
|
|
||||||
|
var web = await WebUiTestHost.StartAsync(s =>
|
||||||
|
{
|
||||||
|
WebUiTestHost.AddCopyTradingStore(s);
|
||||||
|
WebUiTestHost.AddResolutionFarmingStore(s);
|
||||||
|
s.AddSingleton(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
web.State.Accounts[1] = new AccountState
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
Name = "Live-Konto",
|
||||||
|
WalletAddress = "0x00000000000000000000000000000000000000a1",
|
||||||
|
ApiKey = ApiKeyCanary,
|
||||||
|
ApiSecret = ApiSecretCanary,
|
||||||
|
ApiPassphrase = ApiPassphraseCanary,
|
||||||
|
PrivateKey = PrivateKeyCanary,
|
||||||
|
TotalBalance = 1000m,
|
||||||
|
AvailableBalance = 800m
|
||||||
|
};
|
||||||
|
|
||||||
|
return web;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alle GET-Routen unter /api, mit eingesetzten Parametern. Ausgenommen ist nur der Live-Log-Stream:
|
||||||
|
/// Server-Sent Events enden nie. Er liefert dieselben Eintraege wie GET /api/logs, und die
|
||||||
|
/// Route ist abgedeckt.
|
||||||
|
/// </summary>
|
||||||
|
private static List<string> ApiGetPaths(WebUiTestHost web)
|
||||||
|
{
|
||||||
|
var paths = new List<string>();
|
||||||
|
foreach (var endpoint in web.Get<EndpointDataSource>().Endpoints.OfType<RouteEndpoint>())
|
||||||
|
{
|
||||||
|
string raw = endpoint.RoutePattern.RawText ?? string.Empty;
|
||||||
|
if (!raw.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
if (raw.Contains('*')) continue;
|
||||||
|
if (raw.EndsWith("/stream", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
|
var methods = endpoint.Metadata.GetMetadata<HttpMethodMetadata>()?.HttpMethods;
|
||||||
|
if (methods == null || !methods.Contains("GET")) continue;
|
||||||
|
|
||||||
|
paths.Add(Regex.Replace(raw, @"\{([^}:]+)(:([^}]+))?\}",
|
||||||
|
m => m.Groups[3].Value is "int" or "long" ? "1" : "x"));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Keine_GET_Route_liefert_ein_Geheimnis_aus()
|
||||||
|
{
|
||||||
|
await using var web = await StartWithSecretsAsync();
|
||||||
|
|
||||||
|
var paths = ApiGetPaths(web);
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
using var res = await web.Client.GetAsync(path);
|
||||||
|
string body = await res.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
foreach (var secret in Secrets)
|
||||||
|
{
|
||||||
|
Assert.False(body.Contains(secret, StringComparison.Ordinal),
|
||||||
|
$"GET {path} ({(int)res.StatusCode}) liefert ein Geheimnis aus: {secret}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ohne Untergrenze bestuende der Test auch dann, wenn die Aufzaehlung leer bliebe.
|
||||||
|
Assert.True(paths.Count >= 25, $"Nur {paths.Count} GET-Routen gefunden: {string.Join(", ", paths)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Zugangseinstellungen_zeigen_vom_Token_nur_ob_es_gesetzt_ist()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
var options = await web.GetJsonAsync("/api/settings/webui");
|
||||||
|
|
||||||
|
Assert.True(options.GetProperty("hasToken").GetBoolean());
|
||||||
|
Assert.False(options.TryGetProperty("token", out _));
|
||||||
|
Assert.Equal("desktop", options.GetProperty("managedIn").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Zugangseinstellungen_lassen_sich_ueber_das_WebUI_nicht_aendern()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
var (saveStatus, saveBody) = await web.PostJsonAsync("/api/settings/webui",
|
||||||
|
new { enabled = true, port = 7000, localOnly = false, authToken = "selbst-gewaehlt" });
|
||||||
|
var (generateStatus, generateBody) = await web.PostJsonAsync("/api/settings/webui/generate-token");
|
||||||
|
|
||||||
|
Assert.Equal(403, saveStatus);
|
||||||
|
Assert.Equal(403, generateStatus);
|
||||||
|
Assert.Contains("Desktop", saveBody.GetProperty("error").GetString());
|
||||||
|
Assert.False(generateBody.TryGetProperty("token", out _));
|
||||||
|
|
||||||
|
// Nichts davon darf den laufenden Zustand beruehrt haben.
|
||||||
|
Assert.Equal(WebUiTestHost.Token, web.WebOptions.AuthToken);
|
||||||
|
Assert.True(web.WebOptions.LocalOnly);
|
||||||
|
Assert.Equal(6001, web.WebOptions.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Polymarket_Konten_sind_im_WebUI_nur_lesbar()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
var accountRoutes = web.Get<EndpointDataSource>().Endpoints.OfType<RouteEndpoint>()
|
||||||
|
.Where(e => (e.RoutePattern.RawText ?? string.Empty)
|
||||||
|
.StartsWith("/api/accounts", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.NotEmpty(accountRoutes);
|
||||||
|
Assert.All(accountRoutes, e =>
|
||||||
|
Assert.Equal(new[] { "GET" }, e.Metadata.GetMetadata<HttpMethodMetadata>()!.HttpMethods));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user