using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; using FrameworkBLL.SessionStore; using FrameworkDAL.CustomCode.User; using FrameworkDAL.DTO.User; using FrameworkDAL.DTO.UserLogin; using GB5Shared.Connection; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ServerConfig; using GB5Shared.EntityHandler; using GB5Shared.GB5Exception; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Validation; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.User { public class UserBLL : IUserBLL { private readonly IUserDAL _UserDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly IApplicationConnection _ApplicationConnection; private readonly ISessionStoreBLL _SessionStoreBLL; private readonly IQueryExecutor _QueryExecutor; private readonly BaseEntityAppService _BaseEntityAppService; public UserBLL( IUserDAL IUserDAL, AutoNumber AutoNumber, IValidation Validation, IApplicationConnection ApplicationConnection, ISessionStoreBLL SessionStoreBLL, IQueryExecutor QueryExecutor, BaseEntityAppService BaseEntityAppService) { _UserDAL = IUserDAL; _AutoNumber = AutoNumber; _Validation = Validation; _ApplicationConnection = ApplicationConnection; _SessionStoreBLL = SessionStoreBLL; _QueryExecutor = QueryExecutor; _BaseEntityAppService = BaseEntityAppService; } public async Task GetUser(int UserId, LoginDTO LoginDTO) { try { return await _UserDAL.GetUser(UserId, LoginDTO); } catch (Exception) { throw; } } public async Task SaveUser(UserDTO UserDTO, LoginDTO LoginDTO) { if (UserDTO == null) throw new ArgumentNullException(nameof(UserDTO)); // Entity-save now routes through ExecuteSaveAsync (GB5Shared/EntityHandler/EventHandler.cs) // per CLAUDE.md's mandatory pipeline — this is the prerequisite for GB5->Keycloak user // sync (a future subscriber reacts to this event) and also gives user create/update the // same GB5Trace/EventLogPublish/outbox coverage every other entity save already has. // Transaction is opened here (not left to ExecuteSaveAsync) so the AutoNumber reservation // and the user insert/update are atomic — mirrors FrameworkBLL/Role/RoleBLL.cs's SaveRole. var transaction = await _QueryExecutor.BeginTransactionAsync(LoginDTO); try { await _Validation.NotEmpty(UserDTO.UserCode, nameof(UserDTO.UserCode)); await _Validation.NotEmpty(UserDTO.UserName, nameof(UserDTO.UserName)); await _Validation.NotEmpty(UserDTO.UserPrimaryMail, nameof(UserDTO.UserPrimaryMail)); await _Validation.NotEmpty(UserDTO.UserPrimaryMobile, nameof(UserDTO.UserPrimaryMobile)); UserDTO.UserModifiedById = LoginDTO.UserId; UserDTO.UserModifiedOn = DateTime.UtcNow; UserDTO.TenantId = LoginDTO.ClientId; if (UserDTO.UserPasswordType == 0) { if (UserDTO.UserId == 0 || (UserDTO.UserId != 0 && UserDTO.ResetuserPassword == 0)) { // A 12-char plaintext here AES-GCM-encrypts to IV(12)+ciphertext(12)+tag(16) // = 40 raw bytes = 56 Base64 chars — confirmed live (tracker §31.13) that // this overflows MUSER.PASSWORD's real NVARCHAR(50) outright, meaning no // user could ever be created through this path with a system-generated // password. 8 chars encrypts to 36 raw bytes = 48 Base64 chars, safely // under 50 (same fix already applied to EntitlementBLL.ClientUserProvisioner, // which mirrors this exact method). string password = _ApplicationConnection.RandomGenerateAlphaNumeric(8); UserDTO.UserGeneratedPassword = password; UserDTO.UserPassword = _ApplicationConnection.Encrypt(password, UserDTO.UserCode); } } UserDTO.UserWorkPeriodId = LoginDTO.WorkPeriodId; bool isNew = UserDTO.UserId == 0; if (isNew) { var autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "USER", LoginDTO, transaction); UserDTO.UserId = autoNumberDTO.StartNumber; UserDTO.UserCreatedById = LoginDTO.UserId; UserDTO.UserCreatedOn = DateTime.UtcNow; } await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTUSER, EventTypeConstant.SAVEUSEREVENTTYPEID, UserDTO, LoginDTO, async tx => { if (isNew) await _UserDAL.SaveUser(UserDTO, LoginDTO, tx); else await _UserDAL.UpdateUser(UserDTO, LoginDTO, tx); return UserDTO.UserId; }, null, -1, -1, transaction, isNew); await _QueryExecutor.CommitAsync(transaction); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {UserDTO.UserId}" : $"{SuccessResponse.UpdateSuccessMessage} {UserDTO.UserId}"; } catch (ValidationException vex) { await _QueryExecutor.RollbackAsync(transaction); throw new Exception(vex.Message); } catch (Exception) { await _QueryExecutor.RollbackAsync(transaction); throw; } } public async Task DeleteUser(int UserId, LoginDTO LoginDTO) { try { return await _UserDAL.DeleteUser(UserId, LoginDTO); } catch (Exception) { throw; } } public async Task GetUserLogin(LoginDTO LoginDTO) { try { return await _UserDAL.GetUserLogin(LoginDTO); } catch (Exception) { throw; } } public async Task GetSelectListUser(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _UserDAL.GetSelectListUser(criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task GetUserSettingDetail(int UserId, LoginDTO LoginDTO) { try { return await _UserDAL.GetUserSettingDetail(UserId, LoginDTO); } catch (Exception) { throw; } } public async Task UpdateUserSettings(UpdateUserSettingsDTO UpdateUserSettingsDTO, LoginDTO LoginDTO) { if (UpdateUserSettingsDTO == null) throw new ArgumentNullException(nameof(UpdateUserSettingsDTO)); try { await _UserDAL.UpdateUserSettings(UpdateUserSettingsDTO, LoginDTO); return "User Settings Updated"; } catch (Exception) { throw; } } public async Task UpdateUserTheme(int ThemeOption, LoginDTO LoginDTO) { try { await _UserDAL.UpdateUserTheme(ThemeOption, LoginDTO); return "User Theme Updated"; } catch (Exception) { throw; } } public async Task UpdateUserThemeAndMode(int ThemeOption, int ThemeMode, LoginDTO LoginDTO) { try { await _UserDAL.UpdateUserThemeAndMode(ThemeOption, ThemeMode, LoginDTO); return "Details updated."; } catch (Exception) { throw; } } public async Task LogOutUser(string ConnectionName, string UserCode, LoginDTO LoginDTO) { try { // GB4 parity: AuthenticationBLL.LogOutUser looks the user up by Code within // the caller's tenant and throws if it isn't found (or is ambiguous) before // doing anything else. int userCount = await _UserDAL.GetUserCountByCode(UserCode, LoginDTO); if (userCount == 0) throw new MethodNotAllowedException($"There are no user found for given code {UserCode}"); if (userCount > 1) throw new MethodNotAllowedException($"More than one user found for given code {UserCode}"); ServerConfigDTO ServerConfigDTO = await _ApplicationConnection.DatabaseConnectionObject(ConnectionName); // GB4 parity: the session actually torn down is the CALLER's own current // session (LoginDTO.LoginEventLogId, resolved from the Login header) — // UserCode is validated above but is not the session lookup key. bool wasLoggedOut = await _SessionStoreBLL.LogOutIfPresentAsync( ServerConfigDTO.ServerConfigId, LoginDTO.LoginEventLogId, LoginDTO); return wasLoggedOut ? "User Successfully LoggedOut." : "User Not LoggedIn."; } catch (Exception) { throw; } } } }