188 lines
6.6 KiB
C#
188 lines
6.6 KiB
C#
// <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>
|
|
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
|
|
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService" />.</param>
|
|
/// <param name="userGroupService">Instance of <see cref="IUserGroupService" />.</param>
|
|
/// <param name="mapper">Instance of <see cref="IMapper" />.</param>
|
|
/// <param name="createUserValidator">Instance of <see cref="CreateUserValidator" />.</param>
|
|
/// <param name="registrationConfig">Instance of <see cref="IOptions{RegistrationOptions}" />.</param>
|
|
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
|
|
/// <param name="userProfileRepository">Instance of <see cref="IUserProfileRepository" />.</param>
|
|
/// <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 />
|
|
public async Task<UserDto> FindById(Guid userId)
|
|
{
|
|
var user = await _userRepository.FindByIdAsync(userId);
|
|
|
|
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;
|
|
}
|
|
}
|