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:
parent
5214c7c4ed
commit
d89e44b8df
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
@ -0,0 +1,6 @@
|
||||
namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
||||
|
||||
public sealed class ChangePasswordCommand
|
||||
{
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
|
||||
|
||||
public sealed class LoginCommand
|
||||
{
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
8
CleanArchitecture.Domain/Settings/TokenSettings.cs
Normal file
8
CleanArchitecture.Domain/Settings/TokenSettings.cs
Normal 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!;
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user