diff --git a/phpstan.neon b/phpstan.neon index 39bcda5..eeec0d6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,11 +1,16 @@ includes: -- vendor/nunomaduro/larastan/extension.neon -- vendor/phpstan/phpstan-mockery/extension.neon -- vendor/phpstan/phpstan-phpunit/extension.neon -- vendor/phpstan/phpstan-phpunit/rules.neon -- vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon + - vendor/nunomaduro/larastan/extension.neon + - vendor/phpstan/phpstan-mockery/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon + parameters: level: max paths: - - src - - tests + - src + - tests + ignoreErrors: + - + message: '#Parameter \#1 \$(function|callback) of function call_user_func expects callable\(\): mixed, Closure\|null given\.#' + path: tests/Integration/Services/EmailVerificationServiceTest.php diff --git a/src/Contracts/Services/EmailVerificationServiceInterface.php b/src/Contracts/Services/EmailVerificationServiceInterface.php new file mode 100644 index 0000000..2fc0a12 --- /dev/null +++ b/src/Contracts/Services/EmailVerificationServiceInterface.php @@ -0,0 +1,20 @@ +authManager = $authManager; - $this->config = $config; + protected EmailVerificationServiceInterface $emailVerificationService; + + public function __construct( + AuthManager $authManager, + Config $config, + EmailVerificationServiceInterface $emailVerificationService + ) { + $this->authManager = $authManager; + $this->config = $config; + $this->emailVerificationService = $emailVerificationService; } /** @@ -43,19 +48,7 @@ public function __invoke($_, array $args): array if ($user instanceof MustVerifyEmail) { if ($args['verification_url']) { - VerifyEmail::createUrlUsing(function ($notifiable) use ($args) { - $urlWithHash = str_replace( - '{{HASH}}', - sha1($notifiable->getEmailForVerification()), - $args['verification_url'], - ); - - return str_replace( - '{{ID}}', - $notifiable->getKey(), - $urlWithHash, - ); - }); + $this->emailVerificationService->setVerificationUrl($args['verification_url']); } $user->sendEmailVerificationNotification(); diff --git a/src/GraphQL/Mutations/VerifyEmail.php b/src/GraphQL/Mutations/VerifyEmail.php index 2ebf33e..cabd4d8 100644 --- a/src/GraphQL/Mutations/VerifyEmail.php +++ b/src/GraphQL/Mutations/VerifyEmail.php @@ -4,13 +4,13 @@ namespace DanielDeWit\LighthouseSanctum\GraphQL\Mutations; +use DanielDeWit\LighthouseSanctum\Contracts\Services\EmailVerificationServiceInterface; use DanielDeWit\LighthouseSanctum\Enums\EmailVerificationStatus; use DanielDeWit\LighthouseSanctum\Traits\CreatesUserProvider; +use Exception; use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Config\Repository as Config; -use Illuminate\Support\Facades\Validator; -use Nuwave\Lighthouse\Exceptions\ValidationException; use RuntimeException; class VerifyEmail @@ -19,18 +19,23 @@ class VerifyEmail protected AuthManager $authManager; protected Config $config; + protected EmailVerificationServiceInterface $emailVerificationService; - public function __construct(AuthManager $authManager, Config $config) - { - $this->authManager = $authManager; - $this->config = $config; + public function __construct( + AuthManager $authManager, + Config $config, + EmailVerificationServiceInterface $emailVerificationService + ) { + $this->authManager = $authManager; + $this->config = $config; + $this->emailVerificationService = $emailVerificationService; } /** * @param mixed $_ * @param array $args * @return array - * @throws ValidationException + * @throws Exception */ public function __invoke($_, array $args): array { @@ -42,10 +47,7 @@ public function __invoke($_, array $args): array throw new RuntimeException('User not instance of MustVerifyEmail'); } - if (! hash_equals((string) $args['hash'], - sha1($user->getEmailForVerification()))) { - throw new ValidationException('The provided id and hash are incorrect.', Validator::make([], [])); - } + $this->emailVerificationService->verify($user, (string) $args['hash']); $user->markEmailAsVerified(); diff --git a/src/Providers/LighthouseSanctumServiceProvider.php b/src/Providers/LighthouseSanctumServiceProvider.php index 9d983a0..05c5b77 100644 --- a/src/Providers/LighthouseSanctumServiceProvider.php +++ b/src/Providers/LighthouseSanctumServiceProvider.php @@ -4,9 +4,11 @@ namespace DanielDeWit\LighthouseSanctum\Providers; +use DanielDeWit\LighthouseSanctum\Contracts\Services\EmailVerificationServiceInterface; use DanielDeWit\LighthouseSanctum\Enums\EmailVerificationStatus; use DanielDeWit\LighthouseSanctum\Enums\LogoutStatus; use DanielDeWit\LighthouseSanctum\Enums\RegisterStatus; +use DanielDeWit\LighthouseSanctum\Services\EmailVerificationService; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\ServiceProvider; use Nuwave\Lighthouse\Schema\TypeRegistry; @@ -16,6 +18,11 @@ class LighthouseSanctumServiceProvider extends ServiceProvider { protected TypeRegistry $typeRegistry; + public function register(): void + { + $this->app->singleton(EmailVerificationServiceInterface::class, EmailVerificationService::class); + } + public function boot(Dispatcher $dispatcher, TypeRegistry $typeRegistry): void { $this->typeRegistry = $typeRegistry; diff --git a/src/Services/EmailVerificationService.php b/src/Services/EmailVerificationService.php new file mode 100644 index 0000000..9ba6901 --- /dev/null +++ b/src/Services/EmailVerificationService.php @@ -0,0 +1,43 @@ +getKey(), + $this->createHash($notifiable), + ], $url); + }); + } + + /** + * @param MustVerifyEmail $user + * @param string $hash + * @throws AuthenticationException + */ + public function verify(MustVerifyEmail $user, string $hash): void + { + if (! hash_equals($hash, $this->createHash($user))) { + throw new AuthenticationException('The provided id and hash are incorrect.'); + } + } + + protected function createHash(MustVerifyEmail $user): string + { + return sha1($user->getEmailForVerification()); + } +} diff --git a/tests/Integration/Services/EmailVerificationServiceTest.php b/tests/Integration/Services/EmailVerificationServiceTest.php new file mode 100644 index 0000000..824f172 --- /dev/null +++ b/tests/Integration/Services/EmailVerificationServiceTest.php @@ -0,0 +1,54 @@ +service = new EmailVerificationService(); + } + + /** + * @test + */ + public function it_sets_the_verification_url(): void + { + /** @var UserMustVerifyEmail $user */ + $user = UserMustVerifyEmail::factory()->create([ + 'id' => 12345, + 'email' => 'user@example.com', + ]); + + $this->service->setVerificationUrl('https://mysite.com/verify-email/{{ID}}/{{HASH}}'); + + $url = call_user_func(VerifyEmail::$createUrlCallback, $user); + + static::assertSame('https://mysite.com/verify-email/12345/' . sha1($user->getEmailForVerification()), $url); + } + + /** + * @test + */ + public function it_throws_an_exception_if_the_hash_is_incorrect(): void + { + static::expectException(AuthenticationException::class); + static::expectExceptionMessage('The provided id and hash are incorrect.'); + + $user = UserMustVerifyEmail::factory()->create(); + + $this->service->verify($user, 'foobar'); + } +} diff --git a/tests/Unit/Services/EmailVerificationServiceTest.php b/tests/Unit/Services/EmailVerificationServiceTest.php new file mode 100644 index 0000000..ef8c63a --- /dev/null +++ b/tests/Unit/Services/EmailVerificationServiceTest.php @@ -0,0 +1,64 @@ +service = new EmailVerificationService(); + } + + /** + * @test + */ + public function it_throws_an_exception_if_the_hash_is_incorrect(): void + { + static::expectException(AuthenticationException::class); + static::expectExceptionMessage('The provided id and hash are incorrect.'); + + $this->service->verify( + $this->mockUser('user@example.com'), + sha1('foo@bar.com'), + ); + } + + /** + * @test + */ + public function it_does_nothing_if_the_hash_is_correct(): void + { + $this->service->verify( + $this->mockUser('user@example.com'), + sha1('user@example.com'), + ); + } + + /** + * @param string $email + * @return MustVerifyEmail|MockInterface + */ + protected function mockUser(string $email) + { + /** @var MustVerifyEmail|MockInterface $user */ + $user = Mockery::mock(MustVerifyEmail::class) + ->shouldReceive('getEmailForVerification') + ->andReturn($email) + ->getMock(); + + return $user; + } +}