Four fixes from a security review pass: - Neutralise CSV formula injection in the payments export: fields with a leading =, +, -, @, tab, or CR (e.g. a hostile student display name) are apostrophe-prefixed in PaymentReport::csvLine() so they open as text in Excel/Google Sheets. Fixes #39. - Sanitise policy bodies with wp_kses_post at output in PolicyEndpoint::index() (the booking JS renders that HTML raw), so a future write path that forgets kses can never become stored XSS. Fixes #40. - Store invite tokens hashed (SHA-256) at rest: a database leak can no longer redeem pending invites. The registration link is shown once, at creation; the pending list shows email/invited date; lookups hash the submitted token. Existing plaintext pending invites must be re-issued. Fixes #41. - Validate availability slot datetimes on both creation paths (REST and admin form) via AvailabilitySlot::normalizeDateTime(): canonical and datetime-local forms normalise to Y-m-d H:i:s, garbage and end <= start are rejected (REST 400) instead of reaching the DATETIME column or throwing inside the weekly-series date arithmetic. Fixes #42. composer test (204 tests, 594 assertions), PHPStan L6, and PHPCS all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.7 KiB
Feature: Account Registration
Overview
People register for a student account through a front-end page, accepting any
signup-scoped policies at that time. Registration is invite-only by default: a
studio admin sends an invite, and the invitee completes signup via a tokenised
link. A settings seam (us_registration_mode) allows switching to open
self-registration with approval later.
Registration Modes
Stored in the us_registration_mode option (default invite):
invite— only a valid, pending invite token grants access to the registration form. (implemented)self_approval— anyone may register; the account is created in a pending state until a studio admin approves it. (reserved for a later iteration)
Data Model — {prefix}us_invites
| Column | Type | Notes |
|---|---|---|
id |
BIGINT UNSIGNED | Primary key |
email |
VARCHAR(191) | Invited email address |
token |
VARCHAR(64) | SHA-256 hash of the token embedded in the registration link (raw token is never stored) |
role |
VARCHAR(32) | Role granted on acceptance (default us_student) |
status |
VARCHAR(20) | pending / accepted / revoked |
invited_by |
BIGINT UNSIGNED | WordPress user ID of the studio admin who invited |
accepted_user_id |
BIGINT UNSIGNED | The created user's ID once accepted; NULL while pending |
created_at |
DATETIME | Insertion time |
accepted_at |
DATETIME | When accepted; NULL while pending |
Policy Acceptance Scope
Policies declare when they must be accepted via us_policies.acceptance_scope:
signup, booking, or both (see policies.md). The registration form requires
acceptance of every published policy scoped signup or both. Acceptances are
recorded in us_policy_acceptances with registration_type = account and
registration_id = <new user ID>.
Flow (invite mode)
- Studio admin opens Invites (
manage_students) and invites an email; an invite row is created storing the token's SHA-256 hash, and the registration link (with the raw token) is shown once in a notice. To re-send a lost link, revoke and re-invite. - The invitee opens
[us_student_register]with the token (?us_invite=<token>); the lookup hashes the submitted token and matches it against the stored hash. - The form pre-fills the email and collects a display name and password, and renders the signup-scoped published policies, each with a required acceptance checkbox.
- On submit, the token is re-validated (hashed lookup); a
us_studentuser is created, the policy acceptances are recorded (accounttype), the invite is markedaccepted, and the user is logged in.
Admin Interface
Invites in wp-admin (manage_students, studio admin only):
- Select the registration page (the page hosting
[us_student_register]), stored in theus_registration_page_idoption; invitation links point there (falling back to the home page if unset) - Invite an email (creates a pending invite; the link is displayed once, at creation only)
- List pending invites (email + invited date); revoke an invite
Frontend Shortcode
[us_student_register]— the registration page. Shows the form for a valid pending invite; otherwise shows an "by invitation only" message (ininvitemode).
Token Redirect
A template_redirect handler (RegistrationPage::maybeRedirectToRegistrationPage())
sends any front-end request carrying a us_invite token to the configured
registration page (preserving the token), unless it is already on that page. This
covers invitation links generated/shared before a registration page was selected.
No-op when no registration page is set.
Capabilities
manage_students— manage invites (studio admin; administrators inherit it via theuser_has_capfilter). Added toRoleManager::STUDIO_ADMIN_CAPS.
Implementation
- Models:
Unsupervised\Schedular\Auth\Invite - Repository:
Unsupervised\Schedular\Auth\InviteRepository - Admin controller:
Unsupervised\Schedular\Auth\RegistrationController - Frontend:
Unsupervised\Schedular\Auth\RegistrationPage - Reuses
Policy\PolicyRepository,Policy\PolicyVersionRepository,Policy\AcceptanceRepository - Schema:
us_invites;us_policies.acceptance_scope
Tests
tests/Unit/Auth/InviteTest.phptests/Unit/Auth/InviteRepositoryTest.php