From 68cf33d5a6036270e4652f543381948285f120cc Mon Sep 17 00:00:00 2001 From: dibakor Date: Mon, 10 Aug 2026 18:13:54 +0600 Subject: [PATCH] update user password via excel --- .../Requests/Systems/UserRequest.cs | 11 +- .../Responses/Systems/UserResponse.cs | 18 ++ .../Contracts/Systems/IUserService.cs | 1 + .../Integrations/IntegrationService.cs | 1 - .../Services/Systems/UserService.cs | 189 +++++++++++++++++- .../Controllers/Web/UserController.cs | 90 ++++++++- 6 files changed, 306 insertions(+), 4 deletions(-) diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Requests/Systems/UserRequest.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Requests/Systems/UserRequest.cs index 2a0fc26..de5bbc6 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Requests/Systems/UserRequest.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Requests/Systems/UserRequest.cs @@ -1,4 +1,5 @@ -using System; +using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Setups; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -71,6 +72,14 @@ public class ResetPasswordRequest : ByUserIdRequest public string ConfirmPassword { get; set; } } +public class BulkPasswordUploadRequest : FileUploadRequest +{ + /// + /// When true, uploaded users are forced to change their password at next login. + /// + public bool ForcePasswordChange { get; set; } +} + public class PasswordChangeRequest : ResetPasswordRequest { [Required, NotNull, StringLength(maximumLength: 30, MinimumLength = 1, ErrorMessage = "Old Password must be between 1 and 30 characters.")] diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs index ee38085..3f8ba3b 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Models/Responses/Systems/UserResponse.cs @@ -124,4 +124,22 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems public string LoginId { get; set; } public decimal Value { get; set; } } + + public class BulkPasswordUploadItem + { + public int RowNo { get; set; } + public int UserId { get; set; } + public string EmployeeCode { get; set; } + public string LoginId { get; set; } + public bool Updated { get; set; } + public string Message { get; set; } + } + + public class BulkPasswordUploadResponse : ResponseBase + { + public int TotalRows { get; set; } + public int SuccessCount { get; set; } + public int FailedCount { get; set; } + public List Items { get; set; } = []; + } } \ No newline at end of file diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Contracts/Systems/IUserService.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Contracts/Systems/IUserService.cs index 02ca341..b5304cf 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Contracts/Systems/IUserService.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Contracts/Systems/IUserService.cs @@ -32,6 +32,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems Task AddUserAsync(NewUserRequest user, string ipAddress, int createdBy); Task UpdateMyInfoAsync(string address, string contactNo, int modifiedBy, int emplyeeId); Task ResetPasswordAsync(int userId, string newPassword, string ipAddress, int changedBy); + Task UpdatePasswordsFromExcelAsync(string fileSpec, string ipAddress, int changedBy, bool forcePasswordChange); Task UpdateMyThemeAsync(int userId, string menuLayout, string themeName, string schemeName); Task SaveAuthorizeLimitAsync(decimal maxAuthLimit, int userId, string ipAddress, string savedBy); Task UploadDocumentAsync(int userId, int id, int documentOf, string orgFileName, string fileName); diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Integrations/IntegrationService.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Integrations/IntegrationService.cs index da6abe9..56b3ec3 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Integrations/IntegrationService.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Integrations/IntegrationService.cs @@ -292,7 +292,6 @@ public class IntegrationService : IIntegrationService { var materialPriceType = await GetMaterialPriceTypeAsync(tc, request.ConditionType) ; - var validationMessages = Validate(request, materialPriceType.PriceType); if(validationMessages.Any()) diff --git a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs index 15eaf22..5640ed6 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI.Services/Services/Systems/UserService.cs @@ -1170,7 +1170,194 @@ namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Systems } /// - /// + /// Updates password of multiple users from an uploaded excel file having EmployeeCode and Password columns. + /// + /// Full path of the uploaded excel file. + /// + /// + /// When true, user must change the password at next login. + /// + public async Task UpdatePasswordsFromExcelAsync(string fileSpec, string ipAddress, int changedBy, bool forcePasswordChange) + { + BulkPasswordUploadResponse response = new() { ReturnStatus = 200 }; + + try + { + if (string.IsNullOrEmpty(fileSpec) || !System.IO.File.Exists(fileSpec)) + { + response.ReturnStatus = 417; + response.ReturnMessage.Add("Uploaded excel file was not found."); + + return response; + } + + #region Read data from Excel + + DataTable table = null; + DataSet ds = ExcelReader.GetDataSetFromFile(fileSpec: fileSpec, firstRowColumnHeader: true); + if (ds != null && ds.Tables.Count > 0) + table = ds.Tables[0]; + + if (table == null || table.Rows.Count <= 0) + { + response.ReturnStatus = 417; + response.ReturnMessage.Add("There is no data in the excel file to process."); + + return response; + } + + string empCodeColumn = GetColumnName(table: table, columnName: "EmployeeCode"); + string passwordColumn = GetColumnName(table: table, columnName: "Password"); + if (string.IsNullOrEmpty(empCodeColumn) || string.IsNullOrEmpty(passwordColumn)) + { + response.ReturnStatus = 417; + response.ReturnMessage.Add("Excel file must have [EmployeeCode] and [Password] columns in the first row."); + + return response; + } + + #endregion + + using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true); + try + { + int rowNo = 1; //First row of the excel file is the column header + foreach (DataRow row in table.Rows) + { + rowNo++; + + string employeeCode = $"{row[empCodeColumn]}".Trim(); + string password = $"{row[passwordColumn]}".Trim(); + if (string.IsNullOrEmpty(employeeCode) && string.IsNullOrEmpty(password)) + continue; //Skip completely blank row + + response.TotalRows++; + BulkPasswordUploadItem item = new() { RowNo = rowNo, EmployeeCode = employeeCode }; + + if (string.IsNullOrEmpty(employeeCode)) + { + item.Message = "Employee Code is empty."; + response.FailedCount++; + response.Items.Add(item); + + continue; + } + + if (string.IsNullOrEmpty(password)) + { + item.Message = "Password is empty."; + response.FailedCount++; + response.Items.Add(item); + + continue; + } + + #region Find the user by Employee Code + + int userId = 0; + int matchCount = 0; + string loginId = string.Empty; + using (IDataReader dr = tc.ExecuteReader("SELECT UserId, LoginId FROM Users WHERE EmployeeCode=%s OR EmployeeNumber=%s", employeeCode, employeeCode)) + { + while (dr.Read()) + { + matchCount++; + if (matchCount == 1) + { + userId = dr.GetInt32(0); + loginId = dr.IsDBNull(1) ? string.Empty : dr.GetString(1); + } + } + dr.Close(); + } + + if (matchCount <= 0) + { + item.Message = $"No user found against Employee Code [{employeeCode}]."; + response.FailedCount++; + response.Items.Add(item); + + continue; + } + + if (matchCount > 1) + { + item.Message = $"More than one user found against Employee Code [{employeeCode}]."; + response.FailedCount++; + response.Items.Add(item); + + continue; + } + + #endregion + + SqlParameter[] p = + [ + SqlHelperExtension.CreateInParam(pName: "@NewPwd", pType: SqlDbType.VarChar, pValue: EncryptPassword(password: password), size: 75), + SqlHelperExtension.CreateInParam(pName: "@LastPwds", pType: SqlDbType.VarChar, pValue: string.Empty, size: 700), + SqlHelperExtension.CreateInParam(pName: "@LastPwdChgDate", pType: SqlDbType.DateTime, pValue: DateTime.Now), + SqlHelperExtension.CreateInParam(pName: "@PwdExpireDate", pType: SqlDbType.DateTime, pValue: DateTime.Now), + SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: ipAddress, size: 20), + SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId), + SqlHelperExtension.CreateInParam(pName: "@ChangedBy", pType: SqlDbType.Int, pValue: changedBy), + SqlHelperExtension.CreateInParam(pName: "@ResetPwd", pType: SqlDbType.SmallInt, pValue: (short)(forcePasswordChange ? 1 : 0)) + ]; + _ = tc.ExecuteNonQuerySp(spName: "dbo.UpdateUserPassword", parameterValues: p); + + item.Updated = true; + item.UserId = userId; + item.LoginId = loginId; + item.Message = "Password updated successfully."; + response.SuccessCount++; + response.Items.Add(item); + } + + tc.End(); + } + catch (Exception ie) + { + tc?.HandleError(); + + throw DBCustomError.GenerateCustomError(ie); + } + + if (response.TotalRows <= 0) + { + response.ReturnStatus = 417; + response.ReturnMessage.Add("There is no data in the excel file to process."); + } + else + { + response.ReturnMessage.Add($"{response.SuccessCount} of {response.TotalRows} user password(s) updated successfully."); + } + } + catch (Exception e) + { + throw new InvalidOperationException(e.Message, e); + } + + return response; + } + + /// + /// Returns the actual column name of the DataTable ignoring case and spaces, null when not found. + /// + /// + /// + /// + private static string GetColumnName(DataTable table, string columnName) + { + foreach (DataColumn column in table.Columns) + { + if (string.Equals(column.ColumnName.Replace(" ", string.Empty).Trim(), columnName, StringComparison.OrdinalIgnoreCase)) + return column.ColumnName; + } + + return null; + } + + /// + /// /// /// /// diff --git a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs index 9be295e..19b093e 100644 --- a/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs +++ b/Api/OnlineSalesAutoCrop.CoreAPI/Controllers/Web/UserController.cs @@ -605,7 +605,95 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers.Web } /// - /// + /// Updates password of multiple users from an excel file having EmployeeCode and Password columns. + /// + /// + /// + [ValidateSession] + [HttpPost("uploadPasswords")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BulkPasswordUploadResponse))] + public async Task UploadPasswords([FromForm] BulkPasswordUploadRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + BulkPasswordUploadResponse response = new() { ReturnStatus = StatusCodes.Status200OK }; + if (string.IsNullOrEmpty(request.FileName)) + { + response.ReturnStatus = StatusCodes.Status417ExpectationFailed; + response.ReturnMessage.Add("There is no valid file to process."); + return StatusCode(StatusCodes.Status417ExpectationFailed, response); + } + + if (request.FileData == null || request.FileData.Length <= 0) + { + response.ReturnStatus = StatusCodes.Status417ExpectationFailed; + response.ReturnMessage.Add("There is no valid data to process."); + return StatusCode(StatusCodes.Status417ExpectationFailed, response); + } + + string[] allowedExtensions = [".xlsx", ".xls"]; + string fileExtension = Path.GetExtension(request.FileName).ToLowerInvariant(); + if (!allowedExtensions.Contains(fileExtension)) + { + response.ReturnStatus = StatusCodes.Status417ExpectationFailed; + response.ReturnMessage.Add("Only excel file is allowed to process."); + return BadRequest(response); + } + + Result result = fileExtension.Equals(".xls") ? ExcelFileValidator.Validate(request.FileData) : ExcelxFileValidator.Validate(request.FileData); + if (!result.Acceptable) + { + response.ReturnStatus = StatusCodes.Status417ExpectationFailed; + response.ReturnMessage.Add("This is not a valid Excel file."); + return BadRequest(response); + } + + long maxSz = 10 * 1024 * 1024; + if (request.FileData.Length > maxSz) + { + response.ReturnStatus = StatusCodes.Status417ExpectationFailed; + response.ReturnMessage.Add("Maximum allowable size is 10 MB"); + return StatusCode(StatusCodes.Status417ExpectationFailed, response); + } + + bool permitted = await HttpContext.IsPermitted("ELIT.1.2.3_2"); + if (!permitted) + { + response.ReturnStatus = StatusCodes.Status403Forbidden; + response.ReturnMessage.Add("You are not authorize to Reset Password."); + return StatusCode(StatusCodes.Status417ExpectationFailed, response); + } + + string fileSpec = Path.Combine(_appSettings.UploadFolder, $"PWD_{DateTime.Now:yyyyMMddHHmmssfff}{fileExtension}"); + try + { + using (var stream = new FileStream(fileSpec, FileMode.Create)) + { + await request.FileData.CopyToAsync(stream); + } + + string ipAddress = Request.HttpContext.GetIpAddress(); + int changedBy = HttpContext.User.GetClaimValue(Constants.UserId); + response = await _service.UpdatePasswordsFromExcelAsync(fileSpec: fileSpec, ipAddress: ipAddress, changedBy: changedBy, forcePasswordChange: request.ForcePasswordChange); + + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex); + response.ReturnStatus = StatusCodes.Status500InternalServerError; + response.ReturnMessage.Add(ex.InnerException != null ? ex.InnerException.Message : ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, response); + } + finally + { + if (System.IO.File.Exists(fileSpec)) + System.IO.File.Delete(fileSpec); + } + } + + /// + /// /// /// ///