Paralleles WebGUI mit Kestrel, Token-Sicherung und SSE-Logs implementiert
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using PolyTraderSharp;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class AccountsEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapAccountsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/accounts");
|
||||
|
||||
group.MapGet("/", (TradingState state) =>
|
||||
{
|
||||
var list = state.Accounts.Values
|
||||
.OrderBy(a => a.AccountId)
|
||||
.Select(a => new
|
||||
{
|
||||
accountId = a.AccountId,
|
||||
name = a.Name,
|
||||
isActive = a.IsActive,
|
||||
isDemo = a.IsDemo,
|
||||
closeOnlyMode = a.CloseOnlyMode,
|
||||
usdcBalance = a.AvailableBalance,
|
||||
portfolioValue = a.TotalBalance,
|
||||
walletAddress = a.WalletAddress,
|
||||
openPositionsCount = a.OpenPositions.Count
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PolyTrader.Web.Configuration;
|
||||
using PolyTrader.Web.Security;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
public record LoginRequest(string? Token);
|
||||
public record TokenResponse(string Token);
|
||||
|
||||
public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/auth");
|
||||
|
||||
group.MapGet("/status", (HttpContext context, IOptions<WebUiOptions> optionsProvider) =>
|
||||
{
|
||||
var options = optionsProvider.Value;
|
||||
bool isConfigured = !string.IsNullOrWhiteSpace(options.AuthToken);
|
||||
bool isAuthenticated = TokenAuthMiddleware.IsAuthenticated(context, options.AuthToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
isConfigured,
|
||||
isAuthenticated
|
||||
});
|
||||
});
|
||||
|
||||
group.MapPost("/login", (LoginRequest req, HttpContext context, IOptions<WebUiOptions> optionsProvider) =>
|
||||
{
|
||||
var options = optionsProvider.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.AuthToken))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Kein Zugangs-Token im System konfiguriert." });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(req.Token) || !TokenAuthMiddleware.ValidateToken(req.Token, options.AuthToken))
|
||||
{
|
||||
return Results.Json(new { error = "Ungültiges Zugangs-Token." }, statusCode: StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
context.Response.Cookies.Append(TokenAuthMiddleware.CookieName, req.Token.Trim(), new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Secure = context.Request.IsHttps,
|
||||
Expires = DateTimeOffset.UtcNow.AddHours(options.SessionTimeoutHours),
|
||||
Path = "/"
|
||||
});
|
||||
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
group.MapPost("/logout", (HttpContext context) =>
|
||||
{
|
||||
context.Response.Cookies.Delete(TokenAuthMiddleware.CookieName, new CookieOptions
|
||||
{
|
||||
Path = "/"
|
||||
});
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PolyTraderSharp;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class DashboardEndpoints
|
||||
{
|
||||
public record SetTradingModeRequest(string ModeType, string TargetMode);
|
||||
|
||||
public static IEndpointRouteBuilder MapDashboardEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/dashboard");
|
||||
|
||||
group.MapGet("/summary", (TradingState state, IServiceProvider sp) =>
|
||||
{
|
||||
var vpn = sp.GetService<MullvadVpnService>();
|
||||
var watchdog = sp.GetService<WatchdogHeartbeatService>();
|
||||
|
||||
var accounts = state.Accounts.Values.ToList();
|
||||
decimal totalAvailable = accounts.Sum(a => a.AvailableBalance);
|
||||
decimal totalBalance = accounts.Sum(a => a.TotalBalance);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
globalTradingPaused = state.GlobalTradingPaused,
|
||||
liveTradingMode = state.LiveTradingMode.ToString(),
|
||||
demoTradingMode = state.DemoTradingMode.ToString(),
|
||||
alchemyHealthy = state.IsAlchemyHealthy,
|
||||
globalPnl = state.GlobalPnl,
|
||||
vpnConnected = vpn?.IsConnected ?? false,
|
||||
vpnEnabled = vpn != null,
|
||||
watchdogActive = watchdog != null,
|
||||
accountCount = accounts.Count,
|
||||
totalUsdcBalance = totalAvailable,
|
||||
totalPortfolioValue = totalBalance
|
||||
});
|
||||
});
|
||||
|
||||
group.MapPost("/trading-mode", (SetTradingModeRequest req, TradingState state, TerminalLogger logger) =>
|
||||
{
|
||||
if (req.ModeType.Equals("live", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (System.Enum.TryParse<TradingMode>(req.TargetMode, true, out var parsed))
|
||||
{
|
||||
state.LiveTradingMode = parsed;
|
||||
logger.Info($"🌐 WebUI: Live-Trading-Modus auf '{parsed}' geändert.");
|
||||
return Results.Ok(new { success = true, currentMode = state.LiveTradingMode.ToString() });
|
||||
}
|
||||
}
|
||||
else if (req.ModeType.Equals("demo", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (System.Enum.TryParse<TradingMode>(req.TargetMode, true, out var parsed))
|
||||
{
|
||||
state.DemoTradingMode = parsed;
|
||||
logger.Info($"🌐 WebUI: Demo-Trading-Modus auf '{parsed}' geändert.");
|
||||
return Results.Ok(new { success = true, currentMode = state.DemoTradingMode.ToString() });
|
||||
}
|
||||
}
|
||||
else if (req.ModeType.Equals("pause", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
state.GlobalTradingPaused = !state.GlobalTradingPaused;
|
||||
logger.Warning($"🌐 WebUI: GlobalTradingPaused auf '{state.GlobalTradingPaused}' umgeschaltet.");
|
||||
return Results.Ok(new { success = true, isPaused = state.GlobalTradingPaused });
|
||||
}
|
||||
|
||||
return Results.BadRequest(new { error = "Ungültiger Modus oder Parametertyp." });
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class JobsEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapJobsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/jobs");
|
||||
|
||||
group.MapGet("/", (JobManager jobManager) =>
|
||||
{
|
||||
var jobs = jobManager.Jobs.Select(j => new
|
||||
{
|
||||
jobName = j.JobName,
|
||||
description = j.Description,
|
||||
isEnabled = j.IsEnabled,
|
||||
lastRun = j.LastRun,
|
||||
nextRun = j.NextRun,
|
||||
statusText = j.StatusText,
|
||||
canTriggerManually = j.ManualTriggerAction != null
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(jobs);
|
||||
});
|
||||
|
||||
group.MapPost("/{jobName}/run", async (string jobName, JobManager jobManager, TerminalLogger logger) =>
|
||||
{
|
||||
var job = jobManager.Jobs.FirstOrDefault(j => j.JobName.Equals(jobName, StringComparison.OrdinalIgnoreCase));
|
||||
if (job == null)
|
||||
return Results.NotFound(new { error = $"Job '{jobName}' nicht gefunden." });
|
||||
|
||||
if (job.ManualTriggerAction == null)
|
||||
return Results.BadRequest(new { error = $"Job '{jobName}' unterstützt keine manuelle Ausführung." });
|
||||
|
||||
try
|
||||
{
|
||||
logger.Info($"🌐 WebUI: Job '{job.JobName}' manuell angestoßen.");
|
||||
// Nicht blockieren, sondern im Hintergrund ausführen
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await job.ManualTriggerAction(); }
|
||||
catch (Exception ex) { logger.Error($"Fehler bei manuellem Lauf von '{job.JobName}': {ex.Message}"); }
|
||||
});
|
||||
|
||||
return Results.Ok(new { success = true, message = $"Job '{job.JobName}' gestartet." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class LogsEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapLogsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/logs");
|
||||
|
||||
group.MapGet("/", (TerminalLogger logger, int? minutes) =>
|
||||
{
|
||||
var span = TimeSpan.FromMinutes(minutes.GetValueOrDefault(60));
|
||||
var history = logger.GetHistory(span)
|
||||
.Select(e => new
|
||||
{
|
||||
time = e.Timestamp.ToString("HH:mm:ss"),
|
||||
timestamp = e.Timestamp,
|
||||
level = e.Level.ToString(),
|
||||
cid = e.CorrelationId,
|
||||
msg = e.Message
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(history);
|
||||
});
|
||||
|
||||
group.MapGet("/stream", async (HttpContext context, TerminalLogger logger, CancellationToken ct) =>
|
||||
{
|
||||
context.Response.Headers.ContentType = "text/event-stream";
|
||||
context.Response.Headers.CacheControl = "no-cache";
|
||||
context.Response.Headers.Connection = "keep-alive";
|
||||
|
||||
var channel = Channel.CreateUnbounded<LogMessageEventArgs>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleWriter = false,
|
||||
SingleReader = true
|
||||
});
|
||||
|
||||
void Handler(object? _, LogMessageEventArgs e)
|
||||
{
|
||||
channel.Writer.TryWrite(e);
|
||||
}
|
||||
|
||||
logger.OnLogMessage += Handler;
|
||||
|
||||
try
|
||||
{
|
||||
await context.Response.WriteAsync(": connected\n\n", ct);
|
||||
await context.Response.Body.FlushAsync(ct);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var log = await channel.Reader.ReadAsync(ct);
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
time = log.Timestamp.ToString("HH:mm:ss"),
|
||||
level = log.Level.ToString(),
|
||||
cid = log.CorrelationId,
|
||||
msg = log.Message
|
||||
});
|
||||
|
||||
await context.Response.WriteAsync($"data: {payload}\n\n", ct);
|
||||
await context.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Client hat Verbindung getrennt
|
||||
}
|
||||
finally
|
||||
{
|
||||
logger.OnLogMessage -= Handler;
|
||||
channel.Writer.TryComplete();
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PolyTrader.Core.Modularity;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class ModulesEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapModulesEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/modules");
|
||||
|
||||
group.MapGet("/", (IServiceProvider sp) =>
|
||||
{
|
||||
var activeModules = sp.GetServices<IPolyTraderModule>().ToList();
|
||||
var activeNames = activeModules.Select(m => m.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var knownModules = new[]
|
||||
{
|
||||
"CopyTrading",
|
||||
"ResolutionFarming",
|
||||
"Supervisor",
|
||||
"Accounting"
|
||||
};
|
||||
|
||||
var list = knownModules.Select(name => new
|
||||
{
|
||||
name,
|
||||
isActive = activeNames.Contains(name)
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PolyTrader.Web.Configuration;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
{
|
||||
public static class SettingsEndpoints
|
||||
{
|
||||
public record UpdateWebUiSettingsRequest(bool Enabled, int Port, bool LocalOnly, string? AuthToken);
|
||||
|
||||
public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/settings");
|
||||
|
||||
group.MapGet("/webui", (IOptions<WebUiOptions> optionsProvider) =>
|
||||
{
|
||||
var opt = optionsProvider.Value;
|
||||
return Results.Ok(new
|
||||
{
|
||||
enabled = opt.Enabled,
|
||||
port = opt.Port,
|
||||
localOnly = opt.LocalOnly,
|
||||
hasToken = !string.IsNullOrWhiteSpace(opt.AuthToken),
|
||||
token = opt.AuthToken
|
||||
});
|
||||
});
|
||||
|
||||
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 : 5001;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user