Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/Http/Controllers/UserEmailChangeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.'.');
}
}
38 changes: 38 additions & 0 deletions app/Services/UserEmailChangeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,<br>
Expand Down
25 changes: 24 additions & 1 deletion resources/views/profile.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class="border-2 border-solid border-dark-blue-200 w-full rounded-full h-12 px-4
<p class="mb-4 text-base text-slate-500">
A separate notice was sent to {{ $profileUser->email }} — that email does not contain the confirmation link.
</p>
<div class="flex flex-col tablet:flex-row gap-4">
<div class="flex flex-col tablet:flex-row gap-4 mb-6">
<form method="POST" action="{{ route('user.email-change.resend') }}">
{{ csrf_field() }}
<button type="submit" class="bg-dark-blue text-white rounded-full py-2.5 px-6 font-semibold text-base">
Expand All @@ -88,6 +88,29 @@ class="border-2 border-solid border-dark-blue-200 w-full rounded-full h-12 px-4
</button>
</form>
</div>
<div class="border-t border-dark-blue-200 pt-4">
<p class="mb-3 font-medium text-dark-blue">Can't find the email at {{ $profileUser->pending_email }}?</p>
<p class="mb-4 text-base text-slate-500">
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.
</p>
<form method="POST" action="{{ route('user.email-change.confirm-here') }}">
{{ csrf_field() }}
@if (empty($profileUser->provider))
<div class="mb-3">
<label class="block text-lg text-slate-500 mb-2" for="confirm_password">Current password *</label>
<input id="confirm_password" type="password" name="confirm_password"
class="border-2 border-solid border-dark-blue-200 w-full rounded-full h-12 px-4 appearance-none text-slate-600"
required autocomplete="current-password">
@component('components.validation-errors', ['field'=>'confirm_password'])@endcomponent
</div>
@endif
<button type="submit"
class="bg-primary hover:bg-hover-orange duration-300 text-[#20262C] rounded-full py-2.5 px-6 font-semibold text-base">
Confirm change to {{ $profileUser->pending_email }}
</button>
</form>
</div>
</div>
@else
<div class="border-2 border-solid border-dark-blue-200 rounded-xl px-5 py-4">
Expand Down
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
27 changes: 27 additions & 0 deletions tests/Feature/UserEmailChangeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Loading