Creating an API endpoint to create a Member in Umbraco 10

Background

I had a project where I needed to create a Member within the Umbraco backoffice via an API endpoint. This work was done in Umbraco 10. 

The Scenario

An external app calls this API to create a member. A member is only created if the username / email exists in an seperate external system. If it does exist then a Member is created and an email is then sent to the Member asking them to setup a password. This email contains a unique token which is valid of 1 hour from creation.

What's not covered in this blog

I've still to setup the authentication method for this API endpoint so that's not covered.

The code

I started the process really simply by creating a new controller which inherits the base UmbracoApiController and it had a method called public async Task<string> CreateAccount(string userName, string accountCode) For more details about the different types of controllers you can inherit from in Umbraco, have a look at the documentation

In this controller I kicked things off by intially just returning a string saying "API endpoint hit". 

This meant when I used Postman, I could test the URL and routing and make sure it worked. e.g. I post from Postman to the following URL  https://localhost:44339/umbraco/api/manageaccount/createaccount

I then get a message back in Postman saying the API endpoint was hit. Success. 

After that it was a case of building up the functionality. I'll not going in to all the code but I'll highlight some of the key bits that caught me out. 

Getting a setting from appsettings.json

public class ManageAccountController : UmbracoApiController
{
   private readonly IConfiguration _configuration;

   public ManageAccountController(IConfiguration configuration)
   {
      _configuration = configuration;
   }

   [HttpPost]
   public async Task<string> CreateAccount(string userName, string accountCode)
   {
      string APIUrl = _configuration.GetValue<string>("ClientApi:APIUrl");
   }
}

This is what I have in my appsetting.json

 "ClientApi": {
    "APIKey": "",
    "APIUrl": "/the_client/Api_url",
    "Base64Secret": "",
    "ClientId": "",
    "Issuer": ""
  }

Note: You could / should use the IOptions pattern instead of using IConfiguration - this has been brought to my attention and a future refactor will use IOptions. 

I'll be reading a couple of blogs over the weekend regarding this - 
Stackoverflow thread & blog by David Hayden. Thanks Nik for pointing this out to me. #h5yr

Check whether a Member exists and create them

Before I created a Member in the backoffice, I wanted to check that a member hadn't already been created. Perhaps someone manually created an account or the member actually registered themselves already.

To do this, I used the MemberService in to my controller

     private readonly IMemberService _memberService;
    

in a similar way that I injected IConfiguration in for the appsettings code above. 

Once I had access to the memberService, I could do the following to check if they exist already : 

if (_memberService.GetByUsername(userName) != null)
{
   _combinedLogger.LogError("CreateAccount - Username already exists, no account created for {0}", userName);
   return("Username already exists");
}

In this case, the username is actually their email address and so it's unique to them. 

If the Member doesn't exist then we go and create one - 

var member = _memberService.CreateMemberWithIdentity(userName, accountDetails.Email, Member.ModelTypeAlias);

member.SetValue("AccountCode", accountCode);
_memberService.Save(member);

 

I also assign a value to a custom field on the Member called "AccountCode" and then save it. One thing I didn't realise was that `CreateMemberWithIdentity` does a save, so I'm actually double hitting the database in this call but it seemed to be the better option for what I needed. The other option is just `.CreateMember` which doesn't do an initial save but it requires more parameters to be passed in. 
While writing this blog I've actually had a rethink and I've now changed the above code to be 

var member = _memberService.CreateMember(userName, accountDetails.Email, userName, Member.ModelTypeAlias);

This means I only hit the database once when setting up a new member.

 

Send password setup email to the Member

This is pretty clever and I can't take any credit for it. Nik Rimington created the functionality for the Community Quiz
Have a look at the Pull request - you can see how it's all setup. 

In very simple terms an email is sent to the Member saying that a new account has been setup and there is a button in the email which has a unique token attached to the end of the url e.g. 

https://localhost:44339/forgotten-password?u=my_email_address%40gmail.com&t=jfvBMvrv2kiqUX0Q%2bKInRrVl%2femhtkm0ZnByMTE3MA%3d%3d

When you click on the link, you are taken to the forgotten-password page which looks for the token. If the token is valid, the member is prompted to set a password. If it's invalid or has expired, they are informed of this and they can send another password reset request. 

I set this up as a service so that I can hook in to it for some other work that is in the pipeline where I know I will need to send password reset emails. 

I used a local SMTP client called Papercut to test all this functionality and you can see how to use Papercut in Sebastiaan's blog post.

Get the password reset / forgotten page

There is a content picker in the backoffice which allows me to pick the password reset page, this is set on the Root Node of the site.
This allows me to dynamically set where the email directs the Member to. It also means that if, for any reason, this page changes or gets renamed, it's all configurable via the backoffice. 

To get this information by using IPublishedContentQuery

 private readonly IPublishedContentQuery _publishedContentQuery;

 

var root = _publishedContentQuery.ContentAtRoot().OfType<SiteRoot>().FirstOrDefault();

if (root?.PasswordResetPage != null)
   {
      var passwordResetPage = root.PasswordResetPage?.Url(mode: Umbraco.Cms.Core.Models.PublishedContent.UrlMode.Absolute);

    await _passwordService.SendPasswordSetupEmail(accountDetails.Email, passwordResetPage);


   ...
   }

 

 

Published on: 06 January 2023