From b8ddee390a8cce152ecf5daafde062044ea016b8 Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:54:09 +0300 Subject: [PATCH 1/6] Add Role unsetting functionality --- .../Users/DeleteUserRoleHandler.cs | 29 +++++++++++++++++++ .../Controllers/Users/UserRoleController.cs | 16 ++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/Imprink.Application/Users/DeleteUserRoleHandler.cs diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs new file mode 100644 index 0000000..ca536f8 --- /dev/null +++ b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs @@ -0,0 +1,29 @@ +using Imprink.Application.Users.Dtos; +using Imprink.Domain.Entities.Users; +using MediatR; + +namespace Imprink.Application.Users; + +public record DeleteUserRoleCommand(string Sub, Guid RoleId) : IRequest; + +public class DeleteUserRoleHandler(IUnitOfWork uw) : IRequestHandler +{ + public async Task Handle(DeleteUserRoleCommand request, CancellationToken cancellationToken) + { + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return null; + + var userRole = new UserRole + { + UserId = request.Sub, + RoleId = request.RoleId + }; + + var removedRole = await uw.UserRoleRepository.RemoveUserRoleAsync(userRole, cancellationToken); + + return new UserRoleDto + { + UserId = removedRole.UserId, + RoleId = removedRole.RoleId + }; + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs index f2090ed..62da421 100644 --- a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs @@ -11,7 +11,7 @@ namespace Imprink.WebApi.Controllers.Users; public class UserRoleController(IMediator mediator) : ControllerBase { [Authorize] - [HttpGet("me")] + [HttpGet("/me")] public async Task GetMyRoles() { var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); @@ -23,7 +23,7 @@ public class UserRoleController(IMediator mediator) : ControllerBase } [Authorize(Roles = "Admin")] - [HttpPost("set")] + [HttpPost("/set")] public async Task SetUserRole(SetUserRoleCommand command) { var userRole = await mediator.Send(command); @@ -33,4 +33,16 @@ public class UserRoleController(IMediator mediator) : ControllerBase return Ok(userRole); } + + [Authorize(Roles = "Admin")] + [HttpPost("/unset")] + public async Task UnsetUserRole(DeleteUserRoleCommand command) + { + var userRole = await mediator.Send(command); + + if (userRole == null) + return BadRequest(); + + return Ok(userRole); + } } \ No newline at end of file From db61171ada18771107a8716cb8dd1bfa9789d13f Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:30:58 +0300 Subject: [PATCH 2/6] Add Global Exception Handling --- .../Exceptions/BaseApplicationException.cs | 7 ++ .../Exceptions/NotFoundException.cs | 7 ++ .../Users/DeleteUserRoleHandler.cs | 4 +- .../Users/GetUserRolesHandler.cs | 4 +- .../Users/SetUserRoleHandler.cs | 4 +- .../Controllers/Users/UserRoleController.cs | 6 +- .../Middleware/ExceptionHandlingMiddleware.cs | 75 +++++++++++++++++++ src/Imprink.WebApi/Startup.cs | 2 +- 8 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/Imprink.Application/Exceptions/BaseApplicationException.cs create mode 100644 src/Imprink.Application/Exceptions/NotFoundException.cs create mode 100644 src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs diff --git a/src/Imprink.Application/Exceptions/BaseApplicationException.cs b/src/Imprink.Application/Exceptions/BaseApplicationException.cs new file mode 100644 index 0000000..7ce0b6b --- /dev/null +++ b/src/Imprink.Application/Exceptions/BaseApplicationException.cs @@ -0,0 +1,7 @@ +namespace Imprink.Application.Exceptions; + +public abstract class BaseApplicationException : Exception +{ + protected BaseApplicationException(string message) : base(message) { } + protected BaseApplicationException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/src/Imprink.Application/Exceptions/NotFoundException.cs b/src/Imprink.Application/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..ade782f --- /dev/null +++ b/src/Imprink.Application/Exceptions/NotFoundException.cs @@ -0,0 +1,7 @@ +namespace Imprink.Application.Exceptions; + +public class NotFoundException : BaseApplicationException +{ + public NotFoundException(string message) : base(message) { } + public NotFoundException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs index ca536f8..ec950fe 100644 --- a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs +++ b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using Imprink.Domain.Entities.Users; using MediatR; @@ -10,7 +11,8 @@ public class DeleteUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(DeleteUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return null; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var userRole = new UserRole { diff --git a/src/Imprink.Application/Users/GetUserRolesHandler.cs b/src/Imprink.Application/Users/GetUserRolesHandler.cs index ef2091a..dca1ed0 100644 --- a/src/Imprink.Application/Users/GetUserRolesHandler.cs +++ b/src/Imprink.Application/Users/GetUserRolesHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using MediatR; @@ -9,7 +10,8 @@ public class GetUserRolesHandler(IUnitOfWork uw): IRequestHandler> Handle(GetUserRolesCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return []; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var roles = await uw.UserRoleRepository.GetUserRolesAsync(request.Sub, cancellationToken); diff --git a/src/Imprink.Application/Users/SetUserRoleHandler.cs b/src/Imprink.Application/Users/SetUserRoleHandler.cs index 3fe8ca7..76f5349 100644 --- a/src/Imprink.Application/Users/SetUserRoleHandler.cs +++ b/src/Imprink.Application/Users/SetUserRoleHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using Imprink.Domain.Entities.Users; using MediatR; @@ -10,7 +11,8 @@ public class SetUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(SetUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return null; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var userRole = new UserRole { diff --git a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs index 62da421..34a2926 100644 --- a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs @@ -11,7 +11,7 @@ namespace Imprink.WebApi.Controllers.Users; public class UserRoleController(IMediator mediator) : ControllerBase { [Authorize] - [HttpGet("/me")] + [HttpGet("me")] public async Task GetMyRoles() { var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); @@ -23,7 +23,7 @@ public class UserRoleController(IMediator mediator) : ControllerBase } [Authorize(Roles = "Admin")] - [HttpPost("/set")] + [HttpPost("set")] public async Task SetUserRole(SetUserRoleCommand command) { var userRole = await mediator.Send(command); @@ -35,7 +35,7 @@ public class UserRoleController(IMediator mediator) : ControllerBase } [Authorize(Roles = "Admin")] - [HttpPost("/unset")] + [HttpPost("unset")] public async Task UnsetUserRole(DeleteUserRoleCommand command) { var userRole = await mediator.Send(command); diff --git a/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs b/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..eb70eab --- /dev/null +++ b/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,75 @@ +using System.Net; +using System.Text.Json; +using Imprink.Application.Exceptions; + +namespace Imprink.WebApi.Middleware; + +public class ExceptionHandlingMiddleware( + RequestDelegate next, + ILogger logger) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + var (_, _, shouldLog) = GetStatusCodeAndMessage(ex); + + if (shouldLog) + { + logger.LogError(ex, "An unhandled exception occurred: {Message}", ex.Message); + } + else + { + logger.LogInformation("Handled: {Message}", ex.Message); + } + + await HandleExceptionAsync(context, ex); + } + } + + private static async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + var (statusCode, message, _) = GetStatusCodeAndMessage(exception); + + context.Response.StatusCode = (int)statusCode; + + var response = new + { + error = new + { + message, + timestamp = DateTime.UtcNow, + } + }; + + var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + await context.Response.WriteAsync(jsonResponse); + } + + private static (HttpStatusCode statusCode, string message, bool shouldLog) GetStatusCodeAndMessage(Exception exception) + { + return exception switch + { + NotFoundException => (HttpStatusCode.NotFound, exception.Message, false), + _ => (HttpStatusCode.InternalServerError, "An internal server error occurred", true) + }; + } +} + +public static class GlobalExceptionHandlingMiddlewareExtensions +{ + public static void UseGlobalExceptionHandling(this IApplicationBuilder builder) + { + builder.UseMiddleware(); + } +} diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs index cf44d43..60e1eae 100644 --- a/src/Imprink.WebApi/Startup.cs +++ b/src/Imprink.WebApi/Startup.cs @@ -140,13 +140,13 @@ public static class Startup } } + app.UseGlobalExceptionHandling(); app.UseRequestTiming(); if (env.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); - app.UseDeveloperExceptionPage(); } else { From 09f1668351b53a3fad11ea9c52265a0fce21fad1 Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:49:14 +0300 Subject: [PATCH 3/6] Use transactions --- .../Users/DeleteUserRoleHandler.cs | 40 +++++++++++++------ .../Users/SetUserRoleHandler.cs | 37 +++++++++++------ .../Products/CategoryRepository.cs | 11 ++--- .../Products/ProductRepository.cs | 7 +--- .../Products/ProductVariantRepository.cs | 12 ++---- .../Repositories/Users/UserRoleRepository.cs | 11 ++--- .../Products/CategoriesController.cs | 3 +- .../Products/ProductVariantsController.cs | 3 +- .../Products/ProductsController.cs | 2 +- .../Controllers/Users/UserController.cs | 1 - .../Controllers/Users/UserRoleController.cs | 22 ++-------- 11 files changed, 73 insertions(+), 76 deletions(-) diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs index ec950fe..e30ef08 100644 --- a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs +++ b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs @@ -11,21 +11,35 @@ public class DeleteUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(DeleteUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) - throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); + await uw.BeginTransactionAsync(cancellationToken); - var userRole = new UserRole + try { - UserId = request.Sub, - RoleId = request.RoleId - }; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); + + var userRole = new UserRole + { + UserId = request.Sub, + RoleId = request.RoleId + }; + + var removedRole = await uw.UserRoleRepository.RemoveUserRoleAsync(userRole, cancellationToken); + + await uw.SaveAsync(cancellationToken); + await uw.CommitTransactionAsync(cancellationToken); + + return new UserRoleDto + { + UserId = removedRole.UserId, + RoleId = removedRole.RoleId + }; + } + catch + { + await uw.RollbackTransactionAsync(cancellationToken); + throw; + } - var removedRole = await uw.UserRoleRepository.RemoveUserRoleAsync(userRole, cancellationToken); - - return new UserRoleDto - { - UserId = removedRole.UserId, - RoleId = removedRole.RoleId - }; } } \ No newline at end of file diff --git a/src/Imprink.Application/Users/SetUserRoleHandler.cs b/src/Imprink.Application/Users/SetUserRoleHandler.cs index 76f5349..f077e99 100644 --- a/src/Imprink.Application/Users/SetUserRoleHandler.cs +++ b/src/Imprink.Application/Users/SetUserRoleHandler.cs @@ -11,21 +11,34 @@ public class SetUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(SetUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) - throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); + await uw.BeginTransactionAsync(cancellationToken); - var userRole = new UserRole + try { - UserId = request.Sub, - RoleId = request.RoleId - }; - - var addedRole = await uw.UserRoleRepository.AddUserRoleAsync(userRole, cancellationToken); + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); - return new UserRoleDto + var userRole = new UserRole + { + UserId = request.Sub, + RoleId = request.RoleId + }; + + var addedRole = await uw.UserRoleRepository.AddUserRoleAsync(userRole, cancellationToken); + + await uw.SaveAsync(cancellationToken); + await uw.CommitTransactionAsync(cancellationToken); + + return new UserRoleDto + { + UserId = addedRole.UserId, + RoleId = addedRole.RoleId + }; + } + catch { - UserId = addedRole.UserId, - RoleId = addedRole.RoleId - }; + await uw.RollbackTransactionAsync(cancellationToken); + throw; + } } } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/Products/CategoryRepository.cs b/src/Imprink.Infrastructure/Repositories/Products/CategoryRepository.cs index ac1f0c9..b5d9873 100644 --- a/src/Imprink.Infrastructure/Repositories/Products/CategoryRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/Products/CategoryRepository.cs @@ -62,23 +62,21 @@ public class CategoryRepository(ApplicationDbContext context) : ICategoryReposit .ToListAsync(cancellationToken); } - public async Task AddAsync(Category category, CancellationToken cancellationToken = default) + public Task AddAsync(Category category, CancellationToken cancellationToken = default) { category.Id = Guid.NewGuid(); category.CreatedAt = DateTime.UtcNow; category.ModifiedAt = DateTime.UtcNow; context.Categories.Add(category); - await context.SaveChangesAsync(cancellationToken); - return category; + return Task.FromResult(category); } - public async Task UpdateAsync(Category category, CancellationToken cancellationToken = default) + public Task UpdateAsync(Category category, CancellationToken cancellationToken = default) { category.ModifiedAt = DateTime.UtcNow; context.Categories.Update(category); - await context.SaveChangesAsync(cancellationToken); - return category; + return Task.FromResult(category); } public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) @@ -87,7 +85,6 @@ public class CategoryRepository(ApplicationDbContext context) : ICategoryReposit if (category != null) { context.Categories.Remove(category); - await context.SaveChangesAsync(cancellationToken); } } diff --git a/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs b/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs index 4b0427e..95af010 100644 --- a/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs @@ -1,6 +1,5 @@ using Imprink.Domain.Entities.Product; using Imprink.Domain.Models; -using Imprink.Domain.Repositories; using Imprink.Domain.Repositories.Products; using Imprink.Infrastructure.Database; using Microsoft.EntityFrameworkCore; @@ -143,12 +142,11 @@ public class ProductRepository(ApplicationDbContext context) : IProductRepositor return product; } - public async Task UpdateAsync(Product product, CancellationToken cancellationToken = default) + public Task UpdateAsync(Product product, CancellationToken cancellationToken = default) { product.ModifiedAt = DateTime.UtcNow; context.Products.Update(product); - await context.SaveChangesAsync(cancellationToken); - return product; + return Task.FromResult(product); } public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) @@ -157,7 +155,6 @@ public class ProductRepository(ApplicationDbContext context) : IProductRepositor if (product != null) { context.Products.Remove(product); - await context.SaveChangesAsync(cancellationToken); } } diff --git a/src/Imprink.Infrastructure/Repositories/Products/ProductVariantRepository.cs b/src/Imprink.Infrastructure/Repositories/Products/ProductVariantRepository.cs index 4b23bb5..1c892bf 100644 --- a/src/Imprink.Infrastructure/Repositories/Products/ProductVariantRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/Products/ProductVariantRepository.cs @@ -58,23 +58,21 @@ public class ProductVariantRepository(ApplicationDbContext context) : IProductVa .ToListAsync(cancellationToken); } - public async Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + public Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) { productVariant.Id = Guid.NewGuid(); productVariant.CreatedAt = DateTime.UtcNow; productVariant.ModifiedAt = DateTime.UtcNow; context.ProductVariants.Add(productVariant); - await context.SaveChangesAsync(cancellationToken); - return productVariant; + return Task.FromResult(productVariant); } - public async Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + public Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) { productVariant.ModifiedAt = DateTime.UtcNow; context.ProductVariants.Update(productVariant); - await context.SaveChangesAsync(cancellationToken); - return productVariant; + return Task.FromResult(productVariant); } public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) @@ -83,7 +81,6 @@ public class ProductVariantRepository(ApplicationDbContext context) : IProductVa if (productVariant != null) { context.ProductVariants.Remove(productVariant); - await context.SaveChangesAsync(cancellationToken); } } @@ -112,7 +109,6 @@ public class ProductVariantRepository(ApplicationDbContext context) : IProductVa { productVariant.StockQuantity = quantity; productVariant.ModifiedAt = DateTime.UtcNow; - await context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/Users/UserRoleRepository.cs b/src/Imprink.Infrastructure/Repositories/Users/UserRoleRepository.cs index 0fc1f79..e9b04fb 100644 --- a/src/Imprink.Infrastructure/Repositories/Users/UserRoleRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/Users/UserRoleRepository.cs @@ -1,5 +1,4 @@ using Imprink.Domain.Entities.Users; -using Imprink.Domain.Repositories; using Imprink.Domain.Repositories.Users; using Imprink.Infrastructure.Database; using Microsoft.EntityFrameworkCore; @@ -33,17 +32,15 @@ public class UserRoleRepository(ApplicationDbContext context) : IUserRoleReposit .FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken); } - public async Task AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) + public Task AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) { var ur = context.UserRole.Add(userRole); - await context.SaveChangesAsync(cancellationToken); - return ur.Entity; + return Task.FromResult(ur.Entity); } - public async Task RemoveUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) + public Task RemoveUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) { var ur = context.UserRole.Remove(userRole); - await context.SaveChangesAsync(cancellationToken); - return ur.Entity; + return Task.FromResult(ur.Entity); } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs b/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs index 65be479..33a28de 100644 --- a/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs +++ b/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs @@ -12,7 +12,6 @@ public class CategoriesController(IMediator mediator) : ControllerBase [HttpGet] public async Task>> GetCategories([FromQuery] GetCategoriesQuery query) { - var result = await mediator.Send(query); - return Ok(result); + return Ok(await mediator.Send(query)); } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs b/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs index 1bacafc..95950ba 100644 --- a/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs +++ b/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs @@ -13,7 +13,6 @@ public class ProductVariantsController(IMediator mediator) : ControllerBase public async Task>> GetProductVariants( [FromQuery] GetProductVariantsQuery query) { - var result = await mediator.Send(query); - return Ok(result); + return Ok(await mediator.Send(query)); } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs index 63eb0f6..6567082 100644 --- a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs +++ b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs @@ -17,7 +17,7 @@ public class ProductsController(IMediator mediator) : ControllerBase public async Task>> GetProducts( [FromQuery] ProductFilterParameters filterParameters) { - var result = await mediator.Send(new GetProductsQuery { FilterParameters = filterParameters }); + var result = await mediator.Send(new GetProductsQuery { FilterParameters = filterParameters}); return Ok(result); } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UserController.cs b/src/Imprink.WebApi/Controllers/Users/UserController.cs index 75fba37..d4cba8f 100644 --- a/src/Imprink.WebApi/Controllers/Users/UserController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UserController.cs @@ -27,7 +27,6 @@ public class UserController(IMediator mediator) : ControllerBase }; await mediator.Send(new SyncUserCommand(auth0User)); - return Ok("User Synced."); } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs index 34a2926..d6092b0 100644 --- a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs @@ -14,35 +14,21 @@ public class UserRoleController(IMediator mediator) : ControllerBase [HttpGet("me")] public async Task GetMyRoles() { - var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); - var sub = claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty; - - var myRoles = await mediator.Send(new GetUserRolesCommand(sub)); - - return Ok(myRoles); + var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + return Ok(await mediator.Send(new GetUserRolesCommand(sub))); } [Authorize(Roles = "Admin")] [HttpPost("set")] public async Task SetUserRole(SetUserRoleCommand command) { - var userRole = await mediator.Send(command); - - if (userRole == null) - return BadRequest(); - - return Ok(userRole); + return Ok(await mediator.Send(command)); } [Authorize(Roles = "Admin")] [HttpPost("unset")] public async Task UnsetUserRole(DeleteUserRoleCommand command) { - var userRole = await mediator.Send(command); - - if (userRole == null) - return BadRequest(); - - return Ok(userRole); + return Ok(await mediator.Send(command)); } } \ No newline at end of file From d4978dff15f5f536cbb2276aa137d8c2542f3320 Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:50:43 +0300 Subject: [PATCH 4/6] Fix efcore tracking issue --- .../Users/DeleteUserRoleHandler.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs index e30ef08..a5424ee 100644 --- a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs +++ b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs @@ -18,13 +18,12 @@ public class DeleteUserRoleHandler(IUnitOfWork uw) : IRequestHandler Date: Wed, 11 Jun 2025 00:01:50 +0300 Subject: [PATCH 5/6] Add CurrentUserService to access sub from context --- .../Service/ICurrentUserService.cs | 6 +++++ src/Imprink.Application/Users/Dtos/UserDto.cs | 23 +++++++++++++++++++ .../Users/SetUserPhoneHandler.cs | 11 +++++++++ src/Imprink.Domain/Entities/Users/User.cs | 1 - .../Imprink.Infrastructure.csproj | 1 + .../Services/CurrentUserService.cs | 14 +++++++++++ src/Imprink.WebApi/Startup.cs | 5 ++++ 7 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Imprink.Application/Service/ICurrentUserService.cs create mode 100644 src/Imprink.Application/Users/Dtos/UserDto.cs create mode 100644 src/Imprink.Application/Users/SetUserPhoneHandler.cs create mode 100644 src/Imprink.Infrastructure/Services/CurrentUserService.cs diff --git a/src/Imprink.Application/Service/ICurrentUserService.cs b/src/Imprink.Application/Service/ICurrentUserService.cs new file mode 100644 index 0000000..bd6ad1e --- /dev/null +++ b/src/Imprink.Application/Service/ICurrentUserService.cs @@ -0,0 +1,6 @@ +namespace Imprink.Application.Service; + +public interface ICurrentUserService +{ + string? GetCurrentUserId(); +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/Dtos/UserDto.cs b/src/Imprink.Application/Users/Dtos/UserDto.cs new file mode 100644 index 0000000..256629e --- /dev/null +++ b/src/Imprink.Application/Users/Dtos/UserDto.cs @@ -0,0 +1,23 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Entities.Users; + +namespace Imprink.Application.Users.Dtos; + +public class UserDto +{ + public string Id { get; set; } = null!; + public required string Name { get; set; } + public required string Nickname { get; set; } + public required string Email { get; set; } + public bool EmailVerified { get; set; } + public string? FullName { get; set; } + public string? PhoneNumber { get; set; } + public required bool IsActive { get; set; } + + public virtual ICollection
Addresses { get; set; } = new List
(); + public virtual ICollection UserRoles { get; set; } = new List(); + public virtual ICollection Orders { get; set; } = new List(); + + public Address? DefaultAddress => Addresses.FirstOrDefault(a => a is { IsDefault: true, IsActive: true }); + public IEnumerable Roles => UserRoles.Select(ur => ur.Role); +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/SetUserPhoneHandler.cs b/src/Imprink.Application/Users/SetUserPhoneHandler.cs new file mode 100644 index 0000000..5b2cfe3 --- /dev/null +++ b/src/Imprink.Application/Users/SetUserPhoneHandler.cs @@ -0,0 +1,11 @@ +using Imprink.Application.Users.Dtos; +using MediatR; + +namespace Imprink.Application.Users; + +public record SetUserPhoneCommand(string Sub, Guid RoleId) : IRequest; + +public class SetUserPhoneHandler +{ + +} \ No newline at end of file diff --git a/src/Imprink.Domain/Entities/Users/User.cs b/src/Imprink.Domain/Entities/Users/User.cs index 19d8ed1..d1a1a4d 100644 --- a/src/Imprink.Domain/Entities/Users/User.cs +++ b/src/Imprink.Domain/Entities/Users/User.cs @@ -8,7 +8,6 @@ public class User public required string Name { get; set; } public required string Nickname { get; set; } public required string Email { get; set; } - public bool EmailVerified { get; set; } public string? FullName { get; set; } public string? PhoneNumber { get; set; } diff --git a/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj index fc58356..8650de1 100644 --- a/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj +++ b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Imprink.Infrastructure/Services/CurrentUserService.cs b/src/Imprink.Infrastructure/Services/CurrentUserService.cs new file mode 100644 index 0000000..6579e54 --- /dev/null +++ b/src/Imprink.Infrastructure/Services/CurrentUserService.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; +using Imprink.Application.Service; +using Microsoft.AspNetCore.Http; + +namespace Imprink.Infrastructure.Services; + +public class CurrentUserService(IHttpContextAccessor httpContextAccessor) : ICurrentUserService +{ + public string? GetCurrentUserId() + { + return httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs index 60e1eae..3ea0697 100644 --- a/src/Imprink.WebApi/Startup.cs +++ b/src/Imprink.WebApi/Startup.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using FluentValidation; using Imprink.Application; using Imprink.Application.Products.Create; +using Imprink.Application.Service; using Imprink.Application.Validation.Models; using Imprink.Domain.Repositories; using Imprink.Domain.Repositories.Products; @@ -10,6 +11,7 @@ using Imprink.Infrastructure; using Imprink.Infrastructure.Database; using Imprink.Infrastructure.Repositories.Products; using Imprink.Infrastructure.Repositories.Users; +using Imprink.Infrastructure.Services; using Imprink.WebApi.Filters; using Imprink.WebApi.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -31,8 +33,11 @@ public static class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); + + services.AddHttpContextAccessor(); services.AddDbContext(options => options.UseSqlServer( From 922021d088d9ca9844c4ac55d5599f0618730ee3 Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Wed, 11 Jun 2025 02:12:49 +0300 Subject: [PATCH 6/6] Redo controllers --- .../Exceptions/DataUpdateException.cs | 7 ++ src/Imprink.Application/Users/Dtos/UserDto.cs | 3 +- .../Users/SetUserFullNameHandler.cs | 48 +++++++++++++ .../Users/SetUserPhoneHandler.cs | 43 +++++++++++- .../Users/SyncUserHandler.cs | 30 +++++--- src/Imprink.Domain/Entities/Users/User.cs | 4 +- .../Repositories/Users/IUserRepository.cs | 6 +- .../Configuration/Users/UserConfiguration.cs | 5 +- ...> 20250610225629_InitialSetup.Designer.cs} | 13 ++-- ...etup.cs => 20250610225629_InitialSetup.cs} | 4 +- .../ApplicationDbContextModelSnapshot.cs | 11 ++- .../Repositories/Users/UserRepository.cs | 43 +++++++++--- .../Controllers/Users/UserController.cs | 32 --------- .../Controllers/Users/UserRoleController.cs | 34 --------- .../Controllers/Users/UsersController.cs | 70 +++++++++++++++++++ 15 files changed, 245 insertions(+), 108 deletions(-) create mode 100644 src/Imprink.Application/Exceptions/DataUpdateException.cs create mode 100644 src/Imprink.Application/Users/SetUserFullNameHandler.cs rename src/Imprink.Infrastructure/Migrations/{20250609202250_InitialSetup.Designer.cs => 20250610225629_InitialSetup.Designer.cs} (99%) rename src/Imprink.Infrastructure/Migrations/{20250609202250_InitialSetup.cs => 20250610225629_InitialSetup.cs} (99%) delete mode 100644 src/Imprink.WebApi/Controllers/Users/UserController.cs delete mode 100644 src/Imprink.WebApi/Controllers/Users/UserRoleController.cs create mode 100644 src/Imprink.WebApi/Controllers/Users/UsersController.cs diff --git a/src/Imprink.Application/Exceptions/DataUpdateException.cs b/src/Imprink.Application/Exceptions/DataUpdateException.cs new file mode 100644 index 0000000..7b2f535 --- /dev/null +++ b/src/Imprink.Application/Exceptions/DataUpdateException.cs @@ -0,0 +1,7 @@ +namespace Imprink.Application.Exceptions; + +public class DataUpdateException : BaseApplicationException +{ + public DataUpdateException(string message) : base(message) { } + public DataUpdateException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/Dtos/UserDto.cs b/src/Imprink.Application/Users/Dtos/UserDto.cs index 256629e..9eb46f6 100644 --- a/src/Imprink.Application/Users/Dtos/UserDto.cs +++ b/src/Imprink.Application/Users/Dtos/UserDto.cs @@ -10,7 +10,8 @@ public class UserDto public required string Nickname { get; set; } public required string Email { get; set; } public bool EmailVerified { get; set; } - public string? FullName { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } public string? PhoneNumber { get; set; } public required bool IsActive { get; set; } diff --git a/src/Imprink.Application/Users/SetUserFullNameHandler.cs b/src/Imprink.Application/Users/SetUserFullNameHandler.cs new file mode 100644 index 0000000..68efb8a --- /dev/null +++ b/src/Imprink.Application/Users/SetUserFullNameHandler.cs @@ -0,0 +1,48 @@ +using Imprink.Application.Exceptions; +using Imprink.Application.Service; +using Imprink.Application.Users.Dtos; +using MediatR; + +namespace Imprink.Application.Users; + +public record SetUserFullNameCommand(string FirstName, string LastName) : IRequest; + +public class SetUserFullNameHandler(IUnitOfWork uw, ICurrentUserService userService) : IRequestHandler +{ + public async Task Handle(SetUserFullNameCommand request, CancellationToken cancellationToken) + { + await uw.BeginTransactionAsync(cancellationToken); + + try + { + var currentUser = userService.GetCurrentUserId(); + if (currentUser == null) + throw new NotFoundException("User token could not be accessed."); + + var user = await uw.UserRepository.SetUserFullNameAsync(currentUser, request.FirstName, request.LastName, cancellationToken); + if (user == null) + throw new DataUpdateException("User name could not be updated."); + + await uw.SaveAsync(cancellationToken); + await uw.CommitTransactionAsync(cancellationToken); + + return new UserDto + { + Id = user.Id, + Name = user.Name, + Nickname = user.Nickname, + Email = user.Email, + EmailVerified = user.EmailVerified, + FirstName = user.FirstName, + LastName = user.LastName, + PhoneNumber = user.PhoneNumber, + IsActive = user.IsActive + }; + } + catch + { + await uw.RollbackTransactionAsync(cancellationToken); + throw; + } + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/SetUserPhoneHandler.cs b/src/Imprink.Application/Users/SetUserPhoneHandler.cs index 5b2cfe3..47c064b 100644 --- a/src/Imprink.Application/Users/SetUserPhoneHandler.cs +++ b/src/Imprink.Application/Users/SetUserPhoneHandler.cs @@ -1,11 +1,48 @@ +using Imprink.Application.Exceptions; +using Imprink.Application.Service; using Imprink.Application.Users.Dtos; using MediatR; namespace Imprink.Application.Users; -public record SetUserPhoneCommand(string Sub, Guid RoleId) : IRequest; +public record SetUserPhoneCommand(string PhoneNumber) : IRequest; -public class SetUserPhoneHandler +public class SetUserPhoneHandler(IUnitOfWork uw, ICurrentUserService userService) : IRequestHandler { - + public async Task Handle(SetUserPhoneCommand request, CancellationToken cancellationToken) + { + await uw.BeginTransactionAsync(cancellationToken); + + try + { + var currentUser = userService.GetCurrentUserId(); + if (currentUser == null) + throw new NotFoundException("User token could not be accessed."); + + var user = await uw.UserRepository.SetUserPhoneAsync(currentUser, request.PhoneNumber, cancellationToken); + if (user == null) + throw new DataUpdateException("User phone could not be updated."); + + await uw.SaveAsync(cancellationToken); + await uw.CommitTransactionAsync(cancellationToken); + + return new UserDto + { + Id = user.Id, + Name = user.Name, + Nickname = user.Nickname, + Email = user.Email, + EmailVerified = user.EmailVerified, + FirstName = user.FirstName, + LastName = user.LastName, + PhoneNumber = user.PhoneNumber, + IsActive = user.IsActive + }; + } + catch + { + await uw.RollbackTransactionAsync(cancellationToken); + throw; + } + } } \ No newline at end of file diff --git a/src/Imprink.Application/Users/SyncUserHandler.cs b/src/Imprink.Application/Users/SyncUserHandler.cs index aadbf43..edcf817 100644 --- a/src/Imprink.Application/Users/SyncUserHandler.cs +++ b/src/Imprink.Application/Users/SyncUserHandler.cs @@ -1,26 +1,38 @@ +using Imprink.Application.Users.Dtos; using Imprink.Domain.Models; using MediatR; namespace Imprink.Application.Users; -public record SyncUserCommand(Auth0User User) : IRequest; +public record SyncUserCommand(Auth0User User) : IRequest; -public class SyncUserHandler(IUnitOfWork uw): IRequestHandler +public class SyncUserHandler(IUnitOfWork uw): IRequestHandler { - public async Task Handle(SyncUserCommand request, CancellationToken cancellationToken) + public async Task Handle(SyncUserCommand request, CancellationToken cancellationToken) { await uw.BeginTransactionAsync(cancellationToken); try { - if (!await uw.UserRepository.UpdateOrCreateUserAsync(request.User, cancellationToken)) - { - await uw.RollbackTransactionAsync(cancellationToken); - } - + var user = await uw.UserRepository.UpdateOrCreateUserAsync(request.User, cancellationToken); + + if (user == null) throw new Exception("User exists but could not be updated"); + await uw.SaveAsync(cancellationToken); await uw.CommitTransactionAsync(cancellationToken); - return true; + + return new UserDto + { + Id = user.Id, + Name = user.Name, + Nickname = user.Nickname, + Email = user.Email, + EmailVerified = user.EmailVerified, + FirstName = user.FirstName, + LastName = user.LastName, + PhoneNumber = user.PhoneNumber, + IsActive = user.IsActive + }; } catch { diff --git a/src/Imprink.Domain/Entities/Users/User.cs b/src/Imprink.Domain/Entities/Users/User.cs index d1a1a4d..f6d7713 100644 --- a/src/Imprink.Domain/Entities/Users/User.cs +++ b/src/Imprink.Domain/Entities/Users/User.cs @@ -9,7 +9,9 @@ public class User public required string Nickname { get; set; } public required string Email { get; set; } public bool EmailVerified { get; set; } - public string? FullName { get; set; } + + public string? FirstName { get; set; } = null!; + public string? LastName { get; set; } = null!; public string? PhoneNumber { get; set; } public required bool IsActive { get; set; } diff --git a/src/Imprink.Domain/Repositories/Users/IUserRepository.cs b/src/Imprink.Domain/Repositories/Users/IUserRepository.cs index 5809d58..ad16875 100644 --- a/src/Imprink.Domain/Repositories/Users/IUserRepository.cs +++ b/src/Imprink.Domain/Repositories/Users/IUserRepository.cs @@ -1,16 +1,18 @@ using Imprink.Domain.Entities.Users; using Imprink.Domain.Models; -namespace Imprink.Domain.Repositories; +namespace Imprink.Domain.Repositories.Users; public interface IUserRepository { - Task UpdateOrCreateUserAsync(Auth0User user, CancellationToken cancellationToken = default); + Task UpdateOrCreateUserAsync(Auth0User user, CancellationToken cancellationToken = default); Task GetUserByIdAsync(string userId, CancellationToken cancellationToken = default); Task GetUserByEmailAsync(string email, CancellationToken cancellationToken = default); Task> GetAllUsersAsync(CancellationToken cancellationToken = default); Task> GetActiveUsersAsync(CancellationToken cancellationToken = default); Task UserExistsAsync(string userId, CancellationToken cancellationToken = default); Task> GetUsersByRoleAsync(Guid roleId, CancellationToken cancellationToken = default); + Task SetUserPhoneAsync(string userId, string phoneNumber, CancellationToken cancellationToken = default); + Task SetUserFullNameAsync(string userId, string firstName, string lastName, CancellationToken cancellationToken = default); Task GetUserWithAllRelatedDataAsync(string userId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Configuration/Users/UserConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Users/UserConfiguration.cs index 8003def..4667aca 100644 --- a/src/Imprink.Infrastructure/Configuration/Users/UserConfiguration.cs +++ b/src/Imprink.Infrastructure/Configuration/Users/UserConfiguration.cs @@ -28,7 +28,10 @@ public class UserConfiguration : IEntityTypeConfiguration .IsRequired() .HasMaxLength(100); - builder.Property(u => u.FullName) + builder.Property(u => u.FirstName) + .HasMaxLength(100); + + builder.Property(u => u.LastName) .HasMaxLength(100); builder.Property(u => u.PhoneNumber) diff --git a/src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.Designer.cs b/src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.Designer.cs similarity index 99% rename from src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.Designer.cs rename to src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.Designer.cs index 8d719f1..4dfe290 100644 --- a/src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.Designer.cs +++ b/src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Imprink.Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250609202250_InitialSetup")] + [Migration("20250610225629_InitialSetup")] partial class InitialSetup { /// @@ -743,11 +743,6 @@ namespace Imprink.Infrastructure.Migrations b.ToTable("Roles"); b.HasData( - new - { - Id = new Guid("11111111-1111-1111-1111-111111111111"), - RoleName = "User" - }, new { Id = new Guid("22222222-2222-2222-2222-222222222222"), @@ -775,7 +770,7 @@ namespace Imprink.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("bit"); - b.Property("FullName") + b.Property("FirstName") .HasMaxLength(100) .HasColumnType("nvarchar(100)"); @@ -784,6 +779,10 @@ namespace Imprink.Infrastructure.Migrations .HasColumnType("bit") .HasDefaultValue(true); + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + b.Property("Name") .IsRequired() .HasMaxLength(100) diff --git a/src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.cs b/src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.cs similarity index 99% rename from src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.cs rename to src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.cs index f666300..30c801b 100644 --- a/src/Imprink.Infrastructure/Migrations/20250609202250_InitialSetup.cs +++ b/src/Imprink.Infrastructure/Migrations/20250610225629_InitialSetup.cs @@ -85,7 +85,8 @@ namespace Imprink.Infrastructure.Migrations Nickname = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), EmailVerified = table.Column(type: "bit", maxLength: 100, nullable: false), - FullName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + FirstName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + LastName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), PhoneNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), IsActive = table.Column(type: "bit", nullable: false, defaultValue: true) }, @@ -339,7 +340,6 @@ namespace Imprink.Infrastructure.Migrations columns: new[] { "Id", "RoleName" }, values: new object[,] { - { new Guid("11111111-1111-1111-1111-111111111111"), "User" }, { new Guid("22222222-2222-2222-2222-222222222222"), "Merchant" }, { new Guid("33333333-3333-3333-3333-333333333333"), "Admin" } }); diff --git a/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index ee202dc..1195e8b 100644 --- a/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -740,11 +740,6 @@ namespace Imprink.Infrastructure.Migrations b.ToTable("Roles"); b.HasData( - new - { - Id = new Guid("11111111-1111-1111-1111-111111111111"), - RoleName = "User" - }, new { Id = new Guid("22222222-2222-2222-2222-222222222222"), @@ -772,7 +767,7 @@ namespace Imprink.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("bit"); - b.Property("FullName") + b.Property("FirstName") .HasMaxLength(100) .HasColumnType("nvarchar(100)"); @@ -781,6 +776,10 @@ namespace Imprink.Infrastructure.Migrations .HasColumnType("bit") .HasDefaultValue(true); + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + b.Property("Name") .IsRequired() .HasMaxLength(100) diff --git a/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs b/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs index 93b4d9b..93194dd 100644 --- a/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs +++ b/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs @@ -1,6 +1,7 @@ using Imprink.Domain.Entities.Users; using Imprink.Domain.Models; using Imprink.Domain.Repositories; +using Imprink.Domain.Repositories.Users; using Imprink.Infrastructure.Database; using Microsoft.EntityFrameworkCore; @@ -8,7 +9,7 @@ namespace Imprink.Infrastructure.Repositories.Users; public class UserRepository(ApplicationDbContext context) : IUserRepository { - public async Task UpdateOrCreateUserAsync(Auth0User user, CancellationToken cancellationToken = default) + public async Task UpdateOrCreateUserAsync(Auth0User user, CancellationToken cancellationToken = default) { var userToUpdate = await context.Users .Where(u => u.Id.Equals(user.Sub)) @@ -27,16 +28,15 @@ public class UserRepository(ApplicationDbContext context) : IUserRepository }; context.Users.Add(newUser); + return newUser; } - else - { - userToUpdate.Email = user.Email; - userToUpdate.Name = user.Name; - userToUpdate.Nickname = user.Nickname; - userToUpdate.EmailVerified = user.EmailVerified; - } - - return true; + + userToUpdate.Email = user.Email; + userToUpdate.Name = user.Name; + userToUpdate.Nickname = user.Nickname; + userToUpdate.EmailVerified = user.EmailVerified; + + return userToUpdate; } public async Task GetUserByIdAsync(string userId, CancellationToken cancellationToken = default) @@ -92,4 +92,27 @@ public class UserRepository(ApplicationDbContext context) : IUserRepository .Include(u => u.Orders) .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); } + + public async Task SetUserPhoneAsync(string userId, string phoneNumber, CancellationToken cancellationToken = default) + { + var user = await context.Users + .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + + if (user == null) return null; + + user.PhoneNumber = phoneNumber; + return user; + } + + public async Task SetUserFullNameAsync(string userId, string firstName, string lastName, CancellationToken cancellationToken = default) + { + var user = await context.Users + .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + + if (user == null) return null; + + user.FirstName = firstName; + user.LastName = lastName; + return user; + } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UserController.cs b/src/Imprink.WebApi/Controllers/Users/UserController.cs deleted file mode 100644 index d4cba8f..0000000 --- a/src/Imprink.WebApi/Controllers/Users/UserController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Security.Claims; -using Imprink.Application.Users; -using Imprink.Domain.Models; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Imprink.WebApi.Controllers.Users; - -[ApiController] -[Route("/api/users")] -public class UserController(IMediator mediator) : ControllerBase -{ - [Authorize] - [HttpPost("sync")] - public async Task Sync() - { - var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); - - var auth0User = new Auth0User - { - Sub = claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty, - Name = claims.FirstOrDefault(c => c.Type == "name")?.Value ?? string.Empty, - Nickname = claims.FirstOrDefault(c => c.Type == "nickname")?.Value ?? string.Empty, - Email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? string.Empty, - EmailVerified = bool.TryParse(claims.FirstOrDefault(c => c.Type == "email_verified")?.Value, out var emailVerified) && emailVerified - }; - - await mediator.Send(new SyncUserCommand(auth0User)); - return Ok("User Synced."); - } -} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs deleted file mode 100644 index d6092b0..0000000 --- a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Security.Claims; -using Imprink.Application.Users; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Imprink.WebApi.Controllers.Users; - -[ApiController] -[Route("/api/users/roles")] -public class UserRoleController(IMediator mediator) : ControllerBase -{ - [Authorize] - [HttpGet("me")] - public async Task GetMyRoles() - { - var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty; - return Ok(await mediator.Send(new GetUserRolesCommand(sub))); - } - - [Authorize(Roles = "Admin")] - [HttpPost("set")] - public async Task SetUserRole(SetUserRoleCommand command) - { - return Ok(await mediator.Send(command)); - } - - [Authorize(Roles = "Admin")] - [HttpPost("unset")] - public async Task UnsetUserRole(DeleteUserRoleCommand command) - { - return Ok(await mediator.Send(command)); - } -} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UsersController.cs b/src/Imprink.WebApi/Controllers/Users/UsersController.cs new file mode 100644 index 0000000..606f313 --- /dev/null +++ b/src/Imprink.WebApi/Controllers/Users/UsersController.cs @@ -0,0 +1,70 @@ +using System.Security.Claims; +using Imprink.Application.Users; +using Imprink.Domain.Models; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Imprink.WebApi.Controllers.Users; + +[ApiController] +[Route("/api/users")] +public class UsersController(IMediator mediator) : ControllerBase +{ + [Authorize] + [HttpPost("me/sync")] + public async Task SyncMyProfile() + { + var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); + + var auth0User = new Auth0User + { + Sub = claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty, + Name = claims.FirstOrDefault(c => c.Type == "name")?.Value ?? string.Empty, + Nickname = claims.FirstOrDefault(c => c.Type == "nickname")?.Value ?? string.Empty, + Email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? string.Empty, + EmailVerified = bool.TryParse(claims.FirstOrDefault(c => c.Type == "email_verified")?.Value, out var emailVerified) && emailVerified + }; + + await mediator.Send(new SyncUserCommand(auth0User)); + return Ok("User profile synchronized."); + } + + [Authorize] + [HttpGet("me/roles")] + public async Task GetMyRoles() + { + var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + return Ok(await mediator.Send(new GetUserRolesCommand(sub))); + } + + [Authorize] + [HttpPut("me/phone")] + public async Task UpdateMyPhone([FromBody] SetUserPhoneCommand command) + { + return Ok(await mediator.Send(command)); + } + + [Authorize] + [HttpPut("me/fullname")] + public async Task UpdateMyFullName([FromBody] SetUserFullNameCommand command) + { + return Ok(await mediator.Send(command)); + } + + [Authorize(Roles = "Admin")] + [HttpPut("{userId}/roles/{roleId:guid}")] + public async Task AddUserRole(string userId, Guid roleId) + { + var command = new SetUserRoleCommand(userId, roleId); + return Ok(await mediator.Send(command)); + } + + [Authorize(Roles = "Admin")] + [HttpDelete("{userId}/roles/{roleId:guid}")] + public async Task RemoveUserRole(string userId, Guid roleId) + { + var command = new DeleteUserRoleCommand(userId, roleId); + return Ok(await mediator.Send(command)); + } +} \ No newline at end of file