diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 5dbcb4c..b816ad7 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -5,12 +5,13 @@ namespace Hutopy.Application.Common.Interfaces; public interface IIdentityService { Task GetUserNameAsync(string userId); - + Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password); + Task FindUserByIdAsync(string id); + Task GetCurrentUserAsync(); + Task FindUserByEmailAsync(string id); Task IsInRoleAsync(string userId, string role); - Task AuthorizeAsync(string userId, string policyName); - - Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password); - + Task AddRoleAsync(string userId, string role); + Task> GetCurrentUserRolesAsync(); Task DeleteUserAsync(string userId); } diff --git a/src/Application/Common/Interfaces/IRoleService.cs b/src/Application/Common/Interfaces/IRoleService.cs new file mode 100644 index 0000000..96ed4d2 --- /dev/null +++ b/src/Application/Common/Interfaces/IRoleService.cs @@ -0,0 +1,10 @@ +using Hutopy.Application.Common.Models; + +namespace Hutopy.Application.Common.Interfaces; + +public interface IRoleService +{ + public Task CreateRoleAsync(string roleName); + public Task DeleteRoleAsync(string roleName); + public Task FindRoleByIdAsync(string roleId); +} diff --git a/src/Application/Common/Models/RoleModel.cs b/src/Application/Common/Models/RoleModel.cs new file mode 100644 index 0000000..ed49feb --- /dev/null +++ b/src/Application/Common/Models/RoleModel.cs @@ -0,0 +1,7 @@ +namespace Hutopy.Application.Common.Models; + +public class RoleModel +{ + public string? Id { get; set; } + public string? Name { get; set; } +} diff --git a/src/Domain/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs similarity index 83% rename from src/Domain/Models/UserModel.cs rename to src/Application/Common/Models/UserModel.cs index a3c9eb2..5c4358b 100644 --- a/src/Domain/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -1,4 +1,4 @@ -namespace Hutopy.Domain.Models; +namespace Hutopy.Application.Common.Models; public class UserModel { diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index d65e206..41c71da 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Hutopy.Application.Common.Behaviours; using Microsoft.Extensions.DependencyInjection; namespace Hutopy.Application; @@ -15,7 +16,7 @@ public static class DependencyInjection { cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); //cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); - //cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>)); + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>)); //cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); //cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); }); diff --git a/src/Application/FutureCreators/Queries/GetFutureCreatorList.cs b/src/Application/FutureCreators/Queries/GetFutureCreatorList.cs index 1b5fb23..0281518 100644 --- a/src/Application/FutureCreators/Queries/GetFutureCreatorList.cs +++ b/src/Application/FutureCreators/Queries/GetFutureCreatorList.cs @@ -2,9 +2,11 @@ using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Mappings; using Hutopy.Application.Common.Models; +using Hutopy.Application.Common.Security; namespace Hutopy.Application.FutureCreators.Queries; +[Authorize(Roles = "Administrator")] public record GetFutureCreatorListQuery : IRequest> { public int PageNumber { get; init; } = 1; diff --git a/src/Application/Users/Commands/CreateUser.cs b/src/Application/Users/Commands/CreateUser.cs index 8992f01..54077eb 100644 --- a/src/Application/Users/Commands/CreateUser.cs +++ b/src/Application/Users/Commands/CreateUser.cs @@ -1,8 +1,4 @@ -using System.Dynamic; -using Hutopy.Application.Common.Interfaces; -using Hutopy.Domain.Entities; -using Hutopy.Domain.Interfaces; -using Microsoft.EntityFrameworkCore; +using Hutopy.Application.Common.Interfaces; namespace Hutopy.Application.Users.Commands; public record CreateUserCommand : IRequest @@ -17,24 +13,22 @@ public record CreateUserCommand : IRequest public class CreateUserCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; - private readonly IUserService _userService; + private readonly IIdentityService _identityService; - public CreateUserCommandHandler(IApplicationDbContext context, IUserService userService) + public CreateUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) { _context = context; - _userService = userService; + _identityService = identityService; } public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { - // Dont really need the handler for the create. The get will work like this : - var user = await _userService.FindUserByIdAsync("072ae7d5-8c4a-4a0f-b250-7d39941125cb"); - // var user2 = await _userService.FindUserByEmailAsync("test10@hotmail.com"); + await _identityService.CreateUserAsync(request.EmailAddress, request.UserName, request.FirstName, request.LastName, request.Password); - var tt = user?.FirstName; + var user = await _identityService.FindUserByEmailAsync(request.EmailAddress); await _context.SaveChangesAsync(cancellationToken); - return Guid.NewGuid(); + return new Guid(user?.Id ?? string.Empty); } } diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index cada50a..49f1152 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -1,5 +1,4 @@ using Hutopy.Application.Common.Interfaces; -using Hutopy.Domain.Interfaces; namespace Hutopy.Application.Users.Queries.GetCurrentUser; @@ -8,13 +7,13 @@ public record GetCurrentUserQuery : IRequest; public class GetCurrentUserQueryHandler( IApplicationDbContext context, IMapper mapper, - IUserService userService + IIdentityService identityService ) : IRequestHandler { public async Task Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) { - var identityUser = await userService.GetCurrentUserAsync(); + var identityUser = await identityService.GetCurrentUserAsync(); var currentUserId = new Guid(identityUser?.Id ?? ""); var transactions = await context.UserTransactions @@ -24,14 +23,17 @@ public class GetCurrentUserQueryHandler( .Where(x => x.IsConfirmed == true) .ToListAsync(cancellationToken); - var user = new UserDto() + var roles = await identityService.GetCurrentUserRolesAsync(); + + var user = new UserDto { Id = currentUserId, FirstName = identityUser?.FirstName ?? "", LastName = identityUser?.LastName ?? "", UserName =identityUser?.UserName ?? "", UserTransactions = transactions, - TotalBalance = transactions.Sum(x => x.Amount) + TotalBalance = transactions.Sum(x => x.Amount), + UserRoles = roles }; return user; diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index a5e1b49..0e2af25 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -7,6 +7,7 @@ public class UserDto public required string LastName { get; init; } public string UserName { get; init; } = String.Empty; public List UserTransactions { get; init; } = []; + public IList UserRoles { get; init; } = []; public required decimal TotalBalance { get; init; } diff --git a/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs index 4206c79..5fd288d 100644 --- a/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs +++ b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs @@ -1,4 +1,4 @@ -using Hutopy.Domain.Interfaces; +using Hutopy.Application.Common.Interfaces; namespace Hutopy.Application.Users.Queries.GetMinimalUser; @@ -8,15 +8,15 @@ public record GetMinimalUserQuery : IRequest }; public class GetMinimalUserQueryHandler( - IUserService userService + IIdentityService identityService ) : IRequestHandler { public async Task Handle(GetMinimalUserQuery request, CancellationToken cancellationToken) { - var identityUser = await userService.FindUserByIdAsync(request.UserId); + var identityUser = await identityService.FindUserByIdAsync(request.UserId); - var user = new MinimalUserDto() + var user = new MinimalUserDto { FirstName = identityUser?.FirstName ?? "", LastName = identityUser?.LastName ?? "", diff --git a/src/Domain/Interfaces/IUserService.cs b/src/Domain/Interfaces/IUserService.cs deleted file mode 100644 index efe46bb..0000000 --- a/src/Domain/Interfaces/IUserService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Hutopy.Domain.Models; - -namespace Hutopy.Domain.Interfaces; - -public interface IUserService -{ - Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password); - - Task FindUserByIdAsync(string id); - Task GetCurrentUserAsync(); - - Task FindUserByEmailAsync(string id); -} diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index ae83424..31b0b30 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -1,11 +1,8 @@ -using System; -using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Common.Interfaces; using Hutopy.Domain.Constants; -using Hutopy.Domain.Interfaces; using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data.Interceptors; using Hutopy.Infrastructure.Identity; -using Hutopy.Infrastructure.Services; using Hutopy.Infrastructure.Stripe; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -51,8 +48,6 @@ public static class DependencyInjection .AddBearerToken(IdentityConstants.BearerScheme); services.AddAuthorizationBuilder(); - services.AddScoped(); - // Might need to change and use AddIdentity() when we need to integrate connection via third party ( facebook, google ) services @@ -62,7 +57,7 @@ public static class DependencyInjection .AddApiEndpoints(); services.AddSingleton(TimeProvider.System); - services.AddTransient(); + services.AddScoped(); services.AddTransient(); services.AddAuthorization(options => diff --git a/src/Infrastructure/Identity/ApplicationRole.cs b/src/Infrastructure/Identity/ApplicationRole.cs new file mode 100644 index 0000000..eba9904 --- /dev/null +++ b/src/Infrastructure/Identity/ApplicationRole.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Identity; + +namespace Hutopy.Infrastructure.Identity; + +public class ApplicationRole : IdentityRole +{ +} diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 71a3886..618b647 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,6 +1,8 @@ +using System.Security.Claims; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; namespace Hutopy.Infrastructure.Identity; @@ -8,7 +10,9 @@ namespace Hutopy.Infrastructure.Identity; public class IdentityService( UserManager userManager, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IHttpContextAccessor contextAccessor + ) : IIdentityService { public async Task GetUserNameAsync(string userId) @@ -30,6 +34,68 @@ public class IdentityService( return (result.ToApplicationResult(), user.Id); } + + public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) + { + var applicationUser = new ApplicationUser + { + UserName = userName, + Email = email, + FirstName = firstName, + LastName = lastName + }; + + var response = await userManager.CreateAsync(applicationUser, password); + + return response.ToApplicationResult(); + } + + public async Task FindUserByIdAsync(string id) + { + var response = await userManager.FindByIdAsync(id); + + if (response == null) return null; + + var userModel = new UserModel() + { + Id = response.Id, + UserName = response.UserName, + FirstName = response.FirstName, + LastName = response.LastName, + Email = response.Email, + }; + + return userModel; + } + + public async Task GetCurrentUserAsync() + { + var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(currentUserId)) + { + return null; + } + + return await FindUserByIdAsync(currentUserId); + } + + public async Task FindUserByEmailAsync(string email) + { + var response = await userManager.FindByEmailAsync(email); + + if (response == null) return null; + + var userModel = new UserModel + { + Id = response.Id, + UserName = response.UserName, + FirstName = response.FirstName, + LastName = response.LastName, + Email = response.Email + }; + + return userModel; + } public async Task IsInRoleAsync(string userId, string role) { @@ -37,6 +103,14 @@ public class IdentityService( return user != null && await userManager.IsInRoleAsync(user, role); } + + public async Task CurrentUserIsInRoleAsync(string role) + { + var currentUserModel = await GetCurrentUserAsync(); + var currentUser = await userManager.FindByIdAsync(currentUserModel?.Id ?? ""); + + return currentUser != null && await userManager.IsInRoleAsync(currentUser, role); + } public async Task AuthorizeAsync(string userId, string policyName) { @@ -67,4 +141,32 @@ public class IdentityService( return result.ToApplicationResult(); } + + public async Task AddRoleAsync(string userId, string role) + { + var hasAdminAccess = await CurrentUserIsInRoleAsync("Administrator"); + + if (!hasAdminAccess) return Result.Failure(new []{"Only administrator can assign new roles to a user."}); + + var user = await userManager.FindByIdAsync(userId); + + if (user is null) return Result.Failure(new []{"User not found."}); + + var result = await userManager.AddToRoleAsync(user, role); + + return result.ToApplicationResult(); + } + + public async Task> GetCurrentUserRolesAsync() + { + var currentUserModel = await GetCurrentUserAsync(); + + var currentUser = await userManager.FindByIdAsync(currentUserModel?.Id ?? ""); + + if (currentUser is null) return []; + + var userRoles = await userManager.GetRolesAsync(currentUser); + + return userRoles; + } } diff --git a/src/Infrastructure/Identity/RoleService.cs b/src/Infrastructure/Identity/RoleService.cs new file mode 100644 index 0000000..71f0ed0 --- /dev/null +++ b/src/Infrastructure/Identity/RoleService.cs @@ -0,0 +1,49 @@ +using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Common.Models; +using Microsoft.AspNetCore.Identity; + +namespace Hutopy.Infrastructure.Identity; + +public class RoleService( + RoleManager roleManager + ) + : IRoleService +{ + public async Task CreateRoleAsync(string roleName) + { + var role = new ApplicationRole { Name = roleName, Id = Guid.NewGuid().ToString()}; + var result = await roleManager.CreateAsync(role); + + return result.ToApplicationResult(); + } + + public async Task DeleteRoleAsync(string roleName) + { + var role = new ApplicationRole { Name = roleName }; + var result = await roleManager.DeleteAsync(role); + + return result.ToApplicationResult(); + } + + public async Task FindRoleByIdAsync(string roleId) + { + var result = await roleManager.FindByIdAsync(roleId); + + if (result is null) return null; + + var roleModel = new RoleModel { Id = result.Id, Name = result.Name }; + + return roleModel; + } + + public async Task FindRoleByNameAsync(string roleName) + { + var result = await roleManager.FindByNameAsync(roleName); + + if (result is null) return null; + + var roleModel = new RoleModel { Id = result.Id, Name = result.Name }; + + return roleModel; + } +} diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs deleted file mode 100644 index 8546a36..0000000 --- a/src/Infrastructure/Services/UserService.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Security.Claims; -using Hutopy.Domain.Interfaces; -using Hutopy.Domain.Models; -using Hutopy.Infrastructure.Identity; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; - -namespace Hutopy.Infrastructure.Services; - -public class UserService(UserManager userManager, IHttpContextAccessor contextAccessor) : IUserService -{ - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) - { - var applicationUser = new ApplicationUser - { - UserName = userName, - Email = email, - FirstName = firstName, - LastName = lastName - }; - - //todo: Need to handle errors better for the user. - var response = await userManager.CreateAsync(applicationUser, password); - - if (response.Errors.Any()) - { - throw new InvalidOperationException(response.Errors.First().Description); - } - } - - public async Task FindUserByIdAsync(string id) - { - var response = await userManager.FindByIdAsync(id); - - if (response == null) return null; - - var userModel = new UserModel() - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email, - }; - - return userModel; - } - - public async Task GetCurrentUserAsync() - { - var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(currentUserId)) - { - return null; - } - - return await FindUserByIdAsync(currentUserId); - } - - public async Task FindUserByEmailAsync(string email) - { - var response = await userManager.FindByEmailAsync(email); - - if (response == null) return null; - - var userModel = new UserModel() - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email - }; - - return userModel; - } -} - diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index fe6aab9..f8ede38 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -1,8 +1,6 @@ using Azure.Identity; using Hutopy.Application.Common.Interfaces; -using Hutopy.Domain.Interfaces; using Hutopy.Infrastructure.Data; -using Hutopy.Infrastructure.Services; using Hutopy.Web.Services; using Microsoft.AspNetCore.Mvc; using NSwag; @@ -17,8 +15,6 @@ public static class DependencyInjection services.AddDatabaseDeveloperPageExceptionFilter(); services.AddScoped(); - services.AddScoped(); - services.AddHttpContextAccessor(); diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index 1cbd880..32b8acc 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -1,6 +1,5 @@ using Hutopy.Application.Users.Commands; using Hutopy.Application.Users.Queries.GetMinimalUser; -using Hutopy.Domain.Interfaces; using Hutopy.Infrastructure.Identity; namespace Hutopy.Web.Endpoints; @@ -15,9 +14,8 @@ public class Users : EndpointGroupBase .MapIdentityApi(); } - private static async Task CreateUser(ISender sender, CreateUserCommand command, IUserService userService) + private static async Task CreateUser(ISender sender, CreateUserCommand command) { - await userService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password); return await sender.Send(command); } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index b2b34db..646e478 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,8 +1,6 @@ using Hutopy.Application; -using Hutopy.Domain.Interfaces; using Hutopy.Infrastructure; using Hutopy.Infrastructure.Data; -using Hutopy.Infrastructure.Services; using Hutopy.Web; using Azure.Identity; @@ -47,8 +45,6 @@ builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); -builder.Services.AddScoped(); - var app = builder.Build(); app.UseCors("AllowAll"); diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 1b42f2f..e9729d9 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -711,6 +711,12 @@ "$ref": "#/components/schemas/UserTransactionDto" } }, + "userRoles": { + "type": "array", + "items": { + "type": "string" + } + }, "totalBalance": { "type": "number", "format": "decimal"