Add live TLS certificate probing and improve restart error handling.

Configure CertificateCheckUrl per container for curl-like TLS checks, classify Sonic permission errors, and extend setup wizard for container management.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 13:16:21 +02:00
co-authored by Cursor
parent d20e0df618
commit 58b7826162
1430 changed files with 358394 additions and 71 deletions
@@ -380,6 +380,228 @@ public sealed class SonicSetupRepository
}
}
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<IReadOnlyList<SonicCredentialProfile>>
GetCredentialsAsync(
int sonicConnectionId,
@@ -663,6 +885,7 @@ public sealed class SonicSetupRepository
container.[CompanyId],
container.[ContainerName],
container.[ContainerDisplayName],
container.[CertificateCheckUrl],
container.[RestartTimeoutSeconds],
company.[CompanyCode],
connection.[ConnectionName]
@@ -729,6 +952,9 @@ public sealed class SonicSetupRepository
int displayOrdinal =
reader.GetOrdinal("ContainerDisplayName");
int checkUrlOrdinal =
reader.GetOrdinal("CertificateCheckUrl");
containers.Add(
new ContainerOption
{
@@ -753,6 +979,11 @@ public sealed class SonicSetupRepository
? null
: reader.GetString(displayOrdinal),
CertificateCheckUrl =
reader.IsDBNull(checkUrlOrdinal)
? null
: reader.GetString(checkUrlOrdinal),
RestartTimeoutSeconds =
reader.GetInt32(
reader.GetOrdinal("RestartTimeoutSeconds")),
@@ -1057,6 +1288,7 @@ public sealed class SonicSetupRepository
string containerName,
string? containerDisplayName,
int restartTimeoutSeconds,
string? certificateCheckUrl = null,
CancellationToken cancellationToken = default)
{
if (companyId <= 0)
@@ -1093,6 +1325,9 @@ public sealed class SonicSetupRepository
"Timeout muss zwischen 10 und 3600 Sekunden liegen.");
}
string? checkUrl =
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
const string sql = """
INSERT INTO [dbo].[SonicContainer]
(
@@ -1100,6 +1335,7 @@ public sealed class SonicSetupRepository
[SonicConnectionId],
[ContainerName],
[ContainerDisplayName],
[CertificateCheckUrl],
[RestartTimeoutSeconds],
[IsActive],
[CreationDateTime],
@@ -1112,6 +1348,7 @@ public sealed class SonicSetupRepository
@SonicConnectionId,
@ContainerName,
@ContainerDisplayName,
@CertificateCheckUrl,
@RestartTimeoutSeconds,
1,
SYSUTCDATETIME(),
@@ -1159,6 +1396,17 @@ public sealed class SonicSetupRepository
: display
});
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
command.Parameters.Add(
new SqlParameter(
"@RestartTimeoutSeconds",
@@ -1457,6 +1705,7 @@ public sealed class SonicSetupRepository
string containerName,
string? containerDisplayName,
int restartTimeoutSeconds,
string? certificateCheckUrl = null,
CancellationToken cancellationToken = default)
{
if (sonicContainerId <= 0)
@@ -1499,6 +1748,9 @@ public sealed class SonicSetupRepository
"Timeout muss zwischen 10 und 3600 Sekunden liegen.");
}
string? checkUrl =
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
const string sql = """
UPDATE [dbo].[SonicContainer]
SET
@@ -1506,6 +1758,7 @@ public sealed class SonicSetupRepository
[SonicConnectionId] = @SonicConnectionId,
[ContainerName] = @ContainerName,
[ContainerDisplayName] = @ContainerDisplayName,
[CertificateCheckUrl] = @CertificateCheckUrl,
[RestartTimeoutSeconds] = @RestartTimeoutSeconds,
[ModifiedDateTime] = SYSUTCDATETIME(),
[ModifiedBy] = SUSER_SNAME()
@@ -1559,6 +1812,17 @@ public sealed class SonicSetupRepository
: display
});
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
command.Parameters.Add(
new SqlParameter(
"@RestartTimeoutSeconds",