astral-api/Astral.Services/Services/UserService.cs

189 lines
6.6 KiB
C#
Raw Normal View History

2024-12-11 21:36:30 +01:00
// <copyright file="UserService.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Constants;
using Astral.Core.Entities;
using Astral.Core.Exceptions;
using Astral.Core.Extensions;
using Astral.Core.Infrastructure;
using Astral.Core.RepositoryInterfaces;
using Astral.Services.Dtos;
using Astral.Services.Exceptions;
using Astral.Services.Interfaces;
using Astral.Services.Options;
using Astral.Services.Validators;
using AutoMapper;
using FluentValidation;
using Injectio.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Astral.Services.Services;
/// <inheritdoc />
[RegisterScoped]
public class UserService : IUserService
{
private readonly IValidator<CreateUserDto> _createUserValidator;
private readonly ICryptographyService _cryptographyService;
private readonly IMapper _mapper;
private readonly RegistrationOptions _registrationConfiguration;
private readonly ITransactionProvider _transactionProvider;
private readonly IUserGroupService _userGroupService;
private readonly IUserRepository _userRepository;
private readonly IUserProfileRepository _userProfileRepository;
private readonly ILogger<UserService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="UserService" /> class.
/// </summary>
2024-12-11 23:12:53 +01:00
/// <param name="createUserValidator">Instance of <see cref="CreateUserValidator" />.</param>
2024-12-11 21:36:30 +01:00
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService" />.</param>
/// <param name="mapper">Instance of <see cref="IMapper" />.</param>
/// <param name="registrationConfig">Instance of <see cref="IOptions{RegistrationOptions}" />.</param>
2024-12-11 23:12:53 +01:00
/// <param name="userGroupService">Instance of <see cref="IUserGroupService" />.</param>
2024-12-11 21:36:30 +01:00
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
2024-12-11 23:12:53 +01:00
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
2024-12-11 21:51:13 +01:00
/// <param name="userProfileRepository">Instance of <see cref="IUserProfileRepository" />.</param>
2024-12-11 21:36:30 +01:00
/// <param name="logger">Instance of <see cref="ILogger" />.</param>
public UserService(
IUserRepository userRepository,
ICryptographyService cryptographyService,
IUserGroupService userGroupService,
IMapper mapper,
IValidator<CreateUserDto> createUserValidator,
IOptions<RegistrationOptions> registrationConfig,
ITransactionProvider transactionProvider,
IUserProfileRepository userProfileRepository,
ILogger<UserService> logger)
{
_userRepository = userRepository;
_cryptographyService = cryptographyService;
_userGroupService = userGroupService;
_mapper = mapper;
_createUserValidator = createUserValidator;
_registrationConfiguration = registrationConfig.Value;
_transactionProvider = transactionProvider;
_userProfileRepository = userProfileRepository;
_logger = logger;
}
/// <inheritdoc />
public async Task<UserDto> CreateNewUser(CreateUserDto createUser)
{
createUser.Username = createUser.Username.ToLower();
createUser.Email = createUser.Email.ToLowerInvariant();
await _createUserValidator.ValidateAndThrowAsync(createUser);
// Check if username is already taken.
var existingUser = await _userRepository.FindByUsername(createUser.Username);
if (existingUser is not null)
{
throw new UsernameTakenException(createUser.Username);
}
// Check if email already registered.
existingUser = await _userRepository.FindByEmailAddress(createUser.Email);
if (existingUser is not null)
{
throw new EmailAlreadyRegisteredException();
}
var user = new User
{
Id = await GetUniqueId(),
Username = createUser.Username,
EmailAddress = createUser.Email,
CreatedAt = DateTime.UtcNow,
CreatorIp = createUser.IpAddress.ToString(),
UserRole = UserRole.User
};
var salt = _cryptographyService.GenerateSalt();
var hash = _cryptographyService.HashPassword(createUser.Password, salt);
user.PasswordHash = Convert.ToBase64String(hash);
user.PasswordSalt = Convert.ToBase64String(salt);
_logger.LogInformation(
"Creating new user {username} ({email}) [{id}]",
user.Username,
user.EmailAddress.MaskEmailAddress(),
user.Id);
if (!_registrationConfiguration.RequireEmailActivation || createUser.ActivateImmediately)
{
}
using var transaction = await _transactionProvider.BeginTransactionAsync();
await _userRepository.AddAsync(user);
// Setup internal user groups.
var connectionGroup = await _userGroupService.CreateInternalGroup(
user.Id,
$"User {user.Id} connections",
$"Connections for {user.Username}");
var friendsGroup = await _userGroupService.CreateInternalGroup(
user.Id,
$"User {user.Id} friends",
$"Friends for {user.Username}");
user.ConnectionGroup = connectionGroup.Id;
user.FriendsGroup = friendsGroup.Id;
// Setup user profile.
var profile = new UserProfile()
{
Id = user.Id,
CreatedAt = DateTime.UtcNow,
HeroImageUrl = _registrationConfiguration.DefaultHeroImageUrl,
ThumbnailImageUrl = _registrationConfiguration.DefaultThumbnailImageUrl
};
await _userProfileRepository.AddAsync(profile);
await _userRepository.UpdateAsync(user);
transaction.Commit();
return _mapper.Map<UserDto>(user);
}
/// <inheritdoc />
2024-12-11 21:51:13 +01:00
public async Task<UserDto> FindById(Guid userId)
2024-12-11 21:36:30 +01:00
{
2024-12-11 21:51:13 +01:00
var user = await _userRepository.FindByIdAsync(userId);
2024-12-11 21:36:30 +01:00
return _mapper.Map<UserDto>(user);
}
/// <summary>
/// Create a unique guid.
/// </summary>
/// <returns>The new guid.</returns>
private async Task<Guid> GetUniqueId()
{
var attempt = 0;
const int maxAttempts = 10;
var newId = Guid.NewGuid();
while (await _userRepository.FindByIdAsync(newId) is not null)
{
attempt++;
if (attempt >= maxAttempts)
{
_logger.LogCritical("Unable to generate a unique guid for user group");
throw new UnexpectedErrorException();
}
newId = Guid.NewGuid();
}
return newId;
}
}