Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 02 · Registration, sign-in and password recovery

Registering a member

Registration is off until switched on, and its settings decide the new account's type, status and groups. The settings, the form and its fields, adding a field, what happens on submit, and the case most sites handle badly: an email address that already has an account.

Who this is for. The theme developer building the registration page, and whoever configures it. Afterwards you can build the form, add fields to it, and say what a new member gets.

The settings

In the Users module's settings, under Registration:

SettingDefaultEffect
EnableOffOff, /register answers 404
TypeFrontendThe new user's type. It offers Backend too, which would give every registrant the admin: keep it on Frontend (The rule)
GroupsNoneThe groups every new member joins
StatusActiveActive or Inactive; an inactive member cannot sign in again after registering (The states a member can be in)
Success Redirect UrlEmptyWhere a new member lands; a full URL, host included, is followed, always over https
reCAPTCHAv2 CheckboxDisabled, v2 Checkbox or v2 Invisible, for the form and the API route alike (Where the captcha is available)

One more Users setting matters here: Use email as username, off by default. When it is on, the username follows the person's email address every time the person is saved, so members sign in with their email address.

The form

users/register.html, which you write, posts to /register. It is open only to signed-out visitors: a signed-in visitor is redirected to their account with "You are not allowed to view this page while logged in". The fields it may send:

FieldHolds
usernameThe sign-in name. With Use email as username on, send the email address
password, confirmPasswordThe password, twice. The only rule is the minimum length setting, six characters by default
person[email], person[title], person[firstName], person[middleName], person[lastName], person[gender]The person
person[customFieldValues][…]The person's custom fields, posted as on the profile form (Worked case)
g-recaptcha-responseWritten by the captcha widget when the captcha is on
passkey1 to register with a passkey, where passkeys are on; the password is then optional (Passkeys)

Only the fields above are accepted. The type, status and groups always come from the settings, so a visitor cannot register themselves into a group or the admin.

Template code templates/users/register.html, the form
<form method="post" action="{$_url->getPath('register')}">
  <label>Email <input type="email" name="person[email]" value="{$user->getPerson()->getEmail()|escape}" required></label>
  {if isset($_validation) && $_validation->hasFieldError('person[email]')}
    <p class="error">{$_validation->getFieldErrorMessage('person[email]')}</p>
  {/if}
  <input type="hidden" name="username" value="" data-copy-from="person[email]">
  <label>First name <input name="person[firstName]" value="{$user->getPerson()->getFirstName()|escape}"></label>
  <label>Last name <input name="person[lastName]" value="{$user->getPerson()->getLastName()|escape}"></label>
  <label>Password <input type="password" name="password" autocomplete="new-password" required></label>
  <label>Password again <input type="password" name="confirmPassword" autocomplete="new-password" required></label>
  {if isset($_recaptcha)}{$_recaptcha->getWidgetHtml()}{$_recaptcha->getScriptTag()}{/if}
  <button>Create account</button>
</form>
<form method="post" action="{{ _url.getPath('register') }}">
  <label>Email <input type="email" name="person[email]" value="{{ user.getPerson().getEmail()|e }}" required></label>
  {% if _validation is defined and _validation.hasFieldError('person[email]') %}
    <p class="error">{{ _validation.getFieldErrorMessage('person[email]') }}</p>
  {% endif %}
  <input type="hidden" name="username" value="" data-copy-from="person[email]">
  <label>First name <input name="person[firstName]" value="{{ user.getPerson().getFirstName()|e }}"></label>
  <label>Last name <input name="person[lastName]" value="{{ user.getPerson().getLastName()|e }}"></label>
  <label>Password <input type="password" name="password" autocomplete="new-password" required></label>
  <label>Password again <input type="password" name="confirmPassword" autocomplete="new-password" required></label>
  {% if _recaptcha is defined %}{{ _recaptcha.getWidgetHtml() }}{{ _recaptcha.getScriptTag() }}{% endif %}
  <button>Create account</button>
</form>

The hidden username is filled from the email field by a line of the site's script, for a site that signs members in by email address. A failed submit renders the form again with what was typed in user, so the values above survive an error; the passwords do not, by design.

Adding a field

A field the person does not have is a custom field: define it for the Person entity under Customization › Custom Fields, then post it under person[customFieldValues]. Loop over the person's custom fields rather than writing their ids into the template (Profile data has the partial). A value that decides access must not be a custom field the member can set: registration accepts custom field values from the form like any other field (Members can edit their own custom fields).

An email address that already has an account

Email addresses are unique among people, so registering with one that exists fails on the email field with "This email is already used." That message tells the visitor the address has an account, which the forgotten-password flow is careful not to do. Handle it as a next step rather than a dead end: when the email field has that error, show the way in instead.

Template code
{if isset($_validation) && $_validation->hasFieldError('person[email]')}
  <p>That address may already have an account.
    <a href="{$_url->getPath('login')}">Sign in</a> or
    <a href="{$_url->getPath('forgotPassword')}">reset your password</a>.</p>
{/if}
{% if _validation is defined and _validation.hasFieldError('person[email]') %}
  <p>That address may already have an account.
    <a href="{{ _url.getPath('login') }}">Sign in</a> or
    <a href="{{ _url.getPath('forgotPassword') }}">reset your password</a>.</p>
{% endif %}

The address may also belong to a person with no login: someone who submitted a form or was imported from a CRM. Registration cannot attach a login to that person, so the visitor is refused all the same. If the site collects people before they register, decide with the client how those people become members, for example by creating their users in an import.

What happens on submit

  1. A form with no password and no passkey is refused at once with "Please choose a password, or register with a passkey."
  2. The captcha is checked next, when it is on. A missing or failed answer re-renders the form with a 422, "Recaptcha validation failed".
  3. The person and user are created, with the type, status and groups from the settings.
  4. The new member is signed in at once, with the message "Successfully Registered", even when the status setting is Inactive. A member registering with a passkey is sent to create it instead, and is signed in once it is created.
  5. They land on the Success Redirect Url, or failing that the Users setting Login redirect url, or failing that /my-account.

No email is sent. The platform sends neither a welcome nor a confirmation. Send a welcome from a job on the user's create event (Send an email when a record is created).

Go deeper

Was this page helpful?

Your answer goes to the docs team.