Create a Forgotten Password flow similar to the Umbraco User backoffice but for front end Members
Hi,
I am searching high and low for answers to this so will ask here.
How can I create a forgotten password link on my members Login form exactly the same as the Umbraco backoffice flow?
At the moment as a backoffice user when I go to /umbraco and click the Forgotten Password? I am taken to a form on another page where I can type my email. If the email is found I receive an email where it says the following:
"Password reset requested
Your username to login to the Umbraco back-office is: [email protected]
Click this link to reset your password "
Once I click the link I am taken to a form page where I simply type my new password twice. It then says return to the login page where I can log into the backoffice with my new password.
How do I do the exact same flow but for Members??
I can't find the code in Umbraco to copy other than the keys found here: ...\Umbraco\Config\Lang\en.xml
....
<key alias="forgottenPassword">Forgotten password?</key>
<key alias="forgottenPasswordInstruction">An email will be sent to the address specified with a link to reset your password</key>
<key alias="requestPasswordResetConfirmation">An email with password reset instructions will be sent to the specified address if it matched our records</key>
<key alias="showPassword">Show password</key>
<key alias="hidePassword">Hide password</key>
<key alias="returnToLogin">Return to login form</key>
I have searched my project in VS and cannot find any reference to these.
I have actually just completed doing this myself. You will basically need to create yourself some templates and a controller and code it yourself :)
As I have literally just finished it would be a bit difficult to explain, however if you can hang on until tomorrow I will put together a quick outline of what I did, you should be able to adapt it to your needs. (I am using 8.6.3 )
This is not exactly what you want but it is what we needed and I already used a similar process for another non-umbraco website.
Firstly a forgotten password process should have some kind of validation before allowing the change of password, so basically my process is.
Send them to a page with a form where they enter either their username or email address if not the same.
When they enter their username/email check the member exists, generate some sort of reset token, store it against the member and send them an email with a link to the next step in the process (include the token).
Find the member that has that token and validate, if all ok present the member with a form to change their password.
Save the new password and redirect to your login page.
That's the basic process, I will post more details and some code snippets later when I have more time.
Ok, here goes the long explanation :D not sure this is the absolute correct way to do it but it works for me.
Firstly I added a couple of properties to the Member
I then added a new documentType and Template
Document Template code
@inherits Umbraco.Web.Mvc.UmbracoViewPage<ContentModels.PasswordReset>
@using ClientDependency.Core.Mvc
@using ContentModels = Umbraco.Web.PublishedModels;
@{
Layout = "cmsMaster.cshtml";
string title = Model.HasValue("pageTitle") ? Model.Value<string>("pageTitle") : Model.Value<string>("title");
Html.RequiresCss("/css/Article-Clean.css");
Html.RequiresCss("/css/VantagePortal.css");
//grab the querystring parameters if there are any
var user = Request.QueryString.Get("id");
var code = Request.QueryString.Get("val");
}
<div class="container">
<h3>@title</h3>
<div class="row">
<div class="col-xs-12 col-md-6">
<!-- If we have query string params then load the password form otherwise loaad the send reset form -->
@if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(code) && ViewBag.Change != true)
{
Html.RenderAction("ResetPassword", "MemberSurface", new { id = user, token = code });
}
else
{
if (Model.HasValue("contentGrid") && (TempData["Change"] == null || (bool)TempData["Change"]))
{
<div class="text">
@Html.GetGridHtml(Model, "contentGrid", "CSUSKGrid")
</div>
}
Html.RenderAction("RenderLoginReset", "MemberSurface");
}
</div>
</div>
</div>
In the template you can see I am grabbing 2 values from the query string, this will be passed in if the member uses the link they are sent. The TempData values are flags set in the SurfaceController which determines the forms that are displayed.
Methods in The surface Controller
#region Password reset
/// <summary>
/// Display the password reset View
/// </summary>
/// <returns>CRMForms/_SetPasswordPartial</returns>
public ActionResult RenderLoginReset()
{
return PartialView("CRMForms/_SetPasswordPartial");
}
/// <summary>
/// Passsword reset callback, processes the link that was sent via email
/// </summary>
/// <param name="id">UMBRACO Member Id</param>
/// <param name="token">Confirmation Token</param>
/// <returns></returns>
public ActionResult ResetPassword(int id ,string token)
{
try
{
if (String.IsNullOrWhiteSpace(token) || id < 1)
{
return CurrentUmbracoPage();
}
var memberService = Services.MemberService;
var member = memberService.GetById(id);
TempData["Change"] = true;
TempData["Message"] = "Change password";
TempData["Token"] = token;
TempData["UserId"] = id;
return PartialView("CRMForms/_SetPasswordPartial");
}
catch { }
return CurrentUmbracoPage();
}
/// <summary>
/// Process the password reset forms
/// </summary>
/// <param name="form">Posted form collection (Password change or Send email)</param>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ResetPasswordForm(FormCollection form)
{
//check for a password field
bool isPasswordForm = form.AllKeys.Contains("NewPassword");
var memberService = Services.MemberService;
if (isPasswordForm)
{
var member = memberService.GetById(Convert.ToInt32(form["userid"]));
var token = member.GetValue<string>("confirmationToken");
var tokenexpires = member.GetValue<DateTime>("tokenExpires");
#region validate the form
if (string.IsNullOrWhiteSpace(form["NewPassword"]))
{
ModelState.AddModelError("NoPass", "You must enter a password");
}
if (form["NewPassword"] != form["ConfirmPassword"])
{
ModelState.AddModelError("NoMatch", "passwords do not match");
}
if (form["token"] != token)
{
ModelState.AddModelError("TokenInv", "Reset token is invalid");
}
if(DateTime.UtcNow > tokenexpires)
{
ModelState.AddModelError("TokenExp", "Reset token has expired");
}
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
#endregion
#region reset the Umbraco password
//there was a password, so lets do the reset
memberService.SavePassword(member, form["NewPassword"]);
member.LastPasswordChangeDate = DateTime.Now;
Services.MemberService.Save(member);
//remove the token and expiry
member.SetValue("confirmationToken", "");
member.SetValue("tokenExpires", null);
Services.MemberService.Save(member);
#endregion
return RedirectToUmbracoPage(1530); //portal login
}
//not the new password form, so generate a token and send email
var user = memberService.GetByUsername(form["email"]);
if (user == null || user.Id < 0)
{
ModelState.AddModelError("ResetError", "Could not find an account that matches.");
return CurrentUmbracoPage();
}
else
{
var token = CMSUtils.GenerateUniqueCode(24);
user.SetValue("confirmationToken", token);
user.SetValue("tokenExpires", DateTime.UtcNow.AddHours(48));
memberService.Save(user);
EmailHelper.SendResetPasswordConfirmation(ControllerContext, user, token);
TempData["Change"] = false;
TempData["Message"] = "<p>An email has been sent to your registered email address containing a password reset token. <br />Please follow the instructions in the email to reset your password.</p><p>The token will expire in 48 hours.</p>";
}
return CurrentUmbracoPage();
}
#endregion
The partial view containing the two forms (one to get the members login value and the second form do the password change, they are displayed based on the TempData flags.
<div class="">
@using (Html.BeginUmbracoForm("ResetPasswordForm", "MemberSurface"))
{
@Html.AntiForgeryToken()
<!-- If our message is set, show the new password form or summary -->
if (TempData["Message"] != null)
{
<!-- If the change flag is true render the change password form -->
if (TempData["Change"] != null && Convert.ToBoolean(TempData["Change"]))
{
<fieldset>
@*<legend>@Html.Raw(TempData["Message"])</legend>*@
<div class="form-group">
@Html.ValidationSummary()
</div>
<div class="form-group ">
<label class="control-label col-sm-5">New password</label>
<div class="col-sm-7">
<input type="password" id="NewPassword" required minlength="8" name="NewPassword" placeholder="new password" class="form-control ltr" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-5">Confirm password</label>
<div class="col-sm-7">
<input type="password" id="ConfirmPassword" required minlength="8" name="ConfirmPassword" placeholder="confirm password" class="form-control ltr" />
</div>
</div>
<div class="form-group">
<input type="hidden" id="userid" name="userid" value="@TempData["UserId"]" />
<input type="hidden" id="token" name="token" value="@TempData["Token"]" />
<input type="submit" value="Reset password" class="btn btn-orange" />
</div>
</fieldset>
}
else
{
<div class="form-group">
@Html.ValidationSummary()
</div>
@Html.Raw(TempData["Message"])
}
}
else
{
<!-- no message is set, so show the send reset request form -->
<fieldset class="form-group">
<div class="form-group">
<input required type="email" id="email" data-toggle="tooltip" name="email" placeholder="login email" class="form-control ltr" />
</div>
<div class="form-group">
@Html.ValidationSummary()
</div>
<div class="form-group">
<input type="submit" value="Send" class="btn btn-green" />
</div>
</fieldset>
}
}
</div>
Below is an Example of the flow from the users perspective.
Provide Login name
OMG! You are an absolute hero!!! I will have a go with these instructions first thing in the morning thank you so much! Once I have done mine I'll see how it goes then mark as resolved if it works. Fingers crossed!
I feel I'm falling down at the very first hurdle!! :(
I have placed the "Methods in the surface controller" code inside my App_Code/Controllers/MemberControllers.cs code but I am getting the following error:
I tried VS suggestions to fix the code but I still get the same error.
Am I placing this in the wrong place?
I have added the parts to the member and created a new Template (I didn't create a new documentType I added this into my Igloo Theme)
I'm fairly new with controllers so I'm sorry for all the questions!
could you post the content of your membercontroller? something must be amiss in the controller somewhere, or at least the definition of your controller. and maybe the code around the method in case it has something wrong around it.
mine looks like
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
namespace Vantage.Controllers
{
public class MemberSurfaceController : SurfaceController
{
.......
}
}
Ah yes so first of all I didn't wrap a public class around the code. Secondly it was pasted inside another controller used for Members with the Igloo Theme so I created a separate controller called MemberResetController.cs with the following code:
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
namespace Vantage.Controllers
{
public class MemberSurfaceController : SurfaceController
{
#region Password reset
/// <summary>
/// Display the password reset View
/// </summary>
/// <returns>~/Views/Partials/_SetPasswordPartial</returns>
public ActionResult RenderLoginReset()
{
return PartialView("~/Views/Partials/_SetPasswordPartial");
}
/// <summary>
/// Passsword reset callback, processes the link that was sent via email
/// </summary>
/// <param name="id">UMBRACO Member Id</param>
/// <param name="token">Confirmation Token</param>
/// <returns></returns>
public ActionResult ResetPassword(int id, string token)
{
try
{
if (String.IsNullOrWhiteSpace(token) || id < 1)
{
return CurrentUmbracoPage();
}
var memberService = Services.MemberService;
var member = memberService.GetById(id);
TempData["Change"] = true;
TempData["Message"] = "Change password";
TempData["Token"] = token;
TempData["UserId"] = id;
return PartialView("~/Views/Partials/_SetPasswordPartial");
}
catch { }
return CurrentUmbracoPage();
}
/// <summary>
/// Process the password reset forms
/// </summary>
/// <param name="form">Posted form collection (Password change or Send email)</param>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ResetPasswordForm(FormCollection form)
{
//check for a password field
bool isPasswordForm = form.AllKeys.Contains("NewPassword");
var memberService = Services.MemberService;
if (isPasswordForm)
{
var member = memberService.GetById(Convert.ToInt32(form["userid"]));
var token = member.GetValue<string>("confirmationToken");
var tokenexpires = member.GetValue<DateTime>("tokenExpires");
#region validate the form
if (string.IsNullOrWhiteSpace(form["NewPassword"]))
{
ModelState.AddModelError("NoPass", "You must enter a password");
}
if (form["NewPassword"] != form["ConfirmPassword"])
{
ModelState.AddModelError("NoMatch", "passwords do not match");
}
if (form["token"] != token)
{
ModelState.AddModelError("TokenInv", "Reset token is invalid");
}
if (DateTime.UtcNow > tokenexpires)
{
ModelState.AddModelError("TokenExp", "Reset token has expired");
}
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
#endregion
#region reset the Umbraco password
//there was a password, so lets do the reset
memberService.SavePassword(member, form["NewPassword"]);
member.LastPasswordChangeDate = DateTime.Now;
Services.MemberService.Save(member);
//remove the token and expiry
member.SetValue("confirmationToken", "");
member.SetValue("tokenExpires", null);
Services.MemberService.Save(member);
#endregion
return RedirectToUmbracoPage(2390); //portal login
}
//not the new password form, so generate a token and send email
var user = memberService.GetByUsername(form["email"]);
if (user == null || user.Id < 0)
{
ModelState.AddModelError("ResetError", "Could not find an account that matches.");
return CurrentUmbracoPage();
}
else
{
var token = CMSUtils.GenerateUniqueCode(24);
user.SetValue("confirmationToken", token);
user.SetValue("tokenExpires", DateTime.UtcNow.AddHours(48));
memberService.Save(user);
EmailHelper.SendResetPasswordConfirmation(ControllerContext, user, token);
TempData["Change"] = false;
TempData["Message"] = "<p>An email has been sent to your registered email address containing a password reset token. <br />Please follow the instructions in the email to reset your password.</p><p>The token will expire in 48 hours.</p>";
}
return CurrentUmbracoPage();
}
#endregion
}
}
I then get the following error regarding CMSUtils:
I gather this is a plugin that generates tokens for Umbraco? I am on Umbraco 8+ and couldn't find any reference to this on the internet.
I gather this is a plugin that generates tokens for Umbraco? I am on Umbraco 8+ and couldn't find any reference to this on the internet.
yes, it is just a utility function that generates a random hex string the length you specify
public static string GenerateUniqueCode(int length)
{
char[] chars = "ABCDEF0123456789".ToCharArray();
byte[] data = new byte[1];
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
{
crypto.GetNonZeroBytes(data);
data = new byte[length];
crypto.GetNonZeroBytes(data);
}
StringBuilder result = new StringBuilder(length);
foreach (byte b in data)
{
result.Append(chars[b % (chars.Length)]);
}
return result.ToString();
}
brilliant. I added this into my MemberResetController.cs and referenced this direct and seems to work however I am now stuck on the EmailHelper part now (I mentioned this above as I think I posted the exact time you replied about the token but here is the screen again...
If I remove that line the form now renders (But will not do anything I assume because the EmailHelper line is escaped out)
Is EmailHelper another controller I'm guessing I do not have?
I think I must be near the light at the end of the tunnel and I'm so sorry I have hassled you the last few days!! If this works you really deserve a beer or 2 wherever you are!!
You will need to create your own emailhelper method that does the actual creation/sending of the email, My function does not use the standard .net mail setup it is actually creating a record in another system which sends the emails so it' wouldn't help to show me my code for that, but basically just create an email message and use the ..Net mail code to send it (you will need to populate the mail settings in the web.config to point at your emailserver etc.)
Hi Huw, that would be amazing if it's not any trouble? I was currently trying to look at the code on CMS import as we use that to email the members their passwords initially but as it's an external plugin it may not work for me.
This is a vary quick mockup of my emailhelper methhod, I removed the stuff not relevent and just added a basic smtpclient send code instead.
public static void SendResetPasswordConfirmation(ControllerContext controllerContext, IMember member, string token)
{
var requesturl = controllerContext.HttpContext.Request.Url;
string msgBody = "";
var reseturl = $@"{requesturl.Authority}/ResetPassword/?id={member.Id}&val={token}";
//You will probably want to replace this next part, I have an RTE on my Reset page that holds the template for my email
int pageId = 2458; // Page Id of Password reset, to get the email template
UmbracoHelper helper = Umbraco.Web.Composing.Current.UmbracoHelper;
var content = helper.Content(pageId).Value<string>("emailTemplate");
if (!string.IsNullOrWhiteSpace(content))
{
msgBody = content.Replace("[MEMBERNAME]", member.Name ).Replace("[RESETLINK]", reseturl).Replace("\n","");
}
//end if retreive template
var body = "<p>Email From: {0} ({1})</p><p>Message:</p><p>{2}</p>";
var message = new MailMessage();
message.To.Add(new MailAddress(member.Username)); // replace with valid value
message.From = new MailAddress("[email protected]"); // replace with your fromm address
message.Subject = "Password Reset request";
message.Body = string.Format(body, "Your from name", "[email protected]", msgBody);
message.IsBodyHtml = true;
using (var smtp = new SmtpClient())
{
var credential = new NetworkCredential
{
UserName = "[email protected]", // replace with valid value
Password = "password" // replace with valid value
};
smtp.Credentials = credential;
smtp.Host = "smtp-mail.outlook.com";
smtp.Port = 587;
smtp.EnableSsl = true;
await smtp.SendMailAsync(message);
return RedirectToAction("Sent");
}
}
You will need to set the mailSettings section up in your web.config
OK I added that under a new class called Emailhelper but now I am getting the following error (IMember could not be found):
To make it easier here is my full MemberSurface.cs (with email and passwords XXX out)
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
using System.Configuration;
using System.Security.Cryptography;
using System.Text;
namespace Vantage.Controllers
{
public class Emailhelper
{
public static void SendResetPasswordConfirmation(ControllerContext controllerContext, IMember member, string token)
{
var requesturl = controllerContext.HttpContext.Request.Url;
string msgBody = "";
var reseturl = $@"{requesturl.Authority}/ResetPassword/?id={member.Id}&val={token}";
//You will probably want to replace this next part, I have an RTE on my Reset page that holds the template for my email
int pageId = 2390; // Page Id of Password reset, to get the email template
UmbracoHelper helper = Umbraco.Web.Composing.Current.UmbracoHelper;
var content = helper.Content(pageId).Value<string>("emailTemplate");
if (!string.IsNullOrWhiteSpace(content))
{
msgBody = content.Replace("[MEMBERNAME]", member.Name).Replace("[RESETLINK]", reseturl).Replace("\n", "");
}
//end if retreive template
var body = "<p>Email From: {0} ({1})</p><p>Message:</p><p>{2}</p>";
var message = new MailMessage();
message.To.Add(new MailAddress(member.Username)); // replace with valid value
message.From = new MailAddress("[email protected]"); // replace with your from address
message.Subject = "IN-SYNC Van Portal password reset request";
message.Body = string.Format(body, "Name", "noreply@XXXX", msgBody);
message.IsBodyHtml = true;
using (var smtp = new SmtpClient())
{
var credential = new NetworkCredential
{
UserName = "xxxxx", // replace with valid value
Password = "XXXX" // replace with valid value
};
smtp.Credentials = credential;
smtp.Host = "smtp.sendgrid.net";
smtp.Port = 587;
smtp.EnableSsl = true;
await smtp.SendMailAsync(message);
return RedirectToAction("Sent");
}
}
}
public class MemberSurfaceController : SurfaceController
{
#region Password reset
/// <summary>
/// Display the password reset View
/// </summary>
/// <returns>~/Views/Partials/_SetPasswordPartial.cshtml</returns>
public ActionResult RenderLoginReset()
{
return PartialView("~/Views/Partials/_SetPasswordPartial.cshtml");
}
/// <summary>
/// Passsword reset callback, processes the link that was sent via email
/// </summary>
/// <param name="id">UMBRACO Member Id</param>
/// <param name="token">Confirmation Token</param>
/// <returns></returns>
public ActionResult ResetPassword(int id, string token)
{
try
{
if (String.IsNullOrWhiteSpace(token) || id < 1)
{
return CurrentUmbracoPage();
}
var memberService = Services.MemberService;
var member = memberService.GetById(id);
TempData["Change"] = true;
TempData["Message"] = "Change password";
TempData["Token"] = token;
TempData["UserId"] = id;
return PartialView("~/Views/Partials/_SetPasswordPartial.cshtml");
}
catch { }
return CurrentUmbracoPage();
}
public static string GenerateUniqueCode(int length)
{
char[] chars = "ABCDEF0123456789".ToCharArray();
byte[] data = new byte[1];
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
{
crypto.GetNonZeroBytes(data);
data = new byte[length];
crypto.GetNonZeroBytes(data);
}
StringBuilder result = new StringBuilder(length);
foreach (byte b in data)
{
result.Append(chars[b % (chars.Length)]);
}
return result.ToString();
}
/// <summary>
/// Process the password reset forms
/// </summary>
/// <param name="form">Posted form collection (Password change or Send email)</param>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ResetPasswordForm(FormCollection form)
{
//check for a password field
bool isPasswordForm = form.AllKeys.Contains("NewPassword");
var memberService = Services.MemberService;
if (isPasswordForm)
{
var member = memberService.GetById(Convert.ToInt32(form["userid"]));
var token = member.GetValue<string>("confirmationToken");
var tokenexpires = member.GetValue<DateTime>("tokenExpires");
#region validate the form
if (string.IsNullOrWhiteSpace(form["NewPassword"]))
{
ModelState.AddModelError("NoPass", "You must enter a password");
}
if (form["NewPassword"] != form["ConfirmPassword"])
{
ModelState.AddModelError("NoMatch", "passwords do not match");
}
if (form["token"] != token)
{
ModelState.AddModelError("TokenInv", "Reset token is invalid");
}
if (DateTime.UtcNow > tokenexpires)
{
ModelState.AddModelError("TokenExp", "Reset token has expired");
}
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
#endregion
#region reset the Umbraco password
//there was a password, so lets do the reset
memberService.SavePassword(member, form["NewPassword"]);
member.LastPasswordChangeDate = DateTime.Now;
Services.MemberService.Save(member);
//remove the token and expiry
member.SetValue("confirmationToken", "");
member.SetValue("tokenExpires", null);
Services.MemberService.Save(member);
#endregion
return RedirectToUmbracoPage(2390); //portal login
}
//not the new password form, so generate a token and send email
var user = memberService.GetByUsername(form["email"]);
if (user == null || user.Id < 0)
{
ModelState.AddModelError("ResetError", "Could not find an account that matches.");
return CurrentUmbracoPage();
}
else
{
//var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
var token = GenerateUniqueCode(24);
user.SetValue("confirmationToken", token);
user.SetValue("tokenExpires", DateTime.UtcNow.AddHours(48));
memberService.Save(user);
//Below line is not referenced
Emailhelper.SendResetPasswordConfirmation(ControllerContext, user, token);
TempData["Change"] = false;
TempData["Message"] = "<p>An email has been sent to your registered email address containing a password reset token. <br />Please follow the instructions in the email to reset your password.</p><p>The token will expire in 48 hours.</p>";
}
return CurrentUmbracoPage();
}
#endregion
}
}
I tried adding using Umbraco.Core at the top but it still didn't work after a rebuild.
By the way I think I am going to add my email template via RTE within the page too as it means we can edit the email via the umbraco backend. I assume you have the email html code inside the RTE and the id of the RTE is "emailTemplate"?
Yes that's correct, I get the page using it's Id and then grab the content of the RTE. in the backend I use the capitalised strings in square brackets and they get replaced with the mebers name and the link etc
I tried adding "using Umbraco.Web;" which cleared the IMember error but adding various Umbraco.Web.XXX cannot clear this one.
What references do have in your controller for your EmailHelper?
EDIT: I fixed the above by adding :
using Umbraco.Web.Composing;
and changing this to:
UmbracoHelper helper = Current.UmbracoHelper;
Now have an error on
Line 103: var message = new MailMessage();
So I guess I'm missing more references :S
You've probably gathered by now I haven't built a controller before! It's been a steep learning curve but in the last couple days I have learned so so much!!
So thanks to Huw's expert help and going above and beyond I now have this working!!! I'm going to add a few more bits to it to expand but my main issue is fixed and I can now click Reset your password and everything works.
Here is my final controller with a few email smtp parts hidden (I also needed to recode the smpt part a little to get it working on my server)
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
using System.Configuration;
using System.Security.Cryptography;
using System.Text;
using Umbraco.Core.Models;
using Umbraco.Web;
using Umbraco.Web.Composing;
using System.Net.Mail;
namespace Vantage.Controllers
{
public class MemberSurfaceController : SurfaceController
{
#region Password reset
/// <summary>
/// Display the password reset View
/// </summary>
/// <returns>~/Views/Partials/_SetPasswordPartial.cshtml</returns>
public ActionResult RenderLoginReset()
{
return PartialView("~/Views/Partials/_SetPasswordPartial.cshtml");
}
/// <summary>
/// Passsword reset callback, processes the link that was sent via email
/// </summary>
/// <param name="id">UMBRACO Member Id</param>
/// <param name="token">Confirmation Token</param>
/// <returns></returns>
public ActionResult ResetPassword(int id, string token)
{
try
{
if (String.IsNullOrWhiteSpace(token) || id < 1)
{
return CurrentUmbracoPage();
}
var memberService = Services.MemberService;
var member = memberService.GetById(id);
TempData["Change"] = true;
TempData["Message"] = "Change password";
TempData["Token"] = token;
TempData["UserId"] = id;
return PartialView("~/Views/Partials/_SetPasswordPartial.cshtml");
}
catch { }
return CurrentUmbracoPage();
}
public static string GenerateUniqueCode(int length)
{
char[] chars = "ABCDEF0123456789".ToCharArray();
byte[] data = new byte[1];
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
{
crypto.GetNonZeroBytes(data);
data = new byte[length];
crypto.GetNonZeroBytes(data);
}
StringBuilder result = new StringBuilder(length);
foreach (byte b in data)
{
result.Append(chars[b % (chars.Length)]);
}
return result.ToString();
}
public static bool SendResetPasswordConfirmation(ControllerContext controllerContext, IMember member, string token)
{
var requesturl = controllerContext.HttpContext.Request.Url;
string msgBody = "";
var reseturl = $@"{requesturl.Authority}/vans_login/reset/?id={member.Id}&val={token}";
//You will probably want to replace this next part, I have an RTE on my Reset page that holds the template for my email
int pageId = 3414; // Page Id of Password reset, to get the email template
UmbracoHelper helper = Current.UmbracoHelper;
var content = helper.Content(pageId).Value<string>("emailTemplate");
if (!string.IsNullOrWhiteSpace(content))
{
msgBody = content.Replace("[MEMBERNAME]", member.Name).Replace("[RESETLINK]", reseturl).Replace("\n", "");
}
//end if retreive template
var body = "<p>Email From: {0} ({1})</p><p>Message:</p><p>{2}</p>";
var message = new MailMessage();
message.To.Add(new MailAddress(member.Username)); // replace with valid value
message.From = new MailAddress("[email protected]"); // replace with your from address
message.Subject = "Portal password reset request";
message.Body = string.Format(body, "Name", "[email protected]", msgBody);
message.IsBodyHtml = true;
using (var smtp = new SmtpClient())
{
var credential = new NetworkCredential
{
UserName = "xxxxx", // replace with valid value
Password = "12345" // replace with valid value
};
smtp.Credentials = credential;
smtp.Host = "smtp.something.net";
smtp.Port = 587;
smtp.EnableSsl = true;
smtp.Send(message);
return true;
}
}
/// <summary>
/// Process the password reset forms
/// </summary>
/// <param name="form">Posted form collection (Password change or Send email)</param>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ResetPasswordForm(FormCollection form)
{
//check for a password field
bool isPasswordForm = form.AllKeys.Contains("NewPassword");
var memberService = Services.MemberService;
if (isPasswordForm)
{
var member = memberService.GetById(Convert.ToInt32(form["userid"]));
var token = member.GetValue<string>("confirmationToken");
var tokenexpires = member.GetValue<DateTime>("tokenExpires");
#region validate the form
if (string.IsNullOrWhiteSpace(form["NewPassword"]))
{
ModelState.AddModelError("NoPass", "You must enter a password");
}
if (form["NewPassword"] != form["ConfirmPassword"])
{
ModelState.AddModelError("NoMatch", "passwords do not match");
}
if (form["token"] != token)
{
ModelState.AddModelError("TokenInv", "Reset token is invalid");
}
if (DateTime.UtcNow > tokenexpires)
{
ModelState.AddModelError("TokenExp", "Reset token has expired");
}
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
#endregion
#region reset the Umbraco password
//there was a password, so lets do the reset
memberService.SavePassword(member, form["NewPassword"]);
member.LastPasswordChangeDate = DateTime.Now;
Services.MemberService.Save(member);
//remove the token and expiry
member.SetValue("confirmationToken", "");
member.SetValue("tokenExpires", null);
Services.MemberService.Save(member);
#endregion
return RedirectToUmbracoPage(2390); //portal login
}
//not the new password form, so generate a token and send email
var user = memberService.GetByUsername(form["email"]);
if (user == null || user.Id < 0)
{
ModelState.AddModelError("ResetError", "Could not find an account that matches.");
return CurrentUmbracoPage();
}
else
{
//var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
var token = GenerateUniqueCode(24);
user.SetValue("confirmationToken", token);
user.SetValue("tokenExpires", DateTime.UtcNow.AddHours(48));
memberService.Save(user);
SendResetPasswordConfirmation(ControllerContext, user, token);
TempData["Change"] = false;
TempData["Message"] = "<p>An email has been sent to your registered email address containing a password reset token. <br />Please follow the instructions in the email to reset your password.</p><p>The token will expire in 48 hours.</p>";
}
return CurrentUmbracoPage();
}
#endregion
}
}
Create a Forgotten Password flow similar to the Umbraco User backoffice but for front end Members
Hi,
I am searching high and low for answers to this so will ask here.
How can I create a forgotten password link on my members Login form exactly the same as the Umbraco backoffice flow?
At the moment as a backoffice user when I go to /umbraco and click the Forgotten Password? I am taken to a form on another page where I can type my email. If the email is found I receive an email where it says the following:
"Password reset requested Your username to login to the Umbraco back-office is: [email protected]
Click this link to reset your password "
Once I click the link I am taken to a form page where I simply type my new password twice. It then says return to the login page where I can log into the backoffice with my new password.
How do I do the exact same flow but for Members?? I can't find the code in Umbraco to copy other than the keys found here: ...\Umbraco\Config\Lang\en.xml
....
I have searched my project in VS and cannot find any reference to these.
Hi,
I have actually just completed doing this myself. You will basically need to create yourself some templates and a controller and code it yourself :)
As I have literally just finished it would be a bit difficult to explain, however if you can hang on until tomorrow I will put together a quick outline of what I did, you should be able to adapt it to your needs. (I am using 8.6.3 )
This is not exactly what you want but it is what we needed and I already used a similar process for another non-umbraco website.
Firstly a forgotten password process should have some kind of validation before allowing the change of password, so basically my process is.
That's the basic process, I will post more details and some code snippets later when I have more time.
Ok, here goes the long explanation :D not sure this is the absolute correct way to do it but it works for me.
Firstly I added a couple of properties to the Member
I then added a new documentType and Template
Document Template code
In the template you can see I am grabbing 2 values from the query string, this will be passed in if the member uses the link they are sent. The TempData values are flags set in the SurfaceController which determines the forms that are displayed.
Methods in The surface Controller
The partial view containing the two forms (one to get the members login value and the second form do the password change, they are displayed based on the TempData flags.
Below is an Example of the flow from the users perspective. Provide Login name
Message after requesting reset
Email received by member
Example link
Change password form after clicking the link
OMG! You are an absolute hero!!! I will have a go with these instructions first thing in the morning thank you so much! Once I have done mine I'll see how it goes then mark as resolved if it works. Fingers crossed!
Just yell if it isn't clear or you get stuck.
I feel I'm falling down at the very first hurdle!! :(
I have placed the "Methods in the surface controller" code inside my App_Code/Controllers/MemberControllers.cs code but I am getting the following error:
I tried VS suggestions to fix the code but I still get the same error. Am I placing this in the wrong place?
I have added the parts to the member and created a new Template (I didn't create a new documentType I added this into my Igloo Theme)
I'm fairly new with controllers so I'm sorry for all the questions!
could you post the content of your membercontroller? something must be amiss in the controller somewhere, or at least the definition of your controller. and maybe the code around the method in case it has something wrong around it.
mine looks like
Ah yes so first of all I didn't wrap a public class around the code. Secondly it was pasted inside another controller used for Members with the Igloo Theme so I created a separate controller called MemberResetController.cs with the following code:
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; using System.Net; using System.Web; using System.Web.Mvc; using System.Web.Security; using System.Web.UI; using Umbraco.Web.Models; using Umbraco.Web.Mvc;
namespace Vantage.Controllers { public class MemberSurfaceController : SurfaceController {
}
I then get the following error regarding CMSUtils:
I gather this is a plugin that generates tokens for Umbraco? I am on Umbraco 8+ and couldn't find any reference to this on the internet.
So I generated a unique code in another way just to clear the error: var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
but now get the following about EmailHelper (I assume this is a controller I do not have)
If I remove that line: EmailHelper.SendResetPasswordConfirmation(ControllerContext, user, token);
...then I can render the form! But it doesn't send anything (Probably because EmailHelper above is missing)
yes, it is just a utility function that generates a random hex string the length you specify
brilliant. I added this into my MemberResetController.cs and referenced this direct and seems to work however I am now stuck on the EmailHelper part now (I mentioned this above as I think I posted the exact time you replied about the token but here is the screen again...
If I remove that line the form now renders (But will not do anything I assume because the EmailHelper line is escaped out)
Is EmailHelper another controller I'm guessing I do not have?
I think I must be near the light at the end of the tunnel and I'm so sorry I have hassled you the last few days!! If this works you really deserve a beer or 2 wherever you are!!
You will need to create your own emailhelper method that does the actual creation/sending of the email, My function does not use the standard .net mail setup it is actually creating a record in another system which sends the emails so it' wouldn't help to show me my code for that, but basically just create an email message and use the ..Net mail code to send it (you will need to populate the mail settings in the web.config to point at your emailserver etc.)
if you need any help with that bit give me a shout, I can dig out some code from another project to do the emailing bit
Hi Huw, that would be amazing if it's not any trouble? I was currently trying to look at the code on CMS import as we use that to email the members their passwords initially but as it's an external plugin it may not work for me.
This is a vary quick mockup of my emailhelper methhod, I removed the stuff not relevent and just added a basic smtpclient send code instead.
You will need to set the mailSettings section up in your web.config
Great thanks!
and where does this sit? Inside the Member controller or as a new controller Emailhelper.cs?
At the moment I have escaped out :
Just so I can continue to build
EmailHelper is just a standalone class with some static methods like that one, you could add it to your surface controller as just a private method.
I put mine in an EmailHelper class as I have several static methods for doing different things.
OK I added that under a new class called Emailhelper but now I am getting the following error (IMember could not be found):
To make it easier here is my full MemberSurface.cs (with email and passwords XXX out)
I tried adding using Umbraco.Core at the top but it still didn't work after a rebuild.
By the way I think I am going to add my email template via RTE within the page too as it means we can edit the email via the umbraco backend. I assume you have the email html code inside the RTE and the id of the RTE is "emailTemplate"?
you need Umbraco.Core.Models for IMember
Yes that's correct, I get the page using it's Id and then grab the content of the RTE. in the backend I use the capitalised strings in square brackets and they get replaced with the mebers name and the link etc
The link ref just has https://[RESETLINK] in it
Getting more errors now regarding UmbracoHelper.
I tried adding "using Umbraco.Web;" which cleared the IMember error but adding various Umbraco.Web.XXX cannot clear this one.
What references do have in your controller for your EmailHelper?
EDIT: I fixed the above by adding :
and changing this to:
Now have an error on
So I guess I'm missing more references :S You've probably gathered by now I haven't built a controller before! It's been a steep learning curve but in the last couple days I have learned so so much!!
mailMessage is in System.Net.Mail I believe
So thanks to Huw's expert help and going above and beyond I now have this working!!! I'm going to add a few more bits to it to expand but my main issue is fixed and I can now click Reset your password and everything works.
Here is my final controller with a few email smtp parts hidden (I also needed to recode the smpt part a little to get it working on my server)
Huw you are a star!
No problem, glad I could help.
This thread was hugely helpful, thanks to you both.
is working on a reply...