From ed6777ffed01b50227d2e96562fa7da4428b0d8d Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 29 Jul 2026 08:53:13 +0200 Subject: [PATCH] Expand settings UI to manage systems, containers, and certificate paths. Co-authored-by: Cursor --- .../Models/SetupCatalogModels.cs | 70 + .../Services/SonicSetupRepository.cs | 726 +++++++++++ .../Setup/AddContainerForm.cs | 235 ++++ .../Setup/AddSonicSystemForm.cs | 160 +++ .../Setup/AddTargetPathForm.cs | 210 +++ .../Setup/SettingsUi.cs | 156 +++ .../Setup/SetupCoordinator.cs | 79 ++ .../Setup/SetupWizardForm.cs | 1156 ++++++++++------- 8 files changed, 2291 insertions(+), 501 deletions(-) create mode 100644 ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs create mode 100644 ZA.CoreService.ESBCertificateManager/Setup/AddContainerForm.cs create mode 100644 ZA.CoreService.ESBCertificateManager/Setup/AddSonicSystemForm.cs create mode 100644 ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs create mode 100644 ZA.CoreService.ESBCertificateManager/Setup/SettingsUi.cs diff --git a/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs b/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs new file mode 100644 index 0000000..6ce5c2b --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Models/SetupCatalogModels.cs @@ -0,0 +1,70 @@ +namespace ZA.CoreService.ESBCertificateManager.Models; + +public sealed class CompanyOption +{ + public int CompanyId { get; init; } + + public required string CompanyCode { get; init; } + + public required string CompanyName { get; init; } + + public override string ToString() + { + return $"{CompanyCode} — {CompanyName}"; + } +} + +public sealed class ContainerOption +{ + public int SonicContainerId { get; init; } + + public int SonicConnectionId { get; init; } + + public int CompanyId { get; init; } + + public required string ContainerName { get; init; } + + public string? ContainerDisplayName { get; init; } + + public required string CompanyCode { get; init; } + + public required string ConnectionName { get; init; } + + public int RestartTimeoutSeconds { get; init; } + + public override string ToString() + { + string display = + string.IsNullOrWhiteSpace(ContainerDisplayName) + ? ContainerName + : ContainerDisplayName; + + return $"{CompanyCode} / {display} ({ConnectionName})"; + } +} + +public sealed class CertificateTargetOption +{ + public int CertificateTargetId { get; init; } + + public int SonicContainerId { get; init; } + + public required string TargetDirectory { get; init; } + + public required string TargetFileName { get; init; } + + public bool BackupEnabled { get; init; } + + public required string BackupDirectoryName { get; init; } + + public required string ContainerName { get; init; } + + public required string ConnectionName { get; init; } + + public required string CompanyCode { get; init; } + + public override string ToString() + { + return $"{CompanyCode} / {ContainerName}: {TargetDirectory}\\{TargetFileName}"; + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs index 8125008..2298ee7 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs @@ -595,4 +595,730 @@ public sealed class SonicSetupRepository reader.GetOrdinal("IsActive")) }; } + + public async Task> GetCompaniesAsync( + CancellationToken cancellationToken = default) + { + const string sql = """ + SELECT + [CompanyId], + [CompanyCode], + [CompanyName] + FROM [dbo].[Company] + WHERE [IsActive] = 1 + ORDER BY [CompanyCode]; + """; + + List companies = []; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + await using SqlDataReader reader = + await command.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) + { + companies.Add( + new CompanyOption + { + CompanyId = + reader.GetInt32( + reader.GetOrdinal("CompanyId")), + + CompanyCode = + reader.GetString( + reader.GetOrdinal("CompanyCode")), + + CompanyName = + reader.GetString( + reader.GetOrdinal("CompanyName")) + }); + } + + return companies; + } + + public async Task> GetContainersAsync( + string environmentCode, + int? sonicConnectionId = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(environmentCode)) + { + throw new ArgumentException( + "Der EnvironmentCode fehlt.", + nameof(environmentCode)); + } + + const string sql = """ + SELECT + container.[SonicContainerId], + container.[SonicConnectionId], + container.[CompanyId], + container.[ContainerName], + container.[ContainerDisplayName], + container.[RestartTimeoutSeconds], + company.[CompanyCode], + connection.[ConnectionName] + FROM [dbo].[SonicContainer] AS container + INNER JOIN [dbo].[Company] AS company + ON company.[CompanyId] = container.[CompanyId] + INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = + container.[SonicConnectionId] + INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = + connection.[EnvironmentId] + WHERE container.[IsActive] = 1 + AND company.[IsActive] = 1 + AND connection.[IsActive] = 1 + AND environment.[IsActive] = 1 + AND environment.[EnvironmentCode] = @EnvironmentCode + AND + ( + @SonicConnectionId IS NULL + OR container.[SonicConnectionId] = @SonicConnectionId + ) + ORDER BY + company.[CompanyCode], + container.[ContainerName]; + """; + + List containers = []; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + command.Parameters.Add( + new SqlParameter( + "@EnvironmentCode", + SqlDbType.NVarChar, + 30) + { + Value = environmentCode + .Trim() + .ToUpperInvariant() + }); + + command.Parameters.Add( + new SqlParameter( + "@SonicConnectionId", + SqlDbType.Int) + { + Value = sonicConnectionId is null or <= 0 + ? DBNull.Value + : sonicConnectionId.Value + }); + + await using SqlDataReader reader = + await command.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) + { + int displayOrdinal = + reader.GetOrdinal("ContainerDisplayName"); + + containers.Add( + new ContainerOption + { + SonicContainerId = + reader.GetInt32( + reader.GetOrdinal("SonicContainerId")), + + SonicConnectionId = + reader.GetInt32( + reader.GetOrdinal("SonicConnectionId")), + + CompanyId = + reader.GetInt32( + reader.GetOrdinal("CompanyId")), + + ContainerName = + reader.GetString( + reader.GetOrdinal("ContainerName")), + + ContainerDisplayName = + reader.IsDBNull(displayOrdinal) + ? null + : reader.GetString(displayOrdinal), + + RestartTimeoutSeconds = + reader.GetInt32( + reader.GetOrdinal("RestartTimeoutSeconds")), + + CompanyCode = + reader.GetString( + reader.GetOrdinal("CompanyCode")), + + ConnectionName = + reader.GetString( + reader.GetOrdinal("ConnectionName")) + }); + } + + return containers; + } + + public async Task> + GetCertificateTargetsAsync( + string environmentCode, + int? sonicContainerId = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(environmentCode)) + { + throw new ArgumentException( + "Der EnvironmentCode fehlt.", + nameof(environmentCode)); + } + + const string sql = """ + SELECT + target.[CertificateTargetId], + target.[SonicContainerId], + target.[TargetDirectory], + target.[TargetFileName], + target.[BackupEnabled], + target.[BackupDirectoryName], + container.[ContainerName], + connection.[ConnectionName], + company.[CompanyCode] + FROM [dbo].[CertificateTarget] AS target + INNER JOIN [dbo].[SonicContainer] AS container + ON container.[SonicContainerId] = + target.[SonicContainerId] + INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = + container.[SonicConnectionId] + INNER JOIN [dbo].[Company] AS company + ON company.[CompanyId] = container.[CompanyId] + INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = + connection.[EnvironmentId] + WHERE target.[IsActive] = 1 + AND container.[IsActive] = 1 + AND connection.[IsActive] = 1 + AND company.[IsActive] = 1 + AND environment.[IsActive] = 1 + AND environment.[EnvironmentCode] = @EnvironmentCode + AND + ( + @SonicContainerId IS NULL + OR target.[SonicContainerId] = @SonicContainerId + ) + ORDER BY + company.[CompanyCode], + container.[ContainerName], + target.[TargetDirectory], + target.[TargetFileName]; + """; + + List targets = []; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + command.Parameters.Add( + new SqlParameter( + "@EnvironmentCode", + SqlDbType.NVarChar, + 30) + { + Value = environmentCode + .Trim() + .ToUpperInvariant() + }); + + command.Parameters.Add( + new SqlParameter( + "@SonicContainerId", + SqlDbType.Int) + { + Value = sonicContainerId is null or <= 0 + ? DBNull.Value + : sonicContainerId.Value + }); + + await using SqlDataReader reader = + await command.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) + { + targets.Add( + new CertificateTargetOption + { + CertificateTargetId = + reader.GetInt32( + reader.GetOrdinal("CertificateTargetId")), + + SonicContainerId = + reader.GetInt32( + reader.GetOrdinal("SonicContainerId")), + + TargetDirectory = + reader.GetString( + reader.GetOrdinal("TargetDirectory")), + + TargetFileName = + reader.GetString( + reader.GetOrdinal("TargetFileName")), + + BackupEnabled = + reader.GetBoolean( + reader.GetOrdinal("BackupEnabled")), + + BackupDirectoryName = + reader.GetString( + reader.GetOrdinal("BackupDirectoryName")), + + ContainerName = + reader.GetString( + reader.GetOrdinal("ContainerName")), + + ConnectionName = + reader.GetString( + reader.GetOrdinal("ConnectionName")), + + CompanyCode = + reader.GetString( + reader.GetOrdinal("CompanyCode")) + }); + } + + return targets; + } + + public async Task AddSonicConnectionAsync( + string environmentCode, + string connectionName, + string managementHost, + int managementPort, + string domainName, + string connectionProtocol, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(environmentCode)) + { + throw new ArgumentException( + "Der EnvironmentCode fehlt.", + nameof(environmentCode)); + } + + string name = RequireTrimmed( + connectionName, + 150, + "ConnectionName"); + + string host = RequireTrimmed( + managementHost, + 255, + "ManagementHost"); + + string domain = RequireTrimmed( + domainName, + 150, + "DomainName"); + + string protocol = string.IsNullOrWhiteSpace(connectionProtocol) + ? "tcp" + : connectionProtocol.Trim().TrimEnd(':', '/'); + + if (managementPort is < 1 or > 65535) + { + throw new ArgumentOutOfRangeException( + nameof(managementPort), + "Der ManagementPort muss zwischen 1 und 65535 liegen."); + } + + const string sql = """ + INSERT INTO [dbo].[SonicConnection] + ( + [EnvironmentId], + [ConnectionName], + [ManagementHost], + [ManagementPort], + [DomainName], + [ConnectionProtocol], + [IsActive], + [CreationDateTime], + [CreatedBy] + ) + OUTPUT INSERTED.[SonicConnectionId] + SELECT + environment.[EnvironmentId], + @ConnectionName, + @ManagementHost, + @ManagementPort, + @DomainName, + @ConnectionProtocol, + 1, + SYSUTCDATETIME(), + SUSER_SNAME() + FROM [dbo].[Environment] AS environment + WHERE environment.[EnvironmentCode] = @EnvironmentCode + AND environment.[IsActive] = 1; + """; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + command.Parameters.Add( + new SqlParameter( + "@EnvironmentCode", + SqlDbType.NVarChar, + 30) + { + Value = environmentCode + .Trim() + .ToUpperInvariant() + }); + + command.Parameters.Add( + new SqlParameter( + "@ConnectionName", + SqlDbType.NVarChar, + 150) + { + Value = name + }); + + command.Parameters.Add( + new SqlParameter( + "@ManagementHost", + SqlDbType.NVarChar, + 255) + { + Value = host + }); + + command.Parameters.Add( + new SqlParameter( + "@ManagementPort", + SqlDbType.Int) + { + Value = managementPort + }); + + command.Parameters.Add( + new SqlParameter( + "@DomainName", + SqlDbType.NVarChar, + 150) + { + Value = domain + }); + + command.Parameters.Add( + new SqlParameter( + "@ConnectionProtocol", + SqlDbType.NVarChar, + 20) + { + Value = protocol + }); + + object? result = + await command.ExecuteScalarAsync(cancellationToken); + + if (result is null || result is DBNull) + { + throw new InvalidOperationException( + $"Die Umgebung '{environmentCode}' wurde nicht gefunden " + + "oder die Sonic-Verbindung konnte nicht angelegt werden."); + } + + return Convert.ToInt32(result); + } + + public async Task AddSonicContainerAsync( + int companyId, + int sonicConnectionId, + string containerName, + string? containerDisplayName, + int restartTimeoutSeconds, + CancellationToken cancellationToken = default) + { + if (companyId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(companyId)); + } + + if (sonicConnectionId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sonicConnectionId)); + } + + string name = RequireTrimmed( + containerName, + 250, + "ContainerName"); + + string? display = + string.IsNullOrWhiteSpace(containerDisplayName) + ? null + : containerDisplayName.Trim(); + + if (display is { Length: > 250 }) + { + throw new ArgumentException( + "Der Anzeigename darf maximal 250 Zeichen enthalten.", + nameof(containerDisplayName)); + } + + if (restartTimeoutSeconds is < 10 or > 3600) + { + throw new ArgumentOutOfRangeException( + nameof(restartTimeoutSeconds), + "Timeout muss zwischen 10 und 3600 Sekunden liegen."); + } + + const string sql = """ + INSERT INTO [dbo].[SonicContainer] + ( + [CompanyId], + [SonicConnectionId], + [ContainerName], + [ContainerDisplayName], + [RestartTimeoutSeconds], + [IsActive], + [CreationDateTime], + [CreatedBy] + ) + OUTPUT INSERTED.[SonicContainerId] + VALUES + ( + @CompanyId, + @SonicConnectionId, + @ContainerName, + @ContainerDisplayName, + @RestartTimeoutSeconds, + 1, + SYSUTCDATETIME(), + SUSER_SNAME() + ); + """; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + command.Parameters.Add( + new SqlParameter("@CompanyId", SqlDbType.Int) + { + Value = companyId + }); + + command.Parameters.Add( + new SqlParameter("@SonicConnectionId", SqlDbType.Int) + { + Value = sonicConnectionId + }); + + command.Parameters.Add( + new SqlParameter( + "@ContainerName", + SqlDbType.NVarChar, + 250) + { + Value = name + }); + + command.Parameters.Add( + new SqlParameter( + "@ContainerDisplayName", + SqlDbType.NVarChar, + 250) + { + Value = display is null + ? DBNull.Value + : display + }); + + command.Parameters.Add( + new SqlParameter( + "@RestartTimeoutSeconds", + SqlDbType.Int) + { + Value = restartTimeoutSeconds + }); + + object? result = + await command.ExecuteScalarAsync(cancellationToken); + + if (result is null || result is DBNull) + { + throw new InvalidOperationException( + "Der Container konnte nicht angelegt werden."); + } + + return Convert.ToInt32(result); + } + + public async Task AddCertificateTargetAsync( + int sonicContainerId, + string targetDirectory, + string targetFileName, + bool backupEnabled, + string backupDirectoryName, + CancellationToken cancellationToken = default) + { + if (sonicContainerId <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(sonicContainerId)); + } + + string directory = RequireTrimmed( + targetDirectory, + 500, + "TargetDirectory"); + + string fileName = RequireTrimmed( + targetFileName, + 260, + "TargetFileName"); + + string backupName = string.IsNullOrWhiteSpace(backupDirectoryName) + ? "Backup" + : backupDirectoryName.Trim(); + + if (backupName.Length > 100) + { + throw new ArgumentException( + "Der Backup-Ordnername darf maximal 100 Zeichen enthalten.", + nameof(backupDirectoryName)); + } + + const string sql = """ + INSERT INTO [dbo].[CertificateTarget] + ( + [SonicContainerId], + [TargetDirectory], + [TargetFileName], + [BackupEnabled], + [BackupDirectoryName], + [IsActive], + [CreationDateTime], + [CreatedBy] + ) + OUTPUT INSERTED.[CertificateTargetId] + VALUES + ( + @SonicContainerId, + @TargetDirectory, + @TargetFileName, + @BackupEnabled, + @BackupDirectoryName, + 1, + SYSUTCDATETIME(), + SUSER_SNAME() + ); + """; + + await using SqlConnection connection = + new(_connectionString); + + await connection.OpenAsync(cancellationToken); + + await using SqlCommand command = + new(sql, connection); + + command.Parameters.Add( + new SqlParameter("@SonicContainerId", SqlDbType.Int) + { + Value = sonicContainerId + }); + + command.Parameters.Add( + new SqlParameter( + "@TargetDirectory", + SqlDbType.NVarChar, + 500) + { + Value = directory + }); + + command.Parameters.Add( + new SqlParameter( + "@TargetFileName", + SqlDbType.NVarChar, + 260) + { + Value = fileName + }); + + command.Parameters.Add( + new SqlParameter("@BackupEnabled", SqlDbType.Bit) + { + Value = backupEnabled + }); + + command.Parameters.Add( + new SqlParameter( + "@BackupDirectoryName", + SqlDbType.NVarChar, + 100) + { + Value = backupName + }); + + object? result = + await command.ExecuteScalarAsync(cancellationToken); + + if (result is null || result is DBNull) + { + throw new InvalidOperationException( + "Das Zertifikat-Ziel konnte nicht angelegt werden."); + } + + return Convert.ToInt32(result); + } + + private static string RequireTrimmed( + string? value, + int maxLength, + string fieldName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + $"'{fieldName}' fehlt."); + } + + string trimmed = value.Trim(); + + if (trimmed.Length > maxLength) + { + throw new ArgumentException( + $"'{fieldName}' darf maximal {maxLength} Zeichen enthalten."); + } + + return trimmed; + } } + diff --git a/ZA.CoreService.ESBCertificateManager/Setup/AddContainerForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/AddContainerForm.cs new file mode 100644 index 0000000..68a528b --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Setup/AddContainerForm.cs @@ -0,0 +1,235 @@ +using ZA.CoreService.ESBCertificateManager.Models; + +namespace ZA.CoreService.ESBCertificateManager.Setup; + +public sealed class AddContainerForm : Form +{ + private readonly SetupCoordinator _coordinator; + private readonly ComboBox _cmbCompany; + private readonly ComboBox _cmbSystem; + private readonly TextBox _txtName; + private readonly TextBox _txtDisplay; + private readonly TextBox _txtTimeout; + private readonly Button _btnSave; + private bool _running; + + public int? CreatedSonicContainerId { get; private set; } + + public AddContainerForm( + SetupCoordinator coordinator, + int? preferredSonicConnectionId = null) + { + _coordinator = coordinator; + + Text = "Container hinzufügen"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(540, 470); + BackColor = SettingsUi.Background; + ForeColor = SettingsUi.Text; + Font = new Font("Segoe UI", 10f); + + Label title = new() + { + Text = "Neuer Container", + Location = new Point(28, 22), + AutoSize = true, + Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), + ForeColor = SettingsUi.Text + }; + + Label subtitle = new() + { + Text = + "Container einer Company und einem Sonic-System zuordnen.", + Location = new Point(30, 58), + AutoSize = true, + ForeColor = SettingsUi.Muted + }; + + Panel card = SettingsUi.CreateCard(); + card.Location = new Point(28, 95); + card.Size = new Size(484, 290); + card.Paint += SettingsUi.PaintCardBorder; + + _cmbCompany = SettingsUi.CreateComboBox(); + _cmbSystem = SettingsUi.CreateComboBox(); + _txtName = SettingsUi.CreateTextBox(); + _txtDisplay = SettingsUi.CreateTextBox(); + _txtTimeout = SettingsUi.CreateTextBox(); + _txtTimeout.Text = "180"; + + AddField(card, "Company", _cmbCompany, 18, 18, 440); + AddField(card, "Sonic-System", _cmbSystem, 18, 78, 440); + AddField(card, "Container-Name", _txtName, 18, 138, 440); + AddField(card, "Anzeigename (optional)", _txtDisplay, 18, 198, 280); + AddField(card, "Timeout (s)", _txtTimeout, 318, 198, 140); + + _btnSave = SettingsUi.CreatePrimaryButton("Container anlegen"); + _btnSave.Location = new Point(292, 405); + _btnSave.Size = new Size(220, 40); + _btnSave.Click += async (_, _) => await SaveAsync(); + + Button cancel = SettingsUi.CreateGhostButton("Abbrechen"); + cancel.Location = new Point(170, 405); + cancel.Size = new Size(110, 40); + cancel.Click += (_, _) => + { + DialogResult = DialogResult.Cancel; + Close(); + }; + + Controls.Add(title); + Controls.Add(subtitle); + Controls.Add(card); + Controls.Add(cancel); + Controls.Add(_btnSave); + + Shown += async (_, _) => + await LoadLookupsAsync(preferredSonicConnectionId); + } + + private async Task LoadLookupsAsync(int? preferredSonicConnectionId) + { + try + { + IReadOnlyList companies = + await _coordinator.LoadCompaniesAsync(); + + IReadOnlyList systems = + await _coordinator.LoadSystemsAsync(); + + _cmbCompany.Items.Clear(); + foreach (CompanyOption company in companies) + { + _cmbCompany.Items.Add(company); + } + + if (_cmbCompany.Items.Count > 0) + { + _cmbCompany.SelectedIndex = 0; + } + + _cmbSystem.Items.Clear(); + int selected = 0; + + for (int index = 0; index < systems.Count; index++) + { + _cmbSystem.Items.Add(systems[index]); + + if (preferredSonicConnectionId is int id + && systems[index].SonicConnectionId == id) + { + selected = index; + } + } + + if (_cmbSystem.Items.Count > 0) + { + _cmbSystem.SelectedIndex = selected; + } + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + + private async Task SaveAsync() + { + if (_running) + { + return; + } + + if (_cmbCompany.SelectedItem is not CompanyOption company) + { + MessageBox.Show( + this, + "Bitte eine Company wählen.", + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + if (_cmbSystem.SelectedItem is not SonicSystemOption system) + { + MessageBox.Show( + this, + "Bitte ein Sonic-System wählen.", + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + if (!int.TryParse(_txtTimeout.Text.Trim(), out int timeout)) + { + MessageBox.Show( + this, + "Bitte einen gültigen Timeout eingeben.", + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + _running = true; + _btnSave.Enabled = false; + Cursor = Cursors.WaitCursor; + + try + { + CreatedSonicContainerId = + await _coordinator.AddSonicContainerAsync( + company.CompanyId, + system.SonicConnectionId, + _txtName.Text, + _txtDisplay.Text, + timeout); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Container", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + finally + { + _running = false; + _btnSave.Enabled = true; + Cursor = Cursors.Default; + } + } + + private static void AddField( + Control parent, + string label, + Control input, + int x, + int y, + int width) + { + Label fieldLabel = SettingsUi.CreateFieldLabel(label); + fieldLabel.Location = new Point(x, y); + input.Location = new Point(x, y + 22); + input.Width = width; + parent.Controls.Add(fieldLabel); + parent.Controls.Add(input); + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/AddSonicSystemForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/AddSonicSystemForm.cs new file mode 100644 index 0000000..6cf2704 --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Setup/AddSonicSystemForm.cs @@ -0,0 +1,160 @@ +namespace ZA.CoreService.ESBCertificateManager.Setup; + +public sealed class AddSonicSystemForm : Form +{ + private readonly SetupCoordinator _coordinator; + private readonly TextBox _txtName; + private readonly TextBox _txtHost; + private readonly TextBox _txtPort; + private readonly TextBox _txtDomain; + private readonly ComboBox _cmbProtocol; + private readonly Button _btnSave; + private bool _running; + + public int? CreatedSonicConnectionId { get; private set; } + + public AddSonicSystemForm(SetupCoordinator coordinator) + { + _coordinator = coordinator; + + Text = "Sonic-System hinzufügen"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(520, 430); + BackColor = SettingsUi.Background; + ForeColor = SettingsUi.Text; + Font = new Font("Segoe UI", 10f); + + Label title = new() + { + Text = "Neues Sonic-System", + Location = new Point(28, 22), + AutoSize = true, + Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), + ForeColor = SettingsUi.Text + }; + + Label subtitle = new() + { + Text = + $"Umgebung {_coordinator.Settings.EnvironmentCode} · " + + "Management-Verbindung für Container-Neustarts", + Location = new Point(30, 58), + AutoSize = true, + ForeColor = SettingsUi.Muted + }; + + Panel card = SettingsUi.CreateCard(); + card.Location = new Point(28, 95); + card.Size = new Size(464, 250); + card.Paint += SettingsUi.PaintCardBorder; + + _txtName = SettingsUi.CreateTextBox(); + _txtHost = SettingsUi.CreateTextBox(); + _txtPort = SettingsUi.CreateTextBox(); + _txtDomain = SettingsUi.CreateTextBox(); + _cmbProtocol = SettingsUi.CreateComboBox(); + _cmbProtocol.Items.AddRange(["tcp", "ssl"]); + _cmbProtocol.SelectedIndex = 0; + _txtPort.Text = "13070"; + + AddField(card, "Verbindungsname", _txtName, 18, 18, 420); + AddField(card, "Management-Host", _txtHost, 18, 78, 280); + AddField(card, "Port", _txtPort, 316, 78, 122); + AddField(card, "Domain", _txtDomain, 18, 138, 420); + AddField(card, "Protokoll", _cmbProtocol, 18, 198, 160); + + _btnSave = SettingsUi.CreatePrimaryButton("System anlegen"); + _btnSave.Location = new Point(292, 365); + _btnSave.Size = new Size(200, 40); + _btnSave.Click += async (_, _) => await SaveAsync(); + + Button cancel = SettingsUi.CreateGhostButton("Abbrechen"); + cancel.Location = new Point(170, 365); + cancel.Size = new Size(110, 40); + cancel.Click += (_, _) => + { + DialogResult = DialogResult.Cancel; + Close(); + }; + + Controls.Add(title); + Controls.Add(subtitle); + Controls.Add(card); + Controls.Add(cancel); + Controls.Add(_btnSave); + AcceptButton = _btnSave; + CancelButton = cancel; + } + + private async Task SaveAsync() + { + if (_running) + { + return; + } + + if (!int.TryParse(_txtPort.Text.Trim(), out int port)) + { + MessageBox.Show( + this, + "Bitte einen gültigen Port eingeben.", + "Sonic-System", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + _running = true; + _btnSave.Enabled = false; + Cursor = Cursors.WaitCursor; + + try + { + CreatedSonicConnectionId = + await _coordinator.AddSonicConnectionAsync( + _txtName.Text, + _txtHost.Text, + port, + _txtDomain.Text, + _cmbProtocol.SelectedItem?.ToString() ?? "tcp"); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Sonic-System", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + finally + { + _running = false; + _btnSave.Enabled = true; + Cursor = Cursors.Default; + } + } + + private static void AddField( + Control parent, + string label, + Control input, + int x, + int y, + int width) + { + Label fieldLabel = SettingsUi.CreateFieldLabel(label); + fieldLabel.Location = new Point(x, y); + input.Location = new Point(x, y + 22); + input.Width = width; + parent.Controls.Add(fieldLabel); + parent.Controls.Add(input); + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs new file mode 100644 index 0000000..6cee57b --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Setup/AddTargetPathForm.cs @@ -0,0 +1,210 @@ +using ZA.CoreService.ESBCertificateManager.Models; + +namespace ZA.CoreService.ESBCertificateManager.Setup; + +public sealed class AddTargetPathForm : Form +{ + private readonly SetupCoordinator _coordinator; + private readonly ComboBox _cmbContainer; + private readonly TextBox _txtDirectory; + private readonly TextBox _txtFileName; + private readonly TextBox _txtBackupFolder; + private readonly CheckBox _chkBackup; + private readonly Button _btnSave; + private bool _running; + + public int? CreatedCertificateTargetId { get; private set; } + + public AddTargetPathForm( + SetupCoordinator coordinator, + int? preferredSonicContainerId = null) + { + _coordinator = coordinator; + + Text = "Zertifikat-Pfad hinzufügen"; + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + ClientSize = new Size(560, 450); + BackColor = SettingsUi.Background; + ForeColor = SettingsUi.Text; + Font = new Font("Segoe UI", 10f); + + Label title = new() + { + Text = "Neuer Zertifikat-Pfad", + Location = new Point(28, 22), + AutoSize = true, + Font = new Font("Segoe UI Semibold", 18f, FontStyle.Bold), + ForeColor = SettingsUi.Text + }; + + Label subtitle = new() + { + Text = + "Zielverzeichnis und Dateiname für die Zertifikatsablage.", + Location = new Point(30, 58), + AutoSize = true, + ForeColor = SettingsUi.Muted + }; + + Panel card = SettingsUi.CreateCard(); + card.Location = new Point(28, 95); + card.Size = new Size(504, 270); + card.Paint += SettingsUi.PaintCardBorder; + + _cmbContainer = SettingsUi.CreateComboBox(); + _txtDirectory = SettingsUi.CreateTextBox(); + _txtFileName = SettingsUi.CreateTextBox(); + _txtBackupFolder = SettingsUi.CreateTextBox(); + _txtBackupFolder.Text = "Backup"; + _txtFileName.Text = "server.crt"; + + _chkBackup = new CheckBox + { + Text = "Backup vor dem Überschreiben", + AutoSize = true, + Checked = true, + ForeColor = SettingsUi.Text, + Font = new Font("Segoe UI", 9.5f) + }; + + AddField(card, "Container", _cmbContainer, 18, 18, 460); + AddField(card, "Zielverzeichnis", _txtDirectory, 18, 78, 460); + AddField(card, "Dateiname", _txtFileName, 18, 138, 220); + AddField(card, "Backup-Ordner", _txtBackupFolder, 258, 138, 220); + + _chkBackup.Location = new Point(18, 210); + card.Controls.Add(_chkBackup); + + _btnSave = SettingsUi.CreatePrimaryButton("Pfad anlegen"); + _btnSave.Location = new Point(312, 385); + _btnSave.Size = new Size(220, 40); + _btnSave.Click += async (_, _) => await SaveAsync(); + + Button cancel = SettingsUi.CreateGhostButton("Abbrechen"); + cancel.Location = new Point(190, 385); + cancel.Size = new Size(110, 40); + cancel.Click += (_, _) => + { + DialogResult = DialogResult.Cancel; + Close(); + }; + + Controls.Add(title); + Controls.Add(subtitle); + Controls.Add(card); + Controls.Add(cancel); + Controls.Add(_btnSave); + + Shown += async (_, _) => + await LoadContainersAsync(preferredSonicContainerId); + } + + private async Task LoadContainersAsync(int? preferredSonicContainerId) + { + try + { + IReadOnlyList containers = + await _coordinator.LoadContainersAsync(); + + _cmbContainer.Items.Clear(); + int selected = 0; + + for (int index = 0; index < containers.Count; index++) + { + _cmbContainer.Items.Add(containers[index]); + + if (preferredSonicContainerId is int id + && containers[index].SonicContainerId == id) + { + selected = index; + } + } + + if (_cmbContainer.Items.Count > 0) + { + _cmbContainer.SelectedIndex = selected; + } + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Zertifikat-Pfad", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + + private async Task SaveAsync() + { + if (_running) + { + return; + } + + if (_cmbContainer.SelectedItem is not ContainerOption container) + { + MessageBox.Show( + this, + "Bitte einen Container wählen.", + "Zertifikat-Pfad", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + _running = true; + _btnSave.Enabled = false; + Cursor = Cursors.WaitCursor; + + try + { + CreatedCertificateTargetId = + await _coordinator.AddCertificateTargetAsync( + container.SonicContainerId, + _txtDirectory.Text, + _txtFileName.Text, + _chkBackup.Checked, + _txtBackupFolder.Text); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show( + this, + ex.Message, + "Zertifikat-Pfad", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + finally + { + _running = false; + _btnSave.Enabled = true; + Cursor = Cursors.Default; + } + } + + private static void AddField( + Control parent, + string label, + Control input, + int x, + int y, + int width) + { + Label fieldLabel = SettingsUi.CreateFieldLabel(label); + fieldLabel.Location = new Point(x, y); + input.Location = new Point(x, y + 22); + input.Width = width; + parent.Controls.Add(fieldLabel); + parent.Controls.Add(input); + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SettingsUi.cs b/ZA.CoreService.ESBCertificateManager/Setup/SettingsUi.cs new file mode 100644 index 0000000..2909728 --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Setup/SettingsUi.cs @@ -0,0 +1,156 @@ +namespace ZA.CoreService.ESBCertificateManager.Setup; + +internal static class SettingsUi +{ + public static readonly Color Background = + Color.FromArgb(10, 18, 34); + + public static readonly Color Sidebar = + Color.FromArgb(7, 23, 45); + + public static readonly Color Card = + Color.FromArgb(16, 38, 65); + + public static readonly Color CardHover = + Color.FromArgb(23, 55, 94); + + public static readonly Color Border = + Color.FromArgb(36, 74, 117); + + public static readonly Color Text = + Color.FromArgb(241, 245, 249); + + public static readonly Color Muted = + Color.FromArgb(169, 184, 200); + + public static readonly Color Gold = + Color.FromArgb(208, 171, 57); + + public static readonly Color Blue = + Color.FromArgb(0, 110, 182); + + public static readonly Color Green = + Color.FromArgb(60, 203, 127); + + public static readonly Color Red = + Color.FromArgb(239, 106, 106); + + public static Button CreatePrimaryButton(string text) + { + Button button = new() + { + Text = text, + BackColor = Gold, + ForeColor = Color.FromArgb(30, 25, 10), + FlatStyle = FlatStyle.Flat, + Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold), + Cursor = Cursors.Hand, + Height = 38 + }; + + button.FlatAppearance.BorderSize = 0; + button.FlatAppearance.MouseOverBackColor = + Color.FromArgb(226, 196, 93); + + return button; + } + + public static Button CreateGhostButton(string text) + { + Button button = new() + { + Text = text, + BackColor = Card, + ForeColor = Text, + FlatStyle = FlatStyle.Flat, + Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold), + Cursor = Cursors.Hand, + Height = 38 + }; + + button.FlatAppearance.BorderColor = Border; + button.FlatAppearance.BorderSize = 1; + button.FlatAppearance.MouseOverBackColor = CardHover; + + return button; + } + + public static TextBox CreateTextBox() + { + return new TextBox + { + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = Text, + BorderStyle = BorderStyle.FixedSingle, + Font = new Font("Segoe UI", 10f), + Height = 30 + }; + } + + public static ComboBox CreateComboBox() + { + return new ComboBox + { + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = Text, + FlatStyle = FlatStyle.Flat, + DropDownStyle = ComboBoxStyle.DropDownList, + Font = new Font("Segoe UI", 10f), + Height = 30 + }; + } + + public static Label CreateFieldLabel(string text) + { + return new Label + { + Text = text, + AutoSize = true, + ForeColor = Muted, + Font = new Font("Segoe UI Semibold", 8.5f, FontStyle.Bold) + }; + } + + public static Panel CreateCard() + { + return new Panel + { + BackColor = Card, + Padding = new Padding(18) + }; + } + + public static ListView CreateListView() + { + ListView list = new() + { + View = View.Details, + FullRowSelect = true, + MultiSelect = false, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + BorderStyle = BorderStyle.FixedSingle, + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = Text, + Font = new Font("Segoe UI", 9.5f), + HideSelection = false + }; + + return list; + } + + public static void PaintCardBorder( + object? sender, + PaintEventArgs e) + { + if (sender is not Control control) + { + return; + } + + using Pen pen = new(Border, 1); + Rectangle bounds = control.ClientRectangle; + bounds.Width -= 1; + bounds.Height -= 1; + e.Graphics.DrawRectangle(pen, bounds); + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SetupCoordinator.cs b/ZA.CoreService.ESBCertificateManager/Setup/SetupCoordinator.cs index 6c8199b..d61f6a1 100644 --- a/ZA.CoreService.ESBCertificateManager/Setup/SetupCoordinator.cs +++ b/ZA.CoreService.ESBCertificateManager/Setup/SetupCoordinator.cs @@ -325,6 +325,85 @@ public sealed class SetupCoordinator cancellationToken); } + public Task> LoadCompaniesAsync( + CancellationToken cancellationToken = default) + { + return _setupRepository.GetCompaniesAsync(cancellationToken); + } + + public Task> LoadContainersAsync( + int? sonicConnectionId = null, + CancellationToken cancellationToken = default) + { + return _setupRepository.GetContainersAsync( + _settings.EnvironmentCode, + sonicConnectionId, + cancellationToken); + } + + public Task> + LoadCertificateTargetsAsync( + int? sonicContainerId = null, + CancellationToken cancellationToken = default) + { + return _setupRepository.GetCertificateTargetsAsync( + _settings.EnvironmentCode, + sonicContainerId, + cancellationToken); + } + + public Task AddSonicConnectionAsync( + string connectionName, + string managementHost, + int managementPort, + string domainName, + string connectionProtocol, + CancellationToken cancellationToken = default) + { + return _setupRepository.AddSonicConnectionAsync( + _settings.EnvironmentCode, + connectionName, + managementHost, + managementPort, + domainName, + connectionProtocol, + cancellationToken); + } + + public Task AddSonicContainerAsync( + int companyId, + int sonicConnectionId, + string containerName, + string? containerDisplayName, + int restartTimeoutSeconds, + CancellationToken cancellationToken = default) + { + return _setupRepository.AddSonicContainerAsync( + companyId, + sonicConnectionId, + containerName, + containerDisplayName, + restartTimeoutSeconds, + cancellationToken); + } + + public Task AddCertificateTargetAsync( + int sonicContainerId, + string targetDirectory, + string targetFileName, + bool backupEnabled, + string backupDirectoryName, + CancellationToken cancellationToken = default) + { + return _setupRepository.AddCertificateTargetAsync( + sonicContainerId, + targetDirectory, + targetFileName, + backupEnabled, + backupDirectoryName, + cancellationToken); + } + public void SaveSelection( SonicSystemOption system, SonicCredentialProfile profile) diff --git a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs index 534d674..8d3f1c8 100644 --- a/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs +++ b/ZA.CoreService.ESBCertificateManager/Setup/SetupWizardForm.cs @@ -7,46 +7,38 @@ public sealed class SetupWizardForm : Form private readonly SetupCoordinator _coordinator; private readonly bool _isSettingsMode; - private readonly Color _backgroundColor = - Color.FromArgb(10, 18, 34); + private Panel _sidebar = null!; + private Panel _contentHost = null!; + private Panel _panelStatus = null!; + private Panel _panelSystems = null!; + private Panel _panelContainers = null!; + private Panel _panelPaths = null!; + private Panel _panelProfiles = null!; - private readonly Color _cardColor = - Color.FromArgb(16, 38, 65); - - private readonly Color _textColor = - Color.FromArgb(241, 245, 249); - - private readonly Color _mutedTextColor = - Color.FromArgb(169, 184, 200); - - private readonly Color _goldColor = - Color.FromArgb(208, 171, 57); - - private readonly Color _greenColor = - Color.FromArgb(60, 203, 127); - - private readonly Color _redColor = - Color.FromArgb(239, 106, 106); + private readonly List