From 2486a490ec981265a9c37573404dafed6026e4b4 Mon Sep 17 00:00:00 2001 From: Musa Ahmed Date: Thu, 26 Feb 2026 23:15:28 -0500 Subject: [PATCH 01/15] US-003 - Fix build issues with Api --- WardrobeManager.Api/Program.cs | 2 +- .../Repositories/Implementation/ClothingRepository.cs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/WardrobeManager.Api/Program.cs b/WardrobeManager.Api/Program.cs index c50342a..c9efa9c 100644 --- a/WardrobeManager.Api/Program.cs +++ b/WardrobeManager.Api/Program.cs @@ -90,7 +90,7 @@ // Entity Services builder.Services.AddScoped, GenericRepository>(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // add db context builder.Services.AddDbContext((serviceProvider, options) => diff --git a/WardrobeManager.Api/Repositories/Implementation/ClothingRepository.cs b/WardrobeManager.Api/Repositories/Implementation/ClothingRepository.cs index 0f07dcd..6033c78 100644 --- a/WardrobeManager.Api/Repositories/Implementation/ClothingRepository.cs +++ b/WardrobeManager.Api/Repositories/Implementation/ClothingRepository.cs @@ -5,12 +5,8 @@ namespace WardrobeManager.Api.Repositories.Implementation; -public class ClothingRepository: GenericRepository, IClothingRepository +public class ClothingRepository(DatabaseContext context) : GenericRepository(context), IClothingRepository { - public ClothingRepository(DatabaseContext context, DbSet clothingItems) : base(context) - { - } - public async Task GetAsync(string userId, int itemId) { return await _dbSet.FirstOrDefaultAsync(item => item.Id == itemId && item.UserId == userId); From 1ff457fbf0ff13085b8cddb3ae3073ac4b0f2b0b Mon Sep 17 00:00:00 2001 From: Musa Ahmed Date: Fri, 27 Feb 2026 02:30:08 -0500 Subject: [PATCH 02/15] US-003 - Massive commit Working on adding functionality to allow user to do the typical REST actions to a clothing item (get, post, put, delete). This means I neeeded to reuse a lot of old code and move things around. Many tests are probably broken too now. --- .../Services/ClothingServiceTests.cs | 7 +- .../Endpoints/ClothingEndpoints.cs | 42 +++++++-- .../Endpoints/UserEndpoints.cs | 1 + .../Middleware/UserCreationMiddleware.cs | 31 ++----- WardrobeManager.Api/Program.cs | 2 + .../Implementation/ClothingService.cs | 48 ++++++++-- .../Services/Implementation/FileService.cs | 10 +-- .../Services/Implementation/UserService.cs | 1 + .../Services/Interfaces/IClothingService.cs | 7 +- .../Services/Interfaces/IUserService.cs | 1 + .../Components/Shared/Image.razor | 4 +- .../Components/Shared/Notifications.razor | 2 +- .../CustomHttpMessageHandler.cs | 49 ++++++++++ .../Identity/CookieHandler.cs | 30 ------- .../CustomAuthorizationMessageHandler.cs} | 4 +- .../Layout/NavMenu.razor | 90 +++++++++++-------- .../Pages/Authenticated/AddClothingItem.razor | 50 ++++++++++- .../Pages/Authenticated/Clothing.razor | 10 --- .../Pages/Authenticated/Wardrobe.razor | 31 +++++++ .../Pages/Public/Login.razor | 2 +- WardrobeManager.Presentation/Program.cs | 6 +- .../Services/Implementation/ApiService.cs | 29 ++++++ .../Services/Interfaces/IApiService.cs | 15 +++- .../ViewModels/AddClothingItemViewModel.cs | 69 ++++++++++++++ .../ViewModels/LoginViewModel.cs | 2 - .../ViewModels/WardrobeViewModel.cs | 34 +++++++ .../DTOs/ClothingItemDTO.cs | 18 ++++ .../{Models => DTOs}/EditedUserDTO.cs | 2 +- .../DTOs/NewClothingItemDTO.cs | 24 +++++ .../NewOrEditedClothingItemDTO.cs | 2 +- .../Models/ClientClothingItem.cs | 16 ---- .../StaticResources/MiscMethods.cs | 8 +- .../StaticResources/ProjectConstants.cs | 1 + 33 files changed, 494 insertions(+), 154 deletions(-) create mode 100644 WardrobeManager.Presentation/CustomHttpMessageHandler.cs delete mode 100644 WardrobeManager.Presentation/Identity/CookieHandler.cs rename WardrobeManager.Presentation/{HelperClasses.cs => Identity/CustomAuthorizationMessageHandler.cs} (85%) delete mode 100644 WardrobeManager.Presentation/Pages/Authenticated/Clothing.razor create mode 100644 WardrobeManager.Presentation/Pages/Authenticated/Wardrobe.razor create mode 100644 WardrobeManager.Presentation/ViewModels/AddClothingItemViewModel.cs create mode 100644 WardrobeManager.Presentation/ViewModels/WardrobeViewModel.cs create mode 100644 WardrobeManager.Shared/DTOs/ClothingItemDTO.cs rename WardrobeManager.Shared/{Models => DTOs}/EditedUserDTO.cs (95%) create mode 100644 WardrobeManager.Shared/DTOs/NewClothingItemDTO.cs rename WardrobeManager.Shared/{Models => DTOs}/NewOrEditedClothingItemDTO.cs (97%) delete mode 100644 WardrobeManager.Shared/Models/ClientClothingItem.cs diff --git a/WardrobeManager.Api.Tests/Services/ClothingServiceTests.cs b/WardrobeManager.Api.Tests/Services/ClothingServiceTests.cs index 7f80e69..9c0dbad 100644 --- a/WardrobeManager.Api.Tests/Services/ClothingServiceTests.cs +++ b/WardrobeManager.Api.Tests/Services/ClothingServiceTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using AutoMapper; +using FluentAssertions; using FluentAssertions.Execution; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -18,6 +19,7 @@ public class ClothingServiceTests private const int DefaultItemId = 1; private Mock _mockRepo; + private Mock _mockMapper; private ClothingService _serviceToTest; [SetUp] @@ -25,7 +27,8 @@ public void Setup() { // Mock the service and its dependencies _mockRepo = new Mock(); - _serviceToTest = new ClothingService(_mockRepo.Object); + _mockMapper = new Mock(); + _serviceToTest = new ClothingService(_mockRepo.Object, _mockMapper.Object); } #region GetClothingItem diff --git a/WardrobeManager.Api/Endpoints/ClothingEndpoints.cs b/WardrobeManager.Api/Endpoints/ClothingEndpoints.cs index 738562d..492c455 100644 --- a/WardrobeManager.Api/Endpoints/ClothingEndpoints.cs +++ b/WardrobeManager.Api/Endpoints/ClothingEndpoints.cs @@ -7,16 +7,22 @@ using WardrobeManager.Api.Services.Implementation; using WardrobeManager.Api.Services.Interfaces; using WardrobeManager.Shared.Models; +using AutoMapper; +using WardrobeManager.Shared.DTOs; #endregion namespace WardrobeManager.Api.Endpoints; -public static class ClothingEndpoints { - public static void MapClothingEndpoints(this IEndpointRouteBuilder app) { +public static class ClothingEndpoints +{ + public static void MapClothingEndpoints(this IEndpointRouteBuilder app) + { var group = app.MapGroup("/clothing").RequireAuthorization(); group.MapGet("", GetClothing); + group.MapPost("/add", AddNewClothingItem); + group.MapPost("/delete", RemoveClothingItem); // maybe should get a GET request, idc rn } @@ -24,8 +30,9 @@ public static void MapClothingEndpoints(this IEndpointRouteBuilder app) { // Get all clothing items // --------------------- public static async Task GetClothing( - HttpContext context, IClothingService clothingService, DatabaseContext _context - ){ + HttpContext context, IClothingService clothingService + ) + { User? user = context.Items["user"] as User; Debug.Assert(user != null, "Cannot get user"); @@ -34,4 +41,29 @@ public static async Task GetClothing( return Results.Ok(clothes); } -} + public static async Task AddNewClothingItem( + [FromBody] NewClothingItemDTO newNewClothingItem, + HttpContext context, IClothingService clothingService, IMapper mapper + ) + { + User? user = context.Items["user"] as User; + Debug.Assert(user != null, "Cannot get user"); + + await clothingService.AddNewClothingItem(user.Id ,newNewClothingItem); + + return Results.Ok(); + } + + public static async Task RemoveClothingItem( + [FromBody] int itemId, + HttpContext context, IClothingService clothingService, IMapper mapper + ) + { + User? user = context.Items["user"] as User; + Debug.Assert(user != null, "Cannot get user"); + + await clothingService.RemoveClothingItem(user.Id ,itemId); + + return Results.Ok(); + } +} \ No newline at end of file diff --git a/WardrobeManager.Api/Endpoints/UserEndpoints.cs b/WardrobeManager.Api/Endpoints/UserEndpoints.cs index c1d7ded..8466a70 100644 --- a/WardrobeManager.Api/Endpoints/UserEndpoints.cs +++ b/WardrobeManager.Api/Endpoints/UserEndpoints.cs @@ -5,6 +5,7 @@ using WardrobeManager.Api.Database.Entities; using WardrobeManager.Api.Services.Implementation; using WardrobeManager.Api.Services.Interfaces; +using WardrobeManager.Shared.DTOs; using WardrobeManager.Shared.Models; #endregion diff --git a/WardrobeManager.Api/Middleware/UserCreationMiddleware.cs b/WardrobeManager.Api/Middleware/UserCreationMiddleware.cs index 4fa51f4..04a40da 100644 --- a/WardrobeManager.Api/Middleware/UserCreationMiddleware.cs +++ b/WardrobeManager.Api/Middleware/UserCreationMiddleware.cs @@ -23,28 +23,15 @@ public UserCreationMiddleware(RequestDelegate next) public async Task InvokeAsync(HttpContext context) { - - // var userService = context.RequestServices.GetRequiredService(); - - // -------------- !!IMPORTANT!! ------------- - // Previously I was using Auth0 as an authentication provider. - // Coincidentally, Microsoft Identity and Auto0 used the NameIdentifier claim for a user - // So all the old code still works 100% fine. - // For this reason I will not be modifying anything Auth0 at the moment - // var Auth0Id = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - - // only create the user if the consumer of the api is authorized - // if (Auth0Id != null) - // { - // await userService.CreateUser(Auth0Id); - // Debug.Assert(await userService.DoesUserExist(Auth0Id) == true, "User finding/creating is broken"); - // - // var user = await userService.GetUser(Auth0Id); - // Debug.Assert(user != null, "At this point in the pipeline user should exist"); - // - // // pass along to controllers - // context.Items["user"] = user; - // } + var userService = context.RequestServices.GetRequiredService(); + var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? ""; + + var user = await userService.GetUser(userId); + if (user != null) + { + // pass along to controllers + context.Items["user"] = user; + } await _next(context); } diff --git a/WardrobeManager.Api/Program.cs b/WardrobeManager.Api/Program.cs index c9efa9c..88fe4dd 100644 --- a/WardrobeManager.Api/Program.cs +++ b/WardrobeManager.Api/Program.cs @@ -86,6 +86,8 @@ // Add your maps here directly cfg.CreateMap().ReverseMap(); + cfg.CreateMap(); + cfg.CreateMap().ReverseMap(); }); // Entity Services diff --git a/WardrobeManager.Api/Services/Implementation/ClothingService.cs b/WardrobeManager.Api/Services/Implementation/ClothingService.cs index 4ff53a8..40990f3 100644 --- a/WardrobeManager.Api/Services/Implementation/ClothingService.cs +++ b/WardrobeManager.Api/Services/Implementation/ClothingService.cs @@ -1,11 +1,14 @@ #region +using AutoMapper; using Microsoft.EntityFrameworkCore; +using SQLitePCL; using WardrobeManager.Api.Database; using WardrobeManager.Api.Database.Entities; using WardrobeManager.Api.Repositories; using WardrobeManager.Api.Repositories.Interfaces; using WardrobeManager.Api.Services.Interfaces; +using WardrobeManager.Shared.DTOs; using WardrobeManager.Shared.Enums; using WardrobeManager.Shared.Models; using WardrobeManager.Shared.StaticResources; @@ -14,22 +17,51 @@ namespace WardrobeManager.Api.Services.Implementation; -public class ClothingService(IClothingRepository clothingRepository) +public class ClothingService(IClothingRepository clothingRepository, IMapper mapper, IFileService fileService, ILogger logger) : IClothingService { - - // ---- Methods for multiple clothing items --- - public async Task?> GetAllClothingAsync(string userId) + public async Task?> GetAllClothingAsync(string userId) { - return await clothingRepository.GetAllAsync(userId); + var res = await clothingRepository.GetAllAsync(userId); + return mapper.Map>(res); } // ---- Methods for one clothing item --- - public async Task GetClothingItemAsync(string userId, int itemId) + public async Task GetClothingItemAsync(string userId, int itemId) { - return await clothingRepository.GetAsync(userId, itemId); + var res = await clothingRepository.GetAsync(userId, itemId); + return mapper.Map(res); } + public async Task AddNewClothingItem(string userId, NewClothingItemDTO newNewClothingItem) + { + var res = mapper.Map(newNewClothingItem); + Guid? newItemGuid = null; + if (MiscMethods.IsValidBase64(newNewClothingItem.ImageBase64)) + { + newItemGuid = Guid.NewGuid(); + // decode and save file to place on disk with guid as name + await fileService.SaveImage(newItemGuid, newNewClothingItem.ImageBase64!); + } -} + res.UserId = userId; + res.ImageGuid = newItemGuid; + await clothingRepository.CreateAsync(res); + await clothingRepository.SaveAsync(); + } + + public async Task RemoveClothingItem(string userId, int itemId) + { + var res = await clothingRepository.GetAsync(userId, itemId); + if (res != null) + { + clothingRepository.Remove(res); + await clothingRepository.SaveAsync(); + } + else + { + logger.LogInformation($"Clothing item {itemId} not found"); + } + } +} \ No newline at end of file diff --git a/WardrobeManager.Api/Services/Implementation/FileService.cs b/WardrobeManager.Api/Services/Implementation/FileService.cs index b685b0d..c698e55 100644 --- a/WardrobeManager.Api/Services/Implementation/FileService.cs +++ b/WardrobeManager.Api/Services/Implementation/FileService.cs @@ -31,14 +31,14 @@ public async Task SaveImage(Guid? guid, string ImageBase64) byte[] imageBytes = Convert.FromBase64String(ImageBase64); // 5MB default max file size - var max_file_size = configuration["WM_MAX_IMAGE_UPLOAD_SIZE_IN_MB"] ?? "5"; - int max_file_size_num = Convert.ToInt32(max_file_size); - max_file_size_num *= 1024; + var maxFileSize = configuration["WM_MAX_IMAGE_UPLOAD_SIZE_IN_MB"] ?? "5"; + int maxFileSizeNum = Convert.ToInt32(maxFileSize); + maxFileSizeNum *= 1024 * 1024; - if (imageBytes.Length > max_file_size_num) + if (imageBytes.Length > maxFileSizeNum) { throw new Exception( - $"File size too large! Received file size: {imageBytes.Length / 1024} MB. Max file size: {max_file_size_num / 1024} MB"); + $"File size too large! Received file size: {imageBytes.Length / 1024} MB. Max file size: {maxFileSizeNum / 1024} MB"); } string path = Path.Combine(dataDirectoryService.GetUploadsDirectory(), ParseGuid(properGuid)); diff --git a/WardrobeManager.Api/Services/Implementation/UserService.cs b/WardrobeManager.Api/Services/Implementation/UserService.cs index 63bce2a..42ee7ae 100644 --- a/WardrobeManager.Api/Services/Implementation/UserService.cs +++ b/WardrobeManager.Api/Services/Implementation/UserService.cs @@ -8,6 +8,7 @@ using WardrobeManager.Api.Database.Entities; using WardrobeManager.Api.Repositories; using WardrobeManager.Api.Services.Interfaces; +using WardrobeManager.Shared.DTOs; using WardrobeManager.Shared.Enums; using WardrobeManager.Shared.Models; diff --git a/WardrobeManager.Api/Services/Interfaces/IClothingService.cs b/WardrobeManager.Api/Services/Interfaces/IClothingService.cs index 9182ebc..998cb91 100644 --- a/WardrobeManager.Api/Services/Interfaces/IClothingService.cs +++ b/WardrobeManager.Api/Services/Interfaces/IClothingService.cs @@ -1,6 +1,7 @@ #region using WardrobeManager.Api.Database.Entities; +using WardrobeManager.Shared.DTOs; using WardrobeManager.Shared.Enums; using WardrobeManager.Shared.Models; @@ -9,7 +10,9 @@ namespace WardrobeManager.Api.Services.Interfaces; public interface IClothingService { - Task?> GetAllClothingAsync(string userId); + Task?> GetAllClothingAsync(string userId); - Task GetClothingItemAsync(string userId, int itemId); + Task GetClothingItemAsync(string userId, int itemId); + Task AddNewClothingItem(string userId, NewClothingItemDTO newNewClothingItem); + Task RemoveClothingItem(string userId, int itemId); } diff --git a/WardrobeManager.Api/Services/Interfaces/IUserService.cs b/WardrobeManager.Api/Services/Interfaces/IUserService.cs index 1de68d7..7aba802 100644 --- a/WardrobeManager.Api/Services/Interfaces/IUserService.cs +++ b/WardrobeManager.Api/Services/Interfaces/IUserService.cs @@ -1,4 +1,5 @@ using WardrobeManager.Api.Database.Entities; +using WardrobeManager.Shared.DTOs; using WardrobeManager.Shared.Models; namespace WardrobeManager.Api.Services.Interfaces; diff --git a/WardrobeManager.Presentation/Components/Shared/Image.razor b/WardrobeManager.Presentation/Components/Shared/Image.razor index 337f93b..f705497 100644 --- a/WardrobeManager.Presentation/Components/Shared/Image.razor +++ b/WardrobeManager.Presentation/Components/Shared/Image.razor @@ -3,7 +3,7 @@ @code { - [Parameter] public required Guid? ImageGuid { get; set; } + [Parameter] public required string? ImageGuid { get; set; } [Parameter] public required string Css { get; set; } string _imagePath = string.Empty; @@ -13,7 +13,7 @@ // the way my api controller matches routes you need to supply a path for /images/{image_id} // because some clothing items can have no image the Guid with therefore be null and /images/ will be called which doesn't trigger the api // suppress nullable error because it doesn't realize i'm already checking it - _imagePath = (string.IsNullOrEmpty(ImageGuid.ToString()) ? "fakepath" : ImageGuid.ToString())!; + _imagePath = (string.IsNullOrEmpty(ImageGuid) ? "fakepath" : ImageGuid); await base.OnParametersSetAsync(); } diff --git a/WardrobeManager.Presentation/Components/Shared/Notifications.razor b/WardrobeManager.Presentation/Components/Shared/Notifications.razor index 9f7257c..0b528f5 100644 --- a/WardrobeManager.Presentation/Components/Shared/Notifications.razor +++ b/WardrobeManager.Presentation/Components/Shared/Notifications.razor @@ -8,7 +8,7 @@ { var cssClass = GetNotificationButtonType(notification.Type); - + @notification.Message } diff --git a/WardrobeManager.Presentation/CustomHttpMessageHandler.cs b/WardrobeManager.Presentation/CustomHttpMessageHandler.cs new file mode 100644 index 0000000..e52288a --- /dev/null +++ b/WardrobeManager.Presentation/CustomHttpMessageHandler.cs @@ -0,0 +1,49 @@ +#region + +using Microsoft.AspNetCore.Components.WebAssembly.Http; +using WardrobeManager.Presentation.Services.Interfaces; +using WardrobeManager.Shared.Enums; + +#endregion + +namespace WardrobeManager.Presentation; + +/// +/// Handler to ensure cookie credentials are automatically sent over with each request. +/// +public class CustomHttpMessageHandler(INotificationService notificationService) : DelegatingHandler +{ + /// + /// Main method to override for the handler. + /// + /// The original request. + /// The token to handle cancellations. + /// The . + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // include cookies! + request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); + request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]); + + try + { + // This sends the actual HTTP request + var response = await base.SendAsync(request, cancellationToken); + + // 1. Check for standard HTTP errors (like 404 Not Found, 500 Server Error) + if (!response.IsSuccessStatusCode) + { + notificationService.AddNotification($"Server Error: {(int)response.StatusCode}", NotificationType.Error); + } + + return response; + } + catch (HttpRequestException ex) // 2. Catch network failures (offline, CORS, server dead) + { + notificationService.AddNotification($"HttpRequestException: {ex.Message}", NotificationType.Error); + // rethrow so exception is not lost, we just want to log it here. + // If we don't rethrow code called after the http request will still run + throw; + } + } +} diff --git a/WardrobeManager.Presentation/Identity/CookieHandler.cs b/WardrobeManager.Presentation/Identity/CookieHandler.cs deleted file mode 100644 index cee1447..0000000 --- a/WardrobeManager.Presentation/Identity/CookieHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -#region - -using Microsoft.AspNetCore.Components.WebAssembly.Http; - -#endregion - -namespace WardrobeManager.Presentation.Identity; - -/// -/// Handler to ensure cookie credentials are automatically sent over with each request. -/// -public class CookieHandler : DelegatingHandler -{ - /// - /// Main method to override for the handler. - /// - /// The original request. - /// The token to handle cancellations. - /// The . - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - // include cookies! - request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); - request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]); - - return base.SendAsync(request, cancellationToken); - - - } -} \ No newline at end of file diff --git a/WardrobeManager.Presentation/HelperClasses.cs b/WardrobeManager.Presentation/Identity/CustomAuthorizationMessageHandler.cs similarity index 85% rename from WardrobeManager.Presentation/HelperClasses.cs rename to WardrobeManager.Presentation/Identity/CustomAuthorizationMessageHandler.cs index 6a18c1e..3f89b1e 100644 --- a/WardrobeManager.Presentation/HelperClasses.cs +++ b/WardrobeManager.Presentation/Identity/CustomAuthorizationMessageHandler.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using WardrobeManager.Presentation.Services.Interfaces; +using WardrobeManager.Shared.Enums; #endregion @@ -17,4 +19,4 @@ public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, authorizedUrls: new[] { "https://localhost:7026" }); //scopes: new[] { "example.read", "example.write" }); } -} \ No newline at end of file +} diff --git a/WardrobeManager.Presentation/Layout/NavMenu.razor b/WardrobeManager.Presentation/Layout/NavMenu.razor index 0debe40..2c7ed9b 100644 --- a/WardrobeManager.Presentation/Layout/NavMenu.razor +++ b/WardrobeManager.Presentation/Layout/NavMenu.razor @@ -49,54 +49,74 @@
-
+
diff --git a/WardrobeManager.Presentation/Pages/Authenticated/AddClothingItem.razor b/WardrobeManager.Presentation/Pages/Authenticated/AddClothingItem.razor index 3b9b749..673f134 100644 --- a/WardrobeManager.Presentation/Pages/Authenticated/AddClothingItem.razor +++ b/WardrobeManager.Presentation/Pages/Authenticated/AddClothingItem.razor @@ -1,6 +1,52 @@ @page "/add" @using WardrobeManager.Presentation.Components.Clothing - +@using WardrobeManager.Presentation.Components.FormItems +@using WardrobeManager.Shared.StaticResources @namespace WardrobeManager.Presentation.Pages.Authenticated +@inherits MvvmComponentBase + +
+

Add a clothing item

+
+ +