diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 9686bbc..b816ad7 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -1,21 +1,17 @@ using Hutopy.Application.Common.Models; -using Hutopy.Domain.Models; 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 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 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 38cc962..54077eb 100644 --- a/src/Application/Users/Commands/CreateUser.cs +++ b/src/Application/Users/Commands/CreateUser.cs @@ -23,6 +23,8 @@ public class CreateUserCommandHandler : IRequestHandler public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { + await _identityService.CreateUserAsync(request.EmailAddress, request.UserName, request.FirstName, request.LastName, request.Password); + var user = await _identityService.FindUserByEmailAsync(request.EmailAddress); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index d621ab5..49f1152 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -23,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/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 c9cc75a..618b647 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,7 +1,6 @@ using System.Security.Claims; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; -using Hutopy.Domain.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -36,7 +35,7 @@ public class IdentityService( return (result.ToApplicationResult(), user.Id); } - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) + public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { var applicationUser = new ApplicationUser { @@ -46,13 +45,9 @@ public class IdentityService( 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); - } + + return response.ToApplicationResult(); } public async Task FindUserByIdAsync(string id) @@ -108,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) { @@ -138,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/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index f58b366..32b8acc 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -1,5 +1,4 @@ -using Hutopy.Application.Common.Interfaces; -using Hutopy.Application.Users.Commands; +using Hutopy.Application.Users.Commands; using Hutopy.Application.Users.Queries.GetMinimalUser; using Hutopy.Infrastructure.Identity; @@ -15,9 +14,8 @@ public class Users : EndpointGroupBase .MapIdentityApi(); } - private static async Task CreateUser(ISender sender, CreateUserCommand command, IIdentityService identityService) + private static async Task CreateUser(ISender sender, CreateUserCommand command) { - await identityService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password); return await sender.Send(command); } 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"