Password reset

This commit is contained in:
2020-01-26 09:05:54 -05:00
parent 4c51a72544
commit 8ca5105fca
19 changed files with 398 additions and 38 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
using System.Web.Mvc;
using Hangfire;
using log4net;
using LeafWeb.WebCms.Models;
using LeafWeb.WebCms.Services;
@@ -18,8 +19,8 @@ namespace LeafWeb.WebCms.Controllers
{
if (ModelState.IsValid) // HttpParamMatch indicates it's backing out from Confirm
{
// convert viewModel into Model
new EmailNotificationService().SendContactEmail(viewModel);
BackgroundJob.Enqueue<EmailNotificationService>(
e => e.SendContactEmail(viewModel));
var logger = LogManager.GetLogger(GetType());
logger.Info($"Contact: Name:{viewModel.Name} Added, Email:{viewModel.Email}, Message:{viewModel.Message}");
+7
View File
@@ -6,5 +6,12 @@ namespace LeafWeb.WebCms.Controllers
public const int ManageQueue = 1107;
public const int Chart = 1100;
public const int Details = 1111;
public const int PasswordResetRequest = 1164;
}
public static class LeafWebMemberProperties
{
public const string VerificationToken = "VerificationToken";
public const string PasswordResetToken = "PasswordResetToken";
}
}
+138 -2
View File
@@ -1,4 +1,8 @@
using System.Web.Mvc;
using Hangfire;
using LeafWeb.WebCms.Models;
using LeafWeb.WebCms.Services;
using MlkPwgen;
using Umbraco.Core;
namespace LeafWeb.WebCms.Controllers
@@ -23,7 +27,7 @@ namespace LeafWeb.WebCms.Controllers
}
else
{
var storedToken = member.GetValue("VerificationToken") as string;
var storedToken = member.GetValue<string>(LeafWebMemberProperties.VerificationToken);
if (string.IsNullOrEmpty(storedToken))
{
@@ -41,7 +45,7 @@ namespace LeafWeb.WebCms.Controllers
member.IsApproved = true;
// remove the verification
member.SetValue("VerificationToken", string.Empty);
member.SetValue(LeafWebMemberProperties.VerificationToken, string.Empty);
memberService.Save(member);
TempData["StatusMessage"] = "Thank you! Your email is now verified at " + member.Email;
@@ -53,5 +57,137 @@ namespace LeafWeb.WebCms.Controllers
return Redirect(redirectUrl);
}
public ActionResult PasswordResetRequest()
{
var viewModel = new PasswordResetRequestForm();
return PartialView("PasswordResetRequest", viewModel);
}
[HttpPost]
public ActionResult PasswordResetRequest(PasswordResetRequestForm requestForm)
{
if (!ModelState.IsValid)
return CurrentUmbracoPage();
var memberService = ApplicationContext.Current.Services.MemberService;
var member = memberService.GetByEmail(requestForm.Email);
if (member == null)
{
// Send notification of attempt to change
BackgroundJob.Enqueue<EmailNotificationService>(
e => e.SendPasswordResetNotMemberEmail(requestForm.Email));
}
else
{
var token = PasswordGenerator.Generate(12, allowed: "0123456789");
member.SetValue(LeafWebMemberProperties.PasswordResetToken, token);
memberService.Save(member);
// Send Email
BackgroundJob.Enqueue<EmailNotificationService>(
e => e.SendPasswordResetEmail(member.Email));
}
// don't acknowledge their email address
TempData["StatusMessage"] =
$"An email has been sent to {requestForm.Email} with instructions on how to reset your password.";
TempData["StatusMessage-Type"] = "alert-success";
return Redirect("/");
}
public ActionResult PasswordReset(string email, string token)
{
var errorMsg = $"Sorry, a valid password reset was not found for user {email}. " +
$"Please try resetting again, " +
$"or use Contact Us if the issue persists.";
if (!string.IsNullOrEmpty(email) && !string.IsNullOrEmpty(token))
{
var memberService = ApplicationContext.Current.Services.MemberService;
var member = memberService.GetByEmail(email);
if (member == null)
{
// don't acknowledge their email address
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else
{
var storedToken = member.GetValue<string>(LeafWebMemberProperties.PasswordResetToken);
if (string.IsNullOrEmpty(storedToken))
{
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else if (storedToken != token)
{
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else
{
var viewModel = new PasswordResetForm {Email = email, PasswordResetToken = token};
return PartialView(viewModel);
}
}
}
return PasswordResetRequest();
}
[HttpPost]
public ActionResult PasswordReset(PasswordResetForm form)
{
var redirectUrl = "/";
var errorMsg = $"Sorry, a valid password reset was not found for user {form.Email}. " +
$"Please try resetting again, " +
$"or use Contact Us if the issue persists.";
if (ModelState.IsValid)
{
var memberService = ApplicationContext.Current.Services.MemberService;
var member = memberService.GetByEmail(form.Email);
if (member == null)
{
// don't acknowledge their email address
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else
{
var storedToken = member.GetValue<string>(LeafWebMemberProperties.PasswordResetToken);
if (string.IsNullOrEmpty(storedToken))
{
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else if (storedToken != form.PasswordResetToken)
{
TempData["StatusMessage"] = errorMsg;
TempData["StatusMessage-Type"] = "alert-danger";
}
else
{
memberService.SavePassword(member, form.Password);
// remove the token
member.SetValue(LeafWebMemberProperties.PasswordResetToken, string.Empty);
memberService.Save(member);
TempData["StatusMessage"] = "Password updated for " + member.Email + ", use your new password to login.";
TempData["StatusMessage-Type"] = "alert-success";
redirectUrl = "membership/login";
}
}
}
return Redirect(redirectUrl);
}
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
using Hangfire;
using LeafWeb.WebCms.Controllers;
using LeafWeb.WebCms.Services;
using MlkPwgen;
using Umbraco.Core;
@@ -41,7 +42,7 @@ namespace LeafWeb.WebCms.EventHandlers
{
e.Entity.IsApproved = false;
var token = PasswordGenerator.Generate(12, allowed: "0123456789");
e.Entity.SetValue("VerificationToken", token);
e.Entity.SetValue(LeafWebMemberProperties.VerificationToken, token);
sender.Save(e.Entity);
// Send Email
+1 -1
View File
@@ -21,7 +21,7 @@ namespace LeafWeb.WebCms.Models
[Display(Name = "Confirm email address")]
[Required(ErrorMessage = "Enter email exactly as above")]
[System.ComponentModel.DataAnnotations.Compare("Email")]
[Compare("Email")]
public string EmailConfirm { get; set; }
[Display(Name = "A unique identifier for this data")]
+25
View File
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
namespace LeafWeb.WebCms.Models
{
public class PasswordResetForm
{
[Display(Name = "Email address")]
[Required(ErrorMessage = "An email address is required")]
[DataType(DataType.EmailAddress)]
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
public string Email { get; set; }
[Required(ErrorMessage = "A token is required to reset")]
public string PasswordResetToken { get; set; }
[Required(ErrorMessage = "A new password is required")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required(ErrorMessage = "Password must match verify exactly")]
[DataType(DataType.Password)]
[Compare("Password")]
public string PasswordVerify { get; set; }
}
}
+13
View File
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace LeafWeb.WebCms.Models
{
public class PasswordResetRequestForm
{
[Display(Name = "Email address")]
[Required(ErrorMessage = "An email address is required")]
[DataType(DataType.EmailAddress)]
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
public string Email { get; set; }
}
}
+75 -10
View File
@@ -94,13 +94,23 @@ namespace LeafWeb.WebCms.Services
var body = $"Your leaf analysis job, {leafInput.Identifier}, has been cancelled. " +
"Contact the administrator with any questions.";
var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(CancelledSubject, leafInput), body);
using (var message =
new MailMessage(
_emailFromAddress,
leafInput.Email,
FormatSubject(CancelledSubject, leafInput),
body))
SendMessage(message);
}
public void SendAdministratorMessage(string subject, string body)
{
var message = new MailMessage(_emailFromAddress, _adminEmailAddresses, subject, body);
using (var message =
new MailMessage(
_emailFromAddress,
_adminEmailAddresses,
subject, body
))
SendMessage(message);
}
@@ -129,7 +139,12 @@ namespace LeafWeb.WebCms.Services
+ Environment.NewLine + Environment.NewLine
+ chartUrl;
var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(SuccessSubject, leafInput), body);
using (var message =
new MailMessage(
_emailFromAddress,
leafInput.Email,
FormatSubject(SuccessSubject, leafInput),
body))
SendMessage(message);
}
//else
@@ -173,7 +188,12 @@ namespace LeafWeb.WebCms.Services
body += FormatWarningMessage(leafInput);
var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(ErrorSubject, leafInput), body);
using (var message =
new MailMessage(
_emailFromAddress,
leafInput.Email,
FormatSubject(ErrorSubject, leafInput),
body))
SendMessage(message);
}
@@ -183,7 +203,12 @@ namespace LeafWeb.WebCms.Services
+ "System administrators have been notified. You will be notified again when the system error "
+ "has been resolved and your data has been processed.";
var message = new MailMessage(_emailFromAddress, leafInputEmail, SystemErrorSubject, body);
using (var message =
new MailMessage(
_emailFromAddress,
leafInputEmail,
SystemErrorSubject,
body))
SendMessage(message);
}
@@ -198,23 +223,64 @@ namespace LeafWeb.WebCms.Services
Environment.NewLine + Environment.NewLine +
contact.Message;
var message = new MailMessage(_emailFromAddress, _adminEmailAddresses, ContactSubject, body)
using (var message =
new MailMessage(
_emailFromAddress,
_adminEmailAddresses,
ContactSubject, body)
{
From = new MailAddress(contact.Email, contact.Name)
};
})
SendMessage(message);
}
public void SendVerifyMemberEmail(string memberEmail)
{
var member = ApplicationContext.Current.Services.MemberService.GetByEmail(memberEmail);
var verifyEmailURl = _urlService.GetVerifyEmailURl(member);
var verifyEmailURl = _urlService.GetVerifyEmailUrl(member);
var body =
"Welcome to LeafWeb!" + Environment.NewLine + Environment.NewLine +
"Please verify your email address with this link " + verifyEmailURl + Environment.NewLine +
"Read more information about LeafWeb on the site here: https://leafweb.org/information/about/";
var message = new MailMessage(_emailFromAddress, member.Email, "Welcome to LeafWeb, please verify your email address", body);
using (var message =
new MailMessage(
_emailFromAddress,
member.Email,
"Welcome to LeafWeb, please verify your email address",
body))
SendMessage(message);
}
public void SendPasswordResetNotMemberEmail(string memberEmail)
{
var body =
"A password reset has been requested for leafweb.org" + Environment.NewLine + Environment.NewLine +
"We do not have an account attached to this email address, " +
"if you'd like to create one click here " + _urlService.GetRegisterUrl();
using (var message =
new MailMessage(
_emailFromAddress,
memberEmail,
"Reset LeafWeb Password",
body))
SendMessage(message);
}
public void SendPasswordResetEmail(string memberEmail)
{
var member = ApplicationContext.Current.Services.MemberService.GetByEmail(memberEmail);
var passwordResetURl = _urlService.GetPasswordResetUrl(member);
var body =
"A password reset has been requested for leafweb.org" + Environment.NewLine + Environment.NewLine +
"Please click here to enter a new password " + passwordResetURl;
using (var message =
new MailMessage(
_emailFromAddress,
member.Email,
"Reset LeafWeb Password",
body))
SendMessage(message);
}
@@ -246,6 +312,5 @@ namespace LeafWeb.WebCms.Services
{
_dataService.Dispose();
}
}
}
+22 -2
View File
@@ -1,6 +1,7 @@
using System.Configuration;
using System.Web;
using LeafWeb.Core.Entities;
using LeafWeb.WebCms.Controllers;
using Umbraco.Core.Models;
namespace LeafWeb.WebCms.Services
@@ -10,6 +11,8 @@ namespace LeafWeb.WebCms.Services
private readonly string _downloadUrl;
private readonly string _chartUrl;
private readonly string _verifyEmailUrl;
private readonly string _passwordResetUrl;
private readonly string _registerUrl;
public UrlService()
{
@@ -24,6 +27,14 @@ namespace LeafWeb.WebCms.Services
_verifyEmailUrl =
ConfigurationManager.AppSettings["LeafWebUrl"]
+ ConfigurationManager.AppSettings["MemberVerifyPath"];
_passwordResetUrl =
ConfigurationManager.AppSettings["LeafWebUrl"]
+ ConfigurationManager.AppSettings["PasswordResetPath"];
_registerUrl =
ConfigurationManager.AppSettings["LeafWebUrl"]
+ ConfigurationManager.AppSettings["RegisterPath"];
}
public string GetDownloadUrl(LeafInput leafInput)
@@ -36,11 +47,20 @@ namespace LeafWeb.WebCms.Services
return string.Format(_chartUrl, leafInput.UniqueToken);
}
public string GetVerifyEmailURl(IMember member)
public string GetVerifyEmailUrl(IMember member)
{
var memberEmail = member.Email;
var token = member.GetValue("VerificationToken") as string;
var token = member.GetValue<string>(LeafWebMemberProperties.VerificationToken);
return string.Format(_verifyEmailUrl, HttpUtility.UrlEncode(memberEmail), token);
}
public string GetPasswordResetUrl(IMember member)
{
var memberEmail = member.Email;
var token = member.GetValue<string>(LeafWebMemberProperties.PasswordResetToken);
return string.Format(_passwordResetUrl, HttpUtility.UrlEncode(memberEmail), token);
}
public string GetRegisterUrl() => _registerUrl;
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
@Html.EditorFor(m => m.Name)
@Html.EditorFor(m => m.Email)
@Html.EditorFor(m => m.Message)
<input type="submit" id="submit-form" class="hidden"/>}
<input type="submit" id="submit-form" class="d-none"/>}
<label for="submit-form" class="btn btn-primary pull-right">Send</label>
</div>
</div>
@@ -13,8 +13,7 @@
@using (Html.BeginUmbracoForm<UmbLoginController>("HandleLogin"))
{
@Html.EditorFor(m => loginModel.Username)
@Html.EditorFor(m => loginModel.Password, "Password")
@Html.EditorFor(m => loginModel.Password, "PasswordWithForgotLink")
@Html.ValidationSummary("loginModel", true)
<button class="btn btn-primary pull-right">Login</button>
@@ -0,0 +1,4 @@
@inherits Umbraco.Web.Macros.PartialViewMacroPage
@{
Html.RenderAction("PasswordReset", "Membership");
}
@@ -0,0 +1,4 @@
@inherits Umbraco.Web.Macros.PartialViewMacroPage
@{
Html.RenderAction("PasswordResetRequest", "Membership");
}
@@ -1,9 +1,10 @@
@inherits Umbraco.Web.Macros.PartialViewMacroPage
@using ClientDependency.Core.Mvc
@using Umbraco.Web.Controllers
<!-- https://24days.in/umbraco-cms/2015/membership-apis-investigation/ -->
<!-- https://our.umbraco.com/forum/templates-partial-views-and-macros/93133-membership-provider-registration-form -->
@{
// https://24days.in/umbraco-cms/2015/membership-apis-investigation/
// https://our.umbraco.com/forum/templates-partial-views-and-macros/93133-membership-provider-registration-form
var membershipHelper = new Umbraco.Web.Security.MembershipHelper(UmbracoContext.Current);
var registerModel = membershipHelper.CreateRegistrationModel();
@@ -28,7 +29,7 @@
}
else if (user != null)
{
<p>No need to register - you are already logged withe email @user.Email</p>
<p>No need to register - you are already logged with email @user.Email</p>
}
else
{
@@ -0,0 +1,28 @@
@using ClientDependency.Core.Mvc
@using LeafWeb.WebCms.Controllers
@model PasswordResetForm
@{
Html.RequiresJs("~/scripts/jquery.validate.min.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.unobtrusive.min.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.unobtrusive.bootstrap.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.custom.js", 2);
}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6 ">
<p>Resetting password for @Model.Email</p>
@Html.Partial("_ValidationSummary")
@using (Html.BeginUmbracoForm<MembershipController>("PasswordReset"))
{
@Html.HiddenFor(m => m.Email)
@Html.HiddenFor(m => m.PasswordResetToken)
@Html.EditorFor(m => m.Password)
@Html.EditorFor(m => m.PasswordVerify)
<input type="submit" id="submit-form" class="d-none"/>
}
<label for="submit-form" class="btn btn-primary pull-right">Reset</label>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
@using ClientDependency.Core.Mvc
@using LeafWeb.WebCms.Controllers
@model PasswordResetRequestForm
@{
Html.RequiresJs("~/scripts/jquery.validate.min.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.unobtrusive.min.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.unobtrusive.bootstrap.js", 2);
Html.RequiresJs("~/scripts/jquery.validate.custom.js", 2);
}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6 ">
@Html.Partial("_ValidationSummary")
@using (Html.BeginUmbracoForm<MembershipController>("PasswordResetRequest"))
{
@Html.EditorFor(m => m.Email)
<input type="submit" id="submit-form" class="d-none"/>
}
<label for="submit-form" class="btn btn-primary pull-right">Send</label>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
@model object
@{
Layout = "_FieldLayout.cshtml";
}
@{
var htmlAttributes = new RouteValueDictionary();
var controlClass = "form-control mb-0";
if (ViewBag.@class != null)
{
controlClass = string.Concat(controlClass, " ", ViewBag.@class);
}
if (ViewData.ModelState.ContainsKey(ViewData.TemplateInfo.HtmlFieldPrefix) &&
ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix].Errors.Any())
{
controlClass = string.Concat(controlClass, " ", "is-invalid");
}
htmlAttributes.Add("class", controlClass);
}
@Html.Password("", ViewData.TemplateInfo.FormattedModelValue, htmlAttributes)
<p class="text-right pt-0 small">Forgot password? <a href="/membership/password-reset-request" class="small">Reset here</a></p>
+3 -1
View File
@@ -76,8 +76,10 @@
<add key="LeafWebUrl" value="http://localhost:61755/" />
<add key="PiscalNotifyCompleteUrlPath" value="notifycomplete" />
<add key="ResultsDownloadPath" value="Results/Download?token={0}" />
<add key="PasswordResetPath" value="membership/password-reset?email={0}&amp;token={1}" />
<add key="ChartPath" value="leaf-data/chart?token={0}" />
<add key="MemberVerifyPath" value="verify?email={0}&amp;token={1}" />
<add key="RegisterPath" value="membership/register" />
<add key="PiscalUnresponsiveHourCount" value="24" />
</appSettings>
<connectionStrings>
@@ -262,7 +264,7 @@
<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
<providers>
<clear />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="8" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" allowManuallyChangingPassword="true" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="8" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="8" useLegacyEncoding="true" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" passwordFormat="Hashed" />
</providers>
</membership>
+7 -1
View File
@@ -1063,6 +1063,11 @@
<Content Include="scripts\popper-utils.js.map" />
<Content Include="Views\MacroPartials\Membership\Register.cshtml" />
<Content Include="Views\Shared\EditorTemplates\EmailAddress.cshtml" />
<Content Include="Views\Shared\EditorTemplates\PasswordWithForgotLink.cshtml" />
<Content Include="Views\MacroPartials\Membership\PasswordReset.cshtml" />
<Content Include="Views\Membership\PasswordReset.cshtml" />
<Content Include="Views\MacroPartials\Membership\PasswordResetRequest.cshtml" />
<Content Include="Views\Membership\PasswordResetRequest.cshtml" />
<None Include="Web.Debug.config">
<DependentUpon>Web.config</DependentUpon>
</None>
@@ -1101,6 +1106,8 @@
<Compile Include="Controllers\ResultsController.cs" />
<Compile Include="EventHandlers\MemberEvents.cs" />
<Compile Include="Models\ChartViewModel.cs" />
<Compile Include="Models\PasswordResetForm.cs" />
<Compile Include="Models\PasswordResetRequestForm.cs" />
<Compile Include="Models\ContactForm.cs" />
<Compile Include="Models\LeafInputDetails.cs" />
<Compile Include="Models\LeafInputCreate.cs" />
@@ -1128,7 +1135,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Media\1051\" />
<Folder Include="Views\Register\" />
</ItemGroup>
<ItemGroup>
<Service Include="{4A0DDDB5-7A95-4FBF-97CC-616D07737A77}" />