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

Account routes and their templates

Every sign-in, registration and password route, with its URL, who may use it, the template it renders, what that template receives, and whether the framework ships one. The map to read before restyling any account screen, and the conventions every account form shares.

Who this is for. The theme developer whose first member task is restyling the account screens. Afterwards you know which templates to write, what each receives, and how the forms post.

The routes

Paths are the defaults. Templates are paths under templates/; a file at the same path in your site replaces the framework's.

Route namePath and methodsWhoTemplateReceivesFramework copy
login/login, GET, POSTAnyoneusers/login.htmluser, localPasswordEnabled, passkeyEnabled, enabledOAuthAuthenticatorsYes, Smarty
logout/logout, GETSigned inNone: it redirects
register/register, GET, POSTSigned out onlyusers/register.htmluser, passkeyEnabled, and _recaptcha when the captcha is onNo
forgotPassword/forgot-password, GET, POSTSigned outusers/forgot-password.htmlNo
tokenLogin/token-login?t=…, GETAnyone with a tokenNone: it signs in and redirects
resetPassword/reset-password, GET, PUTSigned inusers/reset-password.htmluser, passkeyEnabled, localPasswordEnabledNo
changePassword/change-password, GET, PUTSigned inusers/change-password.htmlNo
updateProfile/profile, GET, PUTSigned inusers/profile.htmluserNo
confirmEmail/confirm-email?token=…, GETAnyone with a tokenusers/users/confirm-email.htmluserNo

After any sign-in, the platform's own /oauth/authenticate route finishes the session and, when remember-me is on, shows the prompt at /oauth/remember-me, whose template the framework ships for both engines (Remember me). The member area's screens under /my-account are listed in The built-in member screens.

Two settings switch whole flows off. Registration answers 404 until the registration setting Enable is on (Registration settings). The Users setting Change Password, under Frontend, on by default, switches off the forgotten, reset and change password routes together.

When a template is missing

The framework ships one of these templates, the sign-in page, and only in Smarty. For any other route your site has not written a template for, the result depends on the engine:

  • On a Smarty site the route answers HTTP 200 with a page that reads only Template not found: users/register.html: no header, no footer, no error in the log to find.
  • On a Twig site the route fails with an error, and the framework's Smarty sign-in page cannot be used either: write users/login.html in Twig too.

Write a template for every route the site links to, and request each one signed out and signed in before launch.

Linking to the routes

Link by route name, so the links follow if a path is changed for the site: $_url->getPath('changePassword') in Smarty, _url.getPath('changePassword') in Twig. The names are those in the table.

How the account forms post

  • Methods. A route that takes PUT is posted as a POST with a hidden _method field.
  • The CSRF token. Forms sent by a signed-in member (reset and change password, the profile) carry the token in a hidden _csrf field, or are refused with a 403. Forms sent signed out (sign-in, registration, forgotten password) are not checked, and _csrfToken does not exist when they render, so do not print it unguarded (CSRF on member forms).
  • Errors. A failed form renders the same template again, with the visitor's input in user where the route has it. Field errors arrive as _validation, with hasFieldError(name) and getFieldErrorMessage(name), usually with HTTP 422; sign-in failures are a message with HTTP 400. Print the site's messages on every account page, since several routes report through them after a redirect.
Template code templates/users/change-password.html, the form
<form method="post" action="{$_url->getPath('changePassword')}">
  <input type="hidden" name="_method" value="PUT">
  <input type="hidden" name="_csrf" value="{$_csrfToken}">
  <label>Current password <input type="password" name="currentPassword" autocomplete="current-password" required></label>
  {if isset($_validation) && $_validation->hasFieldError('currentPassword')}
    <p class="error">{$_validation->getFieldErrorMessage('currentPassword')}</p>
  {/if}
  <label>New password <input type="password" name="password" autocomplete="new-password" required></label>
  <label>New password again <input type="password" name="confirmPassword" autocomplete="new-password" required></label>
  <button>Change password</button>
</form>
<form method="post" action="{{ _url.getPath('changePassword') }}">
  <input type="hidden" name="_method" value="PUT">
  <input type="hidden" name="_csrf" value="{{ _csrfToken }}">
  <label>Current password <input type="password" name="currentPassword" autocomplete="current-password" required></label>
  {% if _validation is defined and _validation.hasFieldError('currentPassword') %}
    <p class="error">{{ _validation.getFieldErrorMessage('currentPassword') }}</p>
  {% endif %}
  <label>New password <input type="password" name="password" autocomplete="new-password" required></label>
  <label>New password again <input type="password" name="confirmPassword" autocomplete="new-password" required></label>
  <button>Change password</button>
</form>

This page is signed in only, so _csrfToken is always there. The pages that follow take each flow in turn; Restyling the account screens covers overriding them as a set.

The same flows from a headless front end

A front end that is not served by Core dna uses the API's visitor routes instead: POST /cdna-api/register, POST /cdna-api/forgot-password and POST /cdna-api/reset-password (Accounts). Registering through the API creates the account but does not sign the person in: the front end then sends them through the OAuth authorisation flow (OAuth clients).

Go deeper

Was this page helpful?

Your answer goes to the docs team.