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:
Richard
2026-09-13 13:45:54 +02:00
co-authored by Claude Opus 5
parent 2278d1ccf3
commit 99172a6be2
5 changed files with 244 additions and 125 deletions
@@ -7,9 +7,22 @@ using PolyTraderSharp.Services;
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 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)
{
@@ -24,51 +37,23 @@ namespace PolyTrader.Web.Endpoints
port = opt.Port,
localOnly = opt.LocalOnly,
hasToken = !string.IsNullOrWhiteSpace(opt.AuthToken),
token = opt.AuthToken
managedIn = "desktop"
});
});
group.MapPost("/webui", (UpdateWebUiSettingsRequest req, IOptions<WebUiOptions> optionsProvider, TerminalLogger logger) =>
{
var opt = optionsProvider.Value;
opt.Enabled = req.Enabled;
opt.Port = req.Port > 0 && req.Port <= 65535 ? req.Port : 6001;
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
});
});
// 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 -
// ein altes Skript hielte das fuer einen Erfolg.
group.MapPost("/webui", RejectDesktopOnly);
group.MapPost("/webui/generate-token", RejectDesktopOnly);
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);
}
}
}
+19
View File
@@ -1162,3 +1162,22 @@ tr:hover td {
content: "⚠️";
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;
}
+20 -39
View File
@@ -1151,46 +1151,27 @@ Wähle ein Profil und stelle mir eine Frage zu deinen Positionen, Risiken oder d
<!-- VIEW: SETTINGS -->
<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">
<form id="settingsForm" onsubmit="saveSettings(event)">
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="cfgEnabled">
WebGUI Server aktiv
</label>
<div class="form-desc">Steuert, ob Kestrel beim Programmstart hochfährt.</div>
</div>
<div class="form-row">
<label for="cfgPort">Server-Port</label>
<input type="number" id="cfgPort" class="form-input" min="1024" max="65535" required>
<div class="form-desc">Standard ist 6001. Änderung wird nach dem nächsten Neustart aktiv.</div>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="cfgLocalOnly">
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 class="card-title"><span>Zugang zum WebUI</span></div>
<div class="form-desc" style="margin-bottom: 14px;">
Zugang, Port und Token werden ausschließlich im Desktop verwaltet
(<b>Einstellungen → WebUI</b>). Hier stehen sie nur zur Ansicht.
</div>
<dl class="settings-readonly">
<dt>WebUI-Server</dt>
<dd id="cfgEnabled">…</dd>
<dt>Port</dt>
<dd id="cfgPort">…</dd>
<dt>Erreichbar</dt>
<dd id="cfgBind">…</dd>
<dt>Zugangs-Token</dt>
<dd id="cfgTokenStatus">…</dd>
</dl>
</div>
</section>
</main>
+8 -45
View File
@@ -585,54 +585,17 @@ async function loadSettings() {
if (!res.ok) return;
const opt = await res.json();
document.getElementById('cfgEnabled').checked = opt.enabled;
document.getElementById('cfgPort').value = opt.port;
document.getElementById('cfgLocalOnly').checked = opt.localOnly;
document.getElementById('cfgToken').value = opt.token || '';
document.getElementById('cfgEnabled').textContent = opt.enabled ? 'aktiv' : 'deaktiviert';
document.getElementById('cfgPort').textContent = opt.port;
document.getElementById('cfgBind').textContent = opt.localOnly
? '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) {}
}
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
async function logout() {
try {