big changes

This commit is contained in:
Mike Gisler
2026-07-28 08:03:12 +02:00
parent c73f53d740
commit 4d19873776
179 changed files with 14953 additions and 494 deletions
@@ -0,0 +1,450 @@
using System.Drawing;
using System.Windows.Forms;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class AddCredentialProfileForm : Form
{
private readonly SetupCoordinator _coordinator;
private readonly SonicSystemOption _system;
private readonly TextBox _txtProfileName;
private readonly TextBox _txtUserName;
private readonly TextBox _txtPassword;
private readonly TextBox _txtPasswordRepeat;
private readonly CheckBox _chkDefault;
private readonly Button _btnSave;
private readonly Button _btnCancel;
private bool _operationRunning;
public SonicCredentialProfile? CreatedProfile
{
get;
private set;
}
public AddCredentialProfileForm(
SetupCoordinator coordinator,
SonicSystemOption system)
{
_coordinator =
coordinator
?? throw new ArgumentNullException(
nameof(coordinator));
_system =
system
?? throw new ArgumentNullException(
nameof(system));
Text = "Benutzerprofil hinzufügen";
StartPosition =
FormStartPosition.CenterParent;
ClientSize =
new Size(560, 525);
FormBorderStyle =
FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
BackColor =
Color.FromArgb(10, 18, 34);
ForeColor =
Color.FromArgb(241, 245, 249);
Font =
new Font("Segoe UI", 10);
Label title = new()
{
Text =
$"Profil für {_system.ConnectionName}",
Location =
new Point(28, 22),
AutoSize = true,
ForeColor =
Color.FromArgb(241, 245, 249),
Font =
new Font(
"Segoe UI",
17,
FontStyle.Bold)
};
Label details = new()
{
Text =
$"Umgebung: {_system.EnvironmentCode}"
+ Environment.NewLine
+ $"Domain: {_system.DomainName}"
+ Environment.NewLine
+ $"Adresse: {_system.ConnectionUrl}"
+ Environment.NewLine
+ $"Prüfcontainer: "
+ $"{_system.ValidationContainerName}",
Location =
new Point(30, 62),
Size =
new Size(495, 90),
ForeColor =
Color.FromArgb(169, 184, 200)
};
Label profileLabel =
CreateLabel(
"Profilname",
160);
_txtProfileName =
CreateTextBox(185);
_txtProfileName.Text =
"Administrator";
Label userLabel =
CreateLabel(
"Sonic-Benutzername",
225);
_txtUserName =
CreateTextBox(250);
_txtUserName.Text =
"Administrator";
Label passwordLabel =
CreateLabel(
"Kennwort",
290);
_txtPassword =
CreateTextBox(315);
_txtPassword.UseSystemPasswordChar =
true;
Label repeatLabel =
CreateLabel(
"Kennwort wiederholen",
355);
_txtPasswordRepeat =
CreateTextBox(380);
_txtPasswordRepeat.UseSystemPasswordChar =
true;
_chkDefault = new CheckBox
{
Text =
"Als Standardprofil verwenden",
Location =
new Point(30, 425),
AutoSize = true,
Checked = true,
ForeColor =
Color.FromArgb(241, 245, 249)
};
_btnCancel = new Button
{
Text = "Abbrechen",
Location =
new Point(125, 465),
Size =
new Size(110, 38),
BackColor =
Color.FromArgb(39, 52, 73),
ForeColor =
Color.FromArgb(241, 245, 249),
FlatStyle =
FlatStyle.Flat,
DialogResult =
DialogResult.Cancel
};
_btnCancel.FlatAppearance.BorderSize = 0;
_btnSave = new Button
{
Text =
"Verbindung prüfen und hinzufügen",
Location =
new Point(245, 465),
Size =
new Size(280, 38),
BackColor =
Color.FromArgb(208, 171, 57),
ForeColor =
Color.FromArgb(30, 25, 10),
FlatStyle =
FlatStyle.Flat,
Font =
new Font(
"Segoe UI",
9,
FontStyle.Bold)
};
_btnSave.FlatAppearance.BorderSize = 0;
_btnSave.Click +=
async (_, _) =>
await TestAndSaveAsync();
Controls.Add(title);
Controls.Add(details);
Controls.Add(profileLabel);
Controls.Add(_txtProfileName);
Controls.Add(userLabel);
Controls.Add(_txtUserName);
Controls.Add(passwordLabel);
Controls.Add(_txtPassword);
Controls.Add(repeatLabel);
Controls.Add(_txtPasswordRepeat);
Controls.Add(_chkDefault);
Controls.Add(_btnCancel);
Controls.Add(_btnSave);
AcceptButton = _btnSave;
CancelButton = _btnCancel;
}
private Label CreateLabel(
string text,
int y)
{
return new Label
{
Text = text,
Location =
new Point(30, y),
AutoSize = true,
ForeColor =
Color.FromArgb(203, 213, 225)
};
}
private TextBox CreateTextBox(int y)
{
return new TextBox
{
Location =
new Point(30, y),
Size =
new Size(495, 27),
BorderStyle =
BorderStyle.FixedSingle,
BackColor =
Color.FromArgb(24, 35, 54),
ForeColor =
Color.FromArgb(241, 245, 249)
};
}
private async Task TestAndSaveAsync()
{
if (_operationRunning)
{
return;
}
string profileName =
_txtProfileName.Text.Trim();
string userName =
_txtUserName.Text.Trim();
string password =
_txtPassword.Text;
string passwordRepeat =
_txtPasswordRepeat.Text;
if (string.IsNullOrWhiteSpace(profileName))
{
ShowWarning(
"Bitte einen Profilnamen eingeben.");
_txtProfileName.Focus();
return;
}
if (profileName.Length > 150)
{
ShowWarning(
"Der Profilname darf maximal 150 Zeichen enthalten.");
_txtProfileName.Focus();
return;
}
if (string.IsNullOrWhiteSpace(userName))
{
ShowWarning(
"Bitte einen Sonic-Benutzernamen eingeben.");
_txtUserName.Focus();
return;
}
if (userName.Length > 256)
{
ShowWarning(
"Der Benutzername darf maximal 256 Zeichen enthalten.");
_txtUserName.Focus();
return;
}
if (string.IsNullOrEmpty(password))
{
ShowWarning(
"Bitte ein Kennwort eingeben.");
_txtPassword.Focus();
return;
}
if (password.Length > 512)
{
ShowWarning(
"Das Kennwort darf maximal 512 Zeichen enthalten.");
_txtPassword.Focus();
return;
}
if (!string.Equals(
password,
passwordRepeat,
StringComparison.Ordinal))
{
ShowWarning(
"Die eingegebenen Kennwörter stimmen nicht überein.");
_txtPasswordRepeat.Clear();
_txtPasswordRepeat.Focus();
return;
}
await RunSaveOperationAsync(
profileName,
userName,
password);
}
private async Task RunSaveOperationAsync(
string profileName,
string userName,
string password)
{
_operationRunning = true;
SetOperationState(isRunning: true);
try
{
CreatedProfile =
await _coordinator.AddCredentialAsync(
_system,
profileName,
userName,
password,
_chkDefault.Checked);
DialogResult = DialogResult.OK;
Close();
}
catch (OperationCanceledException)
{
// Keine Fehlermeldung bei einem regulären Abbruch.
}
catch (Exception exception)
{
MessageBox.Show(
this,
"Das Benutzerprofil konnte nicht angelegt werden."
+ Environment.NewLine
+ Environment.NewLine
+ exception.Message,
"Benutzerprofil",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
_operationRunning = false;
if (!IsDisposed)
{
SetOperationState(isRunning: false);
}
}
}
private void SetOperationState(bool isRunning)
{
UseWaitCursor = isRunning;
_btnSave.Enabled = !isRunning;
_btnCancel.Enabled = !isRunning;
_txtProfileName.Enabled = !isRunning;
_txtUserName.Enabled = !isRunning;
_txtPassword.Enabled = !isRunning;
_txtPasswordRepeat.Enabled = !isRunning;
_chkDefault.Enabled = !isRunning;
_btnSave.Text =
isRunning
? "Verbindung wird geprüft ..."
: "Verbindung prüfen und hinzufügen";
}
private void ShowWarning(string message)
{
MessageBox.Show(
this,
message,
"Eingabe prüfen",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,154 @@
using System.Security.Cryptography.X509Certificates;
using ZA.CoreService.ESBCertificateManager.Models;
using static ZA.CoreService.ESBCertificateManager.Models.RuntimeSettings;
namespace ZA.CoreService.ESBCertificateManager.Services;
public sealed class AlwaysEncryptedCertificateService
{
private readonly string _expectedThumbprint;
private readonly StoreName _storeName;
private readonly StoreLocation _storeLocation;
public AlwaysEncryptedCertificateService(
AlwaysEncryptedSettings settings)
{
if (string.IsNullOrWhiteSpace(
settings.CertificateThumbprint))
{
throw new InvalidOperationException(
"AlwaysEncrypted:CertificateThumbprint fehlt.");
}
_expectedThumbprint = NormalizeThumbprint(
settings.CertificateThumbprint);
if (!Enum.TryParse(
settings.StoreName,
ignoreCase: true,
out _storeName))
{
throw new InvalidOperationException(
$"Ungültiger Zertifikatsspeicher: '{settings.StoreName}'.");
}
if (!Enum.TryParse(
settings.StoreLocation,
ignoreCase: true,
out _storeLocation))
{
throw new InvalidOperationException(
$"Ungültiger Zertifikatsspeicherort: " +
$"'{settings.StoreLocation}'.");
}
}
public CertificateCheckResult CheckCertificate()
{
using X509Store store = new(
_storeName,
_storeLocation);
store.Open(OpenFlags.ReadOnly);
X509Certificate2? certificate = store.Certificates
.Find(
X509FindType.FindByThumbprint,
_expectedThumbprint,
validOnly: false)
.OfType<X509Certificate2>()
.FirstOrDefault();
if (certificate is null)
{
return new CertificateCheckResult(
IsValid: false,
Message:
$"Das Always-Encrypted-Zertifikat wurde nicht gefunden. " +
$"Speicher: {_storeLocation}\\{_storeName}, " +
$"Thumbprint: {_expectedThumbprint}");
}
if (!certificate.HasPrivateKey)
{
return new CertificateCheckResult(
IsValid: false,
Message:
"Das Always-Encrypted-Zertifikat wurde gefunden, " +
"besitzt aber keinen verfügbaren privaten Schlüssel.");
}
return new CertificateCheckResult(
IsValid: true,
Message:
"Always-Encrypted-Zertifikat und privater " +
"Schlüssel sind verfügbar.");
}
public void ImportPfx(
string pfxFilePath,
string pfxPassword)
{
if (string.IsNullOrWhiteSpace(pfxFilePath))
{
throw new ArgumentException(
"Es wurde keine PFX-Datei ausgewählt.",
nameof(pfxFilePath));
}
if (!File.Exists(pfxFilePath))
{
throw new FileNotFoundException(
"Die ausgewählte PFX-Datei wurde nicht gefunden.",
pfxFilePath);
}
X509KeyStorageFlags flags =
X509KeyStorageFlags.UserKeySet
| X509KeyStorageFlags.PersistKeySet;
using X509Certificate2 certificate = new(
pfxFilePath,
pfxPassword,
flags);
string importedThumbprint =
NormalizeThumbprint(certificate.Thumbprint);
if (!string.Equals(
importedThumbprint,
_expectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"Die ausgewählte PFX-Datei gehört nicht zum " +
"erwarteten Always-Encrypted-Zertifikat.");
}
if (!certificate.HasPrivateKey)
{
throw new InvalidOperationException(
"Die ausgewählte PFX-Datei enthält keinen privaten Schlüssel.");
}
using X509Store store = new(
_storeName,
_storeLocation);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
}
private static string NormalizeThumbprint(
string thumbprint)
{
return thumbprint
.Replace(" ", string.Empty)
.Trim()
.ToUpperInvariant();
}
}
public sealed record CertificateCheckResult(
bool IsValid,
string Message);
@@ -0,0 +1,30 @@
namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class SetupCheckResult
{
public bool RuntimeReady { get; init; }
public bool CertificateReady { get; init; }
public bool DatabaseReady { get; init; }
public bool CredentialsReady { get; init; }
public string RuntimeMessage { get; init; } =
string.Empty;
public string CertificateMessage { get; init; } =
string.Empty;
public string DatabaseMessage { get; init; } =
string.Empty;
public string CredentialsMessage { get; init; } =
string.Empty;
public bool IsReady =>
RuntimeReady
&& CertificateReady
&& DatabaseReady
&& CredentialsReady;
}
@@ -0,0 +1,368 @@
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class SetupCoordinator
{
private readonly AppSettings _settings;
private readonly RuntimeEnvironmentValidator
_runtimeValidator;
private readonly AlwaysEncryptedCertificateService
_certificateService;
private readonly SonicSetupRepository
_setupRepository;
private readonly LocalSetupSelectionStore
_selectionStore;
private readonly SonicCredentialTester
_credentialTester;
private readonly SonicSetupRepository _repository;
public SetupCoordinator(
AppSettings settings, SonicSetupRepository repository)
{
_repository =
repository
?? throw new ArgumentNullException(
nameof(repository));
_credentialTester =
new SonicCredentialTester(
settings.Runtime);
_settings =
settings
?? throw new ArgumentNullException(
nameof(settings));
_runtimeValidator =
new RuntimeEnvironmentValidator();
_certificateService =
new AlwaysEncryptedCertificateService(
settings.AlwaysEncrypted);
_setupRepository =
new SonicSetupRepository(
settings.Database.ConnectionString);
_selectionStore =
new LocalSetupSelectionStore();
}
public AppSettings Settings =>
_settings;
public AlwaysEncryptedCertificateService
CertificateService =>
_certificateService;
public LocalSetupSelection? LoadSelection()
{
return _selectionStore.Load();
}
public async Task<SonicCredentialProfile>
TestAndAddCredentialAsync(
SonicSystemOption system,
string credentialName,
string userName,
string secret,
bool isDefault,
CancellationToken cancellationToken = default)
{
CredentialTestResult testResult =
await _credentialTester.TestAsync(
system,
userName,
secret,
system.ValidationContainerName,
cancellationToken);
if (!testResult.Success)
{
throw new InvalidOperationException(
"Das Benutzerprofil wurde nicht gespeichert." +
Environment.NewLine +
Environment.NewLine +
testResult.Message);
}
int credentialId =
await _setupRepository.AddCredentialAsync(
system.SonicConnectionId,
credentialName,
userName,
secret,
isDefault,
cancellationToken);
return new SonicCredentialProfile
{
SonicCredentialId = credentialId,
SonicConnectionId =
system.SonicConnectionId,
CredentialName =
credentialName.Trim(),
UserName =
userName.Trim(),
Secret = secret,
IsDefault = isDefault,
IsActive = true
};
}
public async Task<SonicCredentialProfile> AddCredentialAsync(
SonicSystemOption system,
string credentialName,
string userName,
string secret,
bool isDefault,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(system);
if (system.SonicConnectionId <= 0)
{
throw new ArgumentException(
"Das ausgewählte Sonic-System ist ungültig.",
nameof(system));
}
int credentialId =
await _repository.AddCredentialAsync(
system.SonicConnectionId,
credentialName,
userName,
secret,
isDefault,
cancellationToken);
IReadOnlyList<SonicCredentialProfile> profiles =
await _repository.GetCredentialsAsync(
system.SonicConnectionId,
cancellationToken);
SonicCredentialProfile? createdProfile =
profiles.FirstOrDefault(
profile =>
profile.SonicCredentialId
== credentialId);
if (createdProfile is null)
{
throw new InvalidOperationException(
"Das Benutzerprofil wurde gespeichert, "
+ "konnte anschließend aber nicht geladen werden.");
}
return createdProfile;
}
public async Task<SetupCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
RuntimeValidationResult runtimeResult =
await _runtimeValidator.ValidateAsync(
_settings.Runtime,
cancellationToken);
CertificateCheckResult certificateResult =
_certificateService.CheckCertificate();
if (!certificateResult.IsValid)
{
return new SetupCheckResult
{
RuntimeReady =
runtimeResult.IsValid,
RuntimeMessage =
BuildRuntimeMessage(runtimeResult),
CertificateReady = false,
CertificateMessage =
certificateResult.Message,
DatabaseReady = false,
DatabaseMessage =
"Die verschlüsselten SQL-Daten können ohne " +
"das Always-Encrypted-Zertifikat nicht gelesen werden.",
CredentialsReady = false,
CredentialsMessage =
"System und Benutzerprofil konnten nicht geprüft werden."
};
}
try
{
IReadOnlyList<SonicSystemOption> systems =
await LoadSystemsAsync(
cancellationToken);
LocalSetupSelection? selection =
_selectionStore.Load();
bool systemExists =
selection is not null
&& string.Equals(
selection.EnvironmentCode,
_settings.EnvironmentCode,
StringComparison.OrdinalIgnoreCase)
&& systems.Any(
system =>
system.SonicConnectionId ==
selection.SonicConnectionId);
bool credentialExists = false;
if (systemExists
&& selection is not null)
{
IReadOnlyList<SonicCredentialProfile> profiles =
await LoadCredentialsAsync(
selection.SonicConnectionId,
cancellationToken);
credentialExists =
profiles.Any(
profile =>
profile.SonicCredentialId ==
selection.SonicCredentialId
&& profile.IsComplete);
}
return new SetupCheckResult
{
RuntimeReady =
runtimeResult.IsValid,
RuntimeMessage =
BuildRuntimeMessage(runtimeResult),
CertificateReady = true,
CertificateMessage =
certificateResult.Message,
DatabaseReady =
systems.Count > 0,
DatabaseMessage =
systems.Count > 0
? $"{systems.Count} System(e) für " +
$"{_settings.EnvironmentCode} gefunden."
: "Für die gewählte Umgebung wurde " +
"kein Sonic-System gefunden.",
CredentialsReady =
systemExists
&& credentialExists,
CredentialsMessage =
systemExists
&& credentialExists
? $"System '{selection!.ConnectionName}', " +
$"Profil '{selection.CredentialName}' ausgewählt."
: "Bitte Sonic-System und Benutzerprofil auswählen."
};
}
catch (Exception ex)
{
return new SetupCheckResult
{
RuntimeReady =
runtimeResult.IsValid,
RuntimeMessage =
BuildRuntimeMessage(runtimeResult),
CertificateReady = true,
CertificateMessage =
certificateResult.Message,
DatabaseReady = false,
DatabaseMessage =
$"SQL-Prüfung fehlgeschlagen: {ex.Message}",
CredentialsReady = false,
CredentialsMessage =
"Systemauswahl konnte nicht geprüft werden."
};
}
}
public Task<IReadOnlyList<SonicSystemOption>>
LoadSystemsAsync(
CancellationToken cancellationToken = default)
{
return _setupRepository.GetSystemsAsync(
_settings.EnvironmentCode,
cancellationToken);
}
public Task<IReadOnlyList<SonicCredentialProfile>>
LoadCredentialsAsync(
int sonicConnectionId,
CancellationToken cancellationToken = default)
{
return _setupRepository.GetCredentialsAsync(
sonicConnectionId,
cancellationToken);
}
public void SaveSelection(
SonicSystemOption system,
SonicCredentialProfile profile)
{
ArgumentNullException.ThrowIfNull(system);
ArgumentNullException.ThrowIfNull(profile);
if (profile.SonicConnectionId
!= system.SonicConnectionId)
{
throw new InvalidOperationException(
"Das Benutzerprofil gehört nicht zum ausgewählten System.");
}
_selectionStore.Save(
new LocalSetupSelection
{
EnvironmentCode =
_settings.EnvironmentCode,
SonicConnectionId =
system.SonicConnectionId,
SonicCredentialId =
profile.SonicCredentialId,
ConnectionName =
system.ConnectionName,
CredentialName =
profile.CredentialName,
SavedAtUtc =
DateTimeOffset.UtcNow
});
}
private static string BuildRuntimeMessage(
RuntimeValidationResult runtimeResult)
{
return runtimeResult.IsValid
? "Java und Sonic-Runtime sind verfügbar."
: string.Join(
Environment.NewLine,
runtimeResult.Issues);
}
}
@@ -0,0 +1,802 @@
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Setup;
public sealed class SetupWizardForm : Form
{
private readonly SetupCoordinator _coordinator;
private readonly Color _backgroundColor =
Color.FromArgb(10, 18, 34);
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 Label _lblRuntime = null!;
private Label _lblCertificate = null!;
private Label _lblDatabase = null!;
private Label _lblSelection = null!;
private Label _lblEnvironment = null!;
private Label _lblSystemDetails = null!;
private ComboBox _cmbSystem = null!;
private ComboBox _cmbCredential = null!;
private Button _btnImportCertificate = null!;
private Button _btnRefresh = null!;
private Button _btnSaveSelection = null!;
private Button _btnFinish = null!;
private Button _btnAddCredential = null!;
private IReadOnlyList<SonicSystemOption> _systems = [];
private IReadOnlyList<SonicCredentialProfile> _profiles = [];
private bool _running;
public SetupWizardForm(
SetupCoordinator coordinator)
{
_coordinator = coordinator;
BuildInterface();
Shown += async (_, _) =>
await InitializeAsync();
}
private void BuildInterface()
{
Text = "ESB Certificate Manager einrichten";
StartPosition = FormStartPosition.CenterScreen;
Size = new Size(950, 690);
MinimumSize = new Size(900, 650);
MaximizeBox = false;
BackColor = _backgroundColor;
ForeColor = _textColor;
Font = new Font("Segoe UI", 10);
Label title = new()
{
Text = "Ersteinrichtung",
AutoSize = true,
Location = new Point(38, 25),
ForeColor = _textColor,
Font = new Font(
"Segoe UI",
24,
FontStyle.Bold)
};
Label subtitle = new()
{
Text =
"Runtime, Datenbank, Zertifikat und Systemauswahl prüfen.",
AutoSize = true,
Location = new Point(42, 70),
ForeColor = _mutedTextColor
};
Panel checkCard = CreateCard(
new Point(38, 105),
new Size(870, 225));
Label checkTitle = CreateTitle(
"Systemprüfung",
new Point(20, 16));
_lblRuntime = CreateStatus(
new Point(20, 55));
_lblCertificate = CreateStatus(
new Point(20, 95));
_lblDatabase = CreateStatus(
new Point(20, 135));
_lblSelection = CreateStatus(
new Point(20, 175));
_btnImportCertificate = CreateButton(
"PFX importieren");
_btnImportCertificate.Location =
new Point(680, 90);
_btnImportCertificate.Size =
new Size(160, 38);
_btnImportCertificate.Click +=
async (_, _) =>
await ImportCertificateAsync();
_cmbCredential = new ComboBox
{
Location = new Point(185, 133),
Size = new Size(300, 30),
DropDownStyle =
ComboBoxStyle.DropDownList
};
checkCard.Controls.Add(checkTitle);
checkCard.Controls.Add(_lblRuntime);
checkCard.Controls.Add(_lblCertificate);
checkCard.Controls.Add(_lblDatabase);
checkCard.Controls.Add(_lblSelection);
checkCard.Controls.Add(_btnImportCertificate);
Panel selectionCard = CreateCard(
new Point(38, 350),
new Size(870, 205));
Label selectionTitle = CreateTitle(
"System auswählen",
new Point(20, 16));
_lblEnvironment = new Label
{
Text =
$"Umgebung: {_coordinator.Settings.EnvironmentCode}",
AutoSize = true,
Location = new Point(22, 55),
ForeColor = _goldColor,
Font = new Font(
"Segoe UI",
10,
FontStyle.Bold)
};
Label systemLabel = CreateLabel(
"Sonic-System",
new Point(22, 92));
_cmbSystem = new ComboBox
{
Location = new Point(185, 88),
Size = new Size(300, 30),
DropDownStyle =
ComboBoxStyle.DropDownList
};
_cmbSystem.SelectedIndexChanged +=
async (_, _) =>
await SystemChangedAsync();
Label credentialLabel = CreateLabel(
"Benutzerprofil",
new Point(22, 137));
_cmbCredential = new ComboBox
{
Location = new Point(185, 133),
Size = new Size(300, 30),
DropDownStyle =
ComboBoxStyle.DropDownList
};
_lblSystemDetails = new Label
{
Location = new Point(515, 88),
Size = new Size(325, 72),
ForeColor = _mutedTextColor
};
_btnSaveSelection = CreateButton(
"Auswahl übernehmen");
_btnSaveSelection.Location =
new Point(640, 153);
_btnSaveSelection.Size =
new Size(200, 38);
_btnSaveSelection.Click +=
async (_, _) =>
await SaveSelectionAsync();
_btnAddCredential = CreateButton(
"Profil hinzufügen");
_btnAddCredential.Location =
new Point(495, 132);
_btnAddCredential.Size =
new Size(140, 32);
_btnAddCredential.Click +=
async (_, _) =>
await AddCredentialAsync();
selectionCard.Controls.Add(selectionTitle);
selectionCard.Controls.Add(_lblEnvironment);
selectionCard.Controls.Add(systemLabel);
selectionCard.Controls.Add(_cmbSystem);
selectionCard.Controls.Add(credentialLabel);
selectionCard.Controls.Add(credentialLabel);
selectionCard.Controls.Add(_cmbCredential);
selectionCard.Controls.Add(_btnAddCredential);
selectionCard.Controls.Add(_lblSystemDetails);
selectionCard.Controls.Add(_btnSaveSelection);
_btnRefresh = CreateButton(
"Erneut prüfen");
_btnRefresh.Location =
new Point(525, 580);
_btnRefresh.Size =
new Size(140, 42);
_btnRefresh.Click +=
async (_, _) =>
await InitializeAsync();
Button cancelButton = CreateButton(
"Abbrechen");
cancelButton.Location =
new Point(675, 580);
cancelButton.Size =
new Size(110, 42);
cancelButton.Click += (_, _) =>
{
DialogResult = DialogResult.Cancel;
Close();
};
_btnFinish = CreateButton(
"Setup abschließen");
_btnFinish.Location =
new Point(795, 580);
_btnFinish.Size =
new Size(140, 42);
_btnFinish.Enabled = false;
_btnFinish.Click += (_, _) =>
{
DialogResult = DialogResult.OK;
Close();
};
Controls.Add(title);
Controls.Add(subtitle);
Controls.Add(checkCard);
Controls.Add(selectionCard);
Controls.Add(_btnRefresh);
Controls.Add(cancelButton);
Controls.Add(_btnFinish);
}
private async Task InitializeAsync()
{
if (_running)
{
return;
}
SetRunning(true);
try
{
SetupCheckResult result =
await _coordinator.CheckAsync();
SetStatus(
_lblRuntime,
"Runtime",
result.RuntimeReady,
result.RuntimeMessage);
SetStatus(
_lblCertificate,
"Always Encrypted",
result.CertificateReady,
result.CertificateMessage);
SetStatus(
_lblDatabase,
"Datenbank",
result.DatabaseReady,
result.DatabaseMessage);
SetStatus(
_lblSelection,
"Auswahl",
result.CredentialsReady,
result.CredentialsMessage);
_btnImportCertificate.Visible =
!result.CertificateReady;
_btnFinish.Enabled =
result.IsReady;
if (result.CertificateReady)
{
await LoadSystemsAsync();
}
}
catch (Exception ex)
{
MessageBox.Show(
this,
ex.Message,
"Setup fehlgeschlagen",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
SetRunning(false);
}
}
private async Task LoadSystemsAsync()
{
LocalSetupSelection? previousSelection =
_coordinator.LoadSelection();
_systems =
await _coordinator.LoadSystemsAsync();
_cmbSystem.Items.Clear();
_cmbCredential.Items.Clear();
foreach (SonicSystemOption system in _systems)
{
_cmbSystem.Items.Add(system);
}
if (_systems.Count == 0)
{
_lblSystemDetails.Text =
"Kein System für die gewählte Umgebung gefunden.";
return;
}
int selectedIndex = 0;
if (previousSelection is not null)
{
for (int index = 0;
index < _systems.Count;
index++)
{
if (_systems[index].SonicConnectionId ==
previousSelection.SonicConnectionId)
{
selectedIndex = index;
break;
}
}
}
_cmbSystem.SelectedIndex = selectedIndex;
}
private async Task AddCredentialAsync()
{
if (_running)
{
return;
}
if (_cmbSystem.SelectedItem
is not SonicSystemOption system)
{
ShowWarning(
"Bitte zuerst ein Sonic-System auswählen.");
return;
}
using AddCredentialProfileForm dialog =
new(
_coordinator,
system);
DialogResult dialogResult =
dialog.ShowDialog(this);
if (dialogResult != DialogResult.OK
|| dialog.CreatedProfile is null)
{
return;
}
SetRunning(true);
try
{
_profiles =
await _coordinator.LoadCredentialsAsync(
system.SonicConnectionId);
_cmbCredential.Items.Clear();
foreach (SonicCredentialProfile profile
in _profiles)
{
_cmbCredential.Items.Add(profile);
}
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;
}
}
_lblSystemDetails.Text =
$"Domain: {system.DomainName}"
+ Environment.NewLine
+ $"Adresse: {system.ConnectionUrl}"
+ Environment.NewLine
+ $"Profil: {dialog.CreatedProfile.CredentialName}";
MessageBox.Show(
this,
"Das geprüfte Benutzerprofil wurde geladen. " +
"Klicke jetzt auf „Auswahl übernehmen“.",
"Benutzerprofil hinzugefügt",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(
this,
ex.Message,
"Benutzerprofile konnten nicht neu geladen werden",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
SetRunning(false);
}
}
private async Task SystemChangedAsync()
{
if (_cmbSystem.SelectedItem
is not SonicSystemOption system)
{
return;
}
_lblSystemDetails.Text =
$"Domain: {system.DomainName}" +
Environment.NewLine +
$"Adresse: {system.ConnectionUrl}";
_profiles =
await _coordinator.LoadCredentialsAsync(
system.SonicConnectionId);
_cmbCredential.Items.Clear();
foreach (SonicCredentialProfile profile
in _profiles)
{
_cmbCredential.Items.Add(profile);
}
if (_profiles.Count == 0)
{
_lblSystemDetails.Text +=
Environment.NewLine
+ "Kein Benutzerprofil vorhanden. "
+ "Bitte ein neues Profil hinzufügen.";
_btnSaveSelection.Enabled = false;
return;
}
_btnSaveSelection.Enabled = true;
LocalSetupSelection? selection =
_coordinator.LoadSelection();
int selectedIndex = 0;
if (selection is not null)
{
for (int index = 0;
index < _profiles.Count;
index++)
{
if (_profiles[index].SonicCredentialId ==
selection.SonicCredentialId)
{
selectedIndex = index;
break;
}
}
}
else
{
int defaultIndex =
_profiles
.Select(
(profile, index) =>
new
{
profile,
index
})
.Where(item =>
item.profile.IsDefault)
.Select(item => item.index)
.FirstOrDefault();
selectedIndex = defaultIndex;
}
_cmbCredential.SelectedIndex =
selectedIndex;
}
private async Task SaveSelectionAsync()
{
if (_cmbSystem.SelectedItem
is not SonicSystemOption system)
{
ShowWarning(
"Bitte ein Sonic-System auswählen.");
return;
}
if (_cmbCredential.SelectedItem
is not SonicCredentialProfile profile)
{
ShowWarning(
"Bitte ein Benutzerprofil auswählen.");
return;
}
_coordinator.SaveSelection(
system,
profile);
MessageBox.Show(
this,
"System und Benutzerprofil wurden ausgewählt.",
"Setup",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
await InitializeAsync();
}
private async Task ImportCertificateAsync()
{
using OpenFileDialog dialog = new()
{
Title =
"Always-Encrypted-PFX auswählen",
Filter =
"PFX-Zertifikat (*.pfx)|*.pfx",
CheckFileExists = true,
Multiselect = false
};
if (dialog.ShowDialog(this)
!= DialogResult.OK)
{
return;
}
string? password =
PromptForPassword();
if (password is null)
{
return;
}
try
{
_coordinator.CertificateService.ImportPfx(
dialog.FileName,
password);
password = string.Empty;
await InitializeAsync();
}
finally
{
password = string.Empty;
}
}
private static string? PromptForPassword()
{
using Form form = new()
{
Text = "PFX-Kennwort",
Size = new Size(390, 160),
StartPosition =
FormStartPosition.CenterParent,
FormBorderStyle =
FormBorderStyle.FixedDialog
};
TextBox textBox = new()
{
Location = new Point(15, 35),
Width = 340,
UseSystemPasswordChar = true
};
Button ok = new()
{
Text = "OK",
Location = new Point(190, 75),
DialogResult = DialogResult.OK
};
Button cancel = new()
{
Text = "Abbrechen",
Location = new Point(275, 75),
DialogResult = DialogResult.Cancel
};
form.Controls.Add(textBox);
form.Controls.Add(ok);
form.Controls.Add(cancel);
form.AcceptButton = ok;
form.CancelButton = cancel;
return form.ShowDialog()
== DialogResult.OK
? textBox.Text
: null;
}
private void SetRunning(bool running)
{
_running = running;
_cmbSystem.Enabled = !running;
_cmbCredential.Enabled = !running;
_btnAddCredential.Enabled = !running;
_btnSaveSelection.Enabled = !running;
_btnRefresh.Enabled = !running;
_btnImportCertificate.Enabled = !running;
Cursor =
running
? Cursors.WaitCursor
: Cursors.Default;
}
private void ShowWarning(string message)
{
MessageBox.Show(
this,
message,
"Setup",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
private Panel CreateCard(
Point location,
Size size)
{
return new Panel
{
Location = location,
Size = size,
BackColor = _cardColor,
BorderStyle =
BorderStyle.FixedSingle
};
}
private Label CreateTitle(
string text,
Point location)
{
return new Label
{
Text = text,
Location = location,
AutoSize = true,
ForeColor = _textColor,
Font = new Font(
"Segoe UI",
14,
FontStyle.Bold)
};
}
private Label CreateLabel(
string text,
Point location)
{
return new Label
{
Text = text,
Location = location,
AutoSize = true,
ForeColor = _mutedTextColor
};
}
private Label CreateStatus(
Point location)
{
return new Label
{
Location = location,
Size = new Size(620, 32),
ForeColor = _mutedTextColor
};
}
private Button CreateButton(string text)
{
return new Button
{
Text = text,
BackColor = _goldColor,
ForeColor =
Color.FromArgb(30, 25, 10),
FlatStyle = FlatStyle.Flat,
Font = new Font(
"Segoe UI",
9,
FontStyle.Bold),
Cursor = Cursors.Hand
};
}
private void SetStatus(
Label label,
string category,
bool success,
string message)
{
label.Text =
$"{(success ? "" : "")} " +
$"{category}: {message}";
label.ForeColor =
success
? _greenColor
: _redColor;
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>