CI / Tests (PHP 8.2) (pull_request) Successful in 41s
CI / Tests (PHP 8.1) (pull_request) Successful in 46s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m45s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Skipped
Closes #67 When a student lands on the registration page from the confirmation email (?us_confirmed=1), replace the registration form with the confirmation message and a "Sign in to your account" link — the form is useless at that point and re-submitting would only produce an "account already exists" error. A confirmed-but-unapproved student can already log in (the pending gate only withholds booking), so signing in is the natural next step. The link target follows the booking block's pattern: a loginPageId block attribute (page picker in the editor sidebar) or login_page_id shortcode attribute, falling back to wp_login_url(). The expired-link notice keeps the form as before. Co-Authored-By: Claude Fable 5 <[email protected]>
134 lines
10 KiB
Markdown
134 lines
10 KiB
Markdown
# 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_register` → `1` (Settings → General "Anyone can register")
|
|
- `default_role` → `us_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`). On `?us_confirmed=1` the registration page replaces the form with the confirmation message plus a "Sign in to your account" link — the configured sign-in page (block `loginPageId` / shortcode `login_page_id` attribute), falling back to the WordPress login screen. The `expired` notice keeps the form.
|
|
- **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`
|