hasVerifiedEmail()) { return; } $otp = $this->generateOtp(); EmailVerificationOtp::query() ->where('user_id', $user->id) ->delete(); EmailVerificationOtp::create([ 'user_id' => $user->id, 'code' => Hash::make($otp), 'expires_at' => now()->addMinutes($this->expiryMinutes()), 'attempts' => 0, ]); $user->notify(new EmailVerificationOtpEmail($otp)); } public function verify(User $user, string $otp): void { if ($user->hasVerifiedEmail()) { throw ValidationException::withMessages([ 'email' => ['E-mel anda telah disahkan.'], ]); } $record = EmailVerificationOtp::query() ->where('user_id', $user->id) ->latest() ->first(); if (! $record) { throw ValidationException::withMessages([ 'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'], ]); } if ($record->isExpired()) { $record->delete(); throw ValidationException::withMessages([ 'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'], ]); } if ($record->hasExceededMaxAttempts()) { $record->delete(); throw ValidationException::withMessages([ 'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'], ]); } if (! Hash::check($otp, $record->code)) { $record->increment('attempts'); if ($record->fresh()->hasExceededMaxAttempts()) { $record->delete(); } throw ValidationException::withMessages([ 'otp' => ['Kod OTP tidak sah.'], ]); } $user->forceFill(['email_verified_at' => now()])->save(); EmailVerificationOtp::query() ->where('user_id', $user->id) ->delete(); } protected function generateOtp(): string { return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); } protected function expiryMinutes(): int { return (int) config('auth.email_verification.expiry_minutes', 10); } }