using System.Data; using Microsoft.Data.SqlClient; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; public sealed class SonicSetupRepository { private readonly string _connectionString; public SonicSetupRepository(string connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) { throw new ArgumentException( "Der SQL-Connection-String fehlt.", nameof(connectionString)); } _connectionString = connectionString; } public async Task> GetSystemsAsync( string environmentCode, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(environmentCode)) { throw new ArgumentException( "Der EnvironmentCode fehlt.", nameof(environmentCode)); } const string sql = """ SELECT connection.[SonicConnectionId], connection.[ConnectionName], environment.[EnvironmentCode], connection.[DomainName], connection.[ManagementHost], connection.[ManagementPort], connection.[ConnectionProtocol], MIN(container.[ContainerName]) AS [ValidationContainerName] FROM [dbo].[SonicConnection] AS connection INNER JOIN [dbo].[Environment] AS environment ON environment.[EnvironmentId] = connection.[EnvironmentId] LEFT JOIN [dbo].[SonicContainer] AS container ON container.[SonicConnectionId] = connection.[SonicConnectionId] AND container.[IsActive] = 1 WHERE connection.[IsActive] = 1 AND environment.[IsActive] = 1 AND environment.[EnvironmentCode] = @EnvironmentCode GROUP BY connection.[SonicConnectionId], connection.[ConnectionName], environment.[EnvironmentCode], connection.[DomainName], connection.[ManagementHost], connection.[ManagementPort], connection.[ConnectionProtocol] ORDER BY connection.[ConnectionName]; """; List systems = []; 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() }); await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken); int sonicConnectionIdOrdinal = reader.GetOrdinal("SonicConnectionId"); int connectionNameOrdinal = reader.GetOrdinal("ConnectionName"); int environmentCodeOrdinal = reader.GetOrdinal("EnvironmentCode"); int domainNameOrdinal = reader.GetOrdinal("DomainName"); int managementHostOrdinal = reader.GetOrdinal("ManagementHost"); int managementPortOrdinal = reader.GetOrdinal("ManagementPort"); int connectionProtocolOrdinal = reader.GetOrdinal("ConnectionProtocol"); int validationContainerNameOrdinal = reader.GetOrdinal("ValidationContainerName"); while (await reader.ReadAsync(cancellationToken)) { systems.Add( new SonicSystemOption { SonicConnectionId = reader.GetInt32( sonicConnectionIdOrdinal), ConnectionName = reader.GetString( connectionNameOrdinal), EnvironmentCode = reader.GetString( environmentCodeOrdinal), DomainName = reader.IsDBNull(domainNameOrdinal) ? string.Empty : reader.GetString( domainNameOrdinal), ManagementHost = reader.GetString( managementHostOrdinal), ManagementPort = reader.GetInt32( managementPortOrdinal), ConnectionProtocol = reader.IsDBNull(connectionProtocolOrdinal) ? "tcp" : reader.GetString( connectionProtocolOrdinal), ValidationContainerName = reader.IsDBNull( validationContainerNameOrdinal) ? string.Empty : reader.GetString( validationContainerNameOrdinal) }); } return systems; } public async Task AddCredentialAsync( int sonicConnectionId, string credentialName, string userName, string secret, bool isDefault, CancellationToken cancellationToken = default) { if (sonicConnectionId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicConnectionId)); } if (string.IsNullOrWhiteSpace(credentialName)) { throw new ArgumentException( "Der Profilname fehlt.", nameof(credentialName)); } if (string.IsNullOrWhiteSpace(userName)) { throw new ArgumentException( "Der Sonic-Benutzername fehlt.", nameof(userName)); } if (string.IsNullOrEmpty(secret)) { throw new ArgumentException( "Das Sonic-Kennwort fehlt.", nameof(secret)); } string normalizedCredentialName = credentialName.Trim(); string normalizedUserName = userName.Trim(); if (normalizedCredentialName.Length > 150) { throw new ArgumentException( "Der Profilname darf maximal 150 Zeichen enthalten.", nameof(credentialName)); } if (normalizedUserName.Length > 256) { throw new ArgumentException( "Der Benutzername darf maximal 256 Zeichen enthalten.", nameof(userName)); } if (secret.Length > 512) { throw new ArgumentException( "Das Kennwort darf maximal 512 Zeichen enthalten.", nameof(secret)); } await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlTransaction transaction = (SqlTransaction) await connection.BeginTransactionAsync( cancellationToken); try { if (isDefault) { const string clearDefaultSql = """ UPDATE [dbo].[SonicCredential] SET [IsDefault] = 0, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicConnectionId] = @SonicConnectionId AND [IsDefault] = 1; """; await using SqlCommand clearDefaultCommand = new( clearDefaultSql, connection, transaction); clearDefaultCommand.Parameters.Add( new SqlParameter( "@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); await clearDefaultCommand.ExecuteNonQueryAsync( cancellationToken); } const string insertSql = """ INSERT INTO [dbo].[SonicCredential] ( [SonicConnectionId], [CredentialName], [CredentialUserName], [CredentialSecret], [IsDefault], [IsActive], [CreationDateTime], [CreatedBy], [ModifiedDateTime], [ModifiedBy] ) OUTPUT INSERTED.[SonicCredentialId] VALUES ( @SonicConnectionId, @CredentialName, @CredentialUserName, @CredentialSecret, @IsDefault, 1, SYSUTCDATETIME(), SUSER_SNAME(), NULL, NULL ); """; await using SqlCommand insertCommand = new( insertSql, connection, transaction); insertCommand.Parameters.Add( new SqlParameter( "@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); insertCommand.Parameters.Add( new SqlParameter( "@CredentialName", SqlDbType.NVarChar, 150) { Value = normalizedCredentialName }); insertCommand.Parameters.Add( new SqlParameter( "@CredentialUserName", SqlDbType.NVarChar, 256) { Value = normalizedUserName }); insertCommand.Parameters.Add( new SqlParameter( "@CredentialSecret", SqlDbType.NVarChar, 512) { Value = secret }); insertCommand.Parameters.Add( new SqlParameter( "@IsDefault", SqlDbType.Bit) { Value = isDefault }); object? result = await insertCommand.ExecuteScalarAsync( cancellationToken); if (result is null || result is DBNull) { throw new InvalidOperationException( "Das Benutzerprofil konnte nicht angelegt werden."); } int credentialId = Convert.ToInt32(result); await transaction.CommitAsync( cancellationToken); return credentialId; } catch { try { await transaction.RollbackAsync( CancellationToken.None); } catch { // Die ursprüngliche Exception soll erhalten bleiben. } throw; } } public async Task UpdateCredentialAsync( int sonicCredentialId, int sonicConnectionId, string credentialName, string userName, string secret, bool isDefault, CancellationToken cancellationToken = default) { if (sonicCredentialId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicCredentialId)); } if (sonicConnectionId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicConnectionId)); } if (string.IsNullOrWhiteSpace(credentialName)) { throw new ArgumentException( "Der Profilname fehlt.", nameof(credentialName)); } if (string.IsNullOrWhiteSpace(userName)) { throw new ArgumentException( "Der Sonic-Benutzername fehlt.", nameof(userName)); } if (string.IsNullOrEmpty(secret)) { throw new ArgumentException( "Das Sonic-Kennwort fehlt.", nameof(secret)); } string normalizedCredentialName = credentialName.Trim(); string normalizedUserName = userName.Trim(); if (normalizedCredentialName.Length > 150) { throw new ArgumentException( "Der Profilname darf maximal 150 Zeichen enthalten.", nameof(credentialName)); } if (normalizedUserName.Length > 256) { throw new ArgumentException( "Der Benutzername darf maximal 256 Zeichen enthalten.", nameof(userName)); } if (secret.Length > 512) { throw new ArgumentException( "Das Kennwort darf maximal 512 Zeichen enthalten.", nameof(secret)); } await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlTransaction transaction = (SqlTransaction) await connection.BeginTransactionAsync( cancellationToken); try { if (isDefault) { const string clearDefaultSql = """ UPDATE [dbo].[SonicCredential] SET [IsDefault] = 0, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicConnectionId] = @SonicConnectionId AND [IsDefault] = 1 AND [SonicCredentialId] <> @SonicCredentialId; """; await using SqlCommand clearDefaultCommand = new( clearDefaultSql, connection, transaction); clearDefaultCommand.Parameters.Add( new SqlParameter( "@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); clearDefaultCommand.Parameters.Add( new SqlParameter( "@SonicCredentialId", SqlDbType.Int) { Value = sonicCredentialId }); await clearDefaultCommand.ExecuteNonQueryAsync( cancellationToken); } const string updateSql = """ UPDATE [dbo].[SonicCredential] SET [CredentialName] = @CredentialName, [CredentialUserName] = @CredentialUserName, [CredentialSecret] = @CredentialSecret, [IsDefault] = @IsDefault, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicCredentialId] = @SonicCredentialId AND [SonicConnectionId] = @SonicConnectionId AND [IsActive] = 1; """; await using SqlCommand updateCommand = new( updateSql, connection, transaction); updateCommand.Parameters.Add( new SqlParameter( "@SonicCredentialId", SqlDbType.Int) { Value = sonicCredentialId }); updateCommand.Parameters.Add( new SqlParameter( "@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); updateCommand.Parameters.Add( new SqlParameter( "@CredentialName", SqlDbType.NVarChar, 150) { Value = normalizedCredentialName }); updateCommand.Parameters.Add( new SqlParameter( "@CredentialUserName", SqlDbType.NVarChar, 256) { Value = normalizedUserName }); updateCommand.Parameters.Add( new SqlParameter( "@CredentialSecret", SqlDbType.NVarChar, 512) { Value = secret }); updateCommand.Parameters.Add( new SqlParameter( "@IsDefault", SqlDbType.Bit) { Value = isDefault }); int affected = await updateCommand.ExecuteNonQueryAsync( cancellationToken); if (affected == 0) { throw new InvalidOperationException( $"Benutzerprofil Id {sonicCredentialId} " + "wurde nicht gefunden oder ist inaktiv."); } await transaction.CommitAsync(cancellationToken); } catch { try { await transaction.RollbackAsync( CancellationToken.None); } catch { // Die ursprüngliche Exception soll erhalten bleiben. } throw; } } public async Task> GetCredentialsAsync( int sonicConnectionId, CancellationToken cancellationToken = default) { if (sonicConnectionId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicConnectionId)); } const string sql = """ SELECT [SonicCredentialId], [SonicConnectionId], [CredentialName], [CredentialUserName], [CredentialSecret], [IsDefault], [IsActive] FROM [dbo].[SonicCredential] WHERE [SonicConnectionId] = @SonicConnectionId AND [IsActive] = 1 ORDER BY [IsDefault] DESC, [CredentialName]; """; List profiles = []; await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter( "@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken); int credentialIdOrdinal = reader.GetOrdinal("SonicCredentialId"); int connectionIdOrdinal = reader.GetOrdinal("SonicConnectionId"); int credentialNameOrdinal = reader.GetOrdinal("CredentialName"); int userNameOrdinal = reader.GetOrdinal("CredentialUserName"); int secretOrdinal = reader.GetOrdinal("CredentialSecret"); int isDefaultOrdinal = reader.GetOrdinal("IsDefault"); int isActiveOrdinal = reader.GetOrdinal("IsActive"); while (await reader.ReadAsync(cancellationToken)) { string? userName = reader.IsDBNull(userNameOrdinal) ? null : reader.GetString(userNameOrdinal); string? secret = reader.IsDBNull(secretOrdinal) ? null : reader.GetString(secretOrdinal); if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrEmpty(secret)) { continue; } profiles.Add( new SonicCredentialProfile { SonicCredentialId = reader.GetInt32( credentialIdOrdinal), SonicConnectionId = reader.GetInt32( connectionIdOrdinal), CredentialName = reader.GetString( credentialNameOrdinal), UserName = userName, Secret = secret, IsDefault = reader.GetBoolean( isDefaultOrdinal), IsActive = reader.GetBoolean( isActiveOrdinal) }); } return profiles; } public async Task GetCredentialByIdAsync( int sonicCredentialId, CancellationToken cancellationToken = default) { if (sonicCredentialId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicCredentialId)); } const string sql = """ SELECT [SonicCredentialId], [SonicConnectionId], [CredentialName], [CredentialUserName], [CredentialSecret], [IsDefault], [IsActive] FROM [dbo].[SonicCredential] WHERE [SonicCredentialId] = @SonicCredentialId AND [IsActive] = 1; """; await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter( "@SonicCredentialId", SqlDbType.Int) { Value = sonicCredentialId }); await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken); if (!await reader.ReadAsync(cancellationToken)) { return null; } int userNameOrdinal = reader.GetOrdinal("CredentialUserName"); int secretOrdinal = reader.GetOrdinal("CredentialSecret"); string? userName = reader.IsDBNull(userNameOrdinal) ? null : reader.GetString(userNameOrdinal); string? secret = reader.IsDBNull(secretOrdinal) ? null : reader.GetString(secretOrdinal); if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrEmpty(secret)) { return null; } return new SonicCredentialProfile { SonicCredentialId = reader.GetInt32( reader.GetOrdinal("SonicCredentialId")), SonicConnectionId = reader.GetInt32( reader.GetOrdinal("SonicConnectionId")), CredentialName = reader.GetString( reader.GetOrdinal("CredentialName")), UserName = userName, Secret = secret, IsDefault = reader.GetBoolean( reader.GetOrdinal("IsDefault")), IsActive = reader.GetBoolean( 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.[CertificateCheckUrl], 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"); int checkUrlOrdinal = reader.GetOrdinal("CertificateCheckUrl"); 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), CertificateCheckUrl = reader.IsDBNull(checkUrlOrdinal) ? null : reader.GetString(checkUrlOrdinal), 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, string? certificateCheckUrl = null, 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."); } string? checkUrl = TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl); const string sql = """ INSERT INTO [dbo].[SonicContainer] ( [CompanyId], [SonicConnectionId], [ContainerName], [ContainerDisplayName], [CertificateCheckUrl], [RestartTimeoutSeconds], [IsActive], [CreationDateTime], [CreatedBy] ) OUTPUT INSERTED.[SonicContainerId] VALUES ( @CompanyId, @SonicConnectionId, @ContainerName, @ContainerDisplayName, @CertificateCheckUrl, @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( "@CertificateCheckUrl", SqlDbType.NVarChar, 500) { Value = checkUrl is null ? DBNull.Value : checkUrl }); 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); } public async Task UpdateSonicConnectionAsync( int sonicConnectionId, string connectionName, string managementHost, int managementPort, string domainName, string connectionProtocol, CancellationToken cancellationToken = default) { if (sonicConnectionId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicConnectionId)); } 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 = """ UPDATE [dbo].[SonicConnection] SET [ConnectionName] = @ConnectionName, [ManagementHost] = @ManagementHost, [ManagementPort] = @ManagementPort, [DomainName] = @DomainName, [ConnectionProtocol] = @ConnectionProtocol, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicConnectionId] = @SonicConnectionId AND [IsActive] = 1; """; await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter("@SonicConnectionId", SqlDbType.Int) { Value = sonicConnectionId }); 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 }); int affected = await command.ExecuteNonQueryAsync(cancellationToken); if (affected == 0) { throw new InvalidOperationException( $"Sonic-System Id {sonicConnectionId} " + "wurde nicht gefunden oder ist bereits inaktiv."); } } public async Task DeactivateSonicConnectionAsync( int sonicConnectionId, CancellationToken cancellationToken = default) { if (sonicConnectionId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicConnectionId)); } const string sql = """ UPDATE [dbo].[SonicConnection] SET [IsActive] = 0, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicConnectionId] = @SonicConnectionId AND [IsActive] = 1; """; await ExecuteSoftDeleteAsync( sql, "@SonicConnectionId", sonicConnectionId, $"Sonic-System Id {sonicConnectionId} " + "wurde nicht gefunden oder ist bereits inaktiv.", cancellationToken); } public async Task UpdateSonicContainerAsync( int sonicContainerId, int companyId, int sonicConnectionId, string containerName, string? containerDisplayName, int restartTimeoutSeconds, string? certificateCheckUrl = null, CancellationToken cancellationToken = default) { if (sonicContainerId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicContainerId)); } 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."); } string? checkUrl = TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl); const string sql = """ UPDATE [dbo].[SonicContainer] SET [CompanyId] = @CompanyId, [SonicConnectionId] = @SonicConnectionId, [ContainerName] = @ContainerName, [ContainerDisplayName] = @ContainerDisplayName, [CertificateCheckUrl] = @CertificateCheckUrl, [RestartTimeoutSeconds] = @RestartTimeoutSeconds, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicContainerId] = @SonicContainerId AND [IsActive] = 1; """; 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("@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( "@CertificateCheckUrl", SqlDbType.NVarChar, 500) { Value = checkUrl is null ? DBNull.Value : checkUrl }); command.Parameters.Add( new SqlParameter( "@RestartTimeoutSeconds", SqlDbType.Int) { Value = restartTimeoutSeconds }); int affected = await command.ExecuteNonQueryAsync(cancellationToken); if (affected == 0) { throw new InvalidOperationException( $"Container Id {sonicContainerId} " + "wurde nicht gefunden oder ist bereits inaktiv."); } } public async Task DeactivateSonicContainerAsync( int sonicContainerId, CancellationToken cancellationToken = default) { if (sonicContainerId <= 0) { throw new ArgumentOutOfRangeException( nameof(sonicContainerId)); } const string sql = """ UPDATE [dbo].[SonicContainer] SET [IsActive] = 0, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [SonicContainerId] = @SonicContainerId AND [IsActive] = 1; """; await ExecuteSoftDeleteAsync( sql, "@SonicContainerId", sonicContainerId, $"Container Id {sonicContainerId} " + "wurde nicht gefunden oder ist bereits inaktiv.", cancellationToken); } public async Task UpdateCertificateTargetAsync( int certificateTargetId, int sonicContainerId, string targetDirectory, string targetFileName, bool backupEnabled, string backupDirectoryName, CancellationToken cancellationToken = default) { if (certificateTargetId <= 0) { throw new ArgumentOutOfRangeException( nameof(certificateTargetId)); } 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 = """ UPDATE [dbo].[CertificateTarget] SET [SonicContainerId] = @SonicContainerId, [TargetDirectory] = @TargetDirectory, [TargetFileName] = @TargetFileName, [BackupEnabled] = @BackupEnabled, [BackupDirectoryName] = @BackupDirectoryName, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [CertificateTargetId] = @CertificateTargetId AND [IsActive] = 1; """; await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter("@CertificateTargetId", SqlDbType.Int) { Value = certificateTargetId }); 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 }); int affected = await command.ExecuteNonQueryAsync(cancellationToken); if (affected == 0) { throw new InvalidOperationException( $"Zertifikat-Pfad Id {certificateTargetId} " + "wurde nicht gefunden oder ist bereits inaktiv."); } } public async Task DeactivateCertificateTargetAsync( int certificateTargetId, CancellationToken cancellationToken = default) { if (certificateTargetId <= 0) { throw new ArgumentOutOfRangeException( nameof(certificateTargetId)); } const string sql = """ UPDATE [dbo].[CertificateTarget] SET [IsActive] = 0, [ModifiedDateTime] = SYSUTCDATETIME(), [ModifiedBy] = SUSER_SNAME() WHERE [CertificateTargetId] = @CertificateTargetId AND [IsActive] = 1; """; await ExecuteSoftDeleteAsync( sql, "@CertificateTargetId", certificateTargetId, $"Zertifikat-Pfad Id {certificateTargetId} " + "wurde nicht gefunden oder ist bereits inaktiv.", cancellationToken); } private async Task ExecuteSoftDeleteAsync( string sql, string idParameterName, int id, string notFoundMessage, CancellationToken cancellationToken) { await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter(idParameterName, SqlDbType.Int) { Value = id }); int affected = await command.ExecuteNonQueryAsync(cancellationToken); if (affected == 0) { throw new InvalidOperationException(notFoundMessage); } } public async Task ExistsSonicConnectionAsync( int sonicConnectionId, CancellationToken cancellationToken = default) { return await ExistsAsync( """ SELECT 1 FROM [dbo].[SonicConnection] WHERE [SonicConnectionId] = @Id AND [IsActive] = 1; """, sonicConnectionId, cancellationToken); } public async Task ExistsSonicContainerAsync( int sonicContainerId, CancellationToken cancellationToken = default) { return await ExistsAsync( """ SELECT 1 FROM [dbo].[SonicContainer] WHERE [SonicContainerId] = @Id AND [IsActive] = 1; """, sonicContainerId, cancellationToken); } public async Task ExistsCertificateTargetAsync( int certificateTargetId, CancellationToken cancellationToken = default) { return await ExistsAsync( """ SELECT 1 FROM [dbo].[CertificateTarget] WHERE [CertificateTargetId] = @Id AND [IsActive] = 1; """, certificateTargetId, cancellationToken); } private async Task ExistsAsync( string sql, int id, CancellationToken cancellationToken) { if (id <= 0) { return false; } await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); await using SqlCommand command = new(sql, connection); command.Parameters.Add( new SqlParameter("@Id", SqlDbType.Int) { Value = id }); object? result = await command.ExecuteScalarAsync(cancellationToken); return result is not null && result is not DBNull; } 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; } }