Merge pull request #7231 from pixelfed/fix/settings-email-verification

Fix missing email verification dispatch on settings email change
pull/7232/head
Shlee 1 week ago committed by GitHub
commit be65cce84a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -8,6 +8,7 @@ use App\Models\EmailVerification;
use App\Models\Media;
use App\Models\User;
use App\Services\AccountService;
use App\Services\EmailVerificationService;
use App\Services\PronounService;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\PrettyNumber;
@ -211,6 +212,10 @@ trait HomeSettings
$user->save();
$profile->save();
if ($validate && is_null($user->email_verified_at)) {
EmailVerificationService::send($user);
}
return redirect('/settings/email')->with('status', 'Email successfully updated!');
} else {
return redirect('/settings/email');
@ -218,6 +223,23 @@ trait HomeSettings
}
public function emailVerificationResend(Request $request)
{
$user = $request->user();
if (! is_null($user->email_verified_at)) {
return redirect('/settings/email');
}
if (! EmailVerificationService::send($user)) {
return redirect('/settings/email')->withErrors([
'email' => __('A verification email was sent a moment ago. Check your inbox, then try again in a minute.'),
]);
}
return redirect('/settings/email')->with('status', __('Verification email sent to').' '.$user->email);
}
public function avatar()
{
return view('settings.avatar');

@ -23,7 +23,7 @@
@if(Auth::user()->email_verified_at)
<span class="text-success">{{__('settings.email.verified')}}</span> {{Auth::user()->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">{{__('settings.email.unverified')}}</span> {{__('settings.email.you_need_to')}} <a href="/i/verify-email">{{__('settings.email.verify_your_email')}}</a>.
<span class="text-danger">{{__('settings.email.unverified')}}</span> {{__('settings.email.you_need_to')}} <button type="submit" form="email-verification-resend" class="btn btn-link p-0 border-0 align-baseline font-weight-bold">{{__('settings.email.verify_your_email')}}</button>.
@endif
</p>
</div>
@ -33,4 +33,9 @@
</div>
</div>
</form>
@unless(Auth::user()->email_verified_at)
<form id="email-verification-resend" method="post" action="{{route('settings.email.resend')}}" class="d-none">
@csrf
</form>
@endunless
@endsection

@ -296,6 +296,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['localization'])->grou
Route::post('password', [SettingsController::class, 'passwordUpdate'])->middleware('dangerzone');
Route::get('email', [SettingsController::class, 'email'])->name('settings.email')->middleware('dangerzone');
Route::post('email', [SettingsController::class, 'emailUpdate'])->middleware('dangerzone');
Route::post('email/resend', [SettingsController::class, 'emailVerificationResend'])->name('settings.email.resend')->middleware(['dangerzone', 'throttle:3,10']);
Route::get('notifications', [SettingsController::class, 'notifications'])->name('settings.notifications');
Route::get('privacy', [SettingsController::class, 'privacy'])->name('settings.privacy');
Route::post('privacy', [SettingsController::class, 'privacyStore']);

@ -1,10 +1,23 @@
<?php
use App\Mail\ConfirmEmail;
use App\Models\EmailVerification;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
uses(LazilyRefreshDatabase::class);
/**
* Confirm a password for the dangerzone middleware guarding settings routes.
*/
function confirmedSession(): array
{
return ['auth.password_confirmed_at' => time()];
}
/*
|--------------------------------------------------------------------------
| POST /settings/email
@ -54,3 +67,139 @@ it('rejects an email already used by another account', function () {
expect($user->fresh()->email)->not->toBe('taken@example.com');
});
/*
|--------------------------------------------------------------------------
| Email verification on change (enforce_email_verification=true)
|--------------------------------------------------------------------------
|
| Changing email nulls email_verified_at for non-admins. The removed per-request
| /i/verify-email gate used to let them resend verification; that route is gone,
| so the change itself must now dispatch a verification email and the settings
| page must expose an in-app resend path.
|
*/
it('dispatches a verification email when a non-admin changes email and verification is enforced', function () {
config(['pixelfed.enforce_email_verification' => true]);
Mail::fake();
$user = User::factory()->create();
$user->refresh();
$newEmail = 'changed.'.uniqid().'@example.com';
$this->actingAs($user)
->withSession(confirmedSession())
->post('/settings/email', ['email' => $newEmail])
->assertSessionHasNoErrors();
$fresh = $user->fresh();
expect($fresh->email)->toBe($newEmail)
->and($fresh->email_verified_at)->toBeNull();
expect(
EmailVerification::where('user_id', $user->id)->where('email', $newEmail)->exists()
)->toBeTrue();
Mail::assertSent(ConfirmEmail::class);
});
it('auto-verifies an admin email change without dispatching verification mail', function () {
config(['pixelfed.enforce_email_verification' => true]);
Mail::fake();
$user = User::factory()->admin()->create();
$user->refresh();
$newEmail = 'admin.changed.'.uniqid().'@example.com';
$this->actingAs($user)
->withSession(confirmedSession())
->post('/settings/email', ['email' => $newEmail])
->assertSessionHasNoErrors();
expect($user->fresh()->email_verified_at)->not->toBeNull();
Mail::assertNothingSent();
});
it('does not touch verification state when enforcement is disabled', function () {
config(['pixelfed.enforce_email_verification' => false]);
Mail::fake();
$user = User::factory()->create();
$user->refresh();
$newEmail = 'noenforce.'.uniqid().'@example.com';
$this->actingAs($user)
->withSession(confirmedSession())
->post('/settings/email', ['email' => $newEmail])
->assertSessionHasNoErrors();
expect($user->fresh()->email_verified_at)->not->toBeNull();
Mail::assertNothingSent();
});
it('resends a verification email for an authenticated unverified user', function () {
Mail::fake();
$user = User::factory()->unverified()->create();
$user->refresh();
$this->actingAs($user)
->withSession(confirmedSession())
->withoutMiddleware(ThrottleRequests::class)
->post(route('settings.email.resend'))
->assertRedirect('/settings/email')
->assertSessionHas('status');
expect(
EmailVerification::where('user_id', $user->id)->where('email', $user->email)->exists()
)->toBeTrue();
Mail::assertSent(ConfirmEmail::class);
});
it('does not resend verification for an already-verified user', function () {
Mail::fake();
$user = User::factory()->create();
$user->refresh();
$this->actingAs($user)
->withSession(confirmedSession())
->withoutMiddleware(ThrottleRequests::class)
->post(route('settings.email.resend'))
->assertRedirect('/settings/email');
Mail::assertNothingSent();
});
it('rejects a resend inside the service cooldown window with an error', function () {
Mail::fake();
$user = User::factory()->unverified()->create();
$user->refresh();
// Simulate a verification link that was just sent for the current address,
// putting the account inside the resend cooldown window.
$seed = new EmailVerification;
$seed->user_id = $user->id;
$seed->email = $user->email;
$seed->user_token = (string) Str::uuid().'seed';
$seed->random_token = Str::random(64);
$seed->save();
$this->actingAs($user)
->withSession(confirmedSession())
->withoutMiddleware(ThrottleRequests::class)
->post(route('settings.email.resend'))
->assertRedirect('/settings/email')
->assertSessionHasErrors('email');
Mail::assertNothingSent();
});

Loading…
Cancel
Save