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