Login and Registration

2 views Markdown

Six views, one split layout, one step machine. The auth surface is built once, then rearranged in the browser.

Overview

The login page is one document holding seven states. Nothing reloads, so every string, field and consent must be present in the markup the server produced.

Registration is the opposite problem. Almost every field is optional, so the form is a set of conditionals.

Structure

All six go through one private helper on the sign controller. They share a guard, a canonical link and a title convention.

ViewGuest onlyNotes
auth/loginYesSeven states, one document
auth/registerYes, unless a verification is pendingCountries, custom fields, contracts, gating
auth/forget-passwordYesAlways reports success, existing address or not
auth/reset-passwordYesThe token resolves first: the form, or an expired notice
auth/activateYesThe email confirmation landing page
auth/accept-inviteNoGuests and members; the state says which of four cases
stage_title, stage_text, stage_foot The left column. The foot holds the cross link, wrapped in the matching switch.
split_class, inner_class Shape modifiers for a wider form; register uses both.
layouts/auth.tpl Carries head, scripts, body_class, content, body_end, plus the five above.

Step by Step

Build the Login State Machine

  1. Print every state in one document, one visible and the rest hidden. Each carries a step attribute: login, password, totp, sms, email, code-login, restricted.
  2. Gate the machine behind the login switch, with a notice in the other branch.
  3. Put the passwordless call to action before the social buttons, each behind its own switch and divider.
  4. Use one shared one time code component for all four code states. It binds to the input class, not the state, so a second one gets no paste or auto verify.

Gate the Registration Fields

  1. Read visibility from the gating map, never from a configuration key.
  2. Mark required fields with a data attribute, not the native one alone, so the validator skips hidden ones.
  3. Print custom fields and contracts from the arrays given; neither has a fixed set.
  4. The server is the authority — every browser check is repeated on submit.

Decide Whether Your Form Is Minimal

  1. Set the manifest flag true only when your register view prints none of phone, landline and national id.
  2. Leave it false when you print them behind the visibility switches, as two of three shipped themes do.
  3. It is read from the manifest, not the request, so a submission cannot claim it.

Wrap Every Auth Link in Its Switch

  1. Wrap a sign up link in the registration switch, a sign in link in the login switch. The account menu and cart use the combined one.
  2. Do not rely on hiding — the server refuses a closed registration or login itself.

Reference

Auth Core Signatures

exact signatures
// The single entry point for creating a member. See the input keys below.
public static function register(array $input): array;

// Password login. $type is 'member' or 'admin'; the member master switch is checked here.
public static function attemptLogin(string $type, string $email, string $password, bool $remember = false): array;

// The ONE place a session is established, password or not. Every passwordless flow
// (code login, social, post-registration, email verification) ends here.
public static function completeLogin(string $type, int $userId, object $user, bool $remember = false, array $sso = []): string;

// Passwordless: issue a one-time code, then exchange it for a session.
// issueLoginCode reports success whether or not the address exists.
public static function jetpassEnabled(): bool;
public static function issueLoginCode(string $email): array;
public static function jetpassLogin(string $email, string $code, bool $remember = false): array;

// Password reset. issueReset is also enumeration-safe.
public static function issueReset(string $type, string $email): array;
public static function verifyResetToken(string $rawKey, string $type): object|false;
public static function resetPassword(string $type, int $userId, string $password): void;

// Providers to offer, for a given mode and audience.
public static function activeProviders(string $mode, string $context): array;

What register() Accepts

the $input array, key by key
$input = [
    // Identity
    'first_name'     => 'Ada',
    'last_name'      => 'Lovelace',
    'email'          => '[email protected]',
    'password'       => 'read with the pass-through filter, never a text filter',

    // Account type and the corporate fields it unlocks
    'account_type'   => 'individual',   // or 'corporate'
    'company_name'   => '',
    'tax_number'     => '',
    'tax_office'     => '',

    // Optional contact fields, each behind its own visibility switch
    'phone'          => '',
    'landline_phone' => '',
    'national_id'    => '',

    // Billing address
    'address1'       => '',
    'state'          => '',             // id when picked from the list, free text otherwise
    'city'           => '',             // same rule as state
    'postal_code'    => '',
    'country'        => 0,              // resolved to an id before it gets here

    // Consents
    'marketing_email'    => 0,
    'marketing_sms'      => 0,
    'contract'           => 1,          // the terms checkbox
    'contracts_required' => true,       // whether any contract is actually configured

    // Operator-defined extra fields, collected by the operation
    'custom_fields'      => [],

    // Declared by the THEME MANIFEST, never by the request. True skips the required
    // checks for phone, landline and national id, and nothing else.
    'minimal_fields'     => false,
];

The Field Gating Map

Template variableDefaultControls
$registration.account_type_visibleonThe individual/corporate chooser
$registration.company_name_requiredonCompany name, corporate branch
$registration.tax_number_required, tax_office_requiredoffThe corporate tax fields
$registration.phone_visible, phone_requiredon, onMobile number
$registration.landline_visible, landline_requiredoff, offLandline number
$registration.national_id_visible, national_id_requiredoff, offNational identity, individual branch
$registration.password_min_length12Enforced on both sides

What Each View Receives

ViewVariableHolds
auth/register$countriesid, a2_iso, name. States and cities cascade over AJAX
auth/register$custom_fieldsOperator-defined extra fields, localised. No fixed set
auth/register$contractsContract pages for sign up; each carries a link
auth/register$contract_namesThose titles joined with commas, for a naming label
auth/register$registrationThe gating map above
auth/register$verify_email, $verify_nameFilled when $verify_pending is true, so the step names the member
auth/reset-password$reset_validWhether the token resolved; false means the expired notice
auth/reset-password$verify_keyThe raw token, posted back with the new password
auth/login$social_providers, $jetpass_enabledSet by the login page; register sets its own

Page and Feature Switches

$registration_enabled Whether to show a standalone sign up link: the master switch minus purchase first mode. It can be off while the cart still registers.
$login_enabled The member login master switch; administrator login is separate.
$account_actions_enabled Whether an account is possible at all: the registration master, not the link switch.
$jetpass_enabled The passwordless code flow, off by default. Gate its call to action.
$social_providers The providers for this page; empty means no divider and no buttons.
$verify_pending Set when a signed-in member has an unverified address; the register view shows the verification step.

Example

views/auth/login.tpl
{extends file='layouts/auth.tpl'}

{block name=stage_title}{lang key='auth_stage_login_title'}{/block}
{block name=stage_text}{lang key='auth_stage_login_text'}{/block}

{* The cross link is gated: offering sign-up on a closed installation is a dead end. *}
{block name=stage_foot}
  {if $registration_enabled}
    {lang key='auth_stage_login_foot'} <a href="{link route='sign-up'}">{lang key='auth_stage_login_foot_link'}</a>
  {/if}
{/block}

{block name=content}
{if $login_enabled}

  {* State 1: email. Visible; every other state ships hidden in the SAME document. *}
  <div data-auth-step="login">
    <form data-auth-form="login">
      {csrf form='sign-in'}
      <input type="email" name="email" class="form-control" required>
      {captcha area='sign-in' tray='login-captcha'}
      <button type="submit" class="btn">{lang key='website/sign/continue'}</button>
    </form>

    {* Passwordless BEFORE social, each behind its own switch and divider. *}
    {if $jetpass_enabled}
      <div class="auth-divider">{lang key='website/sign/or'}</div>
      <button type="button" class="btn" data-auth-action="auth-code-login">{lang key='website/sign/jetpass-cta'}</button>
    {/if}
    {if $social_providers}
      <div class="auth-divider">{lang key='website/sign/or'}</div>
      {foreach $social_providers as $p}
        <a class="btn" href="{$p.url}"><i class="bi {$p.icon}"></i>{$p.label}</a>
      {/foreach}
    {/if}
  </div>

  <div class="d-none" data-auth-step="password">{* ... *}</div>

  {* All four code states reuse ONE component: the script binds to .otp-input,
     not to the state, so a second implementation gets none of its behaviour. *}
  <div class="d-none" data-auth-step="code-login">
    <div class="otp-group" data-otp>
      <input class="otp-input" inputmode="numeric" maxlength="1">
    </div>
  </div>

  <div class="d-none" data-auth-step="restricted">{* ... *}</div>

{else}
  <p>{lang key='website/sign/login-disabled'}</p>
{/if}
{/block}
the manifest flag, and the rule for setting it
// templates/website/{Theme}/theme.php

return [
    'meta' => [
        'name'    => 'Acme',
        'version' => '1.0.0',
        'author'  => 'Acme',

        // TRUE only when the register view renders NONE of phone, landline and
        // national id. Auth::register then skips the operator's required checks
        // for exactly those three fields; every other validation still runs.
        //
        // FALSE when the view renders them behind $registration.*_visible, which
        // is what two of the three bundled themes do.
        //
        // Setting it true while still printing the fields is the one wrong answer:
        // the requirement is skipped even though the visitor saw an empty input.
        'signup_minimal' => false,
    ],
    'engine' => 'smarty',
    'status' => 'ready',
];

Pitfalls

Never reveal whether an account exists

Password reset and the passwordless code report success for an unknown address on purpose. Your script must advance in every case.

A password is read with the pass-through filter

Every text filter strips the characters that make a password strong. The stored value then differs from what was typed, with no error.

The minimal flag is a claim about your markup

It is read from your manifest and trusted, so it must describe what your form prints. Declare it while printing the optional fields and an empty submission passes unreported.

Every auth form needs its token and captcha area

One action name is the token form key, the captcha area and the rate limit bucket at once. A new name opts out of all three. The passwordless request and its verification share the login action.

A closed login still lets a new registration in

Post registration sign in calls the session establishing method directly, not the password path where the switch is checked.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.