Add an account block showing who is signed in #156

Merged
thatguygriff merged 2 commits from feature/142-account-block into main 2026-07-30 01:53:31 +00:00
9 changed files with 7 additions and 84 deletions
Showing only changes of commit ab5212282d - Show all commits
+1 -1
View File
@@ -14,7 +14,7 @@ each change under the current top section as you work.
## [1.3.1] ## [1.3.1]
### Added ### 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. - An **Account** block (`[us_account]`) showing who is signed in — their name and their email — 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 ### 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. - 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.
+1 -2
View File
@@ -562,8 +562,7 @@
font-weight: 600; font-weight: 600;
} }
.us-account-email, .us-account-email {
.us-account-students {
display: block; display: block;
font-size: 0.9em; font-size: 0.9em;
opacity: 0.75; opacity: 0.75;
+1 -1
View File
@@ -310,7 +310,7 @@
{ {
name: 'us-scheduler/account', name: 'us-scheduler/account',
title: __('Account', 'unsupervised-schedular'), 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'), description: __('Shows the name and email of whoever is signed in, with a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'),
icon: 'admin-users', icon: 'admin-users',
keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'], keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'],
shortcode: 'us_account', shortcode: 'us_account',
+2 -3
View File
@@ -142,7 +142,6 @@ header is noise that cannot be acted on, so:
- **Login page chosen** → a single **Sign in** link. - **Login page chosen** → a single **Sign in** link.
Signed in, it shows the display name (`Auth\UserName::format()`, so a username 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 is never exposed), the account email, and a **Sign out** link — deliberately
account that books for someone other than itself — the students it books for. nothing else. Signing out returns to the chosen login page, or to the current page when there
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. is none, so a header sign-out does not also navigate the visitor somewhere.
-13
View File
@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Unsupervised\Schedular\Auth; namespace Unsupervised\Schedular\Auth;
use Unsupervised\Schedular\Guardian\GuardianService;
use Unsupervised\Schedular\Val; use Unsupervised\Schedular\Val;
/** /**
@@ -15,8 +14,6 @@ use Unsupervised\Schedular\Val;
*/ */
class AccountPage { class AccountPage {
public function __construct( private GuardianService $guardians ) {}
/** /**
* Renders the account shortcode/block output. * Renders the account shortcode/block output.
* *
@@ -55,16 +52,6 @@ class AccountPage {
$name = UserName::format( $user, get_current_user_id() ); $name = UserName::format( $user, get_current_user_id() );
$email = $user->user_email; $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 // 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 // navigate them somewhere. The login page is the better landing spot when
// one is configured, since the current page may be members-only. // one is configured, since the current page may be members-only.
-8
View File
@@ -220,18 +220,10 @@ class BlockPreview {
'<div class="us-account">%s' '<div class="us-account">%s'
. '<p class="us-account-who"><span class="us-account-name">%s</span>' . '<p class="us-account-who"><span class="us-account-name">%s</span>'
. '<span class="us-account-email">%s</span></p>' . '<span class="us-account-email">%s</span></p>'
. '<p class="us-account-students">%s</p>'
. '<p class="us-account-actions"><a class="us-account-signout" href="#">%s</a></p></div>', . '<p class="us-account-actions"><a class="us-account-signout" href="#">%s</a></p></div>',
self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ), self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ),
esc_html__( 'Grace Hopper', 'unsupervised-schedular' ), esc_html__( 'Grace Hopper', 'unsupervised-schedular' ),
esc_html__( '[email protected]', 'unsupervised-schedular' ), esc_html__( '[email protected]', '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' ) esc_html__( 'Sign out', 'unsupervised-schedular' )
); );
} }
+1 -1
View File
@@ -100,7 +100,7 @@ class Plugin {
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer, $questions, $answers, $groupAccess, $guardians ); $registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer, $questions, $answers, $groupAccess, $guardians );
$groupClassPage = new GroupClassPage( $guardians ); $groupClassPage = new GroupClassPage( $guardians );
$familyPage = new FamilyPage( $guardians, $questions, $answers ); $familyPage = new FamilyPage( $guardians, $questions, $answers );
$accountPage = new AccountPage( $guardians ); $accountPage = new AccountPage();
( new ScheduledBillingRunner( $paymentService, $bookings, $enrollments, $offerings, new PaymentDueMailer(), $guardians ) )->register(); ( new ScheduledBillingRunner( $paymentService, $bookings, $enrollments, $offerings, new PaymentDueMailer(), $guardians ) )->register();
-13
View File
@@ -8,7 +8,6 @@ if (! defined('ABSPATH')) {
/** /**
* @var string $name Display name of the signed-in visitor. * @var string $name Display name of the signed-in visitor.
* @var string $email Their account email. * @var string $email Their account email.
* @var list<string> $students Names this account books for, excluding themselves; empty for a plain student account.
* @var string $logoutUrl Nonced sign-out URL, already carrying its redirect. * @var string $logoutUrl Nonced sign-out URL, already carrying its redirect.
*/ */
?> ?>
@@ -20,18 +19,6 @@ if (! defined('ABSPATH')) {
<?php endif; ?> <?php endif; ?>
</p> </p>
<?php if (! empty($students)) : ?>
<p class="us-account-students">
<?php
printf(
/* translators: %s: comma-separated list of the students this account books for. */
esc_html__('Booking for %s', 'unsupervised-schedular'),
esc_html(implode(', ', $students))
);
?>
</p>
<?php endif; ?>
<p class="us-account-actions"> <p class="us-account-actions">
<a class="us-account-signout" href="<?php echo esc_url($logoutUrl); ?>"><?php esc_html_e('Sign out', 'unsupervised-schedular'); ?></a> <a class="us-account-signout" href="<?php echo esc_url($logoutUrl); ?>"><?php esc_html_e('Sign out', 'unsupervised-schedular'); ?></a>
</p> </p>
+1 -42
View File
@@ -6,20 +6,17 @@ namespace Unsupervised\Schedular\Tests\Unit\Auth;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use Mockery; use Mockery;
use Unsupervised\Schedular\Auth\AccountPage; use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Guardian\GuardianService;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class AccountPageTest extends TestCase class AccountPageTest extends TestCase
{ {
private GuardianService&Mockery\MockInterface $guardians;
private AccountPage $page; private AccountPage $page;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->guardians = Mockery::mock(GuardianService::class); $this->page = new AccountPage();
$this->page = new AccountPage($this->guardians);
Functions\when('is_user_logged_in')->justReturn(true); Functions\when('is_user_logged_in')->justReturn(true);
Functions\when('get_current_user_id')->justReturn(5); Functions\when('get_current_user_id')->justReturn(5);
@@ -47,16 +44,8 @@ class AccountPageTest extends TestCase
return $user; return $user;
} }
/** @param list<array{id: int, name: string, is_self: bool}> $students */
private function bookable(array $students): void
{
$this->guardians->shouldReceive('bookableStudents')->with(5)->andReturn($students);
}
public function testShowsTheSignedInNameAndEmail(): void public function testShowsTheSignedInNameAndEmail(): void
{ {
$this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
$html = $this->page->render([]); $html = $this->page->render([]);
self::assertStringContainsString('Grace Hopper', $html); self::assertStringContainsString('Grace Hopper', $html);
@@ -64,32 +53,8 @@ class AccountPageTest extends TestCase
self::assertStringContainsString('Sign out', $html); self::assertStringContainsString('Sign out', $html);
} }
public function testNamesTheStudentsTheAccountBooksFor(): void
{
$this->bookable([
['id' => 42, 'name' => 'Ada', 'is_self' => false],
['id' => 43, 'name' => 'Alan', 'is_self' => false],
['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true],
]);
self::assertStringContainsString('Booking for Ada, Alan', $this->page->render([]));
}
/**
* An account that only books for itself has nothing to add — "Booking for
* Grace Hopper" under "Grace Hopper" is noise.
*/
public function testSaysNothingAboutStudentsOnAPlainAccount(): void
{
$this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
self::assertStringNotContainsString('Booking for', $this->page->render([]));
}
public function testSigningOutReturnsToTheConfiguredLoginPage(): void public function testSigningOutReturnsToTheConfiguredLoginPage(): void
{ {
$this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
self::assertStringContainsString( self::assertStringContainsString(
rawurlencode('https://studio.test/sign-in/'), rawurlencode('https://studio.test/sign-in/'),
$this->page->render(['loginPageId' => 9]) $this->page->render(['loginPageId' => 9])
@@ -102,8 +67,6 @@ class AccountPageTest extends TestCase
*/ */
public function testSigningOutReturnsToTheCurrentPageWhenNoLoginPageIsSet(): void public function testSigningOutReturnsToTheCurrentPageWhenNoLoginPageIsSet(): void
{ {
$this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
self::assertStringContainsString( self::assertStringContainsString(
rawurlencode('https://studio.test/current/'), rawurlencode('https://studio.test/current/'),
$this->page->render([]) $this->page->render([])
@@ -112,8 +75,6 @@ class AccountPageTest extends TestCase
public function testTheShortcodeAttributeNameIsAccepted(): void public function testTheShortcodeAttributeNameIsAccepted(): void
{ {
$this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
self::assertStringContainsString( self::assertStringContainsString(
rawurlencode('https://studio.test/sign-in/'), rawurlencode('https://studio.test/sign-in/'),
$this->page->render(['login_page_id' => 9]) $this->page->render(['login_page_id' => 9])
@@ -127,7 +88,6 @@ class AccountPageTest extends TestCase
public function testRendersNothingForASignedOutVisitorWithNoLoginPage(): void public function testRendersNothingForASignedOutVisitorWithNoLoginPage(): void
{ {
Functions\when('is_user_logged_in')->justReturn(false); Functions\when('is_user_logged_in')->justReturn(false);
$this->guardians->shouldNotReceive('bookableStudents');
self::assertSame('', $this->page->render([])); self::assertSame('', $this->page->render([]));
} }
@@ -135,7 +95,6 @@ class AccountPageTest extends TestCase
public function testOffersASignInLinkToASignedOutVisitorWhenAPageIsChosen(): void public function testOffersASignInLinkToASignedOutVisitorWhenAPageIsChosen(): void
{ {
Functions\when('is_user_logged_in')->justReturn(false); Functions\when('is_user_logged_in')->justReturn(false);
$this->guardians->shouldNotReceive('bookableStudents');
$html = $this->page->render(['loginPageId' => 9]); $html = $this->page->render(['loginPageId' => 9]);