MediaWizards Grimoire

Welcome,

These days I find myself getting more involved with the Umbraco community, so decided to use this site to keep track of things I have come across and had to deal with while developing sites using Umbraco.

Nov 07, 2023

Email validation flow for Member registration

In this blog post I explain how to implement an email validation flow for Member registration.

To begin with, you will need to create a couple of page nodes in your content tree. You will need one to hold the registration form, this can use whatever document Type and template you wish.

The other page node needs to implement a document Type with an alias of "verify", this is because we will be using a renderController to process the verification link.

In your prefered dev tool, create a partial view to hold the registraton form.

Partials/RegisterForm.cshtml

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@using Microsoft.Extensions.Options
@using Umbraco.Cms.Core.Configuration.Models
@using Umbraco.Cms.Core.Services
@using Umbraco.Cms.Web.Website.Models
@using Umbraco.Extensions
@inject MemberModelBuilderFactory memberModelBuilderFactory;
@inject IOptions<GlobalSettings> hostingSettings

@{
    // Build a registration model with parameters
    var registerModel = memberModelBuilderFactory
        .CreateRegisterModel()
        .WithMemberTypeAlias("member")
        .WithRedirectUrl(null)
        .WithCustomProperties(true)
        .UsernameIsEmail(false)
        .Build();

    var success = TempData["FormSuccess"] != null;
    var _hostingSettings = hostingSettings.Value;
    var warningMsg = "The email will be coming from {0} so please add this domain to your whitelist. Should you not receive your validation email, please contact ...";
}


@if (success)
{
    @* This message will show if registerModel.RedirectUrl is not defined (default) *@
    <p class="text-success">Your Registration was successful, you should now receive an email containing a validation link to verify your email address.</p>
    <p>@String.Format(warningMsg,_hostingSettings.Smtp.From)</p>
}
else
{

    using (Html.BeginUmbracoForm<MemberController>("RegisterMe", null, new { autocomplete = "off" }, FormMethod.Post))
    {
        <hr />
        <div class="mb-3">
            <label asp-for="@registerModel.Name" class="form-label"></label>
            <input asp-for="@registerModel.Name" class="form-control" autocomplete="off" aria_required="true" />
            <span asp-validation-for="@registerModel.Name" class="form-text text-danger"></span>
        </div>
        <div class="mb-3">
            <label asp-for="@registerModel.Username" class="form-label"></label>
            <input asp-for="@registerModel.Username" class="form-control" autocomplete="off" aria_required="true" />
            <span asp-validation-for="@registerModel.Username" class="form-text text-danger"></span>
        </div>
        <div class="mb-3">
            <label asp-for="@registerModel.Email" class="form-label"></label>
            <input asp-for="@registerModel.Email" class="form-control" autocomplete="off" aria_required="true" type="email"/>
            <span asp-validation-for="@registerModel.Email" class="form-text text-danger"></span>
        </div>
        <div class="mb-3">
            <label asp-for="@registerModel.Password" class="form-label"></label>
            <input asp-for="@registerModel.Password" class="form-control" autocomplete="new_password" type="password" aria_required="true" />
            <span asp-validation-for="@registerModel.Password" class="form-text text-danger"></span>
        </div>
        <div class="mb-3">
            <label asp-for="@registerModel.ConfirmPassword" class="form-label"></label>
            <input asp-for="@registerModel.ConfirmPassword" class="form-control" autocomplete="new-password" type="password" aria_required="true" />
            <span asp-validation-for="@registerModel.ConfirmPassword" class="form-text text-danger"></span>
        </div>

        @if (registerModel.MemberProperties != null)
        {
            for (var i = 0; i < registerModel.MemberProperties.Count; i++)
            {
                    <div class="mb-3">
                        <label asp-for="@registerModel.MemberProperties[i].Name" class="form-label"></label>
                        <input asp-for="@registerModel.MemberProperties[i].Value" class="form-control"/>
                        <input asp-for="@registerModel.MemberProperties[i].Alias" type="hidden"/>
                        <span asp-validation-for="@registerModel.MemberProperties[i].Value" class="form-text text-danger"></span>
                    </div>

            }
        }

        <button type="submit" class="btn btn-dark" id="register-submit">Register</button>
    }


}

Add the partial view to your Registration page.

@await Html.PartialAsync("RegisterForm")

Create a new class called MemberController using the following code.

    public class MemberController : UmbRegisterController
    {
        private readonly string _fromEmail;
        private readonly ILogger _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IEmailSender _emailSender;
        private readonly IMemberService _memberService;
        private readonly IMemberManager _memberManager;
        private readonly IMemberSignInManager _memberSignInManager;
        private readonly IHostingEnvironment _hostingEnvironment;

        public MemberController(IMemberManager memberManager, IMemberService memberService, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IMemberSignInManager memberSignInManager, IScopeProvider scopeProvider,ILogger<MemberController> logger,IHttpContextAccessor httpContextAccessor,IEmailSender  emailSender,IOptions<GlobalSettings> globalSettings, IOptions<ContentSettings> contentSettings,IHostingEnvironment hostingEnvironment) 
            : base(memberManager, memberService, umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider, memberSignInManager, scopeProvider)
        {
            _logger = logger;
            _httpContextAccessor = httpContextAccessor;
            _emailSender = emailSender;
            _memberService = memberService;
            _memberManager = memberManager;
            _memberSignInManager = memberSignInManager;
            _hostingEnvironment = hostingEnvironment;

            _fromEmail = globalSettings.Value.Smtp?.From != null ? globalSettings.Value.Smtp.From : contentSettings.Value.Notifications.Email;
        }

        [HttpPost]
        public async Task<IActionResult> RegisterMeAsync([Bind(Prefix = "registerModel")] Umbraco.Cms.Web.Website.Models.RegisterModel newmember)
        {

            if (!ModelState.IsValid)
            {
                return CurrentUmbracoPage();
            }
            var usernamecheck = _memberService.GetByUsername(newmember.Username);
            if (usernamecheck != null)
            {
                ModelState.AddModelError("Registration","The username is already in use, please use another" );
                return CurrentUmbracoPage();
            }
            //create the member in Umbraco, setting isApproved=false
            var identityUser = MemberIdentityUser.CreateNew(newmember.Username, newmember.Email, "member", isApproved: false, newmember.Name);
            IdentityResult identityResult = await _memberManager.CreateAsync(
                identityUser, newmember.Password);
            //retrieve the created member record
            var member = _memberService.GetByEmail(identityUser.Email);

            string resetGuid = null;
            if (member != null)
            {
                //Helper method to generate a random code (you could use the generatetoken here, but they are quite long, I just generate a guid)
                resetGuid = MyHelper.GenerateVerifyCode();
                //store the generated token on the Member record.
                member.SetValue("resetGuid", resetGuid);
                foreach (MemberPropertyModel property in newmember.MemberProperties.Where(p => p.Value != null)
                    .Where(property => member.Properties.Contains(property.Alias)))
                {
                    member.Properties[property.Alias]?.SetValue(property.Value);
                }
            }

            _memberService.Save(member);
            try
            {
                TempData["FormSuccess"] = await SendVerifyAccount(member.Email, resetGuid);
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Problem sending Validation email");
                throw;
            }


            // If there is a specified path to redirect to then use it.
            if (string.IsNullOrWhiteSpace(newmember.RedirectUrl) == false)
            {
                return Redirect(newmember.RedirectUrl!);
            }
            return CurrentUmbracoPage();
        }
}

Add the following method to the MemberController, this will send the verification email.

        private async Task<bool> SendVerifyAccount(string email, string guid)
        {
            try
            {
                string baseURL = _hostingEnvironment.ApplicationMainUrl.AbsoluteUri;
                var resetUrl = baseURL + "/verify/?verifyGUID=" + guid;
                Dictionary<string, string> parameters = new Dictionary<string, string>
                {
                    {"{resetUrl}", resetUrl}
                };
                var messageTemplate = @"<h2>Verify your account</h2>
            <p>in order to use your account, you first need to verify your email address using the link below.</p>
            <p><a href='{resetUrl}'>Verify your account</a></p>";

                var messageBody = messageTemplate.ReplaceMany(parameters);
                EmailMessage message = new EmailMessage(_fromEmail, email,
                    "Verifiy your account", messageBody, true);


                await _emailSender.SendAsync(message, emailType: "Contact");
                return true;
 
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,"Problems sending verify email: {0}", ex.ToString());
                return false;
            }
                
        }

Next, we need to create the RenderController to handle the verification when the member clicks the link in the email.

    public class VerifyController : RenderController
    {
        private readonly IMemberService _memberService;
        private readonly IVariationContextAccessor _variationContextAccessor;
        private readonly ServiceContext _serviceContext;

        public VerifyController(ILogger<VerifyController> logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor,IMemberService memberService, IVariationContextAccessor variationContextAccessor,ServiceContext context) : base(logger, compositeViewEngine, umbracoContextAccessor)
        {
            _memberService = memberService;
            _variationContextAccessor = variationContextAccessor;
            _serviceContext = context;
        }
        public override IActionResult Index()
        {
            VerifyViewModel pageViewModel = new VerifyViewModel(CurrentPage,
                new PublishedValueFallback(_serviceContext, _variationContextAccessor))
            {
                ValidatedMember = null
            };
            return CurrentTemplate(pageViewModel);
        }
        [HttpGet]
        public IActionResult Index([FromQuery(Name = "verifyGuid")] string guid)
        {
            if (guid != null)
            {
                var member = _memberService.GetMembersByPropertyValue("resetGuid", guid, StringPropertyMatchType.Exact);
                var enumerable = member as IMember[] ?? member.ToArray();
                VerifyViewModel pageViewModel = new VerifyViewModel(CurrentPage,
                    new PublishedValueFallback(_serviceContext, _variationContextAccessor));
                if (enumerable.Count()==1)
                {
                    var memberToValidate = enumerable.First();
                    memberToValidate.SetValue("resetGuid",null);
                    memberToValidate.SetValue("joinedDate",DateTime.UtcNow);
                    memberToValidate.SetValue("hasVerifiedAccount",true);
                    memberToValidate.IsApproved = true;
                    _memberService.Save(memberToValidate);
                    _memberService.AssignRole(memberToValidate.Email, "member");
                    TempData["ValidationSuccess"] = true;

                    pageViewModel.ValidatedMember = memberToValidate;
                }
                else
                {
                    TempData["ValidationSuccess"] = null;
                    TempData["ValidationError"] = "Verification code was not found or has expired";
                }
                return CurrentTemplate(pageViewModel);
            }

            VerifyViewModel viewModel = new VerifyViewModel(CurrentPage,
                new PublishedValueFallback(_serviceContext, _variationContextAccessor))
            {
                ValidatedMember = null
            };
            return CurrentTemplate(viewModel);
        }

    }

We need a viewmodel that inherits PublishedContentWrapped to store the model for our rendercontroller.

    public class VerifyViewModel : PublishedContentWrapped
    {
        public IMember ValidatedMember;
        public string NewPassword;
        public string ConfirmPassword;

        public VerifyViewModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback)
        {
        }
    }

Verify.cshtml template, displays the result of the validation.

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<VerifyViewModel>

@{
    Layout = "Master.cshtml";
    var success = TempData["ValidationSuccess"] != null;
    var error = TempData["ValidationError"] != null;
}

<div class="container">
    <div class="page-header">
        <h3>@Model.Value("title")</h3>
    </div>
    <div class="page-intro">
        @Html.Raw(Model.Value("message"))
    </div>
</div>

@if (success)
{
    <p class="text-success">Validation succeeded. You may now login</p>
}
@if (error)
{
    <p class="text-warning">Validation failed. @TempData["ValidationError"]</p>
}

In this blog post I explain how to implement an email validation flow for Member registration.

In part 2 of my Implementing a Forgot password for members I explain how to implement the IMemberMailService to send the reset password email.

How to implement a ForgotPassword process for Umbraco members in Umbraco 9+

Custom views give you complete control over how a Block is rendered in the backoffice and this enables you to give a better representation of the content. In this article I will explain how I created a custom Block view based on the fullcalendar.io javascript library to display events in the backoffice.

These are my experiences of creating an Umbraco package for the MediaWiz Forums, using package targets, razor class libraries, static web assets and template views.

Many thanks go to Kevin Jump and Luuk Peters without whose help I would probably have given up.

In this blog post I explain how to implement an email validation flow for Member registration.

In part 2 of my Implementing a Forgot password for members I explain how to implement the IMemberMailService to send the reset password email.

How to implement a ForgotPassword process for Umbraco members in Umbraco 9+

Custom views give you complete control over how a Block is rendered in the backoffice and this enables you to give a better representation of the content. In this article I will explain how I created a custom Block view based on the fullcalendar.io javascript library to display events in the backoffice.

These are my experiences of creating an Umbraco package for the MediaWiz Forums, using package targets, razor class libraries, static web assets and template views.

Many thanks go to Kevin Jump and Luuk Peters without whose help I would probably have given up.

Custom error handling might make your site look more on-brand and minimize the impact of errors on user experience - for example, a custom 404 with some helpful links (or a search function) could bring some value to the site.

As some of you are aware, adding custom style formats to Umbraco 9+ is somewhat cumbersome as rather than accepting nicely formatted json it is expecting a string which requires the double quotes to be escaped and therfore makes it difficult to maintain.

How to use Umbraco Blocklist to build custom forms in the backoffice..

Popular Tags