diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 68939d50549..348baa6df90 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -41,6 +41,7 @@ public class AccountController : AccountBaseController private readonly IClock _clock; private readonly IDistributedCache _distributedCache; private readonly IEnumerable _externalLoginHandlers; + private readonly IUserControllerService _userControllerService; protected readonly IHtmlLocalizer H; protected readonly IStringLocalizer S; @@ -57,7 +58,8 @@ public AccountController( IClock clock, IDistributedCache distributedCache, IDataProtectionProvider dataProtectionProvider, - IEnumerable externalLoginHandlers) + IEnumerable externalLoginHandlers, + IUserControllerService userControllerService) { _signInManager = signInManager; _userManager = userManager; @@ -70,7 +72,7 @@ public AccountController( _distributedCache = distributedCache; _dataProtectionProvider = dataProtectionProvider; _externalLoginHandlers = externalLoginHandlers; - + _userControllerService = userControllerService; H = htmlLocalizer; S = stringLocalizer; } @@ -424,13 +426,13 @@ public async Task ExternalLoginCallback(string returnUrl = null, if (noInformationRequired) { - iUser = await this.RegisterUser(new RegisterViewModel() + iUser = await _userControllerService.RegisterUser(this, new RegisterViewModel() { UserName = externalLoginViewModel.UserName, Email = externalLoginViewModel.Email, Password = null, ConfirmPassword = null - }, S["Confirm your account"], _logger); + }, S["Confirm your account"]); // If the registration was successful we can link the external provider and redirect the user. if (iUser != null) @@ -540,14 +542,14 @@ public async Task RegisterExternalLogin(RegisterExternalLoginView if (TryValidateModel(model) && ModelState.IsValid) { - var iUser = await this.RegisterUser( + var iUser = await _userControllerService.RegisterUser(this, new RegisterViewModel() { UserName = model.UserName, Email = model.Email, Password = model.Password, ConfirmPassword = model.ConfirmPassword - }, S["Confirm your account"], _logger); + }, S["Confirm your account"]); if (iUser is null) { diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs deleted file mode 100644 index 871ec7dba5a..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ControllerExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OrchardCore.DisplayManagement; -using OrchardCore.Email; -using OrchardCore.Modules; -using OrchardCore.Settings; -using OrchardCore.Users.Events; -using OrchardCore.Users.Models; -using OrchardCore.Users.Services; -using OrchardCore.Users.ViewModels; - -namespace OrchardCore.Users.Controllers -{ - internal static class ControllerExtensions - { - internal static async Task SendEmailAsync(this Controller controller, string email, string subject, IShape model) - { - var smtpService = controller.HttpContext.RequestServices.GetRequiredService(); - var displayHelper = controller.HttpContext.RequestServices.GetRequiredService(); - var htmlEncoder = controller.HttpContext.RequestServices.GetRequiredService(); - var body = string.Empty; - - using (var sw = new StringWriter()) - { - var htmlContent = await displayHelper.ShapeExecuteAsync(model); - htmlContent.WriteTo(sw, htmlEncoder); - body = sw.ToString(); - } - - var message = new MailMessage() - { - To = email, - Subject = subject, - Body = body, - IsHtmlBody = true - }; - - var result = await smtpService.SendAsync(message); - - return result.Succeeded; - } - - /// - /// Returns the created user, otherwise returns null. - /// - /// - /// - /// - /// - /// - internal static async Task RegisterUser(this Controller controller, RegisterViewModel model, string confirmationEmailSubject, ILogger logger) - { - var registrationEvents = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); - var userService = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService(); - var settings = (await controller.ControllerContext.HttpContext.RequestServices.GetRequiredService().GetSiteSettingsAsync()).As(); - var signInManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); - - if (settings.UsersCanRegister != UserRegistrationType.NoRegistration) - { - await registrationEvents.InvokeAsync((e, modelState) => e.RegistrationValidationAsync((key, message) => modelState.AddModelError(key, message)), controller.ModelState, logger); - - if (controller.ModelState.IsValid) - { - var user = await userService.CreateUserAsync(new User { UserName = model.UserName, Email = model.Email, EmailConfirmed = !settings.UsersMustValidateEmail, IsEnabled = !settings.UsersAreModerated }, model.Password, (key, message) => controller.ModelState.AddModelError(key, message)) as User; - - if (user != null && controller.ModelState.IsValid) - { - if (settings.UsersMustValidateEmail && !user.EmailConfirmed) - { - // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - await controller.SendEmailConfirmationTokenAsync(user, confirmationEmailSubject); - } - else if (!(settings.UsersAreModerated && !user.IsEnabled)) - { - await signInManager.SignInAsync(user, isPersistent: false); - } - logger.LogInformation(3, "User created a new account with password."); - await registrationEvents.InvokeAsync((e, user) => e.RegisteredAsync(user), user, logger); - - return user; - } - } - } - return null; - } - - internal static async Task SendEmailConfirmationTokenAsync(this Controller controller, User user, string subject) - { - var userManager = controller.ControllerContext.HttpContext.RequestServices.GetRequiredService>(); - var code = await userManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = controller.Url.Action("ConfirmEmail", "Registration", new { userId = user.UserId, code }, protocol: controller.HttpContext.Request.Scheme); - await SendEmailAsync(controller, user.Email, subject, new ConfirmEmailViewModel() { User = user, ConfirmEmailUrl = callbackUrl }); - - return callbackUrl; - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index fd8aee15ab5..2cd9331b2ea 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -10,6 +10,7 @@ using OrchardCore.Modules; using OrchardCore.Settings; using OrchardCore.Users.Models; +using OrchardCore.Users.Services; using OrchardCore.Users.ViewModels; namespace OrchardCore.Users.Controllers @@ -20,6 +21,7 @@ public class RegistrationController : Controller private readonly UserManager _userManager; private readonly IAuthorizationService _authorizationService; private readonly ISiteService _siteService; + private readonly IUserControllerService _userControllerService; private readonly INotifier _notifier; private readonly ILogger _logger; protected readonly IStringLocalizer S; @@ -29,6 +31,7 @@ public RegistrationController( UserManager userManager, IAuthorizationService authorizationService, ISiteService siteService, + IUserControllerService userControllerService, INotifier notifier, ILogger logger, IHtmlLocalizer htmlLocalizer, @@ -37,6 +40,7 @@ public RegistrationController( _userManager = userManager; _authorizationService = authorizationService; _siteService = siteService; + _userControllerService = userControllerService; _notifier = notifier; _logger = logger; H = htmlLocalizer; @@ -89,7 +93,7 @@ public async Task Register(RegisterViewModel model, string return if (ModelState.IsValid) { - var iUser = await this.RegisterUser(model, S["Confirm your account"], _logger); + var iUser = await _userControllerService.RegisterUser(this, model, S["Confirm your account"]); // If we get a user, redirect to returnUrl if (iUser is User user) { @@ -159,7 +163,7 @@ public async Task SendVerificationEmail(string id) var user = await _userManager.FindByIdAsync(id) as User; if (user != null) { - await this.SendEmailConfirmationTokenAsync(user, S["Confirm your account"]); + await _userControllerService.SendEmailConfirmationTokenAsync(this, user, S["Confirm your account"]); await _notifier.SuccessAsync(H["Verification email sent."]); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs index 31cb64985a1..34c5dd7c9ef 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs @@ -23,6 +23,7 @@ public class ResetPasswordController : Controller private readonly UserManager _userManager; private readonly ISiteService _siteService; private readonly IEnumerable _passwordRecoveryFormEvents; + private readonly IUserControllerService _userControllerService; private readonly ILogger _logger; protected readonly IStringLocalizer S; @@ -30,17 +31,18 @@ public ResetPasswordController( IUserService userService, UserManager userManager, ISiteService siteService, - IStringLocalizer stringLocalizer, + IEnumerable passwordRecoveryFormEvents, + IUserControllerService userControllerService, ILogger logger, - IEnumerable passwordRecoveryFormEvents) + IStringLocalizer stringLocalizer) { _userService = userService; _userManager = userManager; _siteService = siteService; - - S = stringLocalizer; - _logger = logger; _passwordRecoveryFormEvents = passwordRecoveryFormEvents; + _userControllerService = userControllerService; + _logger = logger; + S = stringLocalizer; } [HttpGet] @@ -81,7 +83,7 @@ public async Task ForgotPassword(ForgotPasswordViewModel model) user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(user.ResetToken)); var resetPasswordUrl = Url.Action("ResetPassword", "ResetPassword", new { code = user.ResetToken }, HttpContext.Request.Scheme); // send email with callback link - await this.SendEmailAsync(user.Email, S["Reset your password"], new LostPasswordViewModel() { User = user, LostPasswordUrl = resetPasswordUrl }); + await _userControllerService.SendEmailAsync(user.Email, S["Reset your password"], new LostPasswordViewModel() { User = user, LostPasswordUrl = resetPasswordUrl }); var context = new PasswordRecoveryContext(user); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUserControllerService.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUserControllerService.cs new file mode 100644 index 00000000000..65a889c93a6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUserControllerService.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using OrchardCore.DisplayManagement; +using OrchardCore.Email; +using OrchardCore.Modules; +using OrchardCore.Settings; +using OrchardCore.Users.Events; +using OrchardCore.Users.Models; +using OrchardCore.Users.ViewModels; + +namespace OrchardCore.Users.Services; + +public class DefaultUserControllerService : IUserControllerService +{ + private readonly ISmtpService _smtpService; + private readonly IDisplayHelper _displayHelper; + private readonly HtmlEncoder _htmlEncoder; + private readonly IEnumerable _registrationFormEvents; + private readonly IUserService _userService; + private readonly ISiteService _siteService; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public DefaultUserControllerService( + ISmtpService smtpService, + IDisplayHelper displayHelper, + HtmlEncoder htmlEncoder, + IEnumerable registrationFormEvents, + IUserService userService, + ISiteService siteService, + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _smtpService = smtpService; + _displayHelper = displayHelper; + _htmlEncoder = htmlEncoder; + _registrationFormEvents = registrationFormEvents; + _userService = userService; + _siteService = siteService; + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + public async Task SendEmailAsync(string email, string subject, IShape model) + { + var body = string.Empty; + + using (var sw = new StringWriter()) + { + var htmlContent = await _displayHelper.ShapeExecuteAsync(model); + htmlContent.WriteTo(sw, _htmlEncoder); + body = sw.ToString(); + } + + var message = new MailMessage() + { + To = email, + Subject = subject, + Body = body, + IsHtmlBody = true + }; + + var result = await _smtpService.SendAsync(message); + + return result.Succeeded; + } + + /// + /// Returns the created user, otherwise returns null. + /// + /// + /// + /// + /// + public async Task RegisterUser(Controller controller, RegisterViewModel model, string confirmationEmailSubject) + { + var settings = (await _siteService.GetSiteSettingsAsync()).As(); + + if (settings.UsersCanRegister != UserRegistrationType.NoRegistration) + { + await _registrationFormEvents.InvokeAsync((e, modelState) => e.RegistrationValidationAsync((key, message) => modelState.AddModelError(key, message)), controller.ModelState, _logger); + + if (controller.ModelState.IsValid) + { + var user = await _userService.CreateUserAsync(new User { UserName = model.UserName, Email = model.Email, EmailConfirmed = !settings.UsersMustValidateEmail, IsEnabled = !settings.UsersAreModerated }, model.Password, (key, message) => controller.ModelState.AddModelError(key, message)) as User; + + if (user != null && controller.ModelState.IsValid) + { + if (settings.UsersMustValidateEmail && !user.EmailConfirmed) + { + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + await SendEmailConfirmationTokenAsync(controller, user, confirmationEmailSubject); + } + else if (!(settings.UsersAreModerated && !user.IsEnabled)) + { + await _signInManager.SignInAsync(user, isPersistent: false); + } + + _logger.LogInformation(3, "User created a new account with password."); + + await _registrationFormEvents.InvokeAsync((e, user) => e.RegisteredAsync(user), user, _logger); + + return user; + } + } + } + + return null; + } + + public async Task SendEmailConfirmationTokenAsync(Controller controller, User user, string subject) + { + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = controller.Url.Action("ConfirmEmail", "Registration", new { userId = user.UserId, code }, protocol: controller.HttpContext.Request.Scheme); + await SendEmailAsync(user.Email, subject, new ConfirmEmailViewModel() { User = user, ConfirmEmailUrl = callbackUrl }); + + return callbackUrl; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/IUserControllerService.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/IUserControllerService.cs new file mode 100644 index 00000000000..35f35ea8edc --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/IUserControllerService.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using OrchardCore.DisplayManagement; +using OrchardCore.Users.Models; +using OrchardCore.Users.ViewModels; + +namespace OrchardCore.Users.Services; + +public interface IUserControllerService +{ + Task SendEmailAsync(string email, string subject, IShape model); + + Task RegisterUser(Controller controller, RegisterViewModel model, string confirmationEmailSubject); + + Task SendEmailConfirmationTokenAsync(Controller controller, User user, string subject); +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index dcaff32d83a..c73cdcbce5d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -187,6 +187,8 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, UserOptionsDisplayDriver>(); + services.AddScoped(); + services.AddSingleton(sp => { var filterProviders = sp.GetServices(); diff --git a/test/OrchardCore.Tests.Functional/cms-tests/Recipes/migrations.recipe.json b/test/OrchardCore.Tests.Functional/cms-tests/Recipes/migrations.recipe.json index 580e7d08bb8..c1d53567dbb 100644 --- a/test/OrchardCore.Tests.Functional/cms-tests/Recipes/migrations.recipe.json +++ b/test/OrchardCore.Tests.Functional/cms-tests/Recipes/migrations.recipe.json @@ -23,6 +23,7 @@ "OrchardCore.Admin", "OrchardCore.Diagnostics", "OrchardCore.DynamicCache", + "OrchardCore.Email", "OrchardCore.Features", "OrchardCore.Navigation", "OrchardCore.Recipes", diff --git a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs index de95ccf00f3..921d713baa8 100644 --- a/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs +++ b/test/OrchardCore.Tests/OrchardCore.Users/RegistrationControllerTests.cs @@ -161,10 +161,21 @@ private static RegistrationController SetupRegistrationController(RegistrationSe mockDisplayHelper.Setup(x => x.ShapeExecuteAsync(It.IsAny())) .ReturnsAsync(HtmlString.Empty); + var userControllerService = new DefaultUserControllerService( + mockSmtpService, + mockDisplayHelper.Object, + HtmlEncoder.Default, + Array.Empty(), + userService.Object, mockSiteService, + MockSignInManager(mockUserManager.Object).Object, + mockUserManager.Object, + Mock.Of>()); + var controller = new RegistrationController( mockUserManager.Object, Mock.Of(), mockSiteService, + userControllerService, Mock.Of(), Mock.Of>(), Mock.Of>(),