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