Files
unsupervised-scheduler/docs/features/account-registration.md
T
thatguygriff f3f5c7801f
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.1) (pull_request) Successful in 43s
CI / Tests (PHP 8.3) (pull_request) Successful in 49s
CI / Tests (PHP 8.2) (pull_request) Successful in 59s
CI / Coding Standards (pull_request) Successful in 1m11s
CI / PHPStan (pull_request) Successful in 1m20s
CI / Build Plugin Zip (pull_request) Has been skipped
Security fixes: CSV injection, policy body output, invite hashing, slot datetimes
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>
2026-06-10 16:36:26 -03:00

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)

  1. 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.
  2. 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.
  3. 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.
  4. On submit, the token is re-validated (hashed lookup); a us_student user is created, the policy acceptances are recorded (account type), the invite is marked accepted, 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 the us_registration_page_id option; 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 (in invite mode).

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 the user_has_cap filter). Added to RoleManager::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.php
  • tests/Unit/Auth/InviteRepositoryTest.php