• Mahmoud Aglan's avatar
    feat(portal): self-registration behind a closed door, and the accessibility gate · 500806ca
    Mahmoud Aglan authored
    The last of the programme, plus the exit gate CLAUDE.md requires before any UI
    work counts as finished.
    
    Self-registration (W10) — shipped, and closed
    ---------------------------------------------
    This is the only path in the product that writes into `people` and
    `participants` with no member of staff in the loop, so it ships **off**:
    `portal.self_registration_enabled` defaults to false on every tenant including
    new ones, and the middleware answers 404 — never 403, because a 403 advertises
    that there is a signup form here and invites someone to look for the setting.
    A public form that starts accepting strangers because a deploy happened is not
    a decision anybody made.
    
    Phone verification is the precondition, not a feature. The flow it replaces was
    a study in how not to do this: `verify()` accepted the constant '0000' whenever
    a seeded setting said 'demo', then resolved *any* active user by phone —
    academy owners included — and minted a token with `mobile:*`; and in the other
    mode it generated a code, cached it, and never sent it anywhere, so turning the
    bypass off locked everyone out rather than securing anything.
    
    So: no bypass exists, in any mode, behind any flag. Only the SHA-256 is stored,
    with a bounded attempt count, in a table rather than the cache — a code you
    cannot audit is a code you cannot investigate. A send that fails deletes the
    record, because a stored code nobody received is precisely the old failure.
    There is a test asserting '0000' and '1234' are refused.
    
    Registration goes **through** ParticipantService rather than around it. Writing
    the row directly skipped the participant number, the already-a-member check,
    the audit columns and ParticipantRegistered — a second creation path that
    looked identical and was not. It does not enrol and it does not take money:
    EnrollmentService::enroll() needs an actor authorised to enrol and a
    self-registering guardian is not one. The member asks; staff enrol.
    
    `people.created_by` is NOT NULL and there is no staff member here, so the
    account is created first with no person attached and becomes the author of its
    own records — which is also the truth about who typed them. Loosening the
    column would have weakened it for every other path.
    
    DuplicateDetectionService runs on every signup and its findings are stored on
    the row and shown in the approval queue. It has existed for a long time with
    nothing surfacing what it found, so a second Person for an existing member
    appeared silently and the two drifted apart forever.
    
    The accessibility gate
    ----------------------
    Eleven checks against the HTML the portal actually renders for a real member,
    not against the templates: language matching direction, image alternatives,
    accessible names on every icon-only control, a label for every form control,
    named landmarks, focus never globally removed, reduced motion honoured, a
    stated focus ring, announced errors, and dir=ltr on numeric inputs.
    
    Two things it found. There was no explicit focus-visible style, so the ring was
    the browser default — a thin blue line that disappears against a tenant whose
    brand is blue; it is now `currentColor`, which inherits an already
    contrast-checked colour and is legible on every surface in both themes.
    And validation messages were rendered as plain text: a screen-reader user
    submitted a form and heard nothing. Every one is a live region now, asserted at
    the source, because an error block only renders when there is an error and a
    clean page proves nothing either way.
    
    Also: `:focus` gets scroll-margin so a focused control is never left under the
    sticky header or the bottom tab bar (2.4.11).
    
    Verification
    ------------
    - 145 migrations from zero on an empty database, seeded, booted a second time:
      every portal grant intact, self-registration absent and therefore closed
      (SettingsService returns the default, which is false — it fails closed).
    - The restored oc_sport tenant: nothing to migrate, seeders clean, health 200,
      713 invoices / 356 participants / 649 payments untouched.
    - Suite: 76 pass on SQLite; on the tenant PortalSmoke 3/3, AdminScreens 2/2,
      PaymentProof 11/11, CheckInScan 10/10, ServiceRequestEffect 14/14,
      SelfRegistration 13/13, PortalAccessibility 11/11.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    500806ca
Name
Last commit
Last update
..
Activities Loading commit data...
Admin Loading commit data...
Assignments Loading commit data...
Attendance Loading commit data...
Audit Loading commit data...
Auth Loading commit data...
Branches Loading commit data...
CashSessions Loading commit data...
Components Loading commit data...
Concerns Loading commit data...
Dashboard Loading commit data...
Documents Loading commit data...
Enrollments Loading commit data...
Evaluations Loading commit data...
Events Loading commit data...
Facilities Loading commit data...
Financial Loading commit data...
Groups Loading commit data...
Guardian Loading commit data...
HR Loading commit data...
Inventory Loading commit data...
Invoices Loading commit data...
Messaging Loading commit data...
Notifications Loading commit data...
POS Loading commit data...
Parent Loading commit data...
Participants Loading commit data...
People Loading commit data...
Portal Loading commit data...
Pricing Loading commit data...
Profile Loading commit data...
Programs Loading commit data...
Public Loading commit data...
Receptionist Loading commit data...
Reports Loading commit data...
Requests Loading commit data...
Roles Loading commit data...
Schedule Loading commit data...
Settings Loading commit data...
Trainer Loading commit data...
Training Loading commit data...
Users Loading commit data...
Wallets Loading commit data...
Website Loading commit data...
WhatsApp Loading commit data...
Wizards Loading commit data...
BranchSwitcher.php Loading commit data...
Dashboard.php Loading commit data...
GlobalSearch.php Loading commit data...