From 3f0acc782061e200a4f70f65e5d8ad2ac789a49e Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 01:27:08 +0300 Subject: [PATCH 1/9] Enhance services with IHttpContextAccessor and BaseService Updated AssetService, OrderService, PostService, ReviewService, and UserService to include IHttpContextAccessor for user identity access. Introduced BaseService for common functionality, requiring implementation of GetOwnerIdAsync for resource authorization. Improved method calls for authorization checks, enhanced error handling with NotFoundException, and refined code formatting for better readability. --- .../Services/AssetService.cs | 15 ++- Dentizone.Application/Services/BaseService.cs | 67 ++++++++++ .../Services/OrderService.cs | 15 ++- Dentizone.Application/Services/PostService.cs | 118 ++++++++++-------- .../Services/ReviewService.cs | 18 ++- Dentizone.Application/Services/UserService.cs | 3 +- 6 files changed, 180 insertions(+), 56 deletions(-) create mode 100644 Dentizone.Application/Services/BaseService.cs diff --git a/Dentizone.Application/Services/AssetService.cs b/Dentizone.Application/Services/AssetService.cs index 0d0b390..824b928 100644 --- a/Dentizone.Application/Services/AssetService.cs +++ b/Dentizone.Application/Services/AssetService.cs @@ -4,10 +4,12 @@ using Dentizone.Domain.Entity; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; +using Microsoft.AspNetCore.Http; namespace Dentizone.Application.Services { - public class AssetService(IAssetRepository assetRepository, IMapper mapper) : IAssetService + public class AssetService(IAssetRepository assetRepository, IMapper mapper, IHttpContextAccessor contextAccessor) + : BaseService(contextAccessor), IAssetService { public async Task CreateAssetAsync(CreateAssetDto assetDto) { @@ -39,5 +41,16 @@ public async Task DeleteAssetAsync(string assetId) { await assetRepository.DeleteByIdAsync(assetId); } + + protected override async Task GetOwnerIdAsync(string resourceId) + { + var asset = await assetRepository.GetByIdAsync(resourceId); + if (asset == null) + { + throw new NotFoundException($"Asset with id {resourceId} not found"); + } + + return asset.UserId; + } } } \ No newline at end of file diff --git a/Dentizone.Application/Services/BaseService.cs b/Dentizone.Application/Services/BaseService.cs new file mode 100644 index 0000000..a6d64ff --- /dev/null +++ b/Dentizone.Application/Services/BaseService.cs @@ -0,0 +1,67 @@ +// Dentizone.Application/Services/BaseService.cs + +using System.Security.Claims; +using Dentizone.Domain.Enums; +using Dentizone.Domain.Exceptions; +using Microsoft.AspNetCore.Http; + +public abstract class BaseService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + protected BaseService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Checks if the current user has the ADMIN role. + /// + protected bool IsAdmin() + { + var userRole = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role); + return userRole == UserRoles.ADMIN.ToString(); + } + + /// + /// Ensures the current user is either an Admin or the owner of the specified resource. + /// Throws UnauthorizedAccessException if the check fails. + /// + /// The unique identifier of the resource to check. + protected async Task AuthorizeAdminOrOwnerAsync(string resourceId) + { + // Admins are always authorized. + if (IsAdmin()) + { + return; + } + + var currentUserId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(currentUserId)) + { + throw new UnauthorizedAccessException("Cannot verify user. No user is authenticated."); + } + + // Get the owner ID from the concrete service implementation. + var ownerId = await GetOwnerIdAsync(resourceId); + + if (string.IsNullOrEmpty(ownerId)) + { + throw new NotFoundException("Could not determine the owner of the resource."); + } + + // If the user is not the owner, they are not authorized. + if (ownerId != currentUserId) + { + throw new UnauthorizedAccessException( + "You do not have permission to perform this action on this resource."); + } + } + + /// + /// When implemented in a derived class, this method retrieves the owner's ID for a given resource. + /// + /// The ID of the resource. + /// A Task that represents the asynchronous operation, containing the owner's user ID. + protected abstract Task GetOwnerIdAsync(string resourceId); +} \ No newline at end of file diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index 94f266f..afa4d84 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -13,6 +13,7 @@ using Dentizone.Domain.Interfaces.Repositories; using Dentizone.Infrastructure; using System.Linq.Expressions; +using Microsoft.AspNetCore.Http; namespace Dentizone.Application.Services { @@ -27,8 +28,9 @@ internal class OrderService( IAuthService authService, IPaymentService paymentService, ICartService cartService, + IHttpContextAccessor accessor, AppDbContext dbContext) - : IOrderService + : BaseService(accessor), IOrderService { public async Task CancelOrderAsync(string orderId, string userId) { @@ -277,5 +279,16 @@ public async Task> GetReviewedOrdersByUserId(string userId) ); return orders.Items; } + + protected override async Task GetOwnerIdAsync(string resourceId) + { + var order = await orderRepository.GetByIdAsync(resourceId); + if (order == null) + { + throw new NotFoundException($"Order with id {resourceId} not found"); + } + + return order.BuyerId; + } } } \ No newline at end of file diff --git a/Dentizone.Application/Services/PostService.cs b/Dentizone.Application/Services/PostService.cs index 7c54a76..cdff211 100644 --- a/Dentizone.Application/Services/PostService.cs +++ b/Dentizone.Application/Services/PostService.cs @@ -10,12 +10,14 @@ using Dentizone.Domain.Interfaces.Repositories; using Dentizone.Infrastructure; using Dentizone.Infrastructure.Cache; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; namespace Dentizone.Application.Services { public class PostService( + IHttpContextAccessor accessor, IMapper mapper, IPostRepository repo, IPostAssetRepository postAssetRepository, @@ -24,7 +26,7 @@ public class PostService( IAssetService assetService, AppDbContext dbContext, IRedisService redisService) - : IPostService + : BaseService(accessor), IPostService { public async Task> ValidatePosts(List postIds) { @@ -46,10 +48,10 @@ public async Task> ValidatePosts(List postIds) private async Task ValidateAssetNotUsed(string assetId, string? postIdToExclude = null) { var isExist = await postAssetRepository.FindBy(p => - !p.IsDeleted && - p.AssetId == assetId && - (postIdToExclude == null || p.PostId != postIdToExclude) - ); + !p.IsDeleted && + p.AssetId == assetId && + (postIdToExclude == null || p.PostId != postIdToExclude) + ); if (isExist != null) throw new BadActionException("This photo is already used before"); @@ -74,7 +76,7 @@ private async Task ValidateCategoryAndSubCategory(string categoryId, string subC } private async Task AssociatePostWithAsset(string postId, string assetId, - string? postIdToExclude = null) + string? postIdToExclude = null) { var asset = await assetService.GetAssetByIdAsync(assetId); if (asset == null) @@ -83,10 +85,10 @@ private async Task AssociatePostWithAsset(string postId, string asset await ValidateAssetNotUsed(assetId, postIdToExclude); var postAsset = new PostAsset - { - PostId = postId, - AssetId = assetId - }; + { + PostId = postId, + AssetId = assetId + }; await postAssetRepository.CreateAsync(postAsset); return postAsset; @@ -222,21 +224,21 @@ public async Task GetSidebarFilterAsync() } var availablePosts = repo.GetAllAsync(p => !p.IsDeleted && p.Status == PostStatus.Active, - p => p.CreatedAt, includes: - [ - p => p.Category, - p => p.SubCategory, - ]); + p => p.CreatedAt, includes: + [ + p => p.Category, + p => p.SubCategory, + ]); var cities = availablePosts - .Select(p => p.City) - .Distinct() - .OrderBy(c => c) - .ToList(); + .Select(p => p.City) + .Distinct() + .OrderBy(c => c) + .ToList(); var prices = availablePosts - .Select(p => p.Price) - .ToList(); + .Select(p => p.Price) + .ToList(); decimal minPrice = 0; decimal maxPrice = 0; @@ -247,25 +249,25 @@ public async Task GetSidebarFilterAsync() } var categories = availablePosts - .GroupBy(p => p.Category.Name) - .Select(g => new CategoryFilterDto - { - Id = g.First().Category.Id, - CategoryName = g.Key, - Subcategories = g.Select(p => p.SubCategory.Name) - .Distinct() - .OrderBy(s => s).ToList() - }) - .OrderBy(c => c.CategoryName) - .ToList(); + .GroupBy(p => p.Category.Name) + .Select(g => new CategoryFilterDto + { + Id = g.First().Category.Id, + CategoryName = g.Key, + Subcategories = g.Select(p => p.SubCategory.Name) + .Distinct() + .OrderBy(s => s).ToList() + }) + .OrderBy(c => c.CategoryName) + .ToList(); var sidebarFilterResults = new SidebarFilterDto - { - Cities = cities, - MinPrice = minPrice, - MaxPrice = maxPrice, - Categories = categories - }; + { + Cities = cities, + MinPrice = minPrice, + MaxPrice = maxPrice, + Categories = categories + }; // if the sidebarFilterResults is null, we will not cache it @@ -283,7 +285,7 @@ public async Task GetSidebarFilterAsync() public async Task> Search(UserPreferenceDto userPreferenceDto) { var cacheKey = CacheHelper.GenerateCacheKeyHash("SearchPosts", - userPreferenceDto); + userPreferenceDto); var cachedValue = await redisService.GetValue(cacheKey); if (!string.IsNullOrEmpty(cachedValue)) { @@ -295,21 +297,21 @@ public async Task> Search(UserPreferenceDto userPreferenceDto) } var postsQuery = await repo.SearchAsync( - userPreferenceDto.Keyword, userPreferenceDto.City, - userPreferenceDto.Category, userPreferenceDto.SubCategory, - userPreferenceDto.Condition, userPreferenceDto.MinPrice, - userPreferenceDto.MaxPrice, - userPreferenceDto.SortBy, userPreferenceDto.SortDirection, - userPreferenceDto.PageNumber - ); + userPreferenceDto.Keyword, userPreferenceDto.City, + userPreferenceDto.Category, userPreferenceDto.SubCategory, + userPreferenceDto.Condition, userPreferenceDto.MinPrice, + userPreferenceDto.MaxPrice, + userPreferenceDto.SortBy, userPreferenceDto.SortDirection, + userPreferenceDto.PageNumber + ); var postsWithIncludes = await postsQuery - .Include(p => p.PostAssets).ThenInclude(pa => pa.Asset) - .Include(p => p.Seller) - .ThenInclude(p => p.University) - .Include(p => p.Category) - .Include(p => p.SubCategory) - .ToListAsync(); + .Include(p => p.PostAssets).ThenInclude(pa => pa.Asset) + .Include(p => p.Seller) + .ThenInclude(p => p.University) + .Include(p => p.Category) + .Include(p => p.SubCategory) + .ToListAsync(); var mappedPosts = mapper.Map>(postsWithIncludes); @@ -317,5 +319,17 @@ public async Task> Search(UserPreferenceDto userPreferenceDto) await redisService.SetValue(cacheKey, JsonConvert.SerializeObject(mappedPosts), TimeSpan.FromMinutes(1)); return mappedPosts; } + + protected override async Task GetOwnerIdAsync(string resourceId) + { + var post = await repo.GetByIdAsync(resourceId); + + if (post == null) + { + throw new NotFoundException($"Post with id {resourceId} not found"); + } + + return post.SellerId; + } } } \ No newline at end of file diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 7011eeb..334b888 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -5,11 +5,16 @@ using Dentizone.Domain.Entity; using Dentizone.Domain.Interfaces.Repositories; using Dentizone.Domain.Exceptions; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; namespace Dentizone.Application.Services { - public class ReviewService(IMapper mapper, IReviewRepository repo, IOrderService orderService) : IReviewService + public class ReviewService( + IHttpContextAccessor accessor, + IMapper mapper, + IReviewRepository repo, + IOrderService orderService) : BaseService(accessor), IReviewService { public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto) { @@ -73,5 +78,16 @@ public async Task> GetReceivedReviews(string userId) return reviewDtos.ToList(); } + + protected override async Task GetOwnerIdAsync(string resourceId) + { + var review = await repo.GetByIdAsync(resourceId); + if (review == null) + { + throw new NotFoundException($"Review with id {resourceId} not found"); + } + + return review.UserId; + } } } \ No newline at end of file diff --git a/Dentizone.Application/Services/UserService.cs b/Dentizone.Application/Services/UserService.cs index 75f31cc..4e1090c 100644 --- a/Dentizone.Application/Services/UserService.cs +++ b/Dentizone.Application/Services/UserService.cs @@ -7,6 +7,7 @@ using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; using System.Linq.Expressions; +using Dentizone.Infrastructure; namespace Dentizone.Application.Services { @@ -14,7 +15,7 @@ public class UserService( IUserRepository userRepository, IMapper mapper, IWalletService walletService, - Infrastructure.AppDbContext dbContext) + AppDbContext dbContext) : IUserService { public async Task CreateAsync(CreateAppUser userDto) From b89333d5bebe4b92fd90f1f8b06bc6744a5c31c3 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 01:46:03 +0300 Subject: [PATCH 2/9] Refactor services for authorization and code clarity - Updated `IUploadService` to change `DeleteAssetById` return type to `Task`. - Added authorization checks in `AssetService`, `OrderService`, `PostService`, and `ReviewService` for admin or owner actions. - Improved variable naming and transaction handling in `PaymentService`. - Introduced a required `Email` property in `AppUser`. - Enhanced formatting in `IdentityConfiguration` for consistency. - Made `Email` property required in `UserConfiguration` with constraints. --- .../Interfaces/IUploadService.cs | 2 +- .../Services/AssetService.cs | 4 +++ .../Services/OrderService.cs | 5 +--- .../Services/Payment/PaymentService.cs | 22 ++++++++------- Dentizone.Application/Services/PostService.cs | 1 + .../Services/ReviewService.cs | 12 ++++----- .../Services/UploadService.cs | 27 ++++++++++--------- Dentizone.Domain/Entity/AppUser.cs | 1 + .../Identity/IdentityConfiguration.cs | 17 ++++++------ .../Configurations/UserConfiguration.cs | 5 +++- 10 files changed, 52 insertions(+), 44 deletions(-) diff --git a/Dentizone.Application/Interfaces/IUploadService.cs b/Dentizone.Application/Interfaces/IUploadService.cs index 17d8646..1b25a55 100644 --- a/Dentizone.Application/Interfaces/IUploadService.cs +++ b/Dentizone.Application/Interfaces/IUploadService.cs @@ -8,6 +8,6 @@ public interface IUploadService public Task UploadImageAsync(IFormFile file, string userId); public Task FindAssetById(string id); - public Task DeleteAssetById(string id, string userId); + public Task DeleteAssetById(string id, string userId); } } \ No newline at end of file diff --git a/Dentizone.Application/Services/AssetService.cs b/Dentizone.Application/Services/AssetService.cs index 824b928..1bb1785 100644 --- a/Dentizone.Application/Services/AssetService.cs +++ b/Dentizone.Application/Services/AssetService.cs @@ -32,6 +32,9 @@ public async Task UpdateAssetAsync(string id, UpdateAssetDto assetDto) { var existingAsset = await assetRepository.GetByIdAsync(id) ?? throw new NotFoundException($"Asset with id {id} not found"); + + await AuthorizeAdminOrOwnerAsync(existingAsset.Id); + var u = mapper.Map(assetDto, existingAsset); var updatedAsset = await assetRepository.UpdateAsync(u); return mapper.Map(updatedAsset); @@ -39,6 +42,7 @@ public async Task UpdateAssetAsync(string id, UpdateAssetDto assetDto) public async Task DeleteAssetAsync(string assetId) { + await AuthorizeAdminOrOwnerAsync(assetId); await assetRepository.DeleteByIdAsync(assetId); } diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index afa4d84..ef07952 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -42,10 +42,7 @@ internal class OrderService( throw new NotFoundException("Order not found."); } - if (order.BuyerId != userId) - { - throw new UnauthorizedAccessException("You are not allowed to cancel this order."); - } + await AuthorizeAdminOrOwnerAsync(orderId); if (order.OrderStatuses.Any(os => os.Status == OrderStatues.Cancelled)) { diff --git a/Dentizone.Application/Services/Payment/PaymentService.cs b/Dentizone.Application/Services/Payment/PaymentService.cs index 7eaf0a9..db1c93c 100644 --- a/Dentizone.Application/Services/Payment/PaymentService.cs +++ b/Dentizone.Application/Services/Payment/PaymentService.cs @@ -87,7 +87,7 @@ public async Task CreateSaleTransaction(string paymentId, string walletId, decim public async Task ConfirmPaymentAsync(string orderId) { - await using var DatabaseTransaction = await db.Database.BeginTransactionAsync(); + await using var databaseTransaction = await db.Database.BeginTransactionAsync(); try { @@ -95,7 +95,7 @@ public async Task ConfirmPaymentAsync(string orderId) var payment = await repo.FindBy(p => p.OrderId == orderId && p.Status == PaymentStatus.Pending, - includes: [p => p.SalesTransactions]); + includes: [p => p.SalesTransactions]); // 3. Update Payment Status to Confirmed if (payment == null) { @@ -109,7 +109,8 @@ public async Task ConfirmPaymentAsync(string orderId) if (transaction.Status != SaleStatus.Pending) { throw new - InvalidOperationException($"Sale transaction of id {transaction.Id} is not in pending status."); + InvalidOperationException( + $"Sale transaction of id {transaction.Id} is not in pending status."); } transaction.Status = SaleStatus.Completed; @@ -128,14 +129,14 @@ public async Task ConfirmPaymentAsync(string orderId) } // Commit the transaction - await DatabaseTransaction.CommitAsync(); + await databaseTransaction.CommitAsync(); return mapper.Map(updatedPayment); } catch (Exception) { // Rollback the transaction in case of an error - await DatabaseTransaction.RollbackAsync(); + await databaseTransaction.RollbackAsync(); throw; } @@ -143,12 +144,12 @@ public async Task ConfirmPaymentAsync(string orderId) public async Task CancelPaymentByOrderId(string orderId) { - await using var DatabaseTransaction = await db.Database.BeginTransactionAsync(); + await using var databaseTransaction = await db.Database.BeginTransactionAsync(); try { // Find the payment by order ID var payment = await repo.FindBy(p => p.OrderId == orderId && p.Status == PaymentStatus.Pending, - includes: [p => p.SalesTransactions]); + includes: [p => p.SalesTransactions]); if (payment == null) { throw new NotFoundException("Payment not found."); @@ -168,7 +169,8 @@ public async Task CancelPaymentByOrderId(string orderId) if (transaction.Status != SaleStatus.Pending) { throw new - InvalidOperationException($"Sale transaction of id {transaction.Id} is not in pending status."); + InvalidOperationException( + $"Sale transaction of id {transaction.Id} is not in pending status."); } transaction.Status = SaleStatus.Failed; @@ -177,12 +179,12 @@ public async Task CancelPaymentByOrderId(string orderId) // Commit the transaction - await DatabaseTransaction.CommitAsync(); + await databaseTransaction.CommitAsync(); } catch (Exception) { // Rollback the transaction in case of an error - await DatabaseTransaction.RollbackAsync(); + await databaseTransaction.RollbackAsync(); throw; } } diff --git a/Dentizone.Application/Services/PostService.cs b/Dentizone.Application/Services/PostService.cs index cdff211..41aaa3e 100644 --- a/Dentizone.Application/Services/PostService.cs +++ b/Dentizone.Application/Services/PostService.cs @@ -182,6 +182,7 @@ public async Task> GetPostsBySellerId(string sellerId, int pag public async Task UpdatePost(string postId, UpdatePostDto updatePostDto) { var existingPost = await repo.GetByIdAsync(postId); + await AuthorizeAdminOrOwnerAsync(postId); var post = mapper.Map(updatePostDto, existingPost); diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 334b888..5da2078 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -30,10 +30,7 @@ public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createRe public async Task DeleteReviewAsync(string reviewId) { - var review = await repo.GetByIdAsync(reviewId); - if (review == null || review.IsDeleted) - throw new NotFoundException("Review not found."); - + await AuthorizeAdminOrOwnerAsync(reviewId); await repo.DeleteAsync(reviewId); return true; } @@ -48,9 +45,10 @@ public async Task> GetSubmittedReviews(string userId) public async Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto) { - var review = await repo.GetByIdAsync(reviewId); - if (review == null || review.IsDeleted) - throw new NotFoundException("Review not found."); + await AuthorizeAdminOrOwnerAsync(reviewId); + var review = await repo.FindBy(r => !r.IsDeleted) ?? + throw new NotFoundException("Review with Provided id is not found"); + review.Text = updateReviewDto.Comment; diff --git a/Dentizone.Application/Services/UploadService.cs b/Dentizone.Application/Services/UploadService.cs index fabca71..8601500 100644 --- a/Dentizone.Application/Services/UploadService.cs +++ b/Dentizone.Application/Services/UploadService.cs @@ -8,8 +8,11 @@ namespace Dentizone.Application.Services { - public class UploadService(ICloudinaryService cloudinaryService, IAssetService assetService) - : IUploadService + public class UploadService( + ICloudinaryService cloudinaryService, + IAssetService assetService, + IHttpContextAccessor accessor) + : BaseService(accessor), IUploadService { public async Task FindAssetById(string id) { @@ -32,22 +35,22 @@ public async Task UploadImageAsync(IFormFile file, string userId) return image; } - public async Task DeleteAssetById(string id, string userId) + public async Task DeleteAssetById(string id, string userId) { - var asset = await assetService.GetAssetByIdAsync(id); + await AuthorizeAdminOrOwnerAsync(id); - if (asset == null) - { - throw new NotFoundException("Asset not found"); - } + await assetService.DeleteAssetAsync(id); + } - if (asset.UserId != userId) + protected override async Task GetOwnerIdAsync(string resourceId) + { + var asset = await assetService.GetAssetByIdAsync(resourceId); + if (asset == null) { - throw new UnauthorizedAccessException("You are not authorized to delete this asset"); + throw new NotFoundException($"Asset with id {resourceId} not found"); } - await assetService.DeleteAssetAsync(asset.Id); - return asset; + return asset.UserId; } } } \ No newline at end of file diff --git a/Dentizone.Domain/Entity/AppUser.cs b/Dentizone.Domain/Entity/AppUser.cs index 8bdb9d2..ad9bdd9 100644 --- a/Dentizone.Domain/Entity/AppUser.cs +++ b/Dentizone.Domain/Entity/AppUser.cs @@ -8,6 +8,7 @@ public class AppUser : IBaseEntity, IUpdatable, IDeletable public string FullName { get; set; } public string Username { get; set; } public int AcademicYear { get; set; } + public required string Email { get; set; } public long? NationalId { get; set; } public KycStatus KycStatus { get; set; } public UserState Status { get; set; } diff --git a/Dentizone.Infrastructure/Identity/IdentityConfiguration.cs b/Dentizone.Infrastructure/Identity/IdentityConfiguration.cs index c35cc99..4284b58 100644 --- a/Dentizone.Infrastructure/Identity/IdentityConfiguration.cs +++ b/Dentizone.Infrastructure/Identity/IdentityConfiguration.cs @@ -26,8 +26,7 @@ public static class IdentityConfiguration public static LockoutOptions LockoutOptions { get; } = new() { AllowedForNewUsers = true, - DefaultLockoutTimeSpan = - TimeSpan.FromMinutes(15), + DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15), MaxFailedAccessAttempts = 3, }; @@ -44,10 +43,10 @@ public static TokenValidationParameters GetTokenValidationParameters(ISecretServ ValidIssuer = secretService.GetSecret("JwtIssuer"), ValidAudience = secretService.GetSecret("JwtAudience"), IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8 - .GetBytes(secretService - .GetSecret("JwtSecret")) - ), + Encoding.UTF8 + .GetBytes(secretService + .GetSecret("JwtSecret")) + ), }; } @@ -62,9 +61,9 @@ public static TokenValidationParameters GetTokenValidationParameters(string secr ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8 - .GetBytes(secret) - ), + Encoding.UTF8 + .GetBytes(secret) + ), }; } } diff --git a/Dentizone.Infrastructure/Persistence/Configurations/UserConfiguration.cs b/Dentizone.Infrastructure/Persistence/Configurations/UserConfiguration.cs index 6937b08..4c46822 100644 --- a/Dentizone.Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ b/Dentizone.Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -17,7 +17,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(u => u.UniversityId) .IsRequired(); - + builder.Property(u => u.Email) + .IsRequired() + .HasMaxLength(255) + .IsUnicode(false); builder.Property(u => u.NationalId); From f5e5690ffafb28b06819c22f5d57c76b3ce3a507 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 01:49:08 +0300 Subject: [PATCH 3/9] Add namespace declaration in BaseService.cs This commit introduces a new namespace declaration `namespace Dentizone.Application.Services;` in the `BaseService.cs` file. This change enhances code organization and adheres to best practices for structuring C# applications. --- Dentizone.Application/Services/BaseService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dentizone.Application/Services/BaseService.cs b/Dentizone.Application/Services/BaseService.cs index a6d64ff..5a3118b 100644 --- a/Dentizone.Application/Services/BaseService.cs +++ b/Dentizone.Application/Services/BaseService.cs @@ -5,6 +5,8 @@ using Dentizone.Domain.Exceptions; using Microsoft.AspNetCore.Http; +namespace Dentizone.Application.Services; + public abstract class BaseService { private readonly IHttpContextAccessor _httpContextAccessor; From ec1fcb8ee2d24310c5eaacfdea756e3228271f4c Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 01:50:26 +0300 Subject: [PATCH 4/9] Refactor asset deletion methods and review lookup - Updated `DeleteAssetById` in `IUploadService.cs` and `UploadService.cs` to remove the `userId` parameter, simplifying the method signature. - Modified review retrieval logic in `ReviewService.cs` to specifically search for a review by its `Id`, ensuring accurate results and preventing potential issues with multiple matches. --- Dentizone.Application/Interfaces/IUploadService.cs | 2 +- Dentizone.Application/Services/ReviewService.cs | 2 +- Dentizone.Application/Services/UploadService.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dentizone.Application/Interfaces/IUploadService.cs b/Dentizone.Application/Interfaces/IUploadService.cs index 1b25a55..b0c4fb6 100644 --- a/Dentizone.Application/Interfaces/IUploadService.cs +++ b/Dentizone.Application/Interfaces/IUploadService.cs @@ -8,6 +8,6 @@ public interface IUploadService public Task UploadImageAsync(IFormFile file, string userId); public Task FindAssetById(string id); - public Task DeleteAssetById(string id, string userId); + Task DeleteAssetById(string id); } } \ No newline at end of file diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 5da2078..d04e1f8 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -46,7 +46,7 @@ public async Task> GetSubmittedReviews(string userId) public async Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto) { await AuthorizeAdminOrOwnerAsync(reviewId); - var review = await repo.FindBy(r => !r.IsDeleted) ?? + var review = await repo.FindBy(r => r.Id == reviewId && !r.IsDeleted) ?? throw new NotFoundException("Review with Provided id is not found"); diff --git a/Dentizone.Application/Services/UploadService.cs b/Dentizone.Application/Services/UploadService.cs index 8601500..1f0e1db 100644 --- a/Dentizone.Application/Services/UploadService.cs +++ b/Dentizone.Application/Services/UploadService.cs @@ -35,7 +35,7 @@ public async Task UploadImageAsync(IFormFile file, string userId) return image; } - public async Task DeleteAssetById(string id, string userId) + public async Task DeleteAssetById(string id) { await AuthorizeAdminOrOwnerAsync(id); From d5f0edf48be0223844b329e27b946daa9476a540 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 01:51:56 +0300 Subject: [PATCH 5/9] Refactor CancelOrderAsync to remove userId parameter Updated IOrderService and OrderService to eliminate the userId parameter from the CancelOrderAsync method. Adjusted OrderController to reflect this change, ensuring the method is called without the userId. --- Dentizone.Application/Interfaces/Order/IOrderService.cs | 2 +- Dentizone.Application/Services/OrderService.cs | 2 +- Dentizone.Presentaion/Controllers/OrderController.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dentizone.Application/Interfaces/Order/IOrderService.cs b/Dentizone.Application/Interfaces/Order/IOrderService.cs index 76efa66..8d45836 100644 --- a/Dentizone.Application/Interfaces/Order/IOrderService.cs +++ b/Dentizone.Application/Interfaces/Order/IOrderService.cs @@ -8,7 +8,7 @@ public interface IOrderService Task CreateOrderAsync(CreateOrderDto createOrderDto, string buyerId); Task GetOrderByIdAsync(string orderId, string buyerId); Task> GetOrdersByBuyerAsync(string buyerId); - Task CancelOrderAsync(string orderId, string userId); + Task CancelOrderAsync(string orderId); Task CompleteOrder(string orderId); Task> GetOrders(int page, FilterOrderDto filters); diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index ef07952..c5bb96f 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -32,7 +32,7 @@ internal class OrderService( AppDbContext dbContext) : BaseService(accessor), IOrderService { - public async Task CancelOrderAsync(string orderId, string userId) + public async Task CancelOrderAsync(string orderId) { var order = await orderRepository.FindBy(o => o.Id == orderId, [o => o.OrderStatuses, o => o.OrderItems]); diff --git a/Dentizone.Presentaion/Controllers/OrderController.cs b/Dentizone.Presentaion/Controllers/OrderController.cs index de11b34..45abe88 100644 --- a/Dentizone.Presentaion/Controllers/OrderController.cs +++ b/Dentizone.Presentaion/Controllers/OrderController.cs @@ -41,7 +41,7 @@ public async Task GetMyOrders() public async Task CancelOrder(string orderId) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var result = await orderService.CancelOrderAsync(orderId, userId); + var result = await orderService.CancelOrderAsync(orderId); return Ok(result); } From af319e48393e5e4f1a51a0a428597f19838b4810 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 02:11:24 +0300 Subject: [PATCH 6/9] Enhance authorization policies and refactor controllers - Updated `CreateAppUser.cs` to add `Email` property. - Reordered using directives and reformatted identity options in `AppIdentity.cs`. - Introduced new authorization policies: "IsVerified", "IsPartilyVerified", and "IsAdmin". - Refactored `Posts.cs` for consistency and improved user creation logic. - Required "IsAdmin" policy in `AdminController.cs` and `AnalyticsController.cs`. - Modified `AuthenticationController.cs` to include `Email` in access token generation. - Updated `CartController.cs` to require "IsPartilyVerified" policy. - Enhanced `CatalogController.cs` with "IsAdmin" authorization for category management. - Updated `FavoritesController.cs` and `OrderController.cs` with new authorization requirements. - Removed `PaymentController.cs`. - Updated `PostsController.cs`, `ReviewController.cs`, `ShippingController.cs`, and `UniversitiesController.cs` to enforce "IsAdmin" policy. - Updated `UploadController.cs` to require "IsVerified" policy. - Modified `VerificationController.cs` to include user national ID update logic. - Enhanced `WalletController.cs` with new withdrawal request and history endpoints. --- .../DTOs/User/CreateAppUser.cs | 4 +- .../DependencyInjection/AppIdentity.cs | 40 +++++++---- .../Persistence/Seeder/Posts.cs | 66 ++++++++++--------- .../Controllers/AdminController.cs | 2 + .../Controllers/AnalyticsController.cs | 2 + .../Controllers/AuthenticationController.cs | 7 +- .../Controllers/CartController.cs | 2 +- .../Controllers/CatalogController.cs | 9 ++- .../Controllers/FavoritesController.cs | 6 +- .../Controllers/OrderController.cs | 4 +- .../Controllers/PaymentController.cs | 17 ----- .../Controllers/PostsController.cs | 36 ++++------ .../Controllers/ReviewController.cs | 8 ++- .../Controllers/ShippingController.cs | 2 + .../Controllers/UniversitiesController.cs | 4 ++ .../Controllers/UploadController.cs | 13 +--- .../Controllers/VerificationController.cs | 2 +- .../Controllers/WalletController.cs | 4 ++ 18 files changed, 119 insertions(+), 109 deletions(-) delete mode 100644 Dentizone.Presentaion/Controllers/PaymentController.cs diff --git a/Dentizone.Application/DTOs/User/CreateAppUser.cs b/Dentizone.Application/DTOs/User/CreateAppUser.cs index 4cf0fe2..03c6898 100644 --- a/Dentizone.Application/DTOs/User/CreateAppUser.cs +++ b/Dentizone.Application/DTOs/User/CreateAppUser.cs @@ -5,10 +5,10 @@ namespace Dentizone.Application.DTOs.User public class CreateAppUser { public string - Id - { get; set; } + Id { get; set; } public string FullName { get; set; } + public string Email { get; set; } public string Username { get; set; } public string UniversityId { get; set; } public int AcademicYear { get; set; } diff --git a/Dentizone.Infrastructure/DependencyInjection/AppIdentity.cs b/Dentizone.Infrastructure/DependencyInjection/AppIdentity.cs index f06fddc..5d1c5e0 100644 --- a/Dentizone.Infrastructure/DependencyInjection/AppIdentity.cs +++ b/Dentizone.Infrastructure/DependencyInjection/AppIdentity.cs @@ -1,4 +1,5 @@ -using Dentizone.Domain.Interfaces.Secret; +using Dentizone.Domain.Enums; +using Dentizone.Domain.Interfaces.Secret; using Dentizone.Infrastructure.Identity; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; @@ -11,16 +12,16 @@ internal static class AppIdentity public static IServiceCollection AddAppIdentity(this IServiceCollection services) { services.AddIdentity(options => - { - options.User.RequireUniqueEmail = true; - options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; - options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultProvider; - options.Password = IdentityConfiguration.PasswordRestrictions; - options.SignIn = IdentityConfiguration.SignInOptions; - options.Lockout = IdentityConfiguration.LockoutOptions; - }) - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + { + options.User.RequireUniqueEmail = true; + options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; + options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultProvider; + options.Password = IdentityConfiguration.PasswordRestrictions; + options.SignIn = IdentityConfiguration.SignInOptions; + options.Lockout = IdentityConfiguration.LockoutOptions; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); services.AddAuthentication(options => { @@ -35,6 +36,23 @@ public static IServiceCollection AddAppIdentity(this IServiceCollection services var secretService = scope.ServiceProvider.GetRequiredService(); options.TokenValidationParameters = IdentityConfiguration.GetTokenValidationParameters(secretService); }); + services.AddAuthorization(options => + { + // Policy for actions that require a fully verified (KYC) user + options.AddPolicy("IsVerified", policy => + policy.RequireRole(UserRoles.VERIFIED.ToString())); + + // Policy for actions that require at least an email-verified user + options.AddPolicy("IsPartilyVerified", policy => + policy.RequireRole( + UserRoles.PARTILY_VERIFIED.ToString(), + UserRoles.VERIFIED.ToString() // A verified user is also partially verified + )); + + // Policy for admin-only actions + options.AddPolicy("IsAdmin", policy => + policy.RequireRole(UserRoles.ADMIN.ToString())); + }); return services; diff --git a/Dentizone.Infrastructure/Persistence/Seeder/Posts.cs b/Dentizone.Infrastructure/Persistence/Seeder/Posts.cs index af410e4..ca48435 100644 --- a/Dentizone.Infrastructure/Persistence/Seeder/Posts.cs +++ b/Dentizone.Infrastructure/Persistence/Seeder/Posts.cs @@ -22,9 +22,9 @@ public static async Task SeedAsync(AppDbContext context, UserManager c.SubCategories) - .Where(c => c.SubCategories.Any()) - .ToListAsync(); + .Include(c => c.SubCategories) + .Where(c => c.SubCategories.Any()) + .ToListAsync(); if (!universities.Any() || !categoriesWithSubcategories.Any()) { @@ -38,10 +38,10 @@ public static async Task SeedAsync(AppDbContext context, UserManager(); var identityUserFaker = new Faker() - .RuleFor(u => u.UserName, - f => f.Internet.Email(f.Name.FirstName(), f.Name.LastName())) - .RuleFor(u => u.Email, (f, u) => u.UserName) - .RuleFor(u => u.EmailConfirmed, false); + .RuleFor(u => u.UserName, + f => f.Internet.Email(f.Name.FirstName(), f.Name.LastName())) + .RuleFor(u => u.Email, (f, u) => u.UserName) + .RuleFor(u => u.EmailConfirmed, false); var generatedUsers = identityUserFaker.Generate(10); foreach (var user in generatedUsers) @@ -55,7 +55,8 @@ public static async Task SeedAsync(AppDbContext context, UserManager e.Description))}"); + Console.WriteLine( + $"Failed to create user {user.UserName}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); } } @@ -68,6 +69,7 @@ public static async Task SeedAsync(AppDbContext context, UserManager() - .RuleFor(a => a.Url, f => f.Image.PicsumUrl()) - .RuleFor(a => a.Type, AssetType.Image) - .RuleFor(a => a.Status, AssetStatus.Active) - .RuleFor(a => a.IsDeleted, false) - .RuleFor(a => a.UserId, f => f.PickRandom(userIds)); + .RuleFor(a => a.Url, f => f.Image.PicsumUrl()) + .RuleFor(a => a.Type, AssetType.Image) + .RuleFor(a => a.Status, AssetStatus.Active) + .RuleFor(a => a.IsDeleted, false) + .RuleFor(a => a.UserId, f => f.PickRandom(userIds)); var assets = assetFaker.Generate(50); @@ -96,25 +98,25 @@ public static async Task SeedAsync(AppDbContext context, UserManager() - .RuleFor(p => p.SellerId, f => f.PickRandom(userIds)) - .RuleFor(p => p.Title, f => f.Commerce.ProductName()) - .RuleFor(p => p.Description, f => f.Lorem.Paragraphs(5)) - .RuleFor(p => p.Price, f => f.Random.Decimal(50, 1000)) - .RuleFor(p => p.Condition, f => f.PickRandom()) - .RuleFor(p => p.Status, PostStatus.Active) - .RuleFor(p => p.IsDeleted, false) - .RuleFor(p => p.City, f => f.Address.City()) - .RuleFor(p => p.Street, f => f.Address.StreetAddress()) - .RuleFor(p => p.CreatedAt, f => f.Date.Past(1)) - .RuleFor(p => p.UpdatedAt, f => f.Date.Recent()) - .RuleFor(p => p.ExpireDate, f => f.Date.Future(30)) - .RuleFor(p => p.Slug, (f, p) => f.Lorem.Slug()) - .FinishWith((f, p) => - { - var randomCategory = f.PickRandom(categoriesWithSubcategories); - p.CategoryId = randomCategory.Id; - p.SubCategoryId = f.PickRandom(randomCategory.SubCategories).Id; - }); + .RuleFor(p => p.SellerId, f => f.PickRandom(userIds)) + .RuleFor(p => p.Title, f => f.Commerce.ProductName()) + .RuleFor(p => p.Description, f => f.Lorem.Paragraphs(5)) + .RuleFor(p => p.Price, f => f.Random.Decimal(50, 1000)) + .RuleFor(p => p.Condition, f => f.PickRandom()) + .RuleFor(p => p.Status, PostStatus.Active) + .RuleFor(p => p.IsDeleted, false) + .RuleFor(p => p.City, f => f.Address.City()) + .RuleFor(p => p.Street, f => f.Address.StreetAddress()) + .RuleFor(p => p.CreatedAt, f => f.Date.Past(1)) + .RuleFor(p => p.UpdatedAt, f => f.Date.Recent()) + .RuleFor(p => p.ExpireDate, f => f.Date.Future(30)) + .RuleFor(p => p.Slug, (f, p) => f.Lorem.Slug()) + .FinishWith((f, p) => + { + var randomCategory = f.PickRandom(categoriesWithSubcategories); + p.CategoryId = randomCategory.Id; + p.SubCategoryId = f.PickRandom(randomCategory.SubCategories).Id; + }); var posts = postFaker.Generate(20); await context.Posts.AddRangeAsync(posts); diff --git a/Dentizone.Presentaion/Controllers/AdminController.cs b/Dentizone.Presentaion/Controllers/AdminController.cs index 40c2946..8154299 100644 --- a/Dentizone.Presentaion/Controllers/AdminController.cs +++ b/Dentizone.Presentaion/Controllers/AdminController.cs @@ -1,9 +1,11 @@ using Dentizone.Application.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] + [Authorize("IsAdmin")] [ApiController] public class AdminController(IWithdrawalService withdrawalService) : ControllerBase { diff --git a/Dentizone.Presentaion/Controllers/AnalyticsController.cs b/Dentizone.Presentaion/Controllers/AnalyticsController.cs index f8089cc..d0a2b35 100644 --- a/Dentizone.Presentaion/Controllers/AnalyticsController.cs +++ b/Dentizone.Presentaion/Controllers/AnalyticsController.cs @@ -1,10 +1,12 @@ using Dentizone.Application.Interfaces.Analytics; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize("IsAdmin")] public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase { [HttpGet("user")] diff --git a/Dentizone.Presentaion/Controllers/AuthenticationController.cs b/Dentizone.Presentaion/Controllers/AuthenticationController.cs index 391716f..5f61acd 100644 --- a/Dentizone.Presentaion/Controllers/AuthenticationController.cs +++ b/Dentizone.Presentaion/Controllers/AuthenticationController.cs @@ -29,7 +29,7 @@ public async Task Login([FromBody] LoginRequestDto loginPayload) var token = tokenService.GenerateAccessToken(loggedInUser.User.Id, loggedInUser.User.Email, - loggedInUser.role.ToString()); + loggedInUser.role.ToString()); var refreshToken = tokenService.GenerateRefreshToken(loggedInUser.User.Id); return Ok(new RefreshTokenResponse() { @@ -52,11 +52,12 @@ public async Task Register([FromBody] RegisterRequestDto register Username = registerPayloadDto.Username, Status = UserState.PendingVerification, Id = loggedInUser.User.Id, // IdentityServer uses string IDs for users + Email = registerPayloadDto.Email }; var userData = await userService.CreateAsync(userDataDto); var token = tokenService.GenerateAccessToken(loggedInUser.User.Id, registerPayloadDto.Email, - loggedInUser.role.ToString()); + loggedInUser.role.ToString()); var refreshToken = tokenService.GenerateRefreshToken(loggedInUser.User.Id); return Ok(new RefreshTokenResponse() { @@ -100,7 +101,7 @@ public async Task SendForgetPasswordEmail([FromQuery] string emai public async Task ResetPassword([FromBody] ResetPasswordDto resetPasswordDto) { var result = await authenticationService.ResetPassword(resetPasswordDto.Email, resetPasswordDto.Token, - resetPasswordDto.NewPassword); + resetPasswordDto.NewPassword); return Ok(new { Message = result }); diff --git a/Dentizone.Presentaion/Controllers/CartController.cs b/Dentizone.Presentaion/Controllers/CartController.cs index cd95ad8..3be8cd1 100644 --- a/Dentizone.Presentaion/Controllers/CartController.cs +++ b/Dentizone.Presentaion/Controllers/CartController.cs @@ -8,7 +8,7 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] - [Authorize] + [Authorize(Policy = "IsPartilyVerified")] public class CartController(ICartService cartService) : ControllerBase { [HttpGet] diff --git a/Dentizone.Presentaion/Controllers/CatalogController.cs b/Dentizone.Presentaion/Controllers/CatalogController.cs index dda134c..4235789 100644 --- a/Dentizone.Presentaion/Controllers/CatalogController.cs +++ b/Dentizone.Presentaion/Controllers/CatalogController.cs @@ -1,5 +1,6 @@ using Dentizone.Application.DTOs.Catalog; using Dentizone.Application.Interfaces.Catalog; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Dentizone.Presentaion.Controllers @@ -30,6 +31,7 @@ public async Task GetCategoryById(string categoryId) } [HttpPost("categories")] + [Authorize(Policy = "IsAdmin")] public async Task CreateCategory([FromBody] CategoryDto categoryDto) { var createdCategory = await catalogService.CreateCategory(categoryDto); @@ -37,6 +39,7 @@ public async Task CreateCategory([FromBody] CategoryDto categoryD } [HttpPut("categories/{categoryId}")] + [Authorize(Policy = "IsAdmin")] public async Task UpdateCategory( string categoryId, [FromBody] CategoryDto categoryDto) @@ -46,6 +49,7 @@ public async Task UpdateCategory( } [HttpDelete("categories/{categoryId}")] + [Authorize(Policy = "IsAdmin")] public async Task DeleteCategory(string categoryId) { var deletedCategory = await catalogService.DeleteCategory(categoryId); @@ -73,14 +77,16 @@ public async Task GetSubCategoryById(string subCategoryId) return Ok(subCategory); } + [Authorize(Policy = "IsAdmin")] [HttpPost("subcategories")] public async Task CreateSubCategory([FromBody] SubCategoryDto subCategoryDto) { var createdSubCategory = await catalogService.CreateSubCategory(subCategoryDto); return CreatedAtAction(nameof(GetSubCategoryById), new { subCategoryId = createdSubCategory.Id }, - createdSubCategory); + createdSubCategory); } + [Authorize(Policy = "IsAdmin")] [HttpPut("subcategories")] public async Task UpdateSubCategory([FromBody] SubCategoryDto subCategoryDto) { @@ -88,6 +94,7 @@ public async Task UpdateSubCategory([FromBody] SubCategoryDto sub return Ok(updatedSubCategory); } + [Authorize(Policy = "IsAdmin")] [HttpDelete("subcategories/{subCategoryId}")] public async Task DeleteSubCategory(string subCategoryId) { diff --git a/Dentizone.Presentaion/Controllers/FavoritesController.cs b/Dentizone.Presentaion/Controllers/FavoritesController.cs index f65698d..69b566e 100644 --- a/Dentizone.Presentaion/Controllers/FavoritesController.cs +++ b/Dentizone.Presentaion/Controllers/FavoritesController.cs @@ -8,7 +8,7 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] - [Authorize] + [Authorize(Policy = "IsPartilyVerified")] public class FavoritesController(IFavoritesService favoritesService) : ControllerBase { [HttpGet] @@ -23,8 +23,8 @@ public async Task GetFavorites() public async Task AddToFavorites([FromBody] FavoriteDto dto) { var userId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; - var favorite = await favoritesService.AddToFavoritesAsync(userId, dto.PostId); - return CreatedAtAction(nameof(GetFavorites), null, favorite); + await favoritesService.AddToFavoritesAsync(userId, dto.PostId); + return Ok(); } [HttpDelete("{favoriteId}")] diff --git a/Dentizone.Presentaion/Controllers/OrderController.cs b/Dentizone.Presentaion/Controllers/OrderController.cs index 45abe88..bde7143 100644 --- a/Dentizone.Presentaion/Controllers/OrderController.cs +++ b/Dentizone.Presentaion/Controllers/OrderController.cs @@ -22,6 +22,7 @@ public async Task GetOrderById(string orderId) } [HttpPost] + [Authorize(Policy = "IsVerified")] public async Task CreateOrder([FromBody] CreateOrderDto createOrderDto) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); @@ -46,6 +47,7 @@ public async Task CancelOrder(string orderId) } [HttpPut("{orderId}/confirm")] + [Authorize(Policy = "IsAdmin")] public async Task ConfirmOrder(string orderId) { await orderService.CompleteOrder(orderId); @@ -53,7 +55,7 @@ public async Task ConfirmOrder(string orderId) } [HttpGet("all")] - [AllowAnonymous] + [Authorize(Policy = "IsAdmin")] public async Task GetAllOrders([FromQuery] FilterOrderDto filters, [FromQuery] int page = 1) { var orders = await orderService.GetOrders(page, filters); diff --git a/Dentizone.Presentaion/Controllers/PaymentController.cs b/Dentizone.Presentaion/Controllers/PaymentController.cs deleted file mode 100644 index cae321f..0000000 --- a/Dentizone.Presentaion/Controllers/PaymentController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Dentizone.Application.Services.Payment; -using Microsoft.AspNetCore.Mvc; - -namespace Dentizone.Presentaion.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class PaymentController(IPaymentService paymentService) : ControllerBase - { - [HttpGet] - [Route("confirm")] - public async Task ConfirmPayment(string paymentId, string orderId) - { - return NoContent(); - } - } -} \ No newline at end of file diff --git a/Dentizone.Presentaion/Controllers/PostsController.cs b/Dentizone.Presentaion/Controllers/PostsController.cs index e689e13..b9433ed 100644 --- a/Dentizone.Presentaion/Controllers/PostsController.cs +++ b/Dentizone.Presentaion/Controllers/PostsController.cs @@ -10,9 +10,11 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize] public class PostsController(IPostService postService) : ControllerBase { [HttpGet] + [AllowAnonymous] public async Task GetAllPosts(int page = 1) { var posts = await postService.GetAllPosts(page); @@ -20,14 +22,15 @@ public async Task GetAllPosts(int page = 1) } [HttpGet("{id}")] + [AllowAnonymous] public async Task GetPostById(string id) { var post = await postService.GetPostById(id); return Ok(post); } - [Authorize] [HttpGet("users/{sellerId}/posts")] + [Authorize(Policy = "IsAdmin")] public async Task GetPostsBySellerId(string sellerId, int page = 1) { var posts = await postService.GetPostsBySellerId(sellerId, page); @@ -35,9 +38,7 @@ public async Task GetPostsBySellerId(string sellerId, int page = } [HttpPost] - [Authorize] - - //TODO: Require a Claims + [Authorize(Policy = "IsVerified")] public async Task CreatePost([FromBody] CreatePostDto createPostDto) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); @@ -49,38 +50,26 @@ public async Task CreatePost([FromBody] CreatePostDto createPostD var createdPost = await postService.CreatePost(createPostDto, userId); - ; return Ok(createdPost); } [HttpDelete("{id}")] + [Authorize(Policy = "IsAdmin")] public async Task DeletePost(string id) { - try - { - var deletedPost = await postService.DeletePost(id); - return Ok(deletedPost); - } - catch (Exception ex) - { - return NotFound($"Error : {ex.Message}"); - } + var deletedPost = await postService.DeletePost(id); + return Ok(deletedPost); } + [Authorize(Policy = "IsAdmin")] [HttpPut("{id}")] public async Task UpdatePost(string id, [FromBody] UpdatePostDto updatePostDto) { - try - { - var updatedPost = await postService.UpdatePost(id, updatePostDto); - return Ok(updatedPost); - } - catch (Exception ex) - { - return BadRequest($"Error : {ex.Message}"); - } + var updatedPost = await postService.UpdatePost(id, updatePostDto); + return Ok(updatedPost); } + [AllowAnonymous] [HttpGet("sidebar")] public async Task GetSidebarFilter() { @@ -88,6 +77,7 @@ public async Task GetSidebarFilter() return Ok(sidebarFilter); } + [AllowAnonymous] [HttpGet("search")] public async Task Search([FromQuery] UserPreferenceDto userPreferenceDTO) { diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 6ab1495..d1baf3d 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -8,7 +8,7 @@ namespace Dentizone.Presentaion.Controllers { [ApiController] [Route("api/[controller]")] - [Authorize] + [Authorize(Policy = "IsVerified")] public class ReviewController(IReviewService reviewService) : ControllerBase { [HttpPost] @@ -20,14 +20,16 @@ public async Task CreateOrderReview([FromBody] CreateReviewDto cr return Ok(); } - [HttpPut("{reviewId}")] // ADMIN ONLY + [HttpPut("{reviewId}")] + [Authorize(Policy = "IsAdmin")] public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDto updateReviewDto) { await reviewService.UpdateReviewAsync(reviewId, updateReviewDto); return Ok(); } - [HttpDelete("{reviewId}")] // ADMIN ONLY + [HttpDelete("{reviewId}")] + [Authorize(Policy = "IsAdmin")] public async Task DeleteReview(string reviewId) { await reviewService.DeleteReviewAsync(reviewId); diff --git a/Dentizone.Presentaion/Controllers/ShippingController.cs b/Dentizone.Presentaion/Controllers/ShippingController.cs index db513ee..30cff36 100644 --- a/Dentizone.Presentaion/Controllers/ShippingController.cs +++ b/Dentizone.Presentaion/Controllers/ShippingController.cs @@ -1,6 +1,7 @@ using Dentizone.Application.Interfaces.Order; using Dentizone.Domain.Enums; using Dentizone.Domain.Interfaces.Repositories; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -8,6 +9,7 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize(Policy = "IsAdmin")] public class ShippingController(IShippingService shipmentActivity) : ControllerBase { [HttpPut] diff --git a/Dentizone.Presentaion/Controllers/UniversitiesController.cs b/Dentizone.Presentaion/Controllers/UniversitiesController.cs index b7c059e..6ee0fb6 100644 --- a/Dentizone.Presentaion/Controllers/UniversitiesController.cs +++ b/Dentizone.Presentaion/Controllers/UniversitiesController.cs @@ -1,14 +1,17 @@ using Dentizone.Application.DTOs.University; using Dentizone.Application.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize(Policy = "IsAdmin")] public class UniversitiesController(IUniversityService universityService) : ControllerBase { [HttpGet] + [AllowAnonymous] public async Task GetAll([FromQuery] int page = 1) { var universities = await universityService.GetAllUniversitiesAsync(page); @@ -16,6 +19,7 @@ public async Task GetAll([FromQuery] int page = 1) } [HttpGet("supported")] + [AllowAnonymous] public async Task GetAllUniversities() { var universities = await universityService.GetSupportedUniversitiesAsync(); diff --git a/Dentizone.Presentaion/Controllers/UploadController.cs b/Dentizone.Presentaion/Controllers/UploadController.cs index d87f8f2..e8d9b00 100644 --- a/Dentizone.Presentaion/Controllers/UploadController.cs +++ b/Dentizone.Presentaion/Controllers/UploadController.cs @@ -8,7 +8,7 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] - [Authorize] + [Authorize(Policy = "IsVerified")] public class UploadController(IUploadService uploadService) : ControllerBase { [HttpPost("image")] @@ -41,22 +41,13 @@ public async Task UploadImageAsync(IFormFile file) public async Task GetAssetById(string id) { var asset = await uploadService.FindAssetById(id); - return Ok(asset); } [HttpDelete("{id}")] public async Task DeleteAssetById(string id) { - var asset = await uploadService.FindAssetById(id); - if (asset == null) - { - throw new NotFoundException("Asset not found"); - } - - var userId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; - - await uploadService.DeleteAssetById(id, userId); + await uploadService.DeleteAssetById(id); return NoContent(); } } diff --git a/Dentizone.Presentaion/Controllers/VerificationController.cs b/Dentizone.Presentaion/Controllers/VerificationController.cs index a0064e2..409a009 100644 --- a/Dentizone.Presentaion/Controllers/VerificationController.cs +++ b/Dentizone.Presentaion/Controllers/VerificationController.cs @@ -101,7 +101,7 @@ public async Task AlertWebhook() case "approved": await authService.AlternateUserRoleAsync(UserRoles.VERIFIED, userId); await verificationService.UpdateUserNationalId(userId, - verification.IdVerification.PersonalNumber); + verification.IdVerification.PersonalNumber); break; case "declined": await authService.AlternateUserRoleAsync(UserRoles.BLACKLISTED, userId); diff --git a/Dentizone.Presentaion/Controllers/WalletController.cs b/Dentizone.Presentaion/Controllers/WalletController.cs index cb33f55..eee6b64 100644 --- a/Dentizone.Presentaion/Controllers/WalletController.cs +++ b/Dentizone.Presentaion/Controllers/WalletController.cs @@ -3,11 +3,13 @@ using Dentizone.Application.Services.Payment; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize("IsVerified")] public class WalletController(IWalletService walletService, IWithdrawalService withdrawalService) : ControllerBase { [HttpGet("balance")] @@ -19,6 +21,7 @@ public async Task GetWalletBalance() var wallet = await walletService.GetWalletBalanceAsync(userId); return Ok(wallet); } + [HttpPost("withdraw")] public async Task RequestWithdrawal([FromBody] WithdrawalRequestDto withdrawalRequestDto) { @@ -27,6 +30,7 @@ public async Task RequestWithdrawal([FromBody] WithdrawalRequestD var request = await withdrawalService.CreateWithdrawalRequestAsync(UserId, withdrawalRequestDto); return Ok(request); } + [HttpGet("withdrawal-history")] public async Task GetWithdrawalHistory(int page = 1) { From e1ed86da68872e48e22c4d23653a440cadb6620b Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 13:38:54 +0300 Subject: [PATCH 7/9] Refactor IsAdmin method and reorganize using directives Updated the `IsAdmin` method in the `BaseService` class to use `Enum.TryParse` for safer role checking, enhancing type safety. Reorganized the using directives in the `UserService` class for improved readability without affecting functionality. --- Dentizone.Application/Services/BaseService.cs | 2 +- Dentizone.Application/Services/UserService.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dentizone.Application/Services/BaseService.cs b/Dentizone.Application/Services/BaseService.cs index 5a3118b..bcf00d4 100644 --- a/Dentizone.Application/Services/BaseService.cs +++ b/Dentizone.Application/Services/BaseService.cs @@ -22,7 +22,7 @@ protected BaseService(IHttpContextAccessor httpContextAccessor) protected bool IsAdmin() { var userRole = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role); - return userRole == UserRoles.ADMIN.ToString(); + return Enum.TryParse(userRole, out var role) && role == UserRoles.ADMIN; } /// diff --git a/Dentizone.Application/Services/UserService.cs b/Dentizone.Application/Services/UserService.cs index 4e1090c..d4eb697 100644 --- a/Dentizone.Application/Services/UserService.cs +++ b/Dentizone.Application/Services/UserService.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System.Linq.Expressions; +using AutoMapper; using Dentizone.Application.DTOs.User; using Dentizone.Application.Interfaces.User; using Dentizone.Application.Services.Payment; @@ -6,7 +7,6 @@ using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; -using System.Linq.Expressions; using Dentizone.Infrastructure; namespace Dentizone.Application.Services From a0681a6f635a3d8e9dd53b908b57532c0fac10a6 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 20:42:07 +0300 Subject: [PATCH 8/9] Refactor dependency injection and update environment handling - Removed `IReviewRepository` and `IWithdrawalRequestRepository` from DI in `AddRepositories.cs`. - Updated `Environment` variable initialization in `GetSecret` constructor of `SecretService.cs` to use environment variables. - Simplified lambda expression in `GetSecret` method of `SecretService.cs`. - Removed unused import of `Dentizone.Domain.Exceptions` in `PostsController.cs`. - Eliminated null check for `userId` in `CreatePost` method of `PostsController.cs`. - Changed authorization policy for `DeletePost` and `UpdatePost` methods to "IsVerified". - Made root endpoint redirection permanent in `Program.cs`. - Commented out role and database seeding logic in `Program.cs`. --- .../DependencyInjection/AddRepositories.cs | 4 ---- Dentizone.Infrastructure/Secret/SecretService.cs | 4 ++-- .../Controllers/PostsController.cs | 12 ++---------- Dentizone.Presentaion/Program.cs | 15 +-------------- 4 files changed, 5 insertions(+), 30 deletions(-) diff --git a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs index 05c564a..0aa2af9 100644 --- a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs +++ b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs @@ -25,13 +25,9 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); return services; diff --git a/Dentizone.Infrastructure/Secret/SecretService.cs b/Dentizone.Infrastructure/Secret/SecretService.cs index a217ca2..72b854c 100644 --- a/Dentizone.Infrastructure/Secret/SecretService.cs +++ b/Dentizone.Infrastructure/Secret/SecretService.cs @@ -8,7 +8,7 @@ internal class GetSecret : GetSecretOptions { public GetSecret() { - Environment = "dev"; + Environment = System.Environment.GetEnvironmentVariable("env") ?? "unknown"; ProjectId = System.Environment.GetEnvironmentVariable("ProjectId") ?? throw new ArgumentNullException("Can't find the project id"); } @@ -33,7 +33,7 @@ public string GetSecret(string name) try { return _cache.GetOrAdd(name, - n => infisicalClient.GetSecret(CreateSecret(n)).SecretValue); + n => infisicalClient.GetSecret(CreateSecret(n)).SecretValue); } catch (Exception ex) { diff --git a/Dentizone.Presentaion/Controllers/PostsController.cs b/Dentizone.Presentaion/Controllers/PostsController.cs index b9433ed..7c5b6df 100644 --- a/Dentizone.Presentaion/Controllers/PostsController.cs +++ b/Dentizone.Presentaion/Controllers/PostsController.cs @@ -1,7 +1,6 @@ using Dentizone.Application.DTOs.PostDTO; using Dentizone.Application.DTOs.PostFilterDTO; using Dentizone.Application.Interfaces.Post; -using Dentizone.Domain.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; @@ -42,26 +41,19 @@ public async Task GetPostsBySellerId(string sellerId, int page = public async Task CreatePost([FromBody] CreatePostDto createPostDto) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - - if (string.IsNullOrEmpty(userId)) - { - throw new BadActionException("How in the hell you reached here!"); - } - - var createdPost = await postService.CreatePost(createPostDto, userId); return Ok(createdPost); } [HttpDelete("{id}")] - [Authorize(Policy = "IsAdmin")] + [Authorize(Policy = "IsVerified")] public async Task DeletePost(string id) { var deletedPost = await postService.DeletePost(id); return Ok(deletedPost); } - [Authorize(Policy = "IsAdmin")] + [Authorize(Policy = "IsVerified")] [HttpPut("{id}")] public async Task UpdatePost(string id, [FromBody] UpdatePostDto updatePostDto) { diff --git a/Dentizone.Presentaion/Program.cs b/Dentizone.Presentaion/Program.cs index 9ce67ab..a0a3e62 100644 --- a/Dentizone.Presentaion/Program.cs +++ b/Dentizone.Presentaion/Program.cs @@ -44,12 +44,11 @@ public static void Main(string[] args) { opt.Title = "Dentizone API"; opt.Theme = ScalarTheme.Mars; - opt.DefaultHttpClient = new(ScalarTarget.JavaScript, ScalarClient.Fetch); }); app.MapGet("/", context => { - context.Response.Redirect("/scalar", permanent: false); + context.Response.Redirect("/scalar", permanent: true); return Task.CompletedTask; }); @@ -61,19 +60,7 @@ public static void Main(string[] args) app.MapControllers(); - //RoleSeeder.SeedRolesAsync(app.Services).Wait(); - using (var scope = app.Services.CreateScope()) - { - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var userManager = scope.ServiceProvider.GetRequiredService>(); - - // Seed the database with initial data - //UniversitySeeder.SeedAsync(dbContext).Wait(); - //CatalogSeeder.SeedCategoriesAndSubCategoriesAsync(dbContext).Wait(); - //DataSeeder.SeedAsync(dbContext, userManager).Wait(); - } app.Run(); } From 8eac81d01aaeb60f385fb56dff8155f2513610d4 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 20:52:24 +0300 Subject: [PATCH 9/9] Refactor mapping configurations in AnswerProfile Updated the mapping configurations in the `AnswerProfile` class to use simplified type names instead of fully qualified names. This change improves code readability and reduces verbosity while retaining the existing mapping functionality for `CreateAnswerDto`, `UpdateAnswerDto`, and `AnswerViewDto`. --- Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs index 7ad1976..50ca18a 100644 --- a/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs +++ b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs @@ -6,15 +6,15 @@ public class AnswerProfile : Profile { public AnswerProfile() { - CreateMap() + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid().ToString())) .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) .ReverseMap(); - CreateMap() + CreateMap() .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) .ReverseMap(); - CreateMap() + CreateMap() .ReverseMap(); } }