Add live TLS certificate probing and improve restart error handling.

Configure CertificateCheckUrl per container for curl-like TLS checks, classify Sonic permission errors, and extend setup wizard for container management.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 13:16:21 +02:00
co-authored by Cursor
parent d20e0df618
commit 58b7826162
1430 changed files with 358394 additions and 71 deletions
@@ -1,4 +1,5 @@
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager.Setup;
@@ -11,8 +12,10 @@ public sealed class AddContainerForm : Form
private readonly ComboBox _cmbSystem;
private readonly TextBox _txtName;
private readonly TextBox _txtDisplay;
private readonly TextBox _txtCheckUrl;
private readonly TextBox _txtTimeout;
private readonly Button _btnSave;
private readonly Button _btnProbeUrl;
private bool _running;
public int? CreatedSonicContainerId { get; private set; }
@@ -36,7 +39,7 @@ public sealed class AddContainerForm : Form
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
ClientSize = new Size(540, 470);
ClientSize = new Size(540, 560);
BackColor = SettingsUi.Background;
ForeColor = SettingsUi.Text;
Font = new Font("Segoe UI", 10f);
@@ -55,7 +58,7 @@ public sealed class AddContainerForm : Form
Label subtitle = new()
{
Text =
"Container einer Company und einem Sonic-System zuordnen.",
"Container zuordnen und optional TLS-Prüf-URL hinterlegen.",
Location = new Point(30, 58),
AutoSize = true,
ForeColor = SettingsUi.Muted
@@ -63,13 +66,14 @@ public sealed class AddContainerForm : Form
Panel card = SettingsUi.CreateCard();
card.Location = new Point(28, 95);
card.Size = new Size(484, 290);
card.Size = new Size(484, 370);
card.Paint += SettingsUi.PaintCardBorder;
_cmbCompany = SettingsUi.CreateComboBox();
_cmbSystem = SettingsUi.CreateComboBox();
_txtName = SettingsUi.CreateTextBox();
_txtDisplay = SettingsUi.CreateTextBox();
_txtCheckUrl = SettingsUi.CreateTextBox();
_txtTimeout = SettingsUi.CreateTextBox();
_txtTimeout.Text = "180";
@@ -77,6 +81,7 @@ public sealed class AddContainerForm : Form
{
_txtName.Text = _existing.ContainerName;
_txtDisplay.Text = _existing.ContainerDisplayName ?? string.Empty;
_txtCheckUrl.Text = _existing.CertificateCheckUrl ?? string.Empty;
_txtTimeout.Text = _existing.RestartTimeoutSeconds.ToString();
}
@@ -85,15 +90,39 @@ public sealed class AddContainerForm : Form
AddField(card, "Container-Name", _txtName, 18, 138, 440);
AddField(card, "Anzeigename (optional)", _txtDisplay, 18, 198, 280);
AddField(card, "Timeout (s)", _txtTimeout, 318, 198, 140);
AddField(
card,
"Zertifikat prüfen (URL / Host:Port)",
_txtCheckUrl,
18,
258,
440);
Label urlHint = new()
{
Text = "z. B. https://server:8443 oder server:8443",
Location = new Point(18, 312),
AutoSize = true,
ForeColor = SettingsUi.Muted,
Font = new Font("Segoe UI", 8.5f)
};
_btnProbeUrl = SettingsUi.CreateGhostButton("URL prüfen");
_btnProbeUrl.Location = new Point(318, 318);
_btnProbeUrl.Size = new Size(140, 30);
_btnProbeUrl.Click += (_, _) => ProbeUrl();
card.Controls.Add(urlHint);
card.Controls.Add(_btnProbeUrl);
_btnSave = SettingsUi.CreatePrimaryButton(
IsEditMode ? "Änderungen speichern" : "Container anlegen");
_btnSave.Location = new Point(292, 405);
_btnSave.Location = new Point(292, 490);
_btnSave.Size = new Size(220, 40);
_btnSave.Click += async (_, _) => await SaveAsync();
Button cancel = SettingsUi.CreateGhostButton("Abbrechen");
cancel.Location = new Point(170, 405);
cancel.Location = new Point(170, 490);
cancel.Size = new Size(110, 40);
cancel.Click += (_, _) =>
{
@@ -110,6 +139,53 @@ public sealed class AddContainerForm : Form
Shown += async (_, _) => await LoadLookupsAsync();
}
private void ProbeUrl()
{
string url = _txtCheckUrl.Text.Trim();
if (string.IsNullOrWhiteSpace(url))
{
MessageBox.Show(
this,
"Bitte zuerst eine Prüf-URL eintragen.",
"URL prüfen",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return;
}
TlsEndpointProbeService probe = new();
CertificateProbeResult result = probe.Probe(url);
if (result.Success && result.Certificate is not null)
{
string expiry = result.Certificate.ValidUntil
.ToLocalTime()
.ToString("dd.MM.yyyy HH:mm");
MessageBox.Show(
this,
$"TLS-Zertifikat erreichbar.\n\n"
+ $"Subject: {result.Certificate.Subject}\n"
+ $"Aussteller: {result.Certificate.Issuer}\n"
+ $"Gültig bis: {expiry}\n"
+ $"Status: {(result.Certificate.IsCurrentlyValid ? "gültig" : "abgelaufen")}",
"URL prüfen",
MessageBoxButtons.OK,
result.Certificate.IsCurrentlyValid
? MessageBoxIcon.Information
: MessageBoxIcon.Warning);
return;
}
MessageBox.Show(
this,
result.Message,
"URL prüfen",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
private async Task LoadLookupsAsync()
{
try
@@ -211,6 +287,29 @@ public sealed class AddContainerForm : Form
return;
}
string? checkUrl = _txtCheckUrl.Text.Trim();
if (string.IsNullOrWhiteSpace(checkUrl))
{
checkUrl = null;
}
else if (!TlsEndpointProbeService.TryParseEndpoint(
checkUrl,
out _,
out _,
out _,
out string? parseError))
{
MessageBox.Show(
this,
parseError ?? "Ungültige Prüf-URL.",
"Container",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
_txtCheckUrl.Focus();
return;
}
_running = true;
_btnSave.Enabled = false;
Cursor = Cursors.WaitCursor;
@@ -225,7 +324,8 @@ public sealed class AddContainerForm : Form
system.SonicConnectionId,
_txtName.Text,
_txtDisplay.Text,
timeout);
timeout,
checkUrl);
CreatedSonicContainerId = _existing.SonicContainerId;
}
@@ -237,7 +337,8 @@ public sealed class AddContainerForm : Form
system.SonicConnectionId,
_txtName.Text,
_txtDisplay.Text,
timeout);
timeout,
checkUrl);
}
DialogResult = DialogResult.OK;
@@ -8,6 +8,7 @@ public sealed class AddCredentialProfileForm : Form
{
private readonly SetupCoordinator _coordinator;
private readonly SonicSystemOption _system;
private readonly SonicCredentialProfile? _existing;
private readonly TextBox _txtProfileName;
private readonly TextBox _txtUserName;
@@ -16,6 +17,7 @@ public sealed class AddCredentialProfileForm : Form
private readonly CheckBox _chkDefault;
private readonly Button _btnSave;
private readonly Button _btnCancel;
private readonly Label _lblPasswordHint;
private bool _operationRunning;
@@ -25,9 +27,12 @@ public sealed class AddCredentialProfileForm : Form
private set;
}
public bool IsEditMode => _existing is not null;
public AddCredentialProfileForm(
SetupCoordinator coordinator,
SonicSystemOption system)
SonicSystemOption system,
SonicCredentialProfile? existing = null)
{
_coordinator =
coordinator
@@ -39,13 +44,17 @@ public sealed class AddCredentialProfileForm : Form
?? throw new ArgumentNullException(
nameof(system));
Text = "Benutzerprofil hinzufügen";
_existing = existing;
Text = IsEditMode
? "Benutzerprofil bearbeiten"
: "Benutzerprofil hinzufügen";
StartPosition =
FormStartPosition.CenterParent;
ClientSize =
new Size(560, 525);
new Size(560, 545);
FormBorderStyle =
FormBorderStyle.FixedDialog;
@@ -65,8 +74,9 @@ public sealed class AddCredentialProfileForm : Form
Label title = new()
{
Text =
$"Profil für {_system.ConnectionName}",
Text = IsEditMode
? $"Profil bearbeiten · {_system.ConnectionName}"
: $"Profil für {_system.ConnectionName}",
Location =
new Point(28, 22),
@@ -112,7 +122,7 @@ public sealed class AddCredentialProfileForm : Form
CreateTextBox(185);
_txtProfileName.Text =
"Administrator";
_existing?.CredentialName ?? "Administrator";
Label userLabel =
CreateLabel(
@@ -123,7 +133,7 @@ public sealed class AddCredentialProfileForm : Form
CreateTextBox(250);
_txtUserName.Text =
"Administrator";
_existing?.UserName ?? "Administrator";
Label passwordLabel =
CreateLabel(
@@ -136,13 +146,25 @@ public sealed class AddCredentialProfileForm : Form
_txtPassword.UseSystemPasswordChar =
true;
_lblPasswordHint = new Label
{
Text = IsEditMode
? "Leer lassen = bisheriges Kennwort behalten"
: string.Empty,
Location = new Point(30, 343),
AutoSize = true,
ForeColor = Color.FromArgb(169, 184, 200),
Font = new Font("Segoe UI", 8.5f)
};
Label repeatLabel =
CreateLabel(
"Kennwort wiederholen",
355);
365);
_txtPasswordRepeat =
CreateTextBox(380);
CreateTextBox(390);
_txtPasswordRepeat.UseSystemPasswordChar =
true;
@@ -153,10 +175,10 @@ public sealed class AddCredentialProfileForm : Form
"Als Standardprofil verwenden",
Location =
new Point(30, 425),
new Point(30, 435),
AutoSize = true,
Checked = true,
Checked = _existing?.IsDefault ?? true,
ForeColor =
Color.FromArgb(241, 245, 249)
@@ -167,7 +189,7 @@ public sealed class AddCredentialProfileForm : Form
Text = "Abbrechen",
Location =
new Point(125, 465),
new Point(125, 480),
Size =
new Size(110, 38),
@@ -189,11 +211,12 @@ public sealed class AddCredentialProfileForm : Form
_btnSave = new Button
{
Text =
"Verbindung prüfen und hinzufügen",
Text = IsEditMode
? "Prüfen und speichern"
: "Verbindung prüfen und hinzufügen",
Location =
new Point(245, 465),
new Point(245, 480),
Size =
new Size(280, 38),
@@ -228,6 +251,7 @@ public sealed class AddCredentialProfileForm : Form
Controls.Add(_txtUserName);
Controls.Add(passwordLabel);
Controls.Add(_txtPassword);
Controls.Add(_lblPasswordHint);
Controls.Add(repeatLabel);
Controls.Add(_txtPasswordRepeat);
Controls.Add(_chkDefault);
@@ -328,7 +352,7 @@ public sealed class AddCredentialProfileForm : Form
return;
}
if (string.IsNullOrEmpty(password))
if (!IsEditMode && string.IsNullOrEmpty(password))
{
ShowWarning("Kennwort fehlt.");
@@ -344,7 +368,8 @@ public sealed class AddCredentialProfileForm : Form
return;
}
if (!string.Equals(
if (!string.IsNullOrEmpty(password)
&& !string.Equals(
password,
passwordRepeat,
StringComparison.Ordinal))
@@ -372,13 +397,27 @@ public sealed class AddCredentialProfileForm : Form
try
{
CreatedProfile =
await _coordinator.AddCredentialAsync(
_system,
profileName,
userName,
password,
_chkDefault.Checked);
if (IsEditMode && _existing is not null)
{
CreatedProfile =
await _coordinator.UpdateCredentialAsync(
_system,
_existing,
profileName,
userName,
password,
_chkDefault.Checked);
}
else
{
CreatedProfile =
await _coordinator.TestAndAddCredentialAsync(
_system,
profileName,
userName,
password,
_chkDefault.Checked);
}
DialogResult = DialogResult.OK;
Close();
@@ -391,7 +430,11 @@ public sealed class AddCredentialProfileForm : Form
{
MessageBox.Show(
this,
"Profil konnte nicht angelegt werden.",
string.IsNullOrWhiteSpace(exception.Message)
? (IsEditMode
? "Profil konnte nicht aktualisiert werden."
: "Profil konnte nicht angelegt werden.")
: exception.Message,
"Benutzerprofil",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
@@ -423,7 +466,9 @@ public sealed class AddCredentialProfileForm : Form
_btnSave.Text =
isRunning
? "Verbindung wird geprüft ..."
: "Verbindung prüfen und hinzufügen";
: IsEditMode
? "Prüfen und speichern"
: "Verbindung prüfen und hinzufügen";
}
private void ShowWarning(string message)
@@ -435,4 +480,4 @@ public sealed class AddCredentialProfileForm : Form
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
}
@@ -207,7 +207,10 @@ public sealed class AddTargetPathForm : Form
if (!_lastProbe.Success)
{
_lblProbeResult.ForeColor = Color.FromArgb(220, 100, 100);
_lblProbeResult.Text = _lastProbe.Message;
_lblProbeResult.Text = _lastProbe.AccessDenied
? CertificateProbeResult.AccessDeniedShortText
+ " Anmeldung am Freigabe-Pfad erforderlich."
: _lastProbe.Message;
return;
}
@@ -160,6 +160,78 @@ public sealed class SetupCoordinator
return createdProfile;
}
public async Task<SonicCredentialProfile> UpdateCredentialAsync(
SonicSystemOption system,
SonicCredentialProfile existing,
string credentialName,
string userName,
string secret,
bool isDefault,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(system);
ArgumentNullException.ThrowIfNull(existing);
if (system.SonicConnectionId <= 0
|| existing.SonicCredentialId <= 0)
{
throw new ArgumentException(
"Das ausgewählte Profil oder System ist ungültig.");
}
string passwordToStore =
string.IsNullOrEmpty(secret)
? existing.Secret
: secret;
CredentialTestResult testResult =
await _credentialTester.TestAsync(
system,
userName,
passwordToStore,
system.ValidationContainerName,
cancellationToken);
if (!testResult.Success)
{
throw new InvalidOperationException(
"Das Benutzerprofil wurde nicht gespeichert."
+ Environment.NewLine
+ Environment.NewLine
+ testResult.Message);
}
await _repository.UpdateCredentialAsync(
existing.SonicCredentialId,
system.SonicConnectionId,
credentialName,
userName,
passwordToStore,
isDefault,
cancellationToken);
IReadOnlyList<SonicCredentialProfile> profiles =
await _repository.GetCredentialsAsync(
system.SonicConnectionId,
cancellationToken);
SonicCredentialProfile? updated =
profiles.FirstOrDefault(
profile =>
profile.SonicCredentialId
== existing.SonicCredentialId);
if (updated is null)
{
throw new InvalidOperationException(
"Das Benutzerprofil wurde gespeichert, "
+ "konnte anschließend aber nicht geladen werden.");
}
return updated;
}
public async Task<SetupCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
@@ -396,6 +468,7 @@ public sealed class SetupCoordinator
string containerName,
string? containerDisplayName,
int restartTimeoutSeconds,
string? certificateCheckUrl = null,
CancellationToken cancellationToken = default)
{
int id = await _setupRepository.AddSonicContainerAsync(
@@ -404,6 +477,7 @@ public sealed class SetupCoordinator
containerName,
containerDisplayName,
restartTimeoutSeconds,
certificateCheckUrl,
cancellationToken);
await EnsureExistsAsync(
@@ -520,6 +594,7 @@ public sealed class SetupCoordinator
string containerName,
string? containerDisplayName,
int restartTimeoutSeconds,
string? certificateCheckUrl = null,
CancellationToken cancellationToken = default)
{
await _setupRepository.UpdateSonicContainerAsync(
@@ -529,6 +604,7 @@ public sealed class SetupCoordinator
containerName,
containerDisplayName,
restartTimeoutSeconds,
certificateCheckUrl,
cancellationToken);
await EnsureExistsAsync(
@@ -36,6 +36,7 @@ public sealed class SetupWizardForm : Form
private Button _btnSaveSelection = null!;
private Button _btnFinish = null!;
private Button _btnAddCredential = null!;
private Button _btnEditCredential = null!;
private IReadOnlyList<SonicSystemOption> _systems = [];
private IReadOnlyList<SonicCredentialProfile> _profiles = [];
@@ -385,11 +386,12 @@ public sealed class SetupWizardForm : Form
_lvContainers = SettingsUi.CreateListView();
_lvContainers.Dock = DockStyle.Fill;
_lvContainers.Columns.Add("Company", 80);
_lvContainers.Columns.Add("Container", 180);
_lvContainers.Columns.Add("Anzeige", 160);
_lvContainers.Columns.Add("System", 160);
_lvContainers.Columns.Add("Timeout", 90);
_lvContainers.Columns.Add("Company", 70);
_lvContainers.Columns.Add("Container", 150);
_lvContainers.Columns.Add("Anzeige", 120);
_lvContainers.Columns.Add("System", 120);
_lvContainers.Columns.Add("Prüf-URL", 180);
_lvContainers.Columns.Add("Timeout", 80);
_lvContainers.DoubleClick += async (_, _) => await EditSelectedContainerAsync();
FinishPanelLayout(_panelContainers, _lvContainers, toolbar);
@@ -484,6 +486,12 @@ public sealed class SetupWizardForm : Form
_btnAddCredential.Click += async (_, _) =>
await AddCredentialAsync();
_btnEditCredential = SettingsUi.CreateGhostButton("Profil bearbeiten");
_btnEditCredential.Location = new Point(550, 122);
_btnEditCredential.Size = new Size(160, 34);
_btnEditCredential.Click += async (_, _) =>
await EditCredentialAsync();
_lblSystemDetails = new Label
{
Location = new Point(24, 180),
@@ -502,6 +510,7 @@ public sealed class SetupWizardForm : Form
card.Controls.Add(credentialLabel);
card.Controls.Add(_cmbCredential);
card.Controls.Add(_btnAddCredential);
card.Controls.Add(_btnEditCredential);
card.Controls.Add(_lblSystemDetails);
card.Controls.Add(_btnSaveSelection);
@@ -625,6 +634,10 @@ public sealed class SetupWizardForm : Form
item.SubItems.Add(container.ContainerName);
item.SubItems.Add(container.ContainerDisplayName ?? "—");
item.SubItems.Add(container.ConnectionName);
item.SubItems.Add(
string.IsNullOrWhiteSpace(container.CertificateCheckUrl)
? "—"
: container.CertificateCheckUrl);
item.SubItems.Add(container.RestartTimeoutSeconds + " s");
item.Tag = container;
_lvContainers.Items.Add(item);
@@ -722,7 +735,12 @@ public sealed class SetupWizardForm : Form
return "Kennwort nötig";
}
return result.Success ? "fehlt" : "n/a";
if (result.AccessDenied)
{
return CertificateProbeResult.AccessDeniedShortText;
}
return result.Success ? "fehlt" : "nicht erreichbar";
}
private static string? PromptCertificatePassword(IWin32Window owner)
@@ -1228,19 +1246,9 @@ public sealed class SetupWizardForm : Form
}
await SystemChangedAsync();
SelectCredential(dialog.CreatedProfile.SonicCredentialId);
for (int index = 0; index < _cmbCredential.Items.Count; index++)
{
if (_cmbCredential.Items[index] is SonicCredentialProfile profile
&& profile.SonicCredentialId
== dialog.CreatedProfile.SonicCredentialId)
{
_cmbCredential.SelectedIndex = index;
break;
}
}
MessageBox.Show(
MessageBox.Show(
this,
"Profil geladen.",
"Benutzerprofil",
@@ -1248,6 +1256,53 @@ public sealed class SetupWizardForm : Form
MessageBoxIcon.Information);
}
private async Task EditCredentialAsync()
{
if (_cmbSystem.SelectedItem is not SonicSystemOption system)
{
ShowWarning("Bitte System wählen.");
return;
}
if (_cmbCredential.SelectedItem is not SonicCredentialProfile profile)
{
ShowWarning("Bitte Profil wählen.");
return;
}
using AddCredentialProfileForm dialog =
new(_coordinator, system, profile);
if (dialog.ShowDialog(this) != DialogResult.OK
|| dialog.CreatedProfile is null)
{
return;
}
await SystemChangedAsync();
SelectCredential(dialog.CreatedProfile.SonicCredentialId);
MessageBox.Show(
this,
"Profil aktualisiert.",
"Benutzerprofil",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
private void SelectCredential(int sonicCredentialId)
{
for (int index = 0; index < _cmbCredential.Items.Count; index++)
{
if (_cmbCredential.Items[index] is SonicCredentialProfile profile
&& profile.SonicCredentialId == sonicCredentialId)
{
_cmbCredential.SelectedIndex = index;
break;
}
}
}
private async Task SystemChangedAsync()
{
if (_cmbSystem.SelectedItem is not SonicSystemOption system)
@@ -1275,9 +1330,12 @@ public sealed class SetupWizardForm : Form
Environment.NewLine
+ "Noch kein Benutzerprofil — bitte hinzufügen.";
_btnSaveSelection.Enabled = false;
_btnEditCredential.Enabled = false;
return;
}
_btnEditCredential.Enabled = true;
_btnSaveSelection.Enabled = true;
LocalSetupSelection? selection = _coordinator.LoadSelection();