Expand settings UI to manage systems, containers, and certificate paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 08:53:13 +02:00
co-authored by Cursor
parent 121a428755
commit ed6777ffed
8 changed files with 2291 additions and 501 deletions
@@ -595,4 +595,730 @@ public sealed class SonicSetupRepository
reader.GetOrdinal("IsActive"))
};
}
public async Task<IReadOnlyList<CompanyOption>> GetCompaniesAsync(
CancellationToken cancellationToken = default)
{
const string sql = """
SELECT
[CompanyId],
[CompanyCode],
[CompanyName]
FROM [dbo].[Company]
WHERE [IsActive] = 1
ORDER BY [CompanyCode];
""";
List<CompanyOption> 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<IReadOnlyList<ContainerOption>> 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<ContainerOption> 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<IReadOnlyList<CertificateTargetOption>>
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<CertificateTargetOption> 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<int> 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<int> 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<int> 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;
}
}