0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

Refactor the user endpoints

This commit is contained in:
alex289 2023-03-20 21:33:56 +01:00
parent 5214c7c4ed
commit d89e44b8df
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
17 changed files with 131 additions and 6 deletions

View File

@ -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;
}

View File

@ -40,6 +40,14 @@ public class UserController : ApiController
return Response(user);
}
[Authorize]
[HttpGet("me")]
public async Task<IActionResult> GetCurrentUserAsync()
{
var user = await _userService.GetCurrentUserAsync();
return Response(user);
}
[HttpPost]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
{

View File

@ -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<TokenSettings>()
.Bind(builder.Configuration.GetSection("Auth"))
.ValidateOnStart();
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly);

View File

@ -8,6 +8,7 @@ namespace CleanArchitecture.Application.Interfaces;
public interface IUserService
{
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted);
public Task<UserViewModel?> GetCurrentUserAsync();
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);

View File

@ -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<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted)
{
return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted));
}
public async Task<UserViewModel?> GetCurrentUserAsync()
{
return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false));
}
public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()
{

View File

@ -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);
string GivenName,
UserRole Role);

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
public sealed class ChangePasswordCommand
{
}

View File

@ -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);

View File

@ -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<CreateUserCo
AddRuleForEmail();
AddRuleForSurname();
AddRuleForGivenName();
AddRuleForPassword();
}
private void AddRuleForId()
@ -53,4 +55,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
.WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength)
.WithMessage("Given name may not be longer than 100 characters");
}
private void AddRuleForPassword()
{
RuleFor(cmd => cmd.Password)
.Password();
}
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
public sealed class LoginCommand
{
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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";
}

View File

@ -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<T, string> StringMustBeBase64<T>(this IRuleBuilder<T, string> 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<T, string> Password<T>(this IRuleBuilder<T, string> 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;
}
}

View File

@ -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!;
}

View File

@ -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<User>
.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));
}
}