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 04 · The my-account area and its routing convention
When the catch-all is not enough
The my-account area and its routing conventionPage 25 of 36Updated
The catch-all serves a GET with the signed-in user and nothing else. What to use instead when a member page has to take a form, carry an id in its path, follow other access rules or feed a script.
Who this is for. The theme developer whose member page needs more than a template, before they ask the platform team for a route. Most of these needs already have an answer on the platform.
What the catch-all does, and the answer to each limit
A catch-all page answers GET, for anyone signed in, with one variable, user, and the template named by its path (The catch-all). Beyond that:
The page needs to
Use
Accept a form
A route built for it: the profile route, the address and password routes, a Forms module form, or a synchronous job endpoint (below)
Carry an id
A query string parameter, or the record's own URL (below)
Be open to signed-out visitors
An ordinary page outside /my-account
Be open to some members only
A check in the template backed by restricted records, or a page record restricted to a group (below)
Feed a script
The page requested as JSON, or a synchronous job endpoint (below)
Apply a different rule at the route
Nothing a site can configure: a request to the platform team
A page that takes a form
A POST to a catch-all URL is refused: with a 403 when it carries no CSRF token, and otherwise with a 405, because the catch-all's controller has nothing to handle a POST. Send the form to a route that takes one:
The member's own details: the profile route, PUT /profile (route name updateProfile). By default it accepts the person's name fields and custom fields, and asks for the current password; Profile data lists the fields and settings.
Anything else the member submits: a form built with the Forms module, whose submissions are stored, validated and can start a job (Submissions), or a synchronous job endpoint when the page needs an answer you write yourself (Custom endpoints with synchronous jobs).
A form posted the ordinary way from a member page carries the CSRF token in a hidden _csrf field, and a PUT or DELETE travels as a POST with a _method field:
The catch-all takes the whole remaining path as the template's name, so /my-account/courses/42 looks for my-account/courses/42.html. Put the id in the query string instead, /my-account/course?id=42, and look the record up in the template. Then treat the result with suspicion: the member typed that id, and it can name a record that does not exist, or one that is not theirs.
Template codetemplates/my-account/course.html
{$course = $_query->findCustomEntityRecordBy('course', ['id' => intval($_request->getParameter('id'))])}
{if $course}
<h1>{$course->getName()|escape}</h1>
{else}
<p>That course was not found.</p>
{/if}
{% set course = _query.findCustomEntityRecordBy('course', {'id': (_request.getParameter('id') ?? '0')|integer}) %}
{% if course %}
<h1>{{ course.getName()|e }}</h1>
{% else %}
<p>That course was not found.</p>
{% endif %}
The lookup obeys the record's permissions, so the member must be allowed to read course records for it to find one. Whether this member may see this course is a separate question, and the template has to ask it: Entitlements: access per record shows the check. Where the record has a public URL of its own, linking to that URL and restricting the record there is often simpler than a member page.
A page with other access rules
Every route under /my-account requires someone to be signed in, and a site cannot attach other checks to a route. Choose by who may see the page:
Signed-out visitors too: an ordinary page, outside /my-account.
Some members only: either keep the page under /my-account and decide in the template, or make it a page record restricted to a group, which the platform enforces wherever the record is read (Restricting a record to a group). A template check protects that page only, so whatever it shows must be restricted at the record as well.
A rule neither covers, such as a route that admits one user type: a request to the platform team.
Data for a script
A member page requested with Accept: application/json answers JSON instead of HTML: {"result": "success", "data": {…}}, where data holds the controller's variables without the globals. For a catch-all page that is the signed-in user's record, never including the password. That suits a script that needs the member's id or name. For anything shaped to a purpose, a synchronous job endpoint answers exactly the JSON you design (Custom endpoints with synchronous jobs).
The browser sends the session cookie with that request on its own, since it is the same site. A signed-out request is redirected to the sign-in page rather than answered with JSON, so check response.redirected before reading the body.