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 article I will explain how to configure the TinyMCE rich text editor in Umbraco v14

This is my dive into the new Umbraco 14 backoffice to create a Member EntityAction in order to send an email to the selected member.

Previously known as Tree Actions, Entity Actions is a feature that provides a generic place for secondary or additional functionality for an entity type. An entity type can be a media, document and so on.

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