Skip to content

Commit

Permalink
Convert FogotPassword views to shapes
Browse files Browse the repository at this point in the history
Fix #15702
Fix #12705
  • Loading branch information
MikeAlhayek committed Apr 11, 2024
1 parent 70cc055 commit 9a6ad96
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Threading.Tasks;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.ReCaptcha.Configuration;
using OrchardCore.Settings;
using OrchardCore.Users.Models;

namespace OrchardCore.ReCaptcha.Drivers;

public class ReCaptchaForgotPasswordFormDisplayDriver : DisplayDriver<ForgotPasswordForm>
{
private readonly ISiteService _siteService;

public ReCaptchaForgotPasswordFormDisplayDriver(ISiteService siteService)
{
_siteService = siteService;
}

public override async Task<IDisplayResult> EditAsync(ForgotPasswordForm model, BuildEditorContext context)
{
var _reCaptchaSettings = (await _siteService.GetSiteSettingsAsync()).As<ReCaptchaSettings>();

if (!_reCaptchaSettings.IsValid())
{
return null;
}

return View("FormReCaptcha_Edit", model).Location("Content:after");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ public override async Task<IDisplayResult> EditAsync(LoginForm model, BuildEdito
return null;
}

return View("LoginFormReCaptcha_Edit", model).Location("Content:after");
return View("FormReCaptcha_Edit", model).Location("Content:after");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE

var layout = await _layoutAccessor.GetLayoutAsync();

var afterForgotPasswordZone = layout.Zones["AfterForgotPassword"];
await afterForgotPasswordZone.AddAsync(await _shapeFactory.CreateAsync("ReCaptcha"));

var afterRegisterZone = layout.Zones["AfterRegister"];
await afterRegisterZone.AddAsync(await _shapeFactory.CreateAsync("ReCaptcha"));

Expand Down
12 changes: 11 additions & 1 deletion src/OrchardCore.Modules/OrchardCore.ReCaptcha/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public override void ConfigureServices(IServiceCollection services)
}

[Feature("OrchardCore.ReCaptcha.Users")]
public class StartupUsers : StartupBase
public class UsersStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
Expand All @@ -53,4 +53,14 @@ public override void ConfigureServices(IServiceCollection services)
});
}
}

[Feature("OrchardCore.ReCaptcha.Users")]
[RequireFeatures("OrchardCore.Users.ResetPassword")]
public class UsersResetPasswordStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IDisplayDriver<ForgotPasswordForm>, ReCaptchaForgotPasswordFormDisplayDriver>();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.Environment.Shell;
using OrchardCore.Modules;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Settings;
using OrchardCore.Users.Events;
using OrchardCore.Users.Models;
Expand All @@ -19,31 +24,41 @@ namespace OrchardCore.Users.Controllers
[Feature("OrchardCore.Users.ResetPassword")]
public class ResetPasswordController : Controller
{
private static readonly string _controllerName = typeof(ResetPasswordController).ControllerName();

private readonly IUserService _userService;
private readonly UserManager<IUser> _userManager;
private readonly ISiteService _siteService;
private readonly IEnumerable<IPasswordRecoveryFormEvents> _passwordRecoveryFormEvents;
private readonly ILogger _logger;
private readonly IUpdateModelAccessor _updateModelAccessor;
private readonly IDisplayManager<ForgotPasswordForm> _displayManager;
private readonly IShellFeaturesManager _shellFeaturesManager;

protected readonly IStringLocalizer S;

public ResetPasswordController(
IUserService userService,
UserManager<IUser> userManager,
ISiteService siteService,
IStringLocalizer<ResetPasswordController> stringLocalizer,
ILogger<ResetPasswordController> logger,
IEnumerable<IPasswordRecoveryFormEvents> passwordRecoveryFormEvents)
IUpdateModelAccessor updateModelAccessor,
IDisplayManager<ForgotPasswordForm> displayManager,
IShellFeaturesManager shellFeaturesManager,
IEnumerable<IPasswordRecoveryFormEvents> passwordRecoveryFormEvents,
IStringLocalizer<ResetPasswordController> stringLocalizer)
{
_userService = userService;
_userManager = userManager;
_siteService = siteService;

S = stringLocalizer;
_logger = logger;
_updateModelAccessor = updateModelAccessor;
_displayManager = displayManager;
_shellFeaturesManager = shellFeaturesManager;
_passwordRecoveryFormEvents = passwordRecoveryFormEvents;
S = stringLocalizer;
}

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword()
{
Expand All @@ -52,56 +67,63 @@ public async Task<IActionResult> ForgotPassword()
return NotFound();
}

return View();
var formShape = await _displayManager.BuildEditorAsync(_updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty);

return View(formShape);
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
[ActionName(nameof(ForgotPassword))]
public async Task<IActionResult> ForgotPasswordPOST()
{
if (!(await _siteService.GetSiteSettingsAsync()).As<ResetPasswordSettings>().AllowResetPassword)
{
return NotFound();
}

var model = new ForgotPasswordForm();

var formShape = await _displayManager.UpdateEditorAsync(model, _updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty);

await _passwordRecoveryFormEvents.InvokeAsync((e, modelState) => e.RecoveringPasswordAsync((key, message) => modelState.AddModelError(key, message)), ModelState, _logger);

if (TryValidateModel(model) && ModelState.IsValid)
if (ModelState.IsValid)
{
var user = await _userService.GetForgotPasswordUserAsync(model.Email) as User;
if (user == null || (
(await _siteService.GetSiteSettingsAsync()).As<RegistrationSettings>().UsersMustValidateEmail
&& !await _userManager.IsEmailConfirmedAsync(user))
)
var user = await _userService.GetForgotPasswordUserAsync(model.Identifier) as User;
if (user == null || await MustValidateEmailAsync(user))
{
// returns to confirmation page anyway: we don't want to let scrapers know if a username or an email exist
return RedirectToLocal(Url.Action("ForgotPasswordConfirmation", "ResetPassword"));
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(user.ResetToken));
var resetPasswordUrl = Url.Action("ResetPassword", "ResetPassword", new { code = user.ResetToken }, HttpContext.Request.Scheme);
var resetPasswordUrl = Url.Action(nameof(ResetPassword), _controllerName, 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 this.SendEmailAsync(user.Email, S["Reset your password"], new LostPasswordViewModel()
{
User = user,
LostPasswordUrl = resetPasswordUrl
});

var context = new PasswordRecoveryContext(user);

await _passwordRecoveryFormEvents.InvokeAsync((handler, context) => handler.PasswordRecoveredAsync(context), context, _logger);

return RedirectToLocal(Url.Action("ForgotPasswordConfirmation", "ResetPassword"));
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

// If we got this far, something failed, redisplay form
return View(model);
return View(formShape);
}

[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
return View();
}

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(string code = null)
{
Expand Down Expand Up @@ -132,28 +154,31 @@ public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (await _userService.ResetPasswordAsync(model.Email, Encoding.UTF8.GetString(Convert.FromBase64String(model.ResetToken)), model.NewPassword, (key, message) => ModelState.AddModelError(key == "Password" ? nameof(ResetPasswordViewModel.NewPassword) : key, message)))
{
return RedirectToLocal(Url.Action("ResetPasswordConfirmation", "ResetPassword"));
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}
}

return View(model);
}

[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
return View();
}

private RedirectResult RedirectToLocal(string returnUrl)
private async Task<bool> MustValidateEmailAsync(User user)
{
if (Url.IsLocalUrl(returnUrl))
var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync())
.Any(feature => feature.Id == "OrchardCore.Users.Registration");

if (!registrationFeatureIsAvailable)
{
return Redirect(returnUrl);
return false;
}

return Redirect("~/");
return (await _siteService.GetSiteSettingsAsync()).As<RegistrationSettings>().UsersMustValidateEmail
&& !await _userManager.IsEmailConfirmedAsync(user);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Threading.Tasks;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Users.Models;
using OrchardCore.Users.ViewModels;

namespace OrchardCore.Users.Drivers;

public class ForgotPasswordFormDisplayDriver : DisplayDriver<ForgotPasswordForm>
{
public override IDisplayResult Edit(ForgotPasswordForm model)
{
return Initialize<ForgotPasswordViewModel>("ForgotPasswordFormIdentifier_Edit", vm =>
{
vm.Identifier = model.Identifier;
}).Location("Content");
}

public override async Task<IDisplayResult> UpdateAsync(ForgotPasswordForm model, IUpdateModel updater)
{
var viewModel = new ForgotPasswordViewModel();

await updater.TryUpdateModelAsync(viewModel, Prefix);

model.Identifier = viewModel.Identifier;

return Edit(model);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Entities;
using OrchardCore.Users.Models;
using OrchardCore.Users.ViewModels;

Expand All @@ -29,8 +28,6 @@ public override async Task<IDisplayResult> UpdateAsync(LoginForm model, IUpdateM
model.Password = viewModel.Password;
model.RememberMe = viewModel.RememberMe;

model.Put(viewModel);

return Edit(model);
}
}
3 changes: 3 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Users/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde
pattern: _userOptions.LoginPath,
defaults: new { controller = _accountControllerName, action = nameof(AccountController.Login) }
);

routes.MapAreaControllerRoute(
name: "ChangePassword",
areaName: UserConstants.Features.Users,
Expand Down Expand Up @@ -440,7 +441,9 @@ public override void ConfigureServices(IServiceCollection services)

services.AddScoped<INavigationProvider, ResetPasswordAdminMenu>();
services.AddScoped<IDisplayDriver<ISite>, ResetPasswordSettingsDisplayDriver>();

services.AddScoped<IDisplayDriver<LoginForm>, ForgotPasswordLoginFormDisplayDriver>();
services.AddScoped<IDisplayDriver<ForgotPasswordForm>, ForgotPasswordFormDisplayDriver>();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;

namespace OrchardCore.Users.ViewModels
{
public class ForgotPasswordViewModel
{
[Required(ErrorMessage = "Email is required.")]
[Obsolete("Email property is not longer used and will be removed in future releases. Instead use Identifier.")]
[Email.EmailAddress(ErrorMessage = "Invalid Email.")]
public string Email { get; set; }

[Required(ErrorMessage = "Username or Email is required.")]
public string Identifier { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

<div class="row">
<div class="col-md-offset-4 col-md-8">
<h2>@T["Forgot password?"]</h2>
<hr />

<form asp-action="ForgotPassword" method="post" class="no-multisubmit">

<div asp-validation-summary="All"></div>

@if (Model.Content != null)
{
@await DisplayAsync(Model.Content)
}

<div class="mb-3">
<button type="submit" class="btn btn-primary">@T["Submit"]</button>
</div>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@model ForgotPasswordViewModel

<div class="mb-3">
<label asp-for="Identifier" class="form-label">@T["Username or Email"]</label>
<input asp-for="Identifier" class="form-control" />
</div>
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
@model OrchardCore.Users.ViewModels.ForgotPasswordViewModel

@{
ViewLayout = "Layout__Login";
}

<h2>@T["Forgot password?"]</h2>
<h4>@T["Please check your email to reset your password."]</h4>
<hr />
<div class="row">
<div class="col-md-8">
<form asp-controller="ResetPassword" asp-action="ForgotPassword" method="post" class="form-horizontal no-multisubmit">
<div asp-validation-summary="All"></div>
<div class="mb-3">
<label asp-for="Email" class="col-md-4 form-label">@T["Email"]</label>
<div class="col-md-9">
<input asp-for="Email" class="form-control" />
</div>
</div>
@await RenderSectionAsync("AfterForgotPassword", required: false)
<div class="mb-3">
<div class="col-md-offset-4 col-md-8">
<button type="submit" class="btn btn-primary">@T["Submit"]</button>
</div>
</div>
</form>
</div>
</div>
@await DisplayAsync(Model)
Loading

0 comments on commit 9a6ad96

Please sign in to comment.