using SwDAL.CustomCode.ServerConfig; using SwDAL.DTO.ServerConfig; using GB5Shared.Connection; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace SwBLL.ServerConfig; /// Everything a caller supplies to register a newly-provisioned physical database in /// MSERVER/MSERVERCONFIG. DatabasePassword is plaintext IN (encrypted internally before storage, /// via IApplicationConnection.Encrypt — the same convention ServerConfigCache already decrypts /// with on read) — never log or persist it anywhere else. public class RegisterServerConfigRequest { public string ConnectionName { get; set; } = string.Empty; public string ServerMachineName { get; set; } = string.Empty; public string? ServerName { get; set; } public string? ServerIp { get; set; } /// The IIS/service server that answers requests for this connection (drives /// VersionDAL.VersionUrlIdentifier's PRIMARYBASEURL/SECONDORYBASEURL resolution) — a /// DIFFERENT MSERVER row than (the DB server) whenever /// app and database run on separate boxes. Defaults to /// when omitted, i.e. a single-box install where one machine is both. Leaving this unset /// with no default was the exact bug that left every row this endpoint ever wrote with a /// NULL/-1 SERVICESERVERID, matching the identical defect fixed in /// ClientDetailQB.CREATE_SERVER_CONFIG. public string? ServiceServerMachineName { get; set; } public byte DatabaseType { get; set; } // 0=SqlServer,1=PostgreSQL,2=MySQL,3=Oracle public string DatabaseName { get; set; } = string.Empty; public string DatabaseUserName { get; set; } = string.Empty; public string DatabasePassword { get; set; } = string.Empty; // plaintext in, encrypted before storage public string DatabasePort { get; set; } = string.Empty; /// 0=Production/OLTP, 2=Report, 3=Archive — see MServerConfigWriteDTO's doc /// comment. Never 1 (TCMS-owned, TEST-only). public byte DbInstanceType { get; set; } public int ClientId { get; set; } } /// Registers physical databases in the shared MSERVER/MSERVERCONFIG tables so /// GB5Framework/FrameworkBLL/ReportOrchestration/ReportConnectionResolver can route to them at /// runtime — the write-side counterpart to that resolver's read-only ServerConfigCache. No /// module (SqlWorkbench included) owns this table; this is the first real write path for it. public interface IServerConfigBLL { /// Finds-or-creates the MSERVER row for request.ServerMachineName, then inserts a /// new MSERVERCONFIG row pointing at it. Returns the new ServerConfigId. Task RegisterDatabaseAsync(RegisterServerConfigRequest request, LoginDTO login, CancellationToken ct); /// Points mainServerConfigId's ReportServerConfigId or ArchiveServerConfigId (per /// isArchive) at linkedServerConfigId — the step that makes ReportConnectionResolver actually /// pick up a newly-registered Report/Archive database for that client. Task LinkReportOrArchiveAsync(int mainServerConfigId, int linkedServerConfigId, bool isArchive, CancellationToken ct); } public class ServerConfigBLL : IServerConfigBLL { private readonly IServerConfigDAL _dal; private readonly IApplicationConnection _appConnection; private readonly AutoNumber _autoNumber; private readonly ILogger _logger; public ServerConfigBLL(IServerConfigDAL dal, IApplicationConnection appConnection, AutoNumber autoNumber, ILogger logger) { _dal = dal; _appConnection = appConnection; _autoNumber = autoNumber; _logger = logger; } public async Task RegisterDatabaseAsync(RegisterServerConfigRequest request, LoginDTO login, CancellationToken ct) { var serverId = await _dal.FindServerIdByMachineNameAsync(request.ServerMachineName, ct).ConfigureAwait(false); if (serverId is null) { var now = DateTime.UtcNow; var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.MSERVER, login).ConfigureAwait(false); var serverDto = new MServerDTO { ServerId = auto.StartNumber, ServerName = request.ServerName ?? request.ServerMachineName, ServerIp = request.ServerIp, ServerMachineName = request.ServerMachineName, CreatedById = login.UserId, CreatedOn = now, ModifiedById = login.UserId, ModifiedOn = now }; serverId = await _dal.InsertServerAsync(serverDto, ct).ConfigureAwait(false); _logger.LogInformation("Registered new MSERVER row {ServerId} for machine {ServerMachineName}", serverId, request.ServerMachineName); } // Resolve the service (IIS) server the same find-or-create way as the DB server above. // Defaults to the DB server's own machine name for a single-box install; a genuinely // separate app tier passes ServiceServerMachineName explicitly. This is the fix for // the confirmed bug where this path always left SERVICESERVERID at its DTO default of // -1, reproducing VersionDAL.VersionUrlIdentifier's "Service server is not correctly // assigned" failure for every connection ever registered here. var serviceServerMachineName = string.IsNullOrWhiteSpace(request.ServiceServerMachineName) ? request.ServerMachineName : request.ServiceServerMachineName; var serviceServerId = serviceServerMachineName == request.ServerMachineName ? serverId.Value : await _dal.FindServerIdByMachineNameAsync(serviceServerMachineName, ct).ConfigureAwait(false); if (serviceServerId is null) { var now = DateTime.UtcNow; var svcAuto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.MSERVER, login).ConfigureAwait(false); serviceServerId = await _dal.InsertServerAsync(new MServerDTO { ServerId = svcAuto.StartNumber, ServerName = serviceServerMachineName, ServerMachineName = serviceServerMachineName, CreatedById = login.UserId, CreatedOn = now, ModifiedById = login.UserId, ModifiedOn = now }, ct).ConfigureAwait(false); _logger.LogInformation("Registered new MSERVER row {ServerId} for service-server machine {ServiceServerMachineName}", serviceServerId, serviceServerMachineName); } // Never persist a plaintext password — same encryption convention ServerConfigCache // already decrypts with on read (IApplicationConnection.Decrypt(value, "GB5")). var encryptedPassword = _appConnection.Encrypt(request.DatabasePassword, "GB5"); var nowConfig = DateTime.UtcNow; var configAuto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.MSERVERCONFIG, login).ConfigureAwait(false); var configDto = new MServerConfigWriteDTO { ServerConfigId = configAuto.StartNumber, ConnectionName = request.ConnectionName, ServerId = serverId.Value, DatabaseType = request.DatabaseType, DatabaseName = request.DatabaseName, DatabaseUserName = request.DatabaseUserName, DatabasePassword = encryptedPassword, DatabasePort = request.DatabasePort, DbInstanceType = request.DbInstanceType, ClientId = request.ClientId, CreatedById = login.UserId, CreatedOn = nowConfig, ModifiedById = login.UserId, ModifiedOn = nowConfig, ServiceServerId = serviceServerId.Value }; var serverConfigId = await _dal.InsertServerConfigAsync(configDto, ct).ConfigureAwait(false); _logger.LogInformation( "Registered new MSERVERCONFIG row {ServerConfigId} for database {DatabaseName} (DbInstanceType {DbInstanceType})", serverConfigId, request.DatabaseName, request.DbInstanceType); return serverConfigId; } public async Task LinkReportOrArchiveAsync(int mainServerConfigId, int linkedServerConfigId, bool isArchive, CancellationToken ct) { if (isArchive) await _dal.UpdateArchiveServerConfigIdAsync(mainServerConfigId, linkedServerConfigId, ct).ConfigureAwait(false); else await _dal.UpdateReportServerConfigIdAsync(mainServerConfigId, linkedServerConfigId, ct).ConfigureAwait(false); _logger.LogInformation( "Linked ServerConfigId {LinkedServerConfigId} as {Kind} for main ServerConfigId {MainServerConfigId}", linkedServerConfigId, isArchive ? "Archive" : "Report", mainServerConfigId); } }