diff --git a/CHANGELOG.md b/CHANGELOG.md index 105a748..12f66e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.3.1] +### Added +- An **Account** block (`[us_account]`) showing who is signed in — their name, their email, the students they book for if that is anyone besides themselves — and a **Sign out** link. Signing out returns to the login page chosen in the block, or to the page the visitor was already on when none is set, so putting it in a site header does not also move people somewhere. To a signed-out visitor it shows a **Sign in** link when a login page is chosen, and nothing at all when one is not: a panel about who is signed in has nothing to tell a stranger, and a notice they cannot act on is just clutter in a header. + ### Security - Signup now checks the password properly. The form scores it as you type with the same zxcvbn meter wp-admin uses and will not submit a weak one, and the server refuses — regardless of what the browser allowed — anything shorter than 8 characters, one of the well-known leaked passwords, one built from barely any distinct characters, or one containing your own name or email address. Composition rules ("must contain a symbol") are deliberately not imposed: they mostly produce predictable substitutions. Email addresses are validated on the server on every signup path, with a clear message when one is already registered. diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 55f7e5a..6a3c8c5 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -549,6 +549,30 @@ color: #1a7d2e; } +/* + * The account panel: who is signed in, and the way out. Sized to sit in a + * header or sidebar, so the rules stay minimal and inherit the theme's type — + * a block that lands in a site header should look like it belongs there. + */ +.us-account p { + margin: 0 0 4px; +} + +.us-account-name { + font-weight: 600; +} + +.us-account-email, +.us-account-students { + display: block; + font-size: 0.9em; + opacity: 0.75; +} + +.us-account-actions { + margin-top: 8px; +} + /* Shown only in block-editor previews (see BlockPreview). */ .us-editor-note { font-size: 0.85em; diff --git a/assets/js/blocks.js b/assets/js/blocks.js index d643f28..dcaf81b 100644 --- a/assets/js/blocks.js +++ b/assets/js/blocks.js @@ -307,6 +307,28 @@ }) ), }, + { + name: 'us-scheduler/account', + title: __('Account', 'unsupervised-schedular'), + description: __('Shows who is signed in, who they book for, and a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'), + icon: 'admin-users', + keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'], + shortcode: 'us_account', + attributes: { + loginPageId: { type: 'number', default: 0 }, + }, + inspector: (attributes, setAttributes) => el( + PanelBody, + { title: __('Signing in and out', 'unsupervised-schedular') }, + el(PageSelect, { + label: __('Login page', 'unsupervised-schedular'), + help: __('Where signing out returns to, and where signed-out visitors are offered a link to sign in. Without one, signing out returns to the current page and signed-out visitors see nothing.', 'unsupervised-schedular'), + defaultLabel: __('Stay on the current page', 'unsupervised-schedular'), + value: attributes.loginPageId, + onChange: (loginPageId) => setAttributes({ loginPageId }), + }) + ), + }, ]; blocks.forEach((def) => { diff --git a/docs/features/editor-blocks.md b/docs/features/editor-blocks.md index 3383256..c30a16d 100644 --- a/docs/features/editor-blocks.md +++ b/docs/features/editor-blocks.md @@ -1,8 +1,8 @@ # Editor Blocks -Gutenberg dynamic-block wrappers for the plugin's four front-end shortcodes, -so the pages can be previewed and styled inside the block editor instead of -appearing as grey shortcode text. +Gutenberg dynamic-block wrappers for the plugin's front-end shortcodes, so the +pages can be previewed and styled inside the block editor instead of appearing +as grey shortcode text. ## Blocks @@ -12,6 +12,8 @@ appearing as grey shortcode text. | `us-scheduler/student-login` | `[us_student_login]` | `Auth\LoginPage::render()` | | `us-scheduler/student-register` | `[us_student_register]` | `Auth\RegistrationPage::render()` | | `us-scheduler/group-classes` | `[us_group_classes]` | `GroupClass\GroupClassPage::render()` | +| `us-scheduler/family` | `[us_family]` | `Guardian\FamilyPage::render()` | +| `us-scheduler/account` | `[us_account]` | `Auth\AccountPage::render()` | The shortcodes remain registered for back-compat; blocks and shortcodes share the same page objects (constructed once in `Plugin::boot()`), so front-end @@ -21,7 +23,7 @@ transform. ## Block options -Four blocks have sidebar (inspector) options: +Most blocks have sidebar (inspector) options: | Block | Attribute | Default | Effect | |---|---|---|---| @@ -34,6 +36,8 @@ Four blocks have sidebar (inspector) options: | `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. | | `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page students continue to once registration finishes — the "Sign in to your account" link after they confirm their email, and the "Continue to your account" link an invited student gets on the spot. `0` = the WordPress login screen for the confirmation link, and no link at all for the (already signed-in) invited student. Shortcode equivalent: `[us_student_register login_page_id="…"]`. | | `us-scheduler/student-register` | `autoRedirect` (boolean) | `false` | Send students straight to that page instead of showing the link. Does nothing until a page is chosen — there is no login-screen fallback here. | +| `us-scheduler/family` | `loginPageId` (number) | `0` | Where visitors who are not signed in are sent to log in. Shortcode equivalent: `[us_family login_page_id="…"]`. | +| `us-scheduler/account` | `loginPageId` (number) | `0` | Where signing out returns to, and where a signed-out visitor is offered a **Sign in** link. `0` = signing out returns to the current page, and a signed-out visitor sees **nothing at all** — see below. Shortcode equivalent: `[us_account login_page_id="…"]`. | | `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. The class description is then omitted — only the schedule, instructor, price and enrolment controls are shown, so the surrounding page's own copy is not repeated. `0` = browse all classes, descriptions included. Shortcode equivalent: `[us_group_classes offering="…"]`. | The page selects list all published pages; if a chosen page is later deleted, @@ -105,6 +109,10 @@ placeholder content: - **Login** — the real `templates/frontend/login-page.php` template (it has no request-state dependencies). - **Registration** — a disabled sample of the `.us-register-form` fields. +- **Account** — a populated sample panel. Deliberately populated whatever the + editor user's own state: on the published page a signed-out visitor may see + nothing at all, and an empty box tells the person placing the block nothing + about where it will sit. Each preview starts with a `.us-editor-note` paragraph explaining what the published page shows instead. The note class only appears in editor previews. @@ -118,5 +126,23 @@ published page shows instead. The note class only appears in editor previews. and fallbacks. - `tests/Unit/Auth/LoginPageTest.php` — logged-in booking-link targets and fallbacks. +- `tests/Unit/Auth/AccountPageTest.php` — what each visitor sees, the + sign-out redirect target, and the signed-out empty render. - `tests/Unit/BlockPreviewTest.php` — preview markup mirrors the live CSS classes/ids and includes the editor note. + +## The account block's signed-out behaviour + +`us-scheduler/account` is the one block that can render **nothing**. It is meant +for a header, sidebar or account page, and its whole subject is the person +signed in — which a stranger is not. A bare "you are not signed in" in a site +header is noise that cannot be acted on, so: + +- **No login page chosen** → empty string for signed-out visitors. +- **Login page chosen** → a single **Sign in** link. + +Signed in, it shows the display name (`Auth\UserName::format()`, so a username +is never exposed), the account email, a **Sign out** link, and — only on an +account that books for someone other than itself — the students it books for. +Signing out returns to the chosen login page, or to the current page when there +is none, so a header sign-out does not also navigate the visitor somewhere. diff --git a/src/Auth/AccountPage.php b/src/Auth/AccountPage.php new file mode 100644 index 0000000..1ace546 --- /dev/null +++ b/src/Auth/AccountPage.php @@ -0,0 +1,91 @@ + $atts Block attributes (`loginPageId`) or + * shortcode attributes (`login_page_id`). + */ + public function render( array $atts ): string { + $loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 ); + $loginUrl = $this->pageUrl( $loginPageId ); + + wp_enqueue_style( 'us-scheduler' ); + + if ( ! is_user_logged_in() ) { + if ( null === $loginUrl ) { + return ''; + } + + return sprintf( + '
', + esc_url( $loginUrl ), + esc_html__( 'Sign in', 'unsupervised-schedular' ) + ); + } + + // Always a WP_User here — is_user_logged_in() above rules out the + // id-0 placeholder wp_get_current_user() returns for a visitor. + $user = wp_get_current_user(); + + $name = UserName::format( $user, get_current_user_id() ); + $email = $user->user_email; + + // Whose lessons this account books, when that is more than just their own. + // A parent's first question on seeing "signed in as Grace" is whether this + // is the account their children's lessons are on. + $students = []; + foreach ( $this->guardians->bookableStudents( get_current_user_id() ) as $student ) { + if ( ! $student['is_self'] ) { + $students[] = $student['name']; + } + } + + // Back to where they were, so signing out of a header link does not also + // navigate them somewhere. The login page is the better landing spot when + // one is configured, since the current page may be members-only. + $logoutUrl = wp_logout_url( $loginUrl ?? (string) get_permalink() ); + + ob_start(); + include USC_PLUGIN_DIR . 'templates/frontend/account-page.php'; + return (string) ob_get_clean(); + } + + /** + * Permalink of a configured page, or null when none is chosen or the chosen + * page has since been deleted. + */ + private function pageUrl( int $pageId ): ?string { + if ( $pageId <= 0 ) { + return null; + } + + $url = get_permalink( $pageId ); + + return is_string( $url ) ? $url : null; + } +} diff --git a/src/BlockPreview.php b/src/BlockPreview.php index 47bce0d..9f5fd1d 100644 --- a/src/BlockPreview.php +++ b/src/BlockPreview.php @@ -210,6 +210,32 @@ class BlockPreview { ); } + /** + * Sample account panel. Shown populated whatever the editor's own login + * state, since on the published page a signed-out visitor may see nothing at + * all and an empty box tells the person placing the block nothing. + */ + public static function account(): string { + return sprintf( + '', + self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ), + esc_html__( 'Grace Hopper', 'unsupervised-schedular' ), + esc_html__( 'grace@example.com', 'unsupervised-schedular' ), + esc_html( + sprintf( + /* translators: %s: comma-separated list of the students this account books for. */ + __( 'Booking for %s', 'unsupervised-schedular' ), + __( 'Ada, Alan', 'unsupervised-schedular' ) + ) + ), + esc_html__( 'Sign out', 'unsupervised-schedular' ) + ); + } + private static function note( string $text ): string { return '' . esc_html( $text ) . '
'; } diff --git a/src/BlockRegistrar.php b/src/BlockRegistrar.php index f97c3c6..c59e1ff 100644 --- a/src/BlockRegistrar.php +++ b/src/BlockRegistrar.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Unsupervised\Schedular; +use Unsupervised\Schedular\Auth\AccountPage; use Unsupervised\Schedular\Auth\LoginPage; use Unsupervised\Schedular\Auth\RegistrationPage; use Unsupervised\Schedular\Booking\BookingPage; @@ -30,6 +31,7 @@ class BlockRegistrar { private RegistrationPage $registrationPage, private GroupClassPage $groupClassPage, private FamilyPage $familyPage, + private AccountPage $accountPage, ) {} public function register(): void { @@ -148,6 +150,15 @@ class BlockRegistrar { ], ], ], + 'us-scheduler/account' => [ + 'render' => [ $this, 'renderAccount' ], + 'attributes' => [ + 'loginPageId' => [ + 'type' => 'number', + 'default' => 0, + ], + ], + ], ]; } @@ -195,6 +206,15 @@ class BlockRegistrar { return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 ); } + /** + * Renders the account (who is signed in) block. + * + * @param array