diff --git a/app/Http/Controllers/UserEmailChangeController.php b/app/Http/Controllers/UserEmailChangeController.php index 89976ccc0..c7710c2ca 100644 --- a/app/Http/Controllers/UserEmailChangeController.php +++ b/app/Http/Controllers/UserEmailChangeController.php @@ -77,4 +77,24 @@ public function resend(Request $request): RedirectResponse 'We sent another confirmation link to '.$user->fresh()->pending_email.'. Check that inbox (and spam folder).', ); } + + public function confirmHere(Request $request): RedirectResponse + { + $user = $request->user(); + + $validated = $request->validate([ + 'confirm_password' => $this->emailChangeService->requiresPassword($user) + ? 'required|string|max:72' + : 'nullable|string|max:72', + ]); + + $updatedUser = $this->emailChangeService->confirmPendingForAuthenticatedUser( + $user, + $validated['confirm_password'] ?? null, + ); + + return redirect() + ->route('profile') + ->with('flash', 'Your login email has been updated to '.$updatedUser->email.'.'); + } } diff --git a/app/Services/UserEmailChangeService.php b/app/Services/UserEmailChangeService.php index b7b2f9a62..9a277c792 100644 --- a/app/Services/UserEmailChangeService.php +++ b/app/Services/UserEmailChangeService.php @@ -89,6 +89,44 @@ public function resendConfirmation(User $user): array return ['status' => 'confirmation_resent']; } + /** + * Confirm a pending change while logged in (fallback when the new inbox does not receive mail). + * + * @throws ValidationException + */ + public function confirmPendingForAuthenticatedUser(User $user, ?string $password): User + { + $pendingEmail = SupportEmailAddress::normalize((string) ($user->pending_email ?? '')); + if ($pendingEmail === null || $user->pending_email_token === null) { + throw ValidationException::withMessages([ + 'pending_email' => 'There is no pending email change to confirm.', + ]); + } + + if ($this->requiresPassword($user)) { + if ($password === null || $password === '' || ! Hash::check($password, (string) $user->password)) { + throw ValidationException::withMessages([ + 'confirm_password' => 'Your current password is incorrect.', + ]); + } + } + + if ($this->emailInUseByAnotherUser($pendingEmail, (int) $user->id)) { + $this->cancelPending($user); + + throw ValidationException::withMessages([ + 'new_email' => 'That email address is already in use on another CodeWeek account.', + ]); + } + + return DB::transaction(function () use ($user, $pendingEmail) { + $oldEmail = SupportEmailAddress::normalize((string) ($user->email ?? '')); + $this->applyEmailChange($user, $oldEmail, $pendingEmail); + + return $user->fresh(); + }); + } + private function issuePendingToken(User $user): string { $token = Str::random(64); diff --git a/resources/views/emails/en/pending-email-change-confirmation.blade.php b/resources/views/emails/en/pending-email-change-confirmation.blade.php index df05a77ff..15632875e 100644 --- a/resources/views/emails/en/pending-email-change-confirmation.blade.php +++ b/resources/views/emails/en/pending-email-change-confirmation.blade.php @@ -9,6 +9,10 @@ Confirm new login email @endcomponent +If the button does not work, copy and paste this link into your browser: + +{{ $confirmUrl }} + If you did not request this change, you can ignore this email. Thanks,
diff --git a/resources/views/profile.blade.php b/resources/views/profile.blade.php index 8101d5d0c..15f59c0aa 100755 --- a/resources/views/profile.blade.php +++ b/resources/views/profile.blade.php @@ -74,7 +74,7 @@ class="border-2 border-solid border-dark-blue-200 w-full rounded-full h-12 px-4

A separate notice was sent to {{ $profileUser->email }} — that email does not contain the confirmation link.

-
+
{{ csrf_field() }}
+
+

Can't find the email at {{ $profileUser->pending_email }}?

+

+ If you can access that inbox elsewhere (for example a shared team mailbox), check there first. + Otherwise, while you are signed in here, you can confirm the change on this page. +

+
+ {{ csrf_field() }} + @if (empty($profileUser->provider)) +
+ + + @component('components.validation-errors', ['field'=>'confirm_password'])@endcomponent +
+ @endif + +
+
@else
diff --git a/routes/web.php b/routes/web.php index bb60cfb65..d46ae1a44 100644 --- a/routes/web.php +++ b/routes/web.php @@ -500,6 +500,9 @@ Route::post('user/email-change/resend', [UserEmailChangeController::class, 'resend']) ->name('user.email-change.resend') ->middleware(['auth', 'throttle:6,1']); +Route::post('user/email-change/confirm-here', [UserEmailChangeController::class, 'confirmHere']) + ->name('user.email-change.confirm-here') + ->middleware(['auth', 'throttle:6,1']); Route::get('email/change/confirm/{user}/{token}', [UserEmailChangeController::class, 'confirm']) ->name('user.email-change.confirm') ->middleware('signed'); diff --git a/tests/Feature/UserEmailChangeTest.php b/tests/Feature/UserEmailChangeTest.php index 32a7d5206..8e2239e62 100644 --- a/tests/Feature/UserEmailChangeTest.php +++ b/tests/Feature/UserEmailChangeTest.php @@ -138,6 +138,33 @@ public function test_user_can_resend_pending_confirmation_email(): void Mail::assertNotSent(PendingEmailChangeNotification::class); } + public function test_user_can_confirm_pending_change_from_profile(): void + { + $user = User::factory()->create([ + 'email' => 'old@example.com', + 'email_display' => 'old@example.com', + 'provider' => null, + 'password' => bcrypt('correct-password'), + 'pending_email' => 'new@example.com', + 'pending_email_token' => hash('sha256', 'pending-token'), + 'pending_email_requested_at' => now(), + 'email_verified_at' => now(), + ]); + + $this->signIn($user); + + $response = $this->post(route('user.email-change.confirm-here'), [ + 'confirm_password' => 'correct-password', + ]); + + $response->assertRedirect(route('profile')); + $response->assertSessionHas('flash'); + + $user->refresh(); + $this->assertSame('new@example.com', $user->email); + $this->assertNull($user->pending_email); + } + public function test_profile_update_still_cannot_change_login_email_directly(): void { $user = User::factory()->create([