Files
unsupervised-scheduler/docs/features/account-registration.md
T
thatguygriffandClaude Opus 4.8 7370755951
CI / Tests (PHP 8.1) (pull_request) Successful in 1m18s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m18s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 3m20s
CI / Coding Standards (pull_request) Successful in 3m25s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m33s
CI / Build Plugin Zip (pull_request) Skipped
Add open student registration with email confirmation and approval
Students could previously join by invite only. Add an optional
self-approval mode, toggled from Studio Settings → Registration: anyone
may sign up on the existing [us_student_register] page, confirm their
email via a tokenised link, and then be approved by a studio admin
before the account is usable.

- Enabling the toggle mirrors WordPress's own membership settings
  (users_can_register + default_role = us_student) and snapshots their
  previous values so disabling restores them.
- WordPress's native registration form is blocked while open
  registration is on (login_init redirect + registration_errors
  fail-safe + register_url) so it cannot bypass signup policy acceptance.
- Pending accounts: unconfirmed email cannot log in; confirmed but
  unapproved can log in but the booking capability is withheld and the
  booking page shows an "awaiting approval" screen.
- Approve/reject from Students → Pending Students; reject hard-deletes
  the account so the email is freed to re-apply.
- Invite registration is unchanged; both modes coexist.

Account lifecycle lives in user meta (RegistrationStatus); no new tables.

Closes #63

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 10:50:21 -03:00

9.9 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 studio can instead switch on open (self-approval) registration, where anyone may sign up, confirm their email, and then be approved by a studio admin before the account can be used. Both modes coexist — invites keep working when open registration is on.

Registration Modes

Stored in the us_registration_mode option (default invite), toggled from Studio Settings → Registration:

  • invite — only a valid, pending invite token grants access to the registration form.
  • self_approval — anyone may register on the registration page; each account is created in a pending state, must confirm its email, and is then approved (or rejected) by a studio admin.

Enabling open registration

The Studio Settings toggle is the source of truth. Enabling it mirrors into the two core WordPress options the flow relies on, and snapshots their previous values (us_registration_prev_can_register, us_registration_prev_default_role):

  • users_can_register1 (Settings → General "Anyone can register")
  • default_roleus_student

Disabling restores the snapshot, so the toggle never permanently overwrites a site's own membership settings. Only enable/disable transitions touch the core options — saving unrelated settings leaves them alone. See Payment\StudioSettings::applyRegistrationMode().

Blocking the native registration form

Because users_can_register=1 also switches on WordPress's own wp-login.php?action=register form — which cannot collect the required signup policy acceptances — that form is blocked while open registration is on, so it can never be used to create a policy-less account (Auth\EmailConfirmationHandler):

  • register_url filter points WordPress's "Register" links at the registration page.
  • login_init action redirects any action=register request (GET and POST) to the registration page before any processing runs.
  • registration_errors filter is a fail-safe that rejects register_new_user() outright.

Account Lifecycle (self-approval)

State lives entirely in user meta (Auth\RegistrationStatus). Only the raw confirmation token's SHA-256 hash is stored; the token expires after 48h (EMAIL_CONFIRM_EXPIRY_HOURS).

State User meta Login Booking
Email unconfirmed us_awaiting_approval=1, us_email_confirm_token(hash) + us_email_confirm_expires set blocked ("confirm your email")
Confirmed, awaiting approval us_awaiting_approval=1, us_email_confirmed=1, token/expiry cleared allowed withheld → pending screen
Approved / active us_awaiting_approval deleted, us_email_confirmed=1 allowed full student
Rejected account hard-deleted (wp_delete_user) n/a n/a
Invite/admin-created student none of these metas allowed full student
  • Login gate (Auth\RegistrationLoginGate): the wp_authenticate_user filter blocks login while the email is unconfirmed; the user_has_cap filter withholds book_lesson while us_awaiting_approval is set, so a confirmed-but-unapproved student only reaches the "awaiting approval" screen on the booking page.
  • Email confirmation (Auth\EmailConfirmationHandler on template_redirect): opening the emailed ?us_confirm=<token> link confirms the email, notifies the studio admins, and redirects back to the registration page with ?us_confirmed=1 (or expired).
  • Approval (Auth\RegistrationApprovalController, Students → Pending Students, manage_students): approve clears the pending flags and emails the student; reject emails them and hard-deletes the account so the email is freed to re-apply.
  • Emails: Auth\RegistrationMailer sends the confirmation link, the admin heads-up, and the approval/rejection notices.

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.

Flow (self-approval mode)

  1. Studio admin enables Studio Settings → Registration and selects the registration page (shared with invites, us_registration_page_id).
  2. Anyone opens [us_student_register]; the form collects an editable email, display name, password, and the required signup policies.
  3. On submit a us_student user is created in the pending state (RegistrationStatus::markPending()), acceptances are recorded (account type), a confirmation email is sent, and the user is not logged in.
  4. The applicant opens the emailed ?us_confirm=<token> link → email confirmed, studio admins notified.
  5. Studio admin approves under Students → Pending Students → pending flags cleared, student emailed; they can now log in and book. Rejection deletes the account.

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

Pending Students — submenu under Students (manage_students), only relevant in self_approval mode:

  • "Awaiting approval" (email confirmed) — approve or reject
  • "Awaiting email confirmation" (not yet confirmed) — reject only

Frontend Shortcode

  • [us_student_register] — the registration page. In invite mode: shows the form for a valid pending invite, else an "by invitation only" message. In self_approval mode: shows the form to anyone (editable email), and renders confirmation-result notices from ?us_confirmed=1|expired.

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 and approve/reject pending students (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 controllers: Unsupervised\Schedular\Auth\RegistrationController (invites), Unsupervised\Schedular\Auth\RegistrationApprovalController (pending students)
  • Frontend: Unsupervised\Schedular\Auth\RegistrationPage
  • Self-approval flow: Auth\RegistrationStatus (lifecycle meta), Auth\RegistrationLoginGate (login + booking-cap gate), Auth\EmailConfirmationHandler (confirm link + native-form block), Auth\RegistrationMailer (emails)
  • Settings toggle: Payment\StudioSettings (us_registration_mode, core-option mirror/restore)
  • Reuses Policy\PolicyRepository, Policy\PolicyVersionRepository, Policy\AcceptanceRepository
  • Schema: us_invites; us_policies.acceptance_scope. Self-approval adds no tables — state is WordPress user meta.

Tests

  • tests/Unit/Auth/InviteTest.php
  • tests/Unit/Auth/InviteRepositoryTest.php
  • tests/Unit/Auth/RegistrationStatusTest.php
  • tests/Unit/Auth/RegistrationLoginGateTest.php
  • tests/Unit/Auth/EmailConfirmationHandlerTest.php
  • tests/Unit/Auth/RegistrationPageTest.php
  • tests/Unit/Auth/RegistrationApprovalControllerTest.php
  • tests/Unit/Auth/RegistrationMailerTest.php
  • tests/Unit/Payment/StudioSettingsTest.php