diff --git a/.gitignore b/.gitignore index 9491a2f..0a4b849 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,6 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +**/appsettings.Production.json diff --git a/LSFE.Application/Common/Exceptions/BusinessRuleException.cs b/LSFE.Application/Common/Exceptions/BusinessRuleException.cs new file mode 100644 index 0000000..1762802 --- /dev/null +++ b/LSFE.Application/Common/Exceptions/BusinessRuleException.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Exceptions +{ + public class BusinessRuleException : Exception + { + public BusinessRuleException(string message) + : base(message) { } + } +} diff --git a/LSFE.Application/Common/Exceptions/ForbiddenException.cs b/LSFE.Application/Common/Exceptions/ForbiddenException.cs new file mode 100644 index 0000000..499ab80 --- /dev/null +++ b/LSFE.Application/Common/Exceptions/ForbiddenException.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Exceptions +{ + public class ForbiddenException : Exception + { + public ForbiddenException() + : base("You do not have permission to perform this action.") { } + } +} diff --git a/LSFE.Application/Common/Exceptions/NotFoundException.cs b/LSFE.Application/Common/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..5e672de --- /dev/null +++ b/LSFE.Application/Common/Exceptions/NotFoundException.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Exceptions +{ + public class NotFoundException : Exception + { + public NotFoundException(string name, object key) + : base($"Entity '{name}' with key '{key}' was not found.") { } + } +} diff --git a/LSFE.Application/Common/Exceptions/UnauthorizedException.cs b/LSFE.Application/Common/Exceptions/UnauthorizedException.cs new file mode 100644 index 0000000..1e1cd4e --- /dev/null +++ b/LSFE.Application/Common/Exceptions/UnauthorizedException.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Exceptions +{ + public class UnauthorizedException : Exception + { + public UnauthorizedException(string message) + : base(message) { } + } +} diff --git a/LSFE.Application/Common/Exceptions/ValidationException.cs b/LSFE.Application/Common/Exceptions/ValidationException.cs new file mode 100644 index 0000000..c4dd6e5 --- /dev/null +++ b/LSFE.Application/Common/Exceptions/ValidationException.cs @@ -0,0 +1,20 @@ +using FluentValidation.Results; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Exceptions +{ + public class ValidationException : Exception + { + public IDictionary Errors { get; } + + public ValidationException(IEnumerable failures) + : base("One or more validation errors occurred.") + { + Errors = failures + .GroupBy(f => f.PropertyName, f => f.ErrorMessage) + .ToDictionary(g => g.Key, g => g.ToArray()); + } + } +} diff --git a/LSFE.Application/Common/Models/ErrorResponse.cs b/LSFE.Application/Common/Models/ErrorResponse.cs new file mode 100644 index 0000000..b558fc2 --- /dev/null +++ b/LSFE.Application/Common/Models/ErrorResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LSFE.Application.Common.Models +{ + public class ErrorResponse + { + public int StatusCode { get; set; } + public string Message { get; set; } = string.Empty; + public IDictionary? Errors { get; set; } + public string? StackTrace { get; set; } + } +} diff --git a/LSFE.Application/LSFE.Application.csproj b/LSFE.Application/LSFE.Application.csproj new file mode 100644 index 0000000..c9121b3 --- /dev/null +++ b/LSFE.Application/LSFE.Application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/LSFE.Infrastructure/Converter/GuidListConverter.cs b/LSFE.Infrastructure/Converter/GuidListConverter.cs index 8602617..34433e8 100644 --- a/LSFE.Infrastructure/Converter/GuidListConverter.cs +++ b/LSFE.Infrastructure/Converter/GuidListConverter.cs @@ -30,7 +30,6 @@ namespace LSFE.Infrastructure.Converter } else if (reader.TokenType == JsonTokenType.PropertyName) { - // Handle object format if needed } } diff --git a/LSFE.sln b/LSFE.sln index 7c92945..755226b 100644 --- a/LSFE.sln +++ b/LSFE.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sql", "Sql", "{F7832112-A88 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSFE.MobApp", "LSFE.MobApp\LSFE.MobApp.csproj", "{7AD0D34F-BE46-4141-8D95-9870FE8C1FEC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSFE.Application", "LSFE.Application\LSFE.Application.csproj", "{478AA6B2-E2AA-4ED5-B266-936F913D4996}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,10 @@ Global {7AD0D34F-BE46-4141-8D95-9870FE8C1FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AD0D34F-BE46-4141-8D95-9870FE8C1FEC}.Release|Any CPU.Build.0 = Release|Any CPU {7AD0D34F-BE46-4141-8D95-9870FE8C1FEC}.Release|Any CPU.Deploy.0 = Release|Any CPU + {478AA6B2-E2AA-4ED5-B266-936F913D4996}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {478AA6B2-E2AA-4ED5-B266-936F913D4996}.Debug|Any CPU.Build.0 = Debug|Any CPU + {478AA6B2-E2AA-4ED5-B266-936F913D4996}.Release|Any CPU.ActiveCfg = Release|Any CPU + {478AA6B2-E2AA-4ED5-B266-936F913D4996}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LSFE/Controllers/Account/AnonAccountController.cs b/LSFE/Controllers/Account/AnonAccountController.cs index 4030e6b..d23fd2f 100644 --- a/LSFE/Controllers/Account/AnonAccountController.cs +++ b/LSFE/Controllers/Account/AnonAccountController.cs @@ -46,57 +46,43 @@ namespace LSFE.API.Controllers.Account public async Task Login([FromBody] Users model, [FromServices] IAccountService tokenService) { - try - { - var user = await _userManager.FindByNameAsync(model.UserName.ToLower()); - if (user == null) - return BadRequest(new Infrastructure.Model.Response - { - Success = false, - MessCode = 0, - Message = "Invalid username or password." - }); - - var signInResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); - - if (signInResult.Succeeded) - { - await HandleSuccessfulLogin(user); - - var token = await tokenService.CreateToken(user); - return Ok(new - { - token, - expiration = DateTime.UtcNow.AddMinutes(30), - userId = user.Id, - userName = user.UserName, - fullName = user.FullName, - email = user.Email, - phoneNumber = user.PhoneNumber, - employeeId = user.EmployeeId, - company = user.Company, - Success = true, - MessCode = 1, - Message = "Yehey!" - }); - } - - return await HandleFailedLogin(user, signInResult); - } - catch (Exception ex) - { - var message = ex.InnerException?.Message ?? ex.Message; + var user = await _userManager.FindByNameAsync(model.UserName.ToLower()); + if (user == null) return BadRequest(new Infrastructure.Model.Response { Success = false, MessCode = 0, - Message = message + Message = "Invalid username or password." + }); + + var signInResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); + + if (signInResult.Succeeded) + { + await HandleSuccessfulLogin(user); + + var token = await tokenService.CreateToken(user); + return Ok(new + { + token, + expiration = DateTime.Now.AddMinutes(30), + userId = user.Id, + userName = user.UserName, + fullName = user.FullName, + email = user.Email, + phoneNumber = user.PhoneNumber, + employeeId = user.EmployeeId, + company = user.Company, + Success = true, + MessCode = 1, + Message = "Yehey!" }); } + + return await HandleFailedLogin(user, signInResult); } protected async Task HandleSuccessfulLogin(AppsUsers user) { - // Unlock if necessary if (user.LockoutEnabled || user.LockoutEnd != null) { await _userManager.SetLockoutEnabledAsync(user, false); @@ -104,13 +90,11 @@ namespace LSFE.API.Controllers.Account await _userManager.UpdateAsync(user); } - // Reset failed attempts await _userManager.ResetAccessFailedCountAsync(user); } protected async Task HandleFailedLogin(AppsUsers user, Microsoft.AspNetCore.Identity.SignInResult signInResult) { - // Increment failed attempts await _userManager.AccessFailedAsync(user); if (user.AccessFailedCount > 3 || signInResult.IsLockedOut) diff --git a/LSFE/Controllers/BaseController.cs b/LSFE/Controllers/BaseController.cs index f765c6f..db0d86f 100644 --- a/LSFE/Controllers/BaseController.cs +++ b/LSFE/Controllers/BaseController.cs @@ -15,92 +15,24 @@ namespace LSFE.API.Controllers { private readonly IWebHostEnvironment _env; private readonly IUnitOfWork _unitOfWork; - // public IConfiguration _configuration; public BaseController(IUnitOfWork unitOfWork, IWebHostEnvironment env) { _unitOfWork = unitOfWork; _env = env; - // _configuration = configuration; } - /* [NonAction] - [AllowAnonymous] - [HttpPost("{EMailTemplate}")] - public string EMailTemplate(string relativePath, string emailTemplate) - { - try - { - string templateFolderPath = Path.Combine(_webHostEnvironment.ContentRootPath, relativePath); - string templateFilePath = Path.Combine(templateFolderPath, emailTemplate); - - if (System.IO.File.Exists(templateFilePath)) - { - string body = System.IO.File.ReadAllText(templateFilePath); - return body; - } - else - { - Console.WriteLine($"File not found: {templateFilePath}"); - return "Template file not found"; - } - } - catch (Exception ex) - { - var errorMessage = ex.ToString() ?? ex.InnerException.ToString(); - *//* PostErrorMessage(errorMessage, "WebApi");*//* - throw; - } - }*/ - /* [NonAction] - [AllowAnonymous] - [HttpPost("{GetRelativePath}")] - public string GetRelativePath(string relativePath) - { - try - { - string templateFolderPath = Path.Combine(_webHostEnvironment.ContentRootPath, relativePath); - return templateFolderPath; - } - catch (Exception) - { - throw; - } - }*/ - /* [NonAction] - [HttpPost("{ErrMessage}")] - public async Task PostErrorMessage(string errMessage, string appName) - { - var errorMessage = new ErrorMessage - { - CreatedDate = DateTime.Now, - Message = errMessage, - Application = appName, - CreatedBy = appName - - }; - await ErrorMessageService.PostErrorMessage(errorMessage); - }*/ + [NonAction] [HttpPost("{ErrorHandling}")] protected async Task ExecuteWithErrorHandling( Func> operation, string methodName, bool isPost) { - try + var result = await operation(); + if (isPost) { - var result = await operation(); - if (isPost) - { - return Ok(new { success = true, messCode = 1, message = "Operation completed successfully", data = result }); - } - return Ok(result); - - } - catch (Exception ex) - { - var errorMessage = ex.InnerException?.ToString() ?? ex.Message.ToString(); - // await PostErrorMessage(errorMessage, $"WebApi {methodName}"); - return BadRequest(new { success = false, messCode = 0, message = errorMessage }); + return Ok(new { success = true, messCode = 1, message = "Operation completed successfully", data = result }); } + return Ok(result); } [NonAction] public async Task LogAndReturnBadRequest(string message, string? userId) diff --git a/LSFE/Controllers/InventoryController.cs b/LSFE/Controllers/InventoryController.cs index e3fe4d1..5357ed5 100644 --- a/LSFE/Controllers/InventoryController.cs +++ b/LSFE/Controllers/InventoryController.cs @@ -110,21 +110,12 @@ namespace LSFE.API.Controllers [HttpGet("inventory")] public async Task GetInventory([FromQuery] InventoryDto dto) { - try - { - var inventory = await _unitOfWork.Inventory.GetAllAsync(); - var filtered = dto.UserRole == "ADMIN" || dto.UserRole == "DSM" - ? inventory.Where(i => i.IsActive) - : inventory.Where(i => i.IsActive && i.MedRepName == dto.UpdatedBy); - - return Ok(filtered); - } - catch (Exception ex) - { - ex.ToString(); - throw; - } + var inventory = await _unitOfWork.Inventory.GetAllAsync(); + var filtered = dto.UserRole == "ADMIN" || dto.UserRole == "DSM" + ? inventory.Where(i => i.IsActive) + : inventory.Where(i => i.IsActive && i.MedRepName == dto.UpdatedBy); + return Ok(filtered); } #endregion } diff --git a/LSFE/Controllers/MaintenanceController.cs b/LSFE/Controllers/MaintenanceController.cs index e4457ae..61f4ce2 100644 --- a/LSFE/Controllers/MaintenanceController.cs +++ b/LSFE/Controllers/MaintenanceController.cs @@ -99,10 +99,8 @@ namespace LSFE.API.Controllers } else if (dto.ProductId == 0 || dto.ProductId == null) { - // Only generate GUID for new products without file dto.ProductLink = Guid.NewGuid().ToString(); } - // For updates without new file, keep existing ProductLink var response = await _unitOfWork.Products.AddOrUpdateAsync(dto); if (response.Success) @@ -215,32 +213,22 @@ namespace LSFE.API.Controllers } [HttpGet("productFile/{fileName}")] - [ResponseCache(Duration = 3600)] // Cache for 1 hour + [ResponseCache(Duration = 3600)] public async Task GetProductFile(string fileName) { - try + if (string.IsNullOrWhiteSpace(fileName) || fileName.Contains("..")) { - // Validate filename - if (string.IsNullOrWhiteSpace(fileName) || fileName.Contains("..")) - { - return BadRequest("Invalid file name"); - } - - var uploadsPath = Path.Combine(_env.WebRootPath, "Content/Uploads", "Materials"); - var filePath = Path.Combine(uploadsPath, fileName); - - ContentTypeHelper.ValidateFile(filePath, uploadsPath); - - // Stream the file instead of loading entirely into memory - var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - var contentType = ContentTypeHelper.GetContentType(fileName); - - return File(fileStream, contentType, fileName, enableRangeProcessing: true); - } - catch (Exception ex) - { - return StatusCode(500, "Error retrieving file"); + return BadRequest("Invalid file name"); } + + var uploadsPath = Path.Combine(_env.WebRootPath, "Content/Uploads", "Materials"); + var filePath = Path.Combine(uploadsPath, fileName); + + ContentTypeHelper.ValidateFile(filePath, uploadsPath); + var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + var contentType = ContentTypeHelper.GetContentType(fileName); + + return File(fileStream, contentType, fileName, enableRangeProcessing: true); } [HttpGet("areas/{id:int}")] diff --git a/LSFE/Controllers/ReportsController.cs b/LSFE/Controllers/ReportsController.cs index f03651c..e5ea151 100644 --- a/LSFE/Controllers/ReportsController.cs +++ b/LSFE/Controllers/ReportsController.cs @@ -1,6 +1,5 @@ using LSFE.Domain.Contracts; using LSFE.Domain.Helpers; -using LSFE.Infrastructure.Dto; using LSFE.Infrastructure.Dto.Reports; using Microsoft.AspNetCore.Mvc; diff --git a/LSFE/LSFE.API.csproj b/LSFE/LSFE.API.csproj index ec41386..8cf2de6 100644 --- a/LSFE/LSFE.API.csproj +++ b/LSFE/LSFE.API.csproj @@ -18,6 +18,7 @@ + diff --git a/LSFE/Middleware/GlobalExceptionMiddleware.cs b/LSFE/Middleware/GlobalExceptionMiddleware.cs new file mode 100644 index 0000000..705f7fe --- /dev/null +++ b/LSFE/Middleware/GlobalExceptionMiddleware.cs @@ -0,0 +1,80 @@ +using LSFE.Application.Common.Exceptions; +using LSFE.Application.Common.Models; +using System.Security.Claims; +using System.Text.Json; + +namespace LSFE.API.Middleware +{ + public class GlobalExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IHostEnvironment _env; + + public GlobalExceptionMiddleware( + RequestDelegate next, + ILogger logger, + IHostEnvironment env) + { + _next = next; + _logger = logger; + _env = env; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception: {Message}", ex.Message); + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception ex) + { + var (statusCode, message, errors) = ex switch + { + ValidationException e => (StatusCodes.Status422UnprocessableEntity, + e.Message, e.Errors), + + NotFoundException e => (StatusCodes.Status404NotFound, + e.Message, null), + + UnauthorizedException e => (StatusCodes.Status401Unauthorized, + e.Message, null), + + ForbiddenException e => (StatusCodes.Status403Forbidden, + e.Message, null), + + BusinessRuleException e => (StatusCodes.Status409Conflict, + e.Message, null), + + UnauthorizedAccessException e => (StatusCodes.Status401Unauthorized, + e.Message, null), + + OperationCanceledException _ => (StatusCodes.Status499ClientClosedRequest, + "Request was cancelled.", null), + + _ => (StatusCodes.Status500InternalServerError, + "An unexpected error occurred. Please try again later.", null) + }; + + var response = new ErrorResponse + { + StatusCode = statusCode, + Message = message, + Errors = errors, + StackTrace = _env.IsDevelopment() ? ex.StackTrace : null + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + + await context.Response.WriteAsJsonAsync(response); + } + } +}