Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Mark Pickard 23 posts 116 karma points
    Nov 27, 2020 @ 12:57
    Mark Pickard
    0

    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.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 28, 2020 @ 10:50
    Huw Reddick
    0

    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 )

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 28, 2020 @ 14:40
    Huw Reddick
    0

    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.

    1. Send them to a page with a form where they enter either their username or email address if not the same.
    2. 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).
      1. Find the member that has that token and validate, if all ok present the member with a form to change their password.
      2. 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.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 29, 2020 @ 14:25
    Huw Reddick
    0

    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 Member properties

    I then added a new documentType and Template Document 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 Reset Step 1

    Message after requesting reset Reset Step 2

    Email received by member Reset Email

    Example link Reset Link

    Change password form after clicking the link Reset Password

  • Mark Pickard 23 posts 116 karma points
    Nov 29, 2020 @ 18:05
    Mark Pickard
    0

    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!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 29, 2020 @ 21:23
    Huw Reddick
    0

    Just yell if it isn't clear or you get stuck.

  • Mark Pickard 23 posts 116 karma points
    Nov 30, 2020 @ 12:16
    Mark Pickard
    0

    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: enter image description here

    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!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 30, 2020 @ 12:39
    Huw Reddick
    0

    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
        {
             .......
        }
    }
    
  • Mark Pickard 23 posts 116 karma points
    Nov 30, 2020 @ 13:19
    Mark Pickard
    0

    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:

    enter image description here

    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.

  • Mark Pickard 23 posts 116 karma points
    Nov 30, 2020 @ 15:44
    Mark Pickard
    0

    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)

    enter image description here

    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) enter image description here

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 30, 2020 @ 15:16
    Huw Reddick
    0

    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();
    }
    
  • Mark Pickard 23 posts 116 karma points
    Nov 30, 2020 @ 16:26
    Mark Pickard
    0

    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...

    enter image description here

    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!!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Nov 30, 2020 @ 17:50
    Huw Reddick
    0

    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.)

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 01, 2020 @ 08:57
    Huw Reddick
    1

    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

  • Mark Pickard 23 posts 116 karma points
    Dec 01, 2020 @ 08:59
    Mark Pickard
    0

    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.

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 01, 2020 @ 10:42
    Huw Reddick
    0

    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

  • Mark Pickard 23 posts 116 karma points
    Dec 01, 2020 @ 10:54
    Mark Pickard
    0

    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 :

    EmailHelper.SendResetPasswordConfirmation(ControllerContext, user, token);
    

    Just so I can continue to build

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 01, 2020 @ 11:11
    Huw Reddick
    1

    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.

  • Mark Pickard 23 posts 116 karma points
    Dec 01, 2020 @ 12:29
    Mark Pickard
    0

    OK I added that under a new class called Emailhelper but now I am getting the following error (IMember could not be found):

    enter image description here

    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"?

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 01, 2020 @ 12:44
    Huw Reddick
    1

    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

    emailTemplate

    The link ref just has https://[RESETLINK] in it

  • Mark Pickard 23 posts 116 karma points
    Dec 01, 2020 @ 13:33
    Mark Pickard
    0

    Getting more errors now regarding UmbracoHelper.

    enter image description here

    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!!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 01, 2020 @ 14:09
    Huw Reddick
    1

    mailMessage is in System.Net.Mail I believe

  • Mark Pickard 23 posts 116 karma points
    Dec 02, 2020 @ 17:48
    Mark Pickard
    101

    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
    
        }
    }
    

    Huw you are a star!

  • Huw Reddick 1929 posts 6717 karma points MVP 2x c-trib
    Dec 02, 2020 @ 17:53
    Huw Reddick
    1

    No problem, glad I could help.

  • Amir Khan 1287 posts 2744 karma points
    Aug 18, 2022 @ 20:30
    Amir Khan
    1

    This thread was hugely helpful, thanks to you both.

Please Sign in or register to post replies

Write your reply to:

Draft