emailFingerprint($email); try { $attemptId = DB::transaction(function () use ($email, $emailFingerprint): ?int { $user = User::query() ->where('email', $email) ->lockForUpdate() ->first(); if ($user === null) { Log::warning('Password reset attempt was not created because the user was not found.', [ 'email_fingerprint' => $emailFingerprint, ]); return null; } $user->resetPasswordAttempts() ->whereIn('status', [ ResetPasswordAttempt::STATUS_PENDING, ResetPasswordAttempt::STATUS_VALIDATED, ]) ->update(['status' => ResetPasswordAttempt::STATUS_EXPIRED]); $attempt = $user->resetPasswordAttempts()->create([ 'codigo' => $this->generateCode(), 'status' => ResetPasswordAttempt::STATUS_PENDING, ]); return $attempt->getKey(); }); } catch (Throwable $exception) { Log::error('Failed to create password reset attempt.', [ 'email_fingerprint' => $emailFingerprint, 'exception' => $exception, ]); throw $exception; } if ($attemptId !== null) { try { PasswordResetRequested::dispatch($attemptId, $tenantCode); } catch (Throwable $exception) { Log::error('Failed to dispatch password reset email.', [ 'attempt_id' => $attemptId, 'tenant_code' => $tenantCode, 'email_fingerprint' => $emailFingerprint, 'exception' => $exception, ]); throw $exception; } } } public function validateCode(string $email, string $code): bool { $emailFingerprint = $this->emailFingerprint($email); try { return DB::transaction(function () use ($email, $code, $emailFingerprint): bool { $user = User::query() ->where('email', $email) ->lockForUpdate() ->first(); $attempt = $user?->resetPasswordAttempts() ->where('codigo', $code) ->where('status', ResetPasswordAttempt::STATUS_PENDING) ->latest('id') ->lockForUpdate() ->first(); if ($attempt === null) { Log::warning('Password reset code validation failed: no matching pending attempt.', [ 'email_fingerprint' => $emailFingerprint, ]); return false; } $attempt->update([ 'status' => ResetPasswordAttempt::STATUS_VALIDATED, ]); return true; }); } catch (Throwable $exception) { Log::error('Failed to validate password reset code.', [ 'email_fingerprint' => $emailFingerprint, 'exception' => $exception, ]); throw $exception; } } public function resetPassword(string $email, string $code, string $password): bool { $emailFingerprint = $this->emailFingerprint($email); try { return DB::transaction(function () use ($email, $code, $password, $emailFingerprint): bool { $user = User::query() ->where('email', $email) ->lockForUpdate() ->first(); $attempt = $user?->resetPasswordAttempts() ->where('codigo', $code) ->where('status', ResetPasswordAttempt::STATUS_VALIDATED) ->latest('id') ->lockForUpdate() ->first(); if ($user === null || $attempt === null) { Log::warning('Password reset failed: no matching validated attempt.', [ 'email_fingerprint' => $emailFingerprint, ]); return false; } $user->password = $password; $user->failed_login_attempts = 0; $user->last_failed_login_at = null; $user->locked_until = null; $user->save(); $user->tokens()->delete(); $user->resetPasswordAttempts() ->whereKeyNot($attempt->getKey()) ->whereIn('status', [ ResetPasswordAttempt::STATUS_PENDING, ResetPasswordAttempt::STATUS_VALIDATED, ]) ->update(['status' => ResetPasswordAttempt::STATUS_EXPIRED]); $attempt->update([ 'status' => ResetPasswordAttempt::STATUS_USED, ]); return true; }); } catch (Throwable $exception) { Log::error('Failed to reset user password.', [ 'email_fingerprint' => $emailFingerprint, 'exception' => $exception, ]); throw $exception; } } private function generateCode(): string { return str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT); } private function emailFingerprint(string $email): string { return substr(hash('sha256', strtolower(trim($email))), 0, 12); } }