Merge remote-tracking branch 'origin/master' into feature/oauth

# Conflicts:
#	src/Application/Common/Interfaces/IIdentityService.cs
#	src/Application/Users/Commands/CreateUser.cs
#	src/Infrastructure/Identity/IdentityService.cs
#	src/Infrastructure/Services/UserService.cs
#	src/Web/Program.cs
This commit is contained in:
Dominic Villemure
2024-06-09 18:05:49 -04:00
21 changed files with 259 additions and 61 deletions

View File

@@ -6,10 +6,15 @@ namespace Hutopy.Application.Common.Interfaces;
public interface IIdentityService public interface IIdentityService
{ {
Task<string?> GetUserNameAsync(string userId); Task<string?> GetUserNameAsync(string userId);
Task<Result> CreateUserAsync(string email, string userName, string firstName, string lastName, string password);
Task<UserModel?> FindUserByIdAsync(string id);
Task<UserModel?> GetCurrentUserAsync();
Task<UserModel?> FindUserByEmailAsync(string id);
Task<UserModel?> GetUserByUserNameAsync(string userName);
Task<bool> IsInRoleAsync(string userId, string role); Task<bool> IsInRoleAsync(string userId, string role);
Task<bool> AuthorizeAsync(string userId, string policyName); Task<bool> AuthorizeAsync(string userId, string policyName);
Task<Result> AddRoleAsync(string userId, string role);
Task<IList<string>> GetCurrentUserRolesAsync();
Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password); Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);

View File

@@ -0,0 +1,10 @@
using Hutopy.Application.Common.Models;
namespace Hutopy.Application.Common.Interfaces;
public interface IRoleService
{
public Task<Result> CreateRoleAsync(string roleName);
public Task<Result> DeleteRoleAsync(string roleName);
public Task<RoleModel?> FindRoleByIdAsync(string roleId);
}

View File

@@ -0,0 +1,7 @@
namespace Hutopy.Application.Common.Models;
public class RoleModel
{
public string? Id { get; set; }
public string? Name { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace Hutopy.Domain.Models; namespace Hutopy.Application.Common.Models;
public class UserModel public class UserModel
{ {

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using Hutopy.Application.Common.Behaviours;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Hutopy.Application; namespace Hutopy.Application;
@@ -15,7 +16,7 @@ public static class DependencyInjection
{ {
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); //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(ValidationBehaviour<,>));
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); //cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
}); });

View File

@@ -2,9 +2,11 @@
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Mappings; using Hutopy.Application.Common.Mappings;
using Hutopy.Application.Common.Models; using Hutopy.Application.Common.Models;
using Hutopy.Application.Common.Security;
namespace Hutopy.Application.FutureCreators.Queries; namespace Hutopy.Application.FutureCreators.Queries;
[Authorize(Roles = "Administrator")]
public record GetFutureCreatorListQuery : IRequest<PaginatedList<FutureCreatorListDto>> public record GetFutureCreatorListQuery : IRequest<PaginatedList<FutureCreatorListDto>>
{ {
public int PageNumber { get; init; } = 1; public int PageNumber { get; init; } = 1;

View File

@@ -1,6 +1,4 @@
using System.Dynamic; using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Domain.Interfaces;
namespace Hutopy.Application.Users.Commands; namespace Hutopy.Application.Users.Commands;
public record CreateUserCommand : IRequest<Guid> public record CreateUserCommand : IRequest<Guid>
@@ -15,24 +13,22 @@ public record CreateUserCommand : IRequest<Guid>
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid> public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{ {
private readonly IApplicationDbContext _context; 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; _context = context;
_userService = userService; _identityService = identityService;
} }
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{ {
// Dont really need the handler for the create. The get will work like this : await _identityService.CreateUserAsync(request.EmailAddress, request.UserName, request.FirstName, request.LastName, request.Password);
var user = await _userService.FindUserByIdAsync("072ae7d5-8c4a-4a0f-b250-7d39941125cb");
// var user2 = await _userService.FindUserByEmailAsync("test10@hotmail.com");
var tt = user?.FirstName; var user = await _identityService.FindUserByEmailAsync(request.EmailAddress);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
return Guid.NewGuid(); return new Guid(user?.Id ?? string.Empty);
} }
} }

View File

@@ -1,34 +1,39 @@
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Domain.Interfaces;
namespace Hutopy.Application.Users.Queries; namespace Hutopy.Application.Users.Queries.GetCurrentUser;
public record GetCurrentUserQuery : IRequest<UserDto>; public record GetCurrentUserQuery : IRequest<UserDto>;
public class GetCurrentUserQueryHandler( public class GetCurrentUserQueryHandler(
IApplicationDbContext context, IApplicationDbContext context,
IMapper mapper, IMapper mapper,
IUserService userService IIdentityService identityService
) )
: IRequestHandler<GetCurrentUserQuery, UserDto> : IRequestHandler<GetCurrentUserQuery, UserDto>
{ {
public async Task<UserDto> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) public async Task<UserDto> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken)
{ {
var identityUser = await userService.GetCurrentUserAsync(); var identityUser = await identityService.GetCurrentUserAsync();
var currentUserId = new Guid(identityUser?.Id ?? ""); var currentUserId = new Guid(identityUser?.Id ?? "");
var transactions = await context.UserTransactions var transactions = await context.UserTransactions
.Where(x => x.ApplicationUserId == currentUserId.ToString()) .Where(x => x.ApplicationUserId == currentUserId.ToString())
.OrderBy(x => x.LastModified) .OrderBy(x => x.LastModified)
.ProjectTo<UserTransactionDto>(mapper.ConfigurationProvider) .ProjectTo<UserTransactionDto>(mapper.ConfigurationProvider)
.Where(x => x.IsConfirmed == true)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var user = new UserDto() var roles = await identityService.GetCurrentUserRolesAsync();
var user = new UserDto
{ {
Id = currentUserId, Id = currentUserId,
FirstName = identityUser?.FirstName ?? "", FirstName = identityUser?.FirstName ?? "",
LastName = identityUser?.LastName ?? "", LastName = identityUser?.LastName ?? "",
UserTransactions = transactions UserName =identityUser?.UserName ?? "",
UserTransactions = transactions,
TotalBalance = transactions.Sum(x => x.Amount),
UserRoles = roles
}; };
return user; return user;

View File

@@ -1,13 +1,14 @@
namespace Hutopy.Application.Users.Queries; namespace Hutopy.Application.Users.Queries.GetCurrentUser;
public class UserDto public class UserDto
{ {
public Guid Id { get; init; } public Guid Id { get; init; }
public required string FirstName { get; init; } public required string FirstName { get; init; }
public required string LastName { get; init; } public required string LastName { get; init; }
public string UserName { get; init; } = String.Empty; public string UserName { get; init; } = String.Empty;
public List<UserTransactionDto> UserTransactions { get; init; } = []; public List<UserTransactionDto> UserTransactions { get; init; } = [];
public IList<string> UserRoles { get; init; } = [];
public required decimal TotalBalance { get; init; }
} }

View File

@@ -1,15 +1,18 @@
using Hutopy.Domain.Entities; using Hutopy.Domain.Entities;
namespace Hutopy.Application.Users.Queries; namespace Hutopy.Application.Users.Queries.GetCurrentUser;
public class UserTransactionDto public class UserTransactionDto
{ {
public required decimal Amount { get; init; } public required decimal Amount { get; init; }
public string Currency { get; init; } = "cad"; public string Currency { get; init; } = "cad";
public string TipMessage { get; init; } = string.Empty; public string TipMessage { get; init; } = string.Empty;
public DateTimeOffset Created { get; init; }
public bool IsConfirmed { get; init; }
private class Mapping : Profile private class Mapping : Profile
{ {
public Mapping() public Mapping()

View File

@@ -1,4 +1,4 @@
using Hutopy.Domain.Interfaces; using Hutopy.Application.Common.Interfaces;
namespace Hutopy.Application.Users.Queries.GetMinimalUser; namespace Hutopy.Application.Users.Queries.GetMinimalUser;
@@ -8,15 +8,15 @@ public record GetMinimalUserQuery : IRequest<MinimalUserDto>
}; };
public class GetMinimalUserQueryHandler( public class GetMinimalUserQueryHandler(
IUserService userService IIdentityService identityService
) )
: IRequestHandler<GetMinimalUserQuery, MinimalUserDto> : IRequestHandler<GetMinimalUserQuery, MinimalUserDto>
{ {
public async Task<MinimalUserDto> Handle(GetMinimalUserQuery request, CancellationToken cancellationToken) public async Task<MinimalUserDto> 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 ?? "", FirstName = identityUser?.FirstName ?? "",
LastName = identityUser?.LastName ?? "", LastName = identityUser?.LastName ?? "",

View File

@@ -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<UserModel?> FindUserByIdAsync(string id);
Task<UserModel?> GetCurrentUserAsync();
Task<UserModel?> FindUserByEmailAsync(string id);
}

View File

@@ -1,11 +1,8 @@
using System; using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Domain.Constants; using Hutopy.Domain.Constants;
using Hutopy.Domain.Interfaces;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Data.Interceptors; using Hutopy.Infrastructure.Data.Interceptors;
using Hutopy.Infrastructure.Identity; using Hutopy.Infrastructure.Identity;
using Hutopy.Infrastructure.Services;
using Hutopy.Infrastructure.Stripe; using Hutopy.Infrastructure.Stripe;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -51,8 +48,6 @@ public static class DependencyInjection
.AddBearerToken(IdentityConstants.BearerScheme); .AddBearerToken(IdentityConstants.BearerScheme);
services.AddAuthorizationBuilder(); services.AddAuthorizationBuilder();
services.AddScoped<IUserService, UserService>();
// Might need to change and use AddIdentity<User, Role>() when we need to integrate connection via third party ( facebook, google ) // Might need to change and use AddIdentity<User, Role>() when we need to integrate connection via third party ( facebook, google )
services services
@@ -62,7 +57,7 @@ public static class DependencyInjection
.AddApiEndpoints(); .AddApiEndpoints();
services.AddSingleton(TimeProvider.System); services.AddSingleton(TimeProvider.System);
services.AddTransient<IIdentityService, IdentityService>(); services.AddScoped<IIdentityService, IdentityService>();
services.AddTransient<IStripeService, StripeService>(); services.AddTransient<IStripeService, StripeService>();
services.AddAuthorization(options => services.AddAuthorization(options =>

View File

@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Infrastructure.Identity;
public class ApplicationRole : IdentityRole
{
}

View File

@@ -1,7 +1,9 @@
using Google.Apis.Oauth2.v2.Data; using Google.Apis.Oauth2.v2.Data;
using System.Security.Claims;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models; using Hutopy.Application.Common.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Hutopy.Infrastructure.Identity; namespace Hutopy.Infrastructure.Identity;
@@ -9,7 +11,9 @@ namespace Hutopy.Infrastructure.Identity;
public class IdentityService( public class IdentityService(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory, IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,
IAuthorizationService authorizationService) IAuthorizationService authorizationService,
IHttpContextAccessor contextAccessor
)
: IIdentityService : IIdentityService
{ {
public async Task<string?> GetUserNameAsync(string userId) public async Task<string?> GetUserNameAsync(string userId)
@@ -18,6 +22,24 @@ public class IdentityService(
return user?.UserName; return user?.UserName;
} }
public async Task<UserModel?> GetUserByUserNameAsync(string userName)
{
var response = await userManager.FindByNameAsync(userName);
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<(Result Result, string UserId)> CreateUserAsync(string userName, string password) public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password)
{ {
@@ -48,6 +70,68 @@ public class IdentityService(
return (result.ToApplicationResult(), user.Id); return (result.ToApplicationResult(), user.Id);
} }
public async Task<Result> 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<UserModel?> 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<UserModel?> GetCurrentUserAsync()
{
var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(currentUserId))
{
return null;
}
return await FindUserByIdAsync(currentUserId);
}
public async Task<UserModel?> 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<bool> IsInRoleAsync(string userId, string role) public async Task<bool> IsInRoleAsync(string userId, string role)
{ {
@@ -55,6 +139,14 @@ public class IdentityService(
return user != null && await userManager.IsInRoleAsync(user, role); return user != null && await userManager.IsInRoleAsync(user, role);
} }
public async Task<bool> 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<bool> AuthorizeAsync(string userId, string policyName) public async Task<bool> AuthorizeAsync(string userId, string policyName)
{ {
@@ -85,4 +177,32 @@ public class IdentityService(
return result.ToApplicationResult(); return result.ToApplicationResult();
} }
public async Task<Result> 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<IList<string>> 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;
}
} }

View File

@@ -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<ApplicationRole> roleManager
)
: IRoleService
{
public async Task<Result> 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<Result> DeleteRoleAsync(string roleName)
{
var role = new ApplicationRole { Name = roleName };
var result = await roleManager.DeleteAsync(role);
return result.ToApplicationResult();
}
public async Task<RoleModel?> 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<RoleModel?> 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;
}
}

View File

@@ -1,8 +1,6 @@
using Azure.Identity; using Azure.Identity;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Domain.Interfaces;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Services;
using Hutopy.Web.Services; using Hutopy.Web.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag; using NSwag;
@@ -17,8 +15,6 @@ public static class DependencyInjection
services.AddDatabaseDeveloperPageExceptionFilter(); services.AddDatabaseDeveloperPageExceptionFilter();
services.AddScoped<IUser, CurrentUser>(); services.AddScoped<IUser, CurrentUser>();
services.AddScoped<IUserService, UserService>();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();

View File

@@ -1,4 +1,5 @@
using Hutopy.Application.Users.Queries; using Hutopy.Application.Users.Queries;
using Hutopy.Application.Users.Queries.GetCurrentUser;
namespace Hutopy.Web.Endpoints; namespace Hutopy.Web.Endpoints;

View File

@@ -1,6 +1,5 @@
using Hutopy.Application.Users.Commands; using Hutopy.Application.Users.Commands;
using Hutopy.Application.Users.Queries.GetMinimalUser; using Hutopy.Application.Users.Queries.GetMinimalUser;
using Hutopy.Domain.Interfaces;
using Hutopy.Infrastructure.Identity; using Hutopy.Infrastructure.Identity;
namespace Hutopy.Web.Endpoints; namespace Hutopy.Web.Endpoints;
@@ -15,9 +14,8 @@ public class Users : EndpointGroupBase
.MapIdentityApi<ApplicationUser>(); .MapIdentityApi<ApplicationUser>();
} }
private static async Task<Guid> CreateUser(ISender sender, CreateUserCommand command, IUserService userService) private static async Task<Guid> CreateUser(ISender sender, CreateUserCommand command)
{ {
await userService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password);
return await sender.Send(command); return await sender.Send(command);
} }

View File

@@ -1,8 +1,6 @@
using Hutopy.Application; using Hutopy.Application;
using Hutopy.Domain.Interfaces;
using Hutopy.Infrastructure; using Hutopy.Infrastructure;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Services;
using Hutopy.Web; using Hutopy.Web;
using Azure.Identity; using Azure.Identity;
using Hutopy.Infrastructure.Identity; using Hutopy.Infrastructure.Identity;

View File

@@ -736,6 +736,16 @@
"items": { "items": {
"$ref": "#/components/schemas/UserTransactionDto" "$ref": "#/components/schemas/UserTransactionDto"
} }
},
"userRoles": {
"type": "array",
"items": {
"type": "string"
}
},
"totalBalance": {
"type": "number",
"format": "decimal"
} }
} }
}, },
@@ -752,6 +762,13 @@
}, },
"tipMessage": { "tipMessage": {
"type": "string" "type": "string"
},
"created": {
"type": "string",
"format": "date-time"
},
"isConfirmed": {
"type": "boolean"
} }
} }
}, },