Add initial implementation of API, database, and user management components.
This commit is contained in:
133
MikrocopApi/Services/UserService.cs
Normal file
133
MikrocopApi/Services/UserService.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user