using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using ZA.CoreService.ESBCertificateManager.Configuration; using ZA.CoreService.ESBCertificateManager.Data; using ZA.CoreService.ESBCertificateManager.Models; using ZA.CoreService.ESBCertificateManager.Services; namespace ZA.CoreService.ESBCertificateManager; public partial class Form1 : Form { private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34); private readonly Color SidebarColor = Color.FromArgb(7, 23, 45); private readonly Color CardColor = Color.FromArgb(16, 38, 65); private readonly Color BorderColor = Color.FromArgb(36, 74, 117); private readonly Color BlueColor = Color.FromArgb(0, 110, 182); private readonly Color GoldColor = Color.FromArgb(208, 171, 57); private readonly Color TextColor = Color.FromArgb(241, 245, 249); private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200); private readonly Color GreenColor = Color.FromArgb(60, 203, 127); private readonly Color RedColor = Color.FromArgb(239, 106, 106); private readonly AppSettings _settings; private readonly DeploymentOrchestrator _orchestrator; private readonly SonicContainerDiscovery _sonicDiscovery; private TextBox txtCertificatePath = null!; private Label lblCertificateSubject = null!; private Label lblCertificateIssuer = null!; private Label lblCertificateExpiry = null!; private Label lblCertificateFingerprint = null!; private Label lblCertificateValidity = null!; private Label lblStatus = null!; private Label lblSonicStatus = null!; private ComboBox cmbSonicConnection = null!; private DataGridView dgvTargets = null!; private Button btnRestartOnly = null!; private Button btnCancelRun = null!; private CertificateInfo? _loadedCertificateInfo; private IReadOnlyList _loadedTargets = []; private bool _isOperationRunning; private CancellationTokenSource? _runCts; public Form1() { InitializeComponent(); _settings = AppSettingsLoader.Load(); _orchestrator = new DeploymentOrchestrator(_settings); _sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections); BuildUi(); Shown += async (_, _) => await LoadTargetsAsync(); } private void BuildUi() { SuspendLayout(); Text = "ESB Certificate Manager"; FormBorderStyle = FormBorderStyle.Sizable; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(960, 640); Size = new Size(1100, 720); BackColor = BackgroundColor; ForeColor = TextColor; Font = new Font("Segoe UI", 9.5f); Panel sidebar = new() { Dock = DockStyle.Left, Width = 220, BackColor = SidebarColor, Padding = new Padding(16) }; sidebar.Controls.Add(new Label { Text = "ESB Certificate\nManager", Dock = DockStyle.Top, Height = 70, Font = new Font("Segoe UI Semibold", 14f), ForeColor = GoldColor }); sidebar.Controls.Add(new Label { Text = "1. Zertifikat erkennen\n2. Container neu starten", Dock = DockStyle.Top, Height = 80, ForeColor = MutedTextColor }); Panel main = new() { Dock = DockStyle.Fill, Padding = new Padding(20), BackColor = BackgroundColor }; TableLayoutPanel layout = new() { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 4, BackColor = BackgroundColor }; layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 160)); layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 56)); layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 70)); layout.Controls.Add(BuildCertificateCard(), 0, 0); layout.Controls.Add(BuildSonicBar(), 0, 1); layout.Controls.Add(BuildTargetsCard(), 0, 2); layout.Controls.Add(BuildActionBar(), 0, 3); main.Controls.Add(layout); Controls.Add(main); Controls.Add(sidebar); ResumeLayout(); } private Panel BuildCertificateCard() { Panel card = MakeCard(); card.Controls.Add(SectionTitle("Zertifikat erkennen", 12)); txtCertificatePath = new TextBox { Left = 16, Top = 48, Width = 620, Height = 28, ReadOnly = true, BackColor = Color.FromArgb(12, 28, 48), ForeColor = TextColor, BorderStyle = BorderStyle.FixedSingle }; Button btnSelect = MakeButton("Datei wählen…", BlueColor); btnSelect.Left = 650; btnSelect.Top = 46; btnSelect.Width = 140; btnSelect.Click += (_, _) => TrySelectCertificateFile(); lblCertificateSubject = MetaLabel(16, 90, "Subject: –"); lblCertificateIssuer = MetaLabel(16, 112, "Issuer: –"); lblCertificateExpiry = MetaLabel(400, 90, "Gültig bis: –"); lblCertificateFingerprint = MetaLabel(400, 112, "SHA-256: –"); lblCertificateValidity = MetaLabel(16, 134, "Status: –"); card.Controls.AddRange([ txtCertificatePath, btnSelect, lblCertificateSubject, lblCertificateIssuer, lblCertificateExpiry, lblCertificateFingerprint, lblCertificateValidity ]); return card; } private Panel BuildSonicBar() { Panel bar = MakeCard(); Label lbl = new() { Text = "Sonic:", Left = 16, Top = 16, AutoSize = true, ForeColor = MutedTextColor }; cmbSonicConnection = new ComboBox { Left = 70, Top = 12, Width = 200, DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Color.FromArgb(12, 28, 48), ForeColor = TextColor, FlatStyle = FlatStyle.Flat }; foreach (SonicConnection c in _settings.SonicConnections) { cmbSonicConnection.Items.Add(c.Name); } if (cmbSonicConnection.Items.Count > 0) { cmbSonicConnection.SelectedIndex = 0; } Button btnLoad = MakeButton("Container laden", BlueColor); btnLoad.Left = 290; btnLoad.Top = 10; btnLoad.Width = 140; btnLoad.Click += async (_, _) => await LoadFromSonicAsync(); lblSonicStatus = new Label { Left = 450, Top = 16, AutoSize = true, ForeColor = MutedTextColor, Text = "MfApi" }; bar.Controls.AddRange([lbl, cmbSonicConnection, btnLoad, lblSonicStatus]); return bar; } private Panel BuildTargetsCard() { Panel card = MakeCard(); card.Controls.Add(SectionTitle("Ziele / Container", 12)); dgvTargets = new DataGridView { Left = 16, Top = 44, Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, Width = 780, Height = 220, BackgroundColor = Color.FromArgb(12, 28, 48), ForeColor = TextColor, GridColor = BorderColor, BorderStyle = BorderStyle.None, RowHeadersVisible = false, AllowUserToAddRows = false, AllowUserToDeleteRows = false, ReadOnly = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, EnableHeadersVisualStyles = false }; dgvTargets.ColumnHeadersDefaultCellStyle.BackColor = SidebarColor; dgvTargets.ColumnHeadersDefaultCellStyle.ForeColor = GoldColor; dgvTargets.DefaultCellStyle.BackColor = Color.FromArgb(12, 28, 48); dgvTargets.DefaultCellStyle.ForeColor = TextColor; dgvTargets.DefaultCellStyle.SelectionBackColor = BlueColor; dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Selected", HeaderText = "", Width = 40, FillWeight = 15 }); dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Name", HeaderText = "Name", ReadOnly = true }); dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Container", HeaderText = "Container", ReadOnly = true }); dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Connection", HeaderText = "Verbindung", ReadOnly = true }); dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Status", HeaderText = "Status", ReadOnly = true }); dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "TargetId", Visible = false }); card.Resize += (_, _) => { dgvTargets.Width = Math.Max(200, card.ClientSize.Width - 32); dgvTargets.Height = Math.Max(80, card.ClientSize.Height - 60); }; card.Controls.Add(dgvTargets); return card; } private Panel BuildActionBar() { Panel bar = MakeCard(); btnRestartOnly = MakeButton("ESB neu starten", GoldColor); btnRestartOnly.ForeColor = Color.Black; btnRestartOnly.Left = 16; btnRestartOnly.Top = 14; btnRestartOnly.Width = 180; btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync(); btnCancelRun = MakeButton("Abbrechen", RedColor); btnCancelRun.Left = 210; btnCancelRun.Top = 14; btnCancelRun.Width = 120; btnCancelRun.Enabled = false; btnCancelRun.Click += (_, _) => _runCts?.Cancel(); lblStatus = new Label { Left = 350, Top = 20, AutoSize = true, ForeColor = MutedTextColor, Text = "Bereit." }; bar.Controls.AddRange([btnRestartOnly, btnCancelRun, lblStatus]); return bar; } private async Task LoadTargetsAsync() { try { (ITargetRepository repo, string msg) = await TargetRepositoryFactory.CreateAsync(_settings); _loadedTargets = await repo.GetActiveTargetsAsync(); BindTargets(_loadedTargets); SetStatus(msg, false); } catch (Exception ex) { SetStatus($"Ziele laden fehlgeschlagen: {ex.Message}", true); } } private async Task LoadFromSonicAsync() { if (cmbSonicConnection.SelectedItem is not string name) { SetStatus("Keine Sonic-Verbindung gewählt.", true); return; } SetStatus($"Lade Container von '{name}'…", false); SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync(name, _loadedTargets); if (!result.Success) { SetStatus(result.ErrorMessage ?? "Discovery fehlgeschlagen", true); return; } _loadedTargets = result.DiscoveredTargets; BindTargets(_loadedTargets); lblSonicStatus.Text = $"Domain={result.DomainName}; {result.DiscoveredTargets.Count} Container"; SetStatus(result.ErrorMessage ?? $"Container geladen ({result.DiscoveredTargets.Count}).", false); } private void BindTargets(IReadOnlyList targets) { dgvTargets.Rows.Clear(); foreach (DeploymentTarget t in targets) { int row = dgvTargets.Rows.Add(true, t.Name, t.ContainerName, t.SonicConnectionName, "bereit", t.Id); dgvTargets.Rows[row].Tag = t; } } private List GetSelectedTargets() => dgvTargets.Rows.Cast() .Where(r => !r.IsNewRow && r.Cells["Selected"].Value is true) .Select(r => r.Tag) .OfType() .ToList(); private async Task RunRestartOnlyAsync() { if (_isOperationRunning) { return; } List selected = GetSelectedTargets(); PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected); if (!preflight.IsValid) { MessageBox.Show( this, string.Join(Environment.NewLine, preflight.Issues.Select(i => i.Message)), "Neustart blockiert", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string names = string.Join(", ", selected.Select(t => t.ContainerName)); if (MessageBox.Show( this, $"Container wirklich neu starten?\n\n{names}", "ESB neu starten", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) { return; } _runCts?.Dispose(); _runCts = new CancellationTokenSource(); SetOperationRunning(true); SetStatus($"Neustart läuft ({selected.Count})…", false); Progress progress = new(u => { SetTargetRowStatus(u.TargetId, u.StatusText); SetStatus(u.StatusText, u.SuccessHint == false); }); try { DeploymentRunResult run = await _orchestrator.RestartOnlyAsync(selected, progress, _runCts.Token); foreach (TargetStepResult r in run.TargetResults) { SetTargetRowStatus(r.TargetId, r.StatusText); } string details = string.Join( Environment.NewLine + Environment.NewLine, run.TargetResults.Select(r => $"{r.TargetName}: {r.StatusText}" + (string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail))); SetStatus( run.OverallSuccess ? "Neustart erfolgreich." : "Neustart fehlgeschlagen.", !run.OverallSuccess); MessageBox.Show( this, details, run.OverallSuccess ? "Neustart erfolgreich" : "Neustart fehlgeschlagen", MessageBoxButtons.OK, run.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning); } catch (OperationCanceledException) { SetStatus("Neustart abgebrochen.", true); } catch (Exception ex) { SetStatus(ex.Message, true); MessageBox.Show(this, ex.Message, "Neustart", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { SetOperationRunning(false); _runCts?.Dispose(); _runCts = null; } } private void SetTargetRowStatus(int targetId, string status) { foreach (DataGridViewRow row in dgvTargets.Rows) { if (row.Tag is DeploymentTarget t && t.Id == targetId) { row.Cells["Status"].Value = status; break; } } } private void SetOperationRunning(bool running) { _isOperationRunning = running; btnRestartOnly.Enabled = !running; btnCancelRun.Enabled = running; dgvTargets.Enabled = !running; } private void SetStatus(string text, bool isError) { lblStatus.Text = text; lblStatus.ForeColor = isError ? RedColor : MutedTextColor; } private void TrySelectCertificateFile() { using OpenFileDialog dialog = new() { Title = "Zertifikat wählen", Filter = "Zertifikate (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|Alle Dateien (*.*)|*.*" }; if (dialog.ShowDialog(this) != DialogResult.OK) { return; } string path = dialog.FileName; if (!IsSupportedCertificateFile(path)) { MessageBox.Show(this, "Dateityp wird nicht unterstützt (.cer/.crt/.pem/.pfx).", "Zertifikat", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { CertificateInfo info = ReadCertificateWithOptionalPassword(path); txtCertificatePath.Text = path; _loadedCertificateInfo = info; DisplayCertificateInfo(info); SetStatus("Zertifikat erkannt.", false); } catch (Exception ex) { _loadedCertificateInfo = null; SetStatus($"Zertifikat ungültig: {ex.Message}", true); MessageBox.Show(this, ex.Message, "Zertifikat", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private CertificateInfo ReadCertificateWithOptionalPassword(string path) { string ext = Path.GetExtension(path); if (!ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase)) { return ReadCertificate(path, null); } try { return ReadCertificate(path, null); } catch (CryptographicException) { string? pwd = PromptPassword(); if (pwd is null) { throw new InvalidOperationException("PFX-Passwort abgebrochen."); } return ReadCertificate(path, pwd); } } private string? PromptPassword() { using Form dialog = new() { Text = "PFX-Passwort", FormBorderStyle = FormBorderStyle.FixedDialog, StartPosition = FormStartPosition.CenterParent, ClientSize = new Size(360, 140), MaximizeBox = false, MinimizeBox = false, BackColor = CardColor, ForeColor = TextColor }; Label label = new() { Text = "Passwort:", Left = 16, Top = 20, AutoSize = true }; TextBox txt = new() { Left = 16, Top = 48, Width = 320, UseSystemPasswordChar = true }; Button ok = new() { Text = "OK", DialogResult = DialogResult.OK, Left = 160, Top = 90, Width = 80 }; Button cancel = new() { Text = "Abbrechen", DialogResult = DialogResult.Cancel, Left = 250, Top = 90, Width = 90 }; dialog.Controls.AddRange([label, txt, ok, cancel]); dialog.AcceptButton = ok; dialog.CancelButton = cancel; return dialog.ShowDialog(this) == DialogResult.OK ? txt.Text : null; } private void DisplayCertificateInfo(CertificateInfo info) { lblCertificateSubject.Text = "Subject: " + info.Subject; lblCertificateIssuer.Text = "Issuer: " + info.Issuer; lblCertificateExpiry.Text = "Gültig bis: " + info.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm"); lblCertificateFingerprint.Text = "SHA-256: " + info.FingerprintSha256; lblCertificateValidity.Text = info.IsCurrentlyValid ? "Status: GÜLTIG" : "Status: UNGÜLTIG / ABGELAUFEN"; lblCertificateValidity.ForeColor = info.IsCurrentlyValid ? GreenColor : RedColor; } private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword) { string extension = Path.GetExtension(certificatePath); using X509Certificate2 certificate = extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) ? new X509Certificate2(certificatePath, pfxPassword, X509KeyStorageFlags.EphemeralKeySet) : new X509Certificate2(certificatePath); string subject = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false); string issuer = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: true); if (string.IsNullOrWhiteSpace(subject)) subject = certificate.Subject; if (string.IsNullOrWhiteSpace(issuer)) issuer = certificate.Issuer; string fingerprint = FormatFingerprint(certificate.GetCertHashString(HashAlgorithmName.SHA256)); DateTimeOffset validFrom = new(certificate.NotBefore); DateTimeOffset validUntil = new(certificate.NotAfter); DateTimeOffset now = DateTimeOffset.Now; return new CertificateInfo { Subject = subject, Issuer = issuer, ValidFrom = validFrom, ValidUntil = validUntil, FingerprintSha256 = fingerprint, IsCurrentlyValid = now >= validFrom && now <= validUntil }; } private static string FormatFingerprint(string fingerprint) => string.IsNullOrWhiteSpace(fingerprint) ? string.Empty : string.Join(":", Enumerable.Range(0, fingerprint.Length / 2) .Select(i => fingerprint.Substring(i * 2, 2))); private static bool IsSupportedCertificateFile(string filePath) { if (!File.Exists(filePath)) { return false; } string ext = Path.GetExtension(filePath); return ext.Equals(".cer", StringComparison.OrdinalIgnoreCase) || ext.Equals(".crt", StringComparison.OrdinalIgnoreCase) || ext.Equals(".pem", StringComparison.OrdinalIgnoreCase) || ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase); } private Panel MakeCard() => new() { Dock = DockStyle.Fill, BackColor = CardColor, Padding = new Padding(8), Margin = new Padding(0, 0, 0, 10) }; private Label SectionTitle(string text, int top) => new() { Text = text, Left = 16, Top = top, AutoSize = true, Font = new Font("Segoe UI Semibold", 11f), ForeColor = GoldColor }; private Label MetaLabel(int left, int top, string text) => new() { Text = text, Left = left, Top = top, AutoSize = true, ForeColor = MutedTextColor }; private Button MakeButton(string text, Color back) => new() { Text = text, Height = 32, FlatStyle = FlatStyle.Flat, BackColor = back, ForeColor = TextColor, FlatAppearance = { BorderSize = 0 }, Cursor = Cursors.Hand }; }