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(); } } 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.Application/Interfaces/IUploadService.cs b/Dentizone.Application/Interfaces/IUploadService.cs index 17d8646..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/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/AssetService.cs b/Dentizone.Application/Services/AssetService.cs index 0d0b390..1bb1785 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) { @@ -30,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); @@ -37,7 +42,19 @@ public async Task UpdateAssetAsync(string id, UpdateAssetDto assetDto) public async Task DeleteAssetAsync(string assetId) { + await AuthorizeAdminOrOwnerAsync(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..bcf00d4 --- /dev/null +++ b/Dentizone.Application/Services/BaseService.cs @@ -0,0 +1,69 @@ +// Dentizone.Application/Services/BaseService.cs + +using System.Security.Claims; +using Dentizone.Domain.Enums; +using Dentizone.Domain.Exceptions; +using Microsoft.AspNetCore.Http; + +namespace Dentizone.Application.Services; + +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 Enum.TryParse(userRole, out var role) && role == UserRoles.ADMIN; + } + + /// + /// 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 d40cf0b..30f1f79 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,10 +28,11 @@ 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) + public async Task CancelOrderAsync(string orderId) { var order = await orderRepository.FindBy(o => o.Id == orderId, [o => o.OrderStatuses, o => o.OrderItems]); @@ -40,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)) { @@ -290,5 +289,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/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 7c54a76..41aaa3e 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; @@ -180,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); @@ -222,21 +225,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 +250,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 +286,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 +298,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 +320,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..d04e1f8 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) { @@ -25,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; } @@ -43,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.Id == reviewId && !r.IsDeleted) ?? + throw new NotFoundException("Review with Provided id is not found"); + review.Text = updateReviewDto.Comment; @@ -73,5 +76,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/UploadService.cs b/Dentizone.Application/Services/UploadService.cs index fabca71..1f0e1db 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) { - 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.Application/Services/UserService.cs b/Dentizone.Application/Services/UserService.cs index 75f31cc..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,7 @@ using Dentizone.Domain.Enums; 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) 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/DependencyInjection/AddRepositories.cs b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs index 1361146..1d863bd 100644 --- a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs +++ b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs @@ -30,10 +30,7 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); return services; 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/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); 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.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/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 de11b34..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); @@ -41,11 +42,12 @@ 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); } [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..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; @@ -10,9 +9,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 +21,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,52 +37,31 @@ 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); - - 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 = "IsVerified")] 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 = "IsVerified")] [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 +69,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) { 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(); }