GUI UX, secret visibility, browser-friendly hooks, deploy script
GUI: - URL column in endpoint grid + Copy URL toolbar button so the full http://host:port/hook/<slug> is one click away - Double-click a row to open the edit dialog - Bearer/HMAC sections in the editor hide when the auth mode doesn't use them, and reappear with previously-entered values when switched back - Log panel auto-scroll checkbox (default on) plus 3s polling so log entries stream in without manual refresh - Secret fields are now plain text with a Copy button. Anyone who can open the admin-pipe-ACL'd GUI is already SYSTEM-equivalent on the host, so masking the value just made recovery harder. PFX password in Server Settings gets the same treatment. Service: - Admin pipe ops log info-level lines on every mutation (create/update/delete/enable/disable/update-config/bind-https) so GUI activity is visible in the Serilog file - /hook/{slug} accepts GET as well as POST so a browser smoke-test works without curl - /favicon.ico returns 204 so browser hits don't pollute logs with 404s - AdminPipeServer no longer strips plaintext secrets when sending config to the GUI; the pipe ACL already restricts to SYSTEM/Admins Scripts: - New deploy.ps1: stops + republishes + copies binaries to C:\Program Files\WebhookServer + (re)installs the Windows Service - install-service.ps1 now uses sc.exe argv splatting consistently for both create and config paths Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using WebhookServer.Core.Models;
|
||||
@@ -29,6 +30,29 @@ public sealed partial class EndpointEditorViewModel : ObservableObject
|
||||
public Array ExecutorTypes { get; } = Enum.GetValues(typeof(ExecutorType));
|
||||
public Array ResponseModes { get; } = Enum.GetValues(typeof(ResponseMode));
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for <see cref="EndpointConfig.AuthMode"/> that emits change notifications
|
||||
/// for the visibility flags so the bearer/HMAC sections show/hide reactively.
|
||||
/// </summary>
|
||||
public AuthMode SelectedAuthMode
|
||||
{
|
||||
get => Endpoint.AuthMode;
|
||||
set
|
||||
{
|
||||
if (Endpoint.AuthMode == value) return;
|
||||
Endpoint.AuthMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BearerVisible));
|
||||
OnPropertyChanged(nameof(HmacVisible));
|
||||
}
|
||||
}
|
||||
|
||||
public Visibility BearerVisible =>
|
||||
Endpoint.AuthMode == AuthMode.Bearer ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public Visibility HmacVisible =>
|
||||
Endpoint.AuthMode == AuthMode.Hmac ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public string AllowedClientsText
|
||||
{
|
||||
get => string.Join(Environment.NewLine, Endpoint.AllowedClients);
|
||||
@@ -49,9 +73,9 @@ public sealed partial class EndpointEditorViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public string BearerSecretInput
|
||||
public string BearerSecret
|
||||
{
|
||||
get => "";
|
||||
get => Endpoint.Bearer?.Secret.Plaintext ?? "";
|
||||
set
|
||||
{
|
||||
Endpoint.Bearer ??= new BearerOptions();
|
||||
@@ -60,9 +84,9 @@ public sealed partial class EndpointEditorViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public string HmacSecretInput
|
||||
public string HmacSecret
|
||||
{
|
||||
get => "";
|
||||
get => Endpoint.Hmac?.Secret.Plaintext ?? "";
|
||||
set
|
||||
{
|
||||
Endpoint.Hmac ??= new HmacOptions();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using WebhookServer.Core.Ipc;
|
||||
@@ -24,11 +25,19 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
[ObservableProperty] private string _connectionStatus = "Disconnected";
|
||||
[ObservableProperty] private bool _isConnected;
|
||||
[ObservableProperty] private string _logTail = "";
|
||||
[ObservableProperty] private bool _autoScrollLogs = true;
|
||||
[ObservableProperty] private ServerConfig _serverConfig = new();
|
||||
[ObservableProperty] private string _httpBaseUrl = "http://localhost:8080";
|
||||
[ObservableProperty] private string? _httpsBaseUrl;
|
||||
|
||||
private readonly DispatcherTimer _logTimer;
|
||||
|
||||
public MainViewModel(AdminPipeClient client)
|
||||
{
|
||||
_client = client;
|
||||
_logTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(3) };
|
||||
_logTimer.Tick += async (_, _) => await RefreshLogTailAsync();
|
||||
_logTimer.Start();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -46,6 +55,12 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
? $"Connected — HTTP {status!.HttpPort}{(status.HttpsPort.HasValue ? $" / HTTPS {status.HttpsPort}" : "")}"
|
||||
: "Disconnected";
|
||||
|
||||
if (status is not null)
|
||||
{
|
||||
HttpBaseUrl = $"http://localhost:{status.HttpPort}";
|
||||
HttpsBaseUrl = status.HttpsPort.HasValue ? $"https://localhost:{status.HttpsPort.Value}" : null;
|
||||
}
|
||||
|
||||
Endpoints.Clear();
|
||||
if (config is not null)
|
||||
{
|
||||
@@ -159,6 +174,21 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CopyEndpointUrl()
|
||||
{
|
||||
if (SelectedEndpoint is null || string.IsNullOrEmpty(HttpBaseUrl)) return;
|
||||
var url = $"{HttpBaseUrl.TrimEnd('/')}/hook/{SelectedEndpoint.Slug}";
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError("Copy failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditServerSettingsAsync()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private bool _httpsEnabled;
|
||||
[ObservableProperty] private string _httpsMode = "PfxFile";
|
||||
[ObservableProperty] private string _pfxPath = "";
|
||||
[ObservableProperty] private string _pfxPasswordInput = "";
|
||||
[ObservableProperty] private string _pfxPassword = "";
|
||||
[ObservableProperty] private string _thumbprint = "";
|
||||
[ObservableProperty] private string _trustedProxiesText = "";
|
||||
|
||||
@@ -29,6 +29,7 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
HttpsPort = b?.Port ?? 8443;
|
||||
HttpsMode = b?.Kind == HttpsBindingKind.CertStoreThumbprint ? "Thumbprint" : "PfxFile";
|
||||
PfxPath = b?.PfxPath ?? "";
|
||||
PfxPassword = b?.PfxPassword?.Plaintext ?? "";
|
||||
Thumbprint = b?.Thumbprint ?? "";
|
||||
}
|
||||
|
||||
@@ -49,8 +50,8 @@ public sealed partial class ServerSettingsViewModel : ObservableObject
|
||||
{
|
||||
binding.Kind = HttpsBindingKind.PfxFile;
|
||||
binding.PfxPath = PfxPath;
|
||||
if (!string.IsNullOrEmpty(PfxPasswordInput))
|
||||
binding.PfxPassword = ProtectedString.FromPlaintext(PfxPasswordInput);
|
||||
if (!string.IsNullOrEmpty(PfxPassword))
|
||||
binding.PfxPassword = ProtectedString.FromPlaintext(PfxPassword);
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user