From d89e44b8dfec2305c01c3e5ce36d4852a37afbb5 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 20 Mar 2023 21:33:56 +0100 Subject: [PATCH] Refactor the user endpoints --- .../Controllers/ApiController.cs | 5 +++ .../Controllers/UserController.cs | 8 +++++ CleanArchitecture.Api/Program.cs | 7 +++- .../Interfaces/IUserService.cs | 1 + .../Services/UserService.cs | 9 +++++- .../viewmodels/Users/UpdateUserViewModel.cs | 4 ++- .../ChangePassword/ChangePasswordCommand.cs | 6 ++++ .../CreateUser/CreateUserCommandHandler.cs | 12 +++++++ .../CreateUser/CreateUserCommandValidation.cs | 8 +++++ .../DeleteUser/DeleteUserCommandHandler.cs | 2 +- .../Users/LoginUser/LoginUserCommand.cs | 6 ++++ .../UpdateUser/UpdateUserCommandHandler.cs | 2 +- .../Errors/DomainErrorCodes.cs | 9 ++++++ CleanArchitecture.Domain/Errors/ErrorCodes.cs | 2 +- .../Extensions/Validation/CustomValidator.cs | 32 +++++++++++++++++++ .../Settings/TokenSettings.cs | 8 +++++ .../Configurations/UserConfiguration.cs | 16 ++++++++++ 17 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs create mode 100644 CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs create mode 100644 CleanArchitecture.Domain/Settings/TokenSettings.cs diff --git a/CleanArchitecture.Api/Controllers/ApiController.cs b/CleanArchitecture.Api/Controllers/ApiController.cs index ddb8cd0..a191d22 100644 --- a/CleanArchitecture.Api/Controllers/ApiController.cs +++ b/CleanArchitecture.Api/Controllers/ApiController.cs @@ -62,6 +62,11 @@ public class ApiController : ControllerBase { return HttpStatusCode.NotFound; } + + if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.InsufficientPermissions)) + { + return HttpStatusCode.Forbidden; + } return HttpStatusCode.BadRequest; } diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index ff9aa64..ba98cac 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -40,6 +40,14 @@ public class UserController : ApiController return Response(user); } + [Authorize] + [HttpGet("me")] + public async Task GetCurrentUserAsync() + { + var user = await _userService.GetCurrentUserAsync(); + return Response(user); + } + [HttpPost] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) { diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 87dc4ee..5288d7d 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,7 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; using System.Text; using CleanArchitecture.Application.Extensions; using CleanArchitecture.Domain.Extensions; +using CleanArchitecture.Domain.Settings; using CleanArchitecture.gRPC; using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Extensions; @@ -46,6 +46,11 @@ builder.Services.AddCommandHandlers(); builder.Services.AddNotificationHandlers(); builder.Services.AddApiUser(); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection("Auth")) + .ValidateOnStart(); + builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 5f65254..85335d0 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -8,6 +8,7 @@ namespace CleanArchitecture.Application.Interfaces; public interface IUserService { public Task GetUserByUserIdAsync(Guid userId, bool isDeleted); + public Task GetCurrentUserAsync(); public Task> GetAllUsersAsync(); public Task CreateUserAsync(CreateUserViewModel user); public Task UpdateUserAsync(UpdateUserViewModel user); diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 4204870..cd622f5 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -15,16 +15,23 @@ namespace CleanArchitecture.Application.Services; public sealed class UserService : IUserService { private readonly IMediatorHandler _bus; + private readonly IUser _user; - public UserService(IMediatorHandler bus) + public UserService(IMediatorHandler bus, IUser user) { _bus = bus; + _user = user; } public async Task GetUserByUserIdAsync(Guid userId, bool isDeleted) { return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted)); } + + public async Task GetCurrentUserAsync() + { + return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false)); + } public async Task> GetAllUsersAsync() { diff --git a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs index e64fd7d..137d30f 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Application.ViewModels.Users; @@ -6,4 +7,5 @@ public sealed record UpdateUserViewModel( Guid Id, string Email, string Surname, - string GivenName); \ No newline at end of file + string GivenName, + UserRole Role); \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs new file mode 100644 index 0000000..45f6aca --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; + +public sealed class ChangePasswordCommand +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 234364c..2f7f511 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -44,6 +44,18 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, DomainErrorCodes.UserAlreadyExists)); return; } + + existingUser = await _userRepository.GetByEmailAsync(request.Email); + + if (existingUser != null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + request.MessageType, + $"There is already a User with Email {request.Email}", + DomainErrorCodes.UserAlreadyExists)); + return; + } var passwordHash = BC.HashPassword(request.Password); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index 3c14aef..e1b7ed9 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -1,4 +1,5 @@ using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Extensions.Validation; using FluentValidation; namespace CleanArchitecture.Domain.Commands.Users.CreateUser; @@ -11,6 +12,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Password) + .Password(); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index f06cf41..366c240 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -53,7 +53,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, $"No permission to delete user {request.UserId}", - ErrorCodes.Unauthorized)); + ErrorCodes.InsufficientPermissions)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs new file mode 100644 index 0000000..19f2c9b --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Domain.Commands.Users.LoginUser; + +public sealed class LoginCommand +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index a5aff0e..bbf140c 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -53,7 +53,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, $"No permission to update user {request.UserId}", - ErrorCodes.Unauthorized)); + ErrorCodes.InsufficientPermissions)); return; } diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 045b455..9121d0b 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -11,6 +11,15 @@ public static class DomainErrorCodes public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH"; public const string UserInvalidEmail = "USER_INVALID_EMAIL"; + // User Password Validation + public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; + public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS"; + public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS"; + public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER"; + public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER"; + public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; + public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; + // User public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/ErrorCodes.cs b/CleanArchitecture.Domain/Errors/ErrorCodes.cs index 427c50d..6cc6fd7 100644 --- a/CleanArchitecture.Domain/Errors/ErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/ErrorCodes.cs @@ -4,5 +4,5 @@ public static class ErrorCodes { public const string CommitFailed = "COMMIT_FAILED"; public const string ObjectNotFound = "OBJECT_NOT_FOUND"; - public const string Unauthorized = "UNAUTHORIZED"; + public const string InsufficientPermissions = "UNAUTHORIZED"; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs new file mode 100644 index 0000000..ecacf87 --- /dev/null +++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs @@ -0,0 +1,32 @@ +using System.Text.RegularExpressions; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Extensions.Validation; + +public static class CustomValidator +{ + public static IRuleBuilderOptions StringMustBeBase64(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(x => IsBase64String(x)); + } + + private static bool IsBase64String(string base64) + { + base64 = base64.Trim(); + return base64.Length % 4 == 0 && Regex.IsMatch(base64, @"^[a-zA-Z0-9\+/]*={0,3}$", RegexOptions.None); + } + + public static IRuleBuilder Password(this IRuleBuilder ruleBuilder, int minLength = 8, int maxLength = 50) + { + var options = ruleBuilder + .NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword) + .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword) + .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword) + .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword) + .Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword) + .Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword) + .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword); + return options; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Settings/TokenSettings.cs b/CleanArchitecture.Domain/Settings/TokenSettings.cs new file mode 100644 index 0000000..96cbe01 --- /dev/null +++ b/CleanArchitecture.Domain/Settings/TokenSettings.cs @@ -0,0 +1,8 @@ +namespace CleanArchitecture.Domain.Settings; + +public sealed class TokenSettings +{ + public string Issuer { get; set; } = null!; + public string Audience { get; set; } = null!; + public string Secret { get; set; } = null!; +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs index 2723e12..6fe7426 100644 --- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs @@ -1,4 +1,6 @@ +using System; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -22,5 +24,19 @@ public sealed class UserConfiguration : IEntityTypeConfiguration .Property(user => user.Surname) .IsRequired() .HasMaxLength(100); + + builder + .Property(user => user.Password) + .IsRequired() + .HasMaxLength(128); + + builder.HasData(new User( + Guid.NewGuid(), + "admin@email.com", + "Admin", + "User", + // !Password123# + "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + UserRole.Admin)); } } \ No newline at end of file