// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // 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; /// [RegisterScoped] public class UserService : IUserService { private readonly IValidator _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 IUserLockerRepository _userLockerRepository; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . public UserService( IUserRepository userRepository, ICryptographyService cryptographyService, IUserGroupService userGroupService, IMapper mapper, IValidator createUserValidator, IOptions registrationConfig, ITransactionProvider transactionProvider, IUserProfileRepository userProfileRepository, IUserLockerRepository userLockerRepository, ILogger logger) { _userRepository = userRepository; _cryptographyService = cryptographyService; _userGroupService = userGroupService; _mapper = mapper; _createUserValidator = createUserValidator; _registrationConfiguration = registrationConfig.Value; _transactionProvider = transactionProvider; _userProfileRepository = userProfileRepository; _userLockerRepository = userLockerRepository; _logger = logger; } /// public async Task 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(), Role = 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) { user.State = UserState.Normal; } else { user.State = UserState.AwaitingEmailActivation; } 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(user); } /// public async Task FindById(Guid userId) { var user = await _userRepository.FindByIdAsync(userId); return _mapper.Map(user); } /// /// Create a unique guid. /// /// The new guid. private async Task 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; } }