milenmk/laravel-email-change-confirmation
Laravel package for confirming user email address changes. Adds a verification flow (often via signed link/token), protects accounts by requiring confirmation before updating the email, and integrates cleanly with common auth setups in Laravel apps.
Installation
composer require milenmk/laravel-email-change-confirmation
php artisan vendor:publish --provider="MilenMk\EmailChangeConfirmation\EmailChangeConfirmationServiceProvider" --tag="config"
php artisan migrate
config/email-change-confirmation.php) and creates a email_change_confirmations table.Configure
Update config/email-change-confirmation.php with:
token_lifetime (default: 24 hours)email_template (path to Blade template)queue (set to true if using queues)First Use Case Trigger an email change for a user:
use MilenMk\EmailChangeConfirmation\Facades\EmailChangeConfirmation;
$user = User::find(1);
EmailChangeConfirmation::sendChangeRequest($user, '[email protected]');
Trigger Request
public function requestEmailChange(Request $request) {
$validated = $request->validate(['email' => 'required|email']);
EmailChangeConfirmation::sendChangeRequest(auth()->user(), $validated['email']);
return back()->with('status', 'Confirmation email sent!');
}
Confirm Change
public function confirmEmailChange($token) {
$result = EmailChangeConfirmation::confirmChange($token);
if ($result->success()) {
return redirect()->route('dashboard')->with('status', 'Email updated!');
}
return back()->withErrors($result->errors());
}
Resend Token
public function resendConfirmation($token) {
$result = EmailChangeConfirmation::resendConfirmation($token);
return back()->with('status', $result->success() ? 'New email sent!' : 'Invalid token.');
}
Create a Livewire Component
php artisan make:livewire EmailChangeForm
Handle Logic in Component
use MilenMk\EmailChangeConfirmation\Facades\EmailChangeConfirmation;
public function sendChangeRequest() {
$validated = $this->validate(['email' => 'required|email']);
$result = EmailChangeConfirmation::sendChangeRequest(auth()->user(), $validated['email']);
if ($result->success()) {
$this->emit('alert', 'Confirmation email sent!');
} else {
$this->addError('email', $result->errors()->first());
}
}
public function confirmChange($token) {
$result = EmailChangeConfirmation::confirmChange($token);
if ($result->success()) {
$this->emit('alert', 'Email updated!');
$this->redirect('/dashboard');
}
}
Blade Template
<livewire:email-change-form />
@script
window.addEventListener('alert', (e) => {
toast(e.detail);
});
@endscript
Configure Queue in .env
QUEUE_CONNECTION=database
Dispatch Job Manually
EmailChangeConfirmation::sendChangeRequest($user, '[email protected]', true); // Force queue
Run Worker
php artisan queue:work
Publish Views
php artisan vendor:publish --provider="MilenMk\EmailChangeConfirmation\EmailChangeConfirmationServiceProvider" --tag="views"
resources/views/vendor/email-change-confirmation/email.blade.php.Pass Custom Data
EmailChangeConfirmation::sendChangeRequest($user, '[email protected]', [
'custom_data' => 'Hello, User!'
]);
@php echo $custom_data @endphp.Token Expiry
token_lifetime (default: 24h). Use resendConfirmation() if needed.email_change_confirmations table for expired tokens.Email Validation
User model’s email field allows duplicates during the change process (e.g., unique:users,email is temporarily bypassed).Queue Delays
EmailChangeConfirmation::sendChangeRequest($user, '[email protected]', true); // Force sync for testing
failed_jobs table if emails aren’t sent.Livewire Token Handling
public $confirmationToken;
public function mount($token) {
$this->confirmationToken = $token;
}
Log Token Generation
Enable debug mode in config/email-change-confirmation.php:
'debug' => env('APP_DEBUG', false),
storage/logs/laravel.log.Check Database
SELECT * FROM email_change_confirmations WHERE user_id = 1;
pending, confirmed, failed).Test Email Delivery
Use Laravel’s Mail::fake() in tests:
public function test_email_change() {
Mail::fake();
$result = EmailChangeConfirmation::sendChangeRequest($user, '[email protected]');
Mail::assertSent(\MilenMk\EmailChangeConfirmation\Mail\ChangeEmail::class);
}
Custom Events
Listen for events in EventServiceProvider:
protected $listen = [
\MilenMk\EmailChangeConfirmation\Events\ChangeEmailRequested::class => [
\App\Listeners\LogEmailChange::class,
],
\MilenMk\EmailChangeConfirmation\Events\ChangeEmailConfirmed::class => [
\App\Listeners\NotifyAdmin::class,
],
];
Override Confirmation Logic
Bind the confirm-change command to a custom handler:
EmailChangeConfirmation::extend(function ($app) {
$app->bind(\MilenMk\EmailChangeConfirmation\Contracts\ConfirmationHandler::class, \App\Services\CustomConfirmationHandler::class);
});
Add Metadata to Tokens
Extend the email_change_confirmations table and hydrate the model:
public function boot() {
\MilenMk\EmailChangeConfirmation\Models\EmailChangeConfirmation::created(function ($model) {
$model->metadata = ['ip' => request()->ip()];
$model->save();
});
}
Token Length
config/email-change-confirmation.php:
'token_length' => 60,
Rate Limiting
requestEmailChange:
Route::middleware(['throttle:5,1'])->group(function () {
Route::post('/email-change', [EmailChangeController::class, 'requestEmailChange']);
});
Multi-Tenant Support
tenant_id to the email_change_confirmations table and filter queries:
EmailChangeConfirmation::where('tenant_id', auth()->tenant()->id)->get();
How can I help you explore Laravel packages today?