feat(email): implement email change request and verification process

- Added functionality for users to request an email change, including generating a verification code and setting an expiration time.
- Implemented methods in the User model to handle email change requests, code validation, and confirmation.
- Created a new job to update the user's email in Stripe after confirmation.
- Introduced rate limiting for email change requests and verification attempts to prevent abuse.
- Added a new notification for email change verification.
- Updated the profile component to manage email change requests and verification UI.
This commit is contained in:
Andras Bacsai
2025-08-18 14:54:08 +02:00
parent 5cfe6464aa
commit ee502b9f76
12 changed files with 568 additions and 4 deletions

View File

@@ -53,6 +53,7 @@ class User extends Authenticatable implements SendsEmail
'email_verified_at' => 'datetime',
'force_password_reset' => 'boolean',
'show_boarding' => 'boolean',
'email_change_code_expires_at' => 'datetime',
];
protected static function boot()
@@ -320,4 +321,77 @@ class User extends Authenticatable implements SendsEmail
return data_get($user, 'pivot.role');
}
public function requestEmailChange(string $newEmail): void
{
// Generate 6-digit code
$code = sprintf('%06d', mt_rand(0, 999999));
// Set expiration using config value
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
$this->update([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
]);
// Send verification email to new address
$this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt));
}
public function isEmailChangeCodeValid(string $code): bool
{
return $this->email_change_code === $code
&& $this->email_change_code_expires_at
&& Carbon::now()->lessThan($this->email_change_code_expires_at);
}
public function confirmEmailChange(string $code): bool
{
if (! $this->isEmailChangeCodeValid($code)) {
return false;
}
$oldEmail = $this->email;
$newEmail = $this->pending_email;
// Update email and clear change request fields
$this->update([
'email' => $newEmail,
'pending_email' => null,
'email_change_code' => null,
'email_change_code_expires_at' => null,
]);
// For cloud users, dispatch job to update Stripe customer email asynchronously
if (isCloud() && $this->currentTeam()->subscription) {
dispatch(new \App\Jobs\UpdateStripeCustomerEmailJob(
$this->currentTeam(),
$this->id,
$newEmail,
$oldEmail
));
}
return true;
}
public function clearEmailChangeRequest(): void
{
$this->update([
'pending_email' => null,
'email_change_code' => null,
'email_change_code_expires_at' => null,
]);
}
public function hasEmailChangeRequest(): bool
{
return ! is_null($this->pending_email)
&& ! is_null($this->email_change_code)
&& $this->email_change_code_expires_at
&& Carbon::now()->lessThan($this->email_change_code_expires_at);
}
}