diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/CreateUserCommandHandler.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/CreateUserCommandHandler.cs index 0f495493..251016e3 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/CreateUserCommandHandler.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/CreateUserCommandHandler.cs @@ -9,15 +9,26 @@ namespace VirtoCommerce.ProfileExperienceApiModule.Data.Commands public class CreateUserCommandHandler : IRequestHandler { private readonly IAccountService _accountService; + private readonly IMediator _mediator; - public CreateUserCommandHandler(IAccountService accountService) + public CreateUserCommandHandler(IAccountService accountService, IMediator mediator) { _accountService = accountService; + _mediator = mediator; } - public virtual Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + public virtual async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { - return _accountService.CreateAccountAsync(request.ApplicationUser); + var result = await _accountService.CreateAccountAsync(request.ApplicationUser); + + if (result.Succeeded) + { + // Send Email Verification + await _mediator.Send(new SendVerifyEmailCommand(request.ApplicationUser.StoreId, string.Empty, request.ApplicationUser.Email), cancellationToken); + + } + + return result; } } } diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/RegisterRequestCommandHandler.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/RegisterRequestCommandHandler.cs index e5fd0963..ad8b959c 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/RegisterRequestCommandHandler.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/RegisterRequestCommandHandler.cs @@ -34,6 +34,8 @@ namespace VirtoCommerce.ProfileExperienceApiModule.Data.Commands { public class RegisterRequestCommandHandler : IRequestHandler { + private const string UserType = "Manager"; + private readonly IMapper _mapper; private readonly IDynamicPropertyUpdaterService _dynamicPropertyUpdater; private readonly IMemberService _memberService; @@ -46,9 +48,13 @@ public class RegisterRequestCommandHandler : IRequestHandler _securityOptions; + private readonly IMediator _mediator; + + protected Store CurrentStore { get; private set; } + protected string DefaultContactStatus { get; private set; } + protected string DefaultOrganizationStatus { get; private set; } + protected Role MaintainerRole { get; private set; } - private const string Creator = "frontend"; - private const string UserType = "Manager"; #pragma warning disable S107 public RegisterRequestCommandHandler(IMapper mapper, IDynamicPropertyUpdaterService dynamicPropertyUpdater, @@ -61,7 +67,8 @@ public RegisterRequestCommandHandler(IMapper mapper, AccountValidator accountValidator, AddressValidator addressValidator, OrganizationValidator organizationValidator, - IOptions securityOptions) + IOptions securityOptions, + IMediator mediator) #pragma warning restore S107 { _mapper = mapper; @@ -76,6 +83,7 @@ public RegisterRequestCommandHandler(IMapper mapper, _addressValidator = addressValidator; _organizationValidator = organizationValidator; _securityOptions = securityOptions; + _mediator = mediator; } public virtual async Task Handle(RegisterRequestCommand request, CancellationToken cancellationToken) @@ -83,7 +91,17 @@ public virtual async Task Handle(RegisterRequestComm var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var internalToken = cancellationTokenSource.Token; - var result = await ProcessRequestAsync(request, cancellationTokenSource); + var result = new RegisterOrganizationResult(); + + await BeforeProcessRequestAsync(request, result, cancellationTokenSource); + if (internalToken.IsCancellationRequested) + { + return result; + } + + await ProcessRequestAsync(request, result, cancellationTokenSource); + + await AfterProcessRequestAsync(request, result, cancellationTokenSource); if (internalToken.IsCancellationRequested) { @@ -93,28 +111,107 @@ public virtual async Task Handle(RegisterRequestComm return result; } + protected virtual Task AfterProcessRequestAsync(RegisterRequestCommand request, RegisterOrganizationResult result, CancellationTokenSource tokenSource) + { + return Task.CompletedTask; + } + + protected virtual async Task BeforeProcessRequestAsync(RegisterRequestCommand request, RegisterOrganizationResult result, CancellationTokenSource tokenSource) + { + // Resolve Current Store + CurrentStore = await _storeService.GetByIdAsync(request.StoreId); + + if (CurrentStore == null) + { + SetErrorResult(result, "Store not found", $"Store {request.StoreId} has not been found", tokenSource); + return; + } + + // Read Settings + DefaultContactStatus = CurrentStore.Settings + .GetSettingValue(CustomerCore.ModuleConstants.Settings.General.ContactDefaultStatus.Name, null); + + DefaultOrganizationStatus = CurrentStore.Settings + .GetSettingValue(CustomerCore.ModuleConstants.Settings.General.OrganizationDefaultStatus.Name, null); + + MaintainerRole = await GetMaintainerRole(result, tokenSource); + } + #pragma warning disable S138 - private async Task ProcessRequestAsync(RegisterRequestCommand request, CancellationTokenSource tokenSource) + protected virtual async Task ProcessRequestAsync(RegisterRequestCommand request, RegisterOrganizationResult result, CancellationTokenSource tokenSource) { - var result = new RegisterOrganizationResult(); - IList roles = null; + // Map incoming enties from request to Virto Commerce enties + var account = ToApplicationUser(request.Account); - var organization = _mapper.Map(request.Organization); - var contact = _mapper.Map(request.Contact); - var account = GetApplicationUser(request.Account); + var contact = await ToContact(request.Contact, account); + account.MemberId = contact.Id; - FillContactFields(contact); + Organization organization = null; + if (request.Organization != null) + { + organization = await ToOrganization(request.Organization, contact, account); + account.Roles = new List { MaintainerRole }; + } - var validationTasks = new List> + // Validate parameters & stop processing if any error is occured + var isValid = await ValidateAsync(organization, contact, request.Account, result, tokenSource); + if (!isValid) { - _contactValidator.ValidateAsync(contact), - _accountValidator.ValidateAsync(request.Account), - _organizationValidator.ValidateAsync(organization) + return; + } + + // Create Organisation + if (organization != null) + { + await _memberService.SaveChangesAsync(new Member[] { organization }); + + // Create relation between contact and organization + contact.Organizations = new List { organization.Id }; + } + + // Create Contact + await _memberService.SaveChangesAsync(new Member[] { contact }); + + // Create Security Account + var identityResult = await _accountService.CreateAccountAsync(account); + result.AccountCreationResult = ToAccountCreationResult(identityResult, account); + + if (!identityResult.Succeeded) + { + tokenSource.Cancel(); + return; + } + + // Save data to result + result.Organization = organization; + result.Contact = contact; + result.Contact.SecurityAccounts = new List { account }; + + // Send Notifications + var notificationRequest = new RegisterOrganizationNotificationRequest + { + Store = CurrentStore, + LanguageCode = request.LanguageCode, + Organization = organization, + Contact = contact, }; + await SendRegistrationEmailNotificationAsync(notificationRequest); + + // Send Email Verification Command + await SendVerifyEmailCommand(request, account.Email, tokenSource); + } - var orgAdresses = organization?.Addresses ?? new List
(); + private async Task ValidateAsync(Organization organization, Contact contact, Account account, RegisterOrganizationResult result, CancellationTokenSource tokenSource) + { + var validationTasks = new List>(); + + validationTasks.AddRange(new Task[]{ + _organizationValidator.ValidateAsync(organization), + _contactValidator.ValidateAsync(contact), + _accountValidator.ValidateAsync(account)}); - foreach (var address in orgAdresses) + var orgAddresses = organization?.Addresses ?? new List
(); + foreach (var address in orgAddresses) { validationTasks.Add(_addressValidator.ValidateAsync(address)); } @@ -129,88 +226,91 @@ private async Task ProcessRequestAsync(RegisterReque .ToList(); SetErrorResult(result, errors, tokenSource); - return result; + return false; } - await SetDynamicPropertiesAsync(request.Contact.DynamicProperties, contact); + return true; + } - var store = await _storeService.GetByIdAsync(request.StoreId); + private async Task ToOrganization(RegisteredOrganization organization, Contact contact, ApplicationUser account) + { + var result = _mapper.Map(organization); - if (store == null) - { - SetErrorResult(result, "Store not found", $"Store {request.StoreId} has not been found", tokenSource); - return result; - } + result.Status = DefaultOrganizationStatus; + result.OwnerId = contact.Id; + result.Emails = new List { ResolveEmail(account, result.Addresses ?? new List
()) }; - if (organization != null) - { - var maintainerRole = await GetMaintainerRole(result, tokenSource); - if (maintainerRole == null) - { - return result; - } + await SetDynamicPropertiesAsync(organization.DynamicProperties, result); - roles = new List { maintainerRole }; - await SetDynamicPropertiesAsync(request.Organization.DynamicProperties, organization); - var organizationStatus = store - .Settings - .GetSettingValue(CustomerCore.ModuleConstants.Settings.General.OrganizationDefaultStatus.Name, null); - organization.CreatedBy = Creator; - organization.Status = organizationStatus; - organization.OwnerId = contact.Id; - organization.Emails = new List { GetProperAddress(account, orgAdresses) }; + return result; + } - await _memberService.SaveChangesAsync(new Member[] { organization }); + private async Task ToContact(RegisteredContact contact, ApplicationUser account) + { + var result = _mapper.Map(contact); - result.Organization = organization; - contact.Organizations = new List { organization.Id }; - } + result.Id = Guid.NewGuid().ToString(); + result.FullName = contact.FirstName + " " + contact.LastName; + result.Name = result.FullName; + result.Status = DefaultContactStatus; + result.Emails = new List { account.Email }; - var contactStatus = store.Settings - .GetSettingValue(CustomerCore.ModuleConstants.Settings.General.ContactDefaultStatus.Name, null); + await SetDynamicPropertiesAsync(contact.DynamicProperties, result); - contact.Status = contactStatus; - contact.CreatedBy = Creator; - contact.Emails = new List { account.Email }; - await _memberService.SaveChangesAsync(new Member[] { contact }); - result.Contact = contact; + return result; + } - account.StoreId = request.StoreId; - account.Status = contactStatus; - account.UserType = UserType; - account.MemberId = contact.Id; - account.Roles = roles; - account.CreatedBy = Creator; - result.Contact.SecurityAccounts = new List { account }; + protected virtual async Task SendRegistrationEmailNotificationAsync(RegisterOrganizationNotificationRequest request) + { + var notification = request.Organization != null + ? await GetRegisterCompanyNotificationAsync(request) + : await GetRegisterContactNotificationAsync(request); - var identityResult = await _accountService.CreateAccountAsync(account); - result.AccountCreationResult = GetAccountCreationResult(identityResult, account); + await _notificationSender.ScheduleSendNotificationAsync(notification); + } - if (!result.AccountCreationResult.Succeeded) - { - tokenSource.Cancel(); - return result; - } + protected virtual async Task GetRegisterCompanyNotificationAsync(RegisterOrganizationNotificationRequest request) + { + var notification = await _notificationSearchService.GetNotificationAsync(new TenantIdentity(request.Store.Id, nameof(Store))); - var notificationRequest = new RegisterOrganizationNotificationRequest - { - Organization = organization, - Contact = contact, - Store = store, - LanguageCode = request.LanguageCode - }; + notification.From = request.Store.Email; + notification.LanguageCode = string.IsNullOrEmpty(request.LanguageCode) ? request.Store.DefaultLanguage : request.LanguageCode; - await SendNotificationAsync(notificationRequest); - return result; + notification.To = request.Organization.Emails.FirstOrDefault(); + notification.CompanyName = request.Organization.Name; + + return notification; + } + + protected virtual async Task GetRegisterContactNotificationAsync(RegisterOrganizationNotificationRequest request) + { + var notification = await _notificationSearchService.GetNotificationAsync(new TenantIdentity(request.Store.Id, nameof(Store))); + + notification.From = request.Store.Email; + notification.LanguageCode = string.IsNullOrEmpty(request.LanguageCode) ? request.Store.DefaultLanguage : request.LanguageCode; + + notification.To = request.Contact.Emails.FirstOrDefault(); + notification.FirstName = request.Contact.FirstName; + notification.LastName = request.Contact.LastName; + notification.Login = request.Contact.SecurityAccounts.FirstOrDefault()?.UserName; + + return notification; + } + + protected virtual Task SendVerifyEmailCommand(RegisterRequestCommand request, string email, CancellationTokenSource tokenSource) + { + return _mediator.Send(new SendVerifyEmailCommand(request.StoreId, + request.LanguageCode, + email), tokenSource.Token); } - private static string GetProperAddress(ApplicationUser account, IList
orgAdresses) + private static string ResolveEmail(ApplicationUser account, IList
orgAdresses) { return orgAdresses.FirstOrDefault()?.Email ?? account.Email; } - private async Task GetMaintainerRole(RegisterOrganizationResult result, CancellationTokenSource tokenSource) + protected virtual async Task GetMaintainerRole(RegisterOrganizationResult result, CancellationTokenSource tokenSource) { var maintainerRoleId = _securityOptions.Value.OrganizationMaintainerRole; if (maintainerRoleId == null) @@ -228,7 +328,7 @@ private async Task GetMaintainerRole(RegisterOrganizationResult result, Ca return role; } - private static AccountCreationResult GetAccountCreationResult(IdentityResult identityResult, ApplicationUser account) + protected virtual AccountCreationResult ToAccountCreationResult(IdentityResult identityResult, ApplicationUser account) { return new AccountCreationResult { @@ -242,16 +342,20 @@ private static AccountCreationResult GetAccountCreationResult(IdentityResult ide }).ToList() }; } -#pragma warning restore S138 - private static void FillContactFields(Contact contact) + protected virtual ApplicationUser ToApplicationUser(Account account) => new() { - contact.FullName = contact.FirstName + " " + contact.LastName; - contact.Name = contact.FullName; - contact.Id = Guid.NewGuid().ToString(); - } + UserName = account.UserName, + Email = account.Email, + Password = account.Password, + StoreId = CurrentStore.Id, + Status = DefaultContactStatus, + UserType = UserType, + }; + +#pragma warning restore S138 - private async Task RollBackMembersCreationAsync(RegisterOrganizationResult result) + protected virtual async Task RollBackMembersCreationAsync(RegisterOrganizationResult result) { var ids = new[] { result.Organization?.Id, result.Contact?.Id } .Where(x => x != null) @@ -288,41 +392,5 @@ private static void SetErrorResult(RegisterOrganizationResult result, List GetRegisterCompanyNotificationAsync(RegisterOrganizationNotificationRequest request) - { - var notification = await _notificationSearchService.GetNotificationAsync(new TenantIdentity(request.Store.Id, nameof(Store))); - notification.To = request.Organization.Emails.FirstOrDefault(); - notification.CompanyName = request.Organization.Name; - return notification; - } - - protected virtual async Task GetRegisterContactNotificationAsync(RegisterOrganizationNotificationRequest request) - { - var notification = await _notificationSearchService.GetNotificationAsync(new TenantIdentity(request.Store.Id, nameof(Store))); - notification.To = request.Contact.Emails.FirstOrDefault(); - notification.FirstName = request.Contact.FirstName; - notification.LastName = request.Contact.LastName; - notification.Login = request.Contact.SecurityAccounts.FirstOrDefault()?.UserName; - return notification; - } - - protected virtual ApplicationUser GetApplicationUser(Account account) => new() - { - UserName = account.UserName, - Email = account.Email, - Password = account.Password - }; } } diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommand.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommand.cs index cd5c099e..07207fc8 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommand.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommand.cs @@ -4,11 +4,15 @@ namespace VirtoCommerce.ProfileExperienceApiModule.Data.Commands { public class SendVerifyEmailCommand : ICommand { - public SendVerifyEmailCommand(string email) + public SendVerifyEmailCommand(string storeId, string languageCode, string email) { + StoreId = storeId; + LanguageCode = languageCode; Email = email; } public string Email { get; set; } + public string StoreId { get; set; } + public string LanguageCode { get; set; } } } diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommandHandler.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommandHandler.cs index 1ff76526..110462d0 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommandHandler.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Commands/SendVerifyEmailCommandHandler.cs @@ -1,23 +1,38 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediatR; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.WebUtilities; +using VirtoCommerce.NotificationsModule.Core.Extensions; +using VirtoCommerce.NotificationsModule.Core.Model; +using VirtoCommerce.NotificationsModule.Core.Services; +using VirtoCommerce.NotificationsModule.Core.Types; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.GenericCrud; using VirtoCommerce.Platform.Core.Security; -using VirtoCommerce.StoreModule.Core.Services; - +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.StoreModule.Core.Model; namespace VirtoCommerce.ProfileExperienceApiModule.Data.Commands { public class SendVerifyEmailCommandHandler : IRequestHandler { private readonly Func> _userManagerFactory; - private readonly IStoreNotificationSender _storeNotificationSender; + private readonly ICrudService _storeService; + private readonly INotificationSearchService _notificationSearchService; + private readonly INotificationSender _notificationSender; - public SendVerifyEmailCommandHandler(Func> userManagerFactory, IStoreNotificationSender storeNotificationSender) + public SendVerifyEmailCommandHandler(Func> userManagerFactory, + ICrudService storeService, + INotificationSearchService notificationSearchService, + INotificationSender notificationSender) { _userManagerFactory = userManagerFactory; - _storeNotificationSender = storeNotificationSender; + _storeService = storeService; + _notificationSearchService = notificationSearchService; + _notificationSender = notificationSender; } public virtual async Task Handle(SendVerifyEmailCommand request, CancellationToken cancellationToken) @@ -26,13 +41,63 @@ public virtual async Task Handle(SendVerifyEmailCommand request, Cancellat { var user = await userManager.FindByEmailAsync(request.Email); - if (user == null) + if (user == null || user.StoreId != request.StoreId) + { + return true; + } + + var store = await _storeService.GetByIdAsync(request.StoreId); + if (store == null) + { return true; + } - await _storeNotificationSender.SendUserEmailVerificationAsync(user); + var settingDescriptor = VirtoCommerce.StoreModule.Core.ModuleConstants.Settings.General.EmailVerificationEnabled; + + if (store.Settings.GetSettingValue(settingDescriptor.Name, (bool)settingDescriptor.DefaultValue)) + { + await SendConfirmationEmailNotificationAsync(store, user, request.LanguageCode); + } return true; } } + + protected virtual async Task SendConfirmationEmailNotificationAsync(Store store, ApplicationUser user, string languageCode) + { + var emailVerificationNotification = await GetConfirmationEmailNotificationAsync(store, user, languageCode); + + await _notificationSender.ScheduleSendNotificationAsync(emailVerificationNotification); + } + + protected virtual async Task GetConfirmationEmailNotificationAsync(Store store, ApplicationUser user, string languageCode) + { + var notification = await _notificationSearchService.GetNotificationAsync(new TenantIdentity(store.Id, nameof(Store))); + + notification.To = user.Email; + notification.Url = await GenerateEmailVerificationLink(store, user); + notification.From = store.Email; + notification.LanguageCode = string.IsNullOrEmpty(languageCode) ? store.DefaultLanguage : languageCode; + + return notification; + } + + protected virtual async Task GenerateEmailVerificationLink(Store store, ApplicationUser user) + { + if (store.Url.IsNullOrEmpty() || !Uri.IsWellFormedUriString(store.Url, UriKind.Absolute)) + { + throw new OperationCanceledException($"A valid URL is required in Url property for store '{store.Id}'."); + } + + using var userManager = _userManagerFactory(); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); + + return QueryHelpers.AddQueryString(new Uri($"{store.Url.TrimEnd('/')}/account/confirmemail").ToString(), + new Dictionary + { + { "UserId", user.Id }, + { "Token", token } + }); + } } } diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Schemas/InputSendVerifyEmailType.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Schemas/InputSendVerifyEmailType.cs index 4d573b40..ea312542 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Schemas/InputSendVerifyEmailType.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Schemas/InputSendVerifyEmailType.cs @@ -6,6 +6,8 @@ public class InputSendVerifyEmailType : InputObjectGraphType { public InputSendVerifyEmailType() { + Field>("storeId", "Store ID"); + Field("languageCode", "Notification language code"); Field("email"); } } diff --git a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Services/AccountsService.cs b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Services/AccountsService.cs index b29028d5..61fdf3cb 100644 --- a/src/VirtoCommerce.ProfileExperienceApiModule.Data/Services/AccountsService.cs +++ b/src/VirtoCommerce.ProfileExperienceApiModule.Data/Services/AccountsService.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Identity; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Security; -using VirtoCommerce.StoreModule.Core.Services; namespace VirtoCommerce.ProfileExperienceApiModule.Data.Services { @@ -11,14 +10,11 @@ public class AccountsService : IAccountService { private readonly Func> _userManagerFactory; private readonly Func> _roleManagerFactory; - private readonly IStoreNotificationSender _storeNotificationSender; public AccountsService(Func> userManagerFactory, - IStoreNotificationSender storeNotificationSender, Func> roleManagerFactory) { _userManagerFactory = userManagerFactory; - _storeNotificationSender = storeNotificationSender; _roleManagerFactory = roleManagerFactory; } @@ -37,13 +33,6 @@ public async Task CreateAccountAsync(ApplicationUser account) result = await userManager.CreateAsync(account, account.Password); } - if (result.Succeeded) - { - var user = await userManager.FindByNameAsync(account.UserName); - - await _storeNotificationSender.SendUserEmailVerificationAsync(user); - } - return result; }