134 lines
4.7 KiB
C#
134 lines
4.7 KiB
C#
using MikrocopApi.Dtos;
|
|
using MikrocopApi.Exceptions;
|
|
using MikrocopApi.Mappers;
|
|
using MikrocopDb.Repositories;
|
|
|
|
namespace MikrocopApi.Services;
|
|
|
|
public sealed class UserService : IUserService
|
|
{
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly IPasswordHashingService _passwordHashingService;
|
|
|
|
public UserService(IUserRepository userRepository, IPasswordHashingService passwordHashingService)
|
|
{
|
|
_userRepository = userRepository;
|
|
_passwordHashingService = passwordHashingService;
|
|
}
|
|
|
|
public async Task<UserDto> CreateAsync(CreateUserDto request, CancellationToken cancellationToken = default)
|
|
{
|
|
if (await _userRepository.GetByUserNameAsync(request.UserName, cancellationToken) is not null)
|
|
{
|
|
throw new ConflictException("UserName already exists.");
|
|
}
|
|
|
|
if (await _userRepository.GetByEmailAsync(request.Email, cancellationToken) is not null)
|
|
{
|
|
throw new ConflictException("Email already exists.");
|
|
}
|
|
|
|
ValidatePasswordComplexity(request.Password);
|
|
|
|
var entity = request.ToEntity();
|
|
var (hash, salt) = _passwordHashingService.HashPassword(request.Password);
|
|
entity.PasswordHash = hash;
|
|
entity.PasswordSalt = salt;
|
|
|
|
await _userRepository.AddAsync(entity, cancellationToken);
|
|
return entity.ToDto();
|
|
}
|
|
|
|
public async Task UpdateAsync(Guid id, UpdateUserDto request, CancellationToken cancellationToken = default)
|
|
{
|
|
var existing = await _userRepository.GetByIdAsync(id, cancellationToken)
|
|
?? throw new NotFoundException($"User with id '{id}' was not found.");
|
|
|
|
var byUserName = await _userRepository.GetByUserNameAsync(request.UserName, cancellationToken);
|
|
if (byUserName is not null && byUserName.Id != id)
|
|
{
|
|
throw new ConflictException("UserName already exists.");
|
|
}
|
|
|
|
var byEmail = await _userRepository.GetByEmailAsync(request.Email, cancellationToken);
|
|
if (byEmail is not null && byEmail.Id != id)
|
|
{
|
|
throw new ConflictException("Email already exists.");
|
|
}
|
|
|
|
existing.ApplyFromDto(request);
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.Password))
|
|
{
|
|
ValidatePasswordComplexity(request.Password);
|
|
var (hash, salt) = _passwordHashingService.HashPassword(request.Password);
|
|
existing.PasswordHash = hash;
|
|
existing.PasswordSalt = salt;
|
|
}
|
|
|
|
await _userRepository.UpdateAsync(existing, cancellationToken);
|
|
}
|
|
|
|
public async Task<UserDto> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(id, cancellationToken)
|
|
?? throw new NotFoundException($"User with id '{id}' was not found.");
|
|
|
|
return user.ToDto();
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(id, cancellationToken)
|
|
?? throw new NotFoundException($"User with id '{id}' was not found.");
|
|
|
|
await _userRepository.DeleteAsync(user, cancellationToken);
|
|
}
|
|
|
|
public async Task<bool> ValidatePasswordAsync(Guid id, string password, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(id, cancellationToken)
|
|
?? throw new NotFoundException($"User with id '{id}' was not found.");
|
|
|
|
return _passwordHashingService.VerifyPassword(password, user.PasswordHash, user.PasswordSalt);
|
|
}
|
|
|
|
private static void ValidatePasswordComplexity(string password)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(password))
|
|
{
|
|
throw new BadRequestException("Password is required.");
|
|
}
|
|
|
|
if (password.Length < 12)
|
|
{
|
|
throw new BadRequestException("Password must be at least 12 characters long.");
|
|
}
|
|
|
|
if (password.Any(char.IsWhiteSpace))
|
|
{
|
|
throw new BadRequestException("Password must not contain whitespace.");
|
|
}
|
|
|
|
if (!password.Any(char.IsUpper))
|
|
{
|
|
throw new BadRequestException("Password must contain at least one uppercase letter.");
|
|
}
|
|
|
|
if (!password.Any(char.IsLower))
|
|
{
|
|
throw new BadRequestException("Password must contain at least one lowercase letter.");
|
|
}
|
|
|
|
if (!password.Any(char.IsDigit))
|
|
{
|
|
throw new BadRequestException("Password must contain at least one digit.");
|
|
}
|
|
|
|
if (!password.Any(ch => !char.IsLetterOrDigit(ch)))
|
|
{
|
|
throw new BadRequestException("Password must contain at least one special character.");
|
|
}
|
|
}
|
|
}
|