-
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:Claude Opus 5 (1M context) <noreply@anthropic.com>
500806ca