0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 18:42:56 +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

@ -63,6 +63,11 @@ public class ApiController : ControllerBase
return HttpStatusCode.NotFound; return HttpStatusCode.NotFound;
} }
if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.InsufficientPermissions))
{
return HttpStatusCode.Forbidden;
}
return HttpStatusCode.BadRequest; return HttpStatusCode.BadRequest;
} }
} }

View File

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

View File

@ -1,7 +1,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text; using System.Text;
using CleanArchitecture.Application.Extensions; using CleanArchitecture.Application.Extensions;
using CleanArchitecture.Domain.Extensions; using CleanArchitecture.Domain.Extensions;
using CleanArchitecture.Domain.Settings;
using CleanArchitecture.gRPC; using CleanArchitecture.gRPC;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions; using CleanArchitecture.Infrastructure.Extensions;
@ -46,6 +46,11 @@ builder.Services.AddCommandHandlers();
builder.Services.AddNotificationHandlers(); builder.Services.AddNotificationHandlers();
builder.Services.AddApiUser(); builder.Services.AddApiUser();
builder.Services
.AddOptions<TokenSettings>()
.Bind(builder.Configuration.GetSection("Auth"))
.ValidateOnStart();
builder.Services.AddMediatR(cfg => builder.Services.AddMediatR(cfg =>
{ {
cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly);

View File

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

View File

@ -15,10 +15,12 @@ namespace CleanArchitecture.Application.Services;
public sealed class UserService : IUserService public sealed class UserService : IUserService
{ {
private readonly IMediatorHandler _bus; private readonly IMediatorHandler _bus;
private readonly IUser _user;
public UserService(IMediatorHandler bus) public UserService(IMediatorHandler bus, IUser user)
{ {
_bus = bus; _bus = bus;
_user = user;
} }
public async Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted) public async Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted)
@ -26,6 +28,11 @@ public sealed class UserService : IUserService
return await _bus.QueryAsync(new GetUserByIdQuery(userId, 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() public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()
{ {
return await _bus.QueryAsync(new GetAllUsersQuery()); return await _bus.QueryAsync(new GetAllUsersQuery());

View File

@ -1,4 +1,5 @@
using System; using System;
using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Application.ViewModels.Users; namespace CleanArchitecture.Application.ViewModels.Users;
@ -6,4 +7,5 @@ public sealed record UpdateUserViewModel(
Guid Id, Guid Id,
string Email, string Email,
string Surname, 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

@ -45,6 +45,18 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
return; 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); var passwordHash = BC.HashPassword(request.Password);
var user = new User( var user = new User(

View File

@ -1,4 +1,5 @@
using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Extensions.Validation;
using FluentValidation; using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.CreateUser; namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
@ -11,6 +12,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
AddRuleForEmail(); AddRuleForEmail();
AddRuleForSurname(); AddRuleForSurname();
AddRuleForGivenName(); AddRuleForGivenName();
AddRuleForPassword();
} }
private void AddRuleForId() private void AddRuleForId()
@ -53,4 +55,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
.WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength) .WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength)
.WithMessage("Given name may not be longer than 100 characters"); .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( new DomainNotification(
request.MessageType, request.MessageType,
$"No permission to delete user {request.UserId}", $"No permission to delete user {request.UserId}",
ErrorCodes.Unauthorized)); ErrorCodes.InsufficientPermissions));
return; 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( new DomainNotification(
request.MessageType, request.MessageType,
$"No permission to update user {request.UserId}", $"No permission to update user {request.UserId}",
ErrorCodes.Unauthorized)); ErrorCodes.InsufficientPermissions));
return; return;
} }

View File

@ -11,6 +11,15 @@ public static class DomainErrorCodes
public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH"; public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL"; 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 // User
public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; 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 CommitFailed = "COMMIT_FAILED";
public const string ObjectNotFound = "OBJECT_NOT_FOUND"; 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.Entities;
using CleanArchitecture.Domain.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -22,5 +24,19 @@ public sealed class UserConfiguration : IEntityTypeConfiguration<User>
.Property(user => user.Surname) .Property(user => user.Surname)
.IsRequired() .IsRequired()
.HasMaxLength(100); .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));
} }
} }