Skip to content

Commit

Permalink
fix(email-verification): make email verification actually work! hopef…
Browse files Browse the repository at this point in the history
…ully!
  • Loading branch information
Mohammad-Alavi committed Apr 16, 2022
1 parent 253e8a1 commit 49d324a
Show file tree
Hide file tree
Showing 17 changed files with 314 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class ApiLoginProxyForWebClientAction extends Action
{
use LoginAttributeCaseSensitivityTrait;

/**
* @param LoginProxyPasswordGrantRequest $request
* @return array
Expand Down Expand Up @@ -49,6 +49,7 @@ private function enrichSanitizedData(string $username, array $sanitizedData): ar
$sanitizedData['client_secret'] = config('appSection-authentication.clients.web.secret');
$sanitizedData['grant_type'] = 'password';
$sanitizedData['scope'] = '';

return $sanitizedData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function run(RegisterUserRequest $request): User
$user = app(CreateUserByCredentialsTask::class)->run($sanitizedData);

$user->notify(new Welcome());
app(SendVerificationEmailTask::class)->run($user);
app(SendVerificationEmailTask::class)->run($user, $request->verification_url);

return $user;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ class SendVerificationEmailAction extends Action
{
public function run(SendVerificationEmailRequest $request): void
{
app(SendVerificationEmailTask::class)->run($request->user());
app(SendVerificationEmailTask::class)->run($request->user(), $request->verification_url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

'require_email_verification' => true,
'verification_link_expiration_time' => 30, // in minute

/*
|--------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\URL;

class VerifyEmail extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
private string $verification_url,
) {
}

public function via($notifiable): array
{
return ['mail'];
Expand All @@ -21,7 +27,7 @@ public function toMail(UserModel $notifiable): MailMessage
{
return (new MailMessage())
->subject('Verify Email Address')
->line('Please click the button below to verify your email address.')
->line('Please click the below button to verify your email address.')
->action('Verify Email Address', $this->createUrl($notifiable))
->line('If you did not create an account, no further action is required.');
}
Expand All @@ -31,6 +37,13 @@ private function createUrl(UserModel $notifiable): string
$id = config('apiato.hash-id') ? $notifiable->getHashedKey() : $notifiable->getKey();
$hash = sha1($notifiable->getEmailForVerification());

return request('verification_url') . "/$id/$hash";
return $this->verification_url . '?url=' . URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(config('appSection-authentication.verification_link_expiration_time')),
[
'id' => $id,
'hash' => $hash,
]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

namespace App\Containers\AppSection\Authentication\Tasks;

use App\Ship\Parents\Models\UserModel;
use App\Ship\Contracts\MustVerifyEmail;
use App\Ship\Parents\Tasks\Task;

class SendVerificationEmailTask extends Task
{
public function run(UserModel $user): void
public function run(MustVerifyEmail $user, ?string $verificationUrl = null): void
{
if (config('appSection-authentication.require_email_verification') && !$user->hasVerifiedEmail()) {
$user->sendEmailVerificationNotification();
if (config('appSection-authentication.require_email_verification') && !$user->hasVerifiedEmail() && !is_null($verificationUrl)) {
$user->sendEmailVerificationNotificationWithVerificationUrl($verificationUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,48 @@
*/
class RegisterUserActionTest extends TestCase
{
public function testSendNotification_AfterUserRegistration(): void
public function testAfterUserRegistration_GivenEmailVerificationEnabled_SendNotification(): void
{
if (!config('appSection-authentication.require_email_verification')) {
$this->markTestSkipped();
}
Notification::fake();

config(['appSection-authentication.require_email_verification', false]);
$data = [
'email' => '[email protected]',
'password' => 'so-secret',
'verification_url' => config('appSection-authentication.allowed-verify-email-urls')[0],
];

$request = new RegisterUserRequest($data);
request()->merge($request->all());
$user = app(RegisterUserAction::class)->run($request);

$this->assertEquals($data['email'], $user->email);
$this->assertModelExists($user);
$this->assertEquals(strtolower($data['email']), $user->email);
Notification::assertSentTo($user, Welcome::class);
Notification::assertSentTo($user, VerifyEmail::class);
}

public function testAfterUserRegistration_GivenEmailVerificationDisabled_ShouldNotSendVerifyEmailNotification(): void
{
if (config('appSection-authentication.require_email_verification')) {
Notification::assertSentTo($user, VerifyEmail::class);
$this->markTestSkipped();
}
Notification::fake();
$data = [
'email' => '[email protected]',
'password' => 'so-secret',
'verification_url' => config('appSection-authentication.allowed-verify-email-urls')[0],
];

$request = new RegisterUserRequest($data);
request()->merge($request->all());
$user = app(RegisterUserAction::class)->run($request);

$this->assertModelExists($user);
$this->assertEquals(strtolower($data['email']), $user->email);
Notification::assertSentTo($user, Welcome::class);
Notification::assertNotSentTo($user, VerifyEmail::class);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace App\Containers\AppSection\Authentication\Tests\Unit;

use App\Containers\AppSection\Authentication\Notifications\VerifyEmail;
use App\Containers\AppSection\Authentication\Tasks\SendVerificationEmailTask;
use App\Containers\AppSection\Authentication\Tests\TestCase;
use App\Containers\AppSection\User\Models\User;
use Illuminate\Support\Facades\Notification;

/**
* Class SendVerificationEmailTaskTest.
*
* @group authentication
* @group unit
*/
class SendVerificationEmailTaskTest extends TestCase
{
public function testGivenEmailVerificationEnabled_SendVerificationEmail(): void
{
Notification::fake();
$unverifiedUser = User::factory()->unverified()->create();
config(['appSection-authentication.require_email_verification' => true]);

app(SendVerificationEmailTask::class)->run($unverifiedUser, 'this_doesnt_matter_for_the_test');

Notification::assertSentTo($unverifiedUser, VerifyEmail::class);
}

public function testGivenEmailVerificationDisabled_ShouldNotSendVerificationEmail(): void
{
Notification::fake();
$unverifiedUser = User::factory()->unverified()->create();
config(['appSection-authentication.require_email_verification' => false]);

app(SendVerificationEmailTask::class)->run($unverifiedUser);

Notification::assertNotSentTo($unverifiedUser, VerifyEmail::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,22 @@

namespace App\Containers\AppSection\Authentication\UI\API\Controllers;

use Apiato\Core\Exceptions\IncorrectIdException;
use Apiato\Core\Exceptions\InvalidTransformerException;
use App\Containers\AppSection\Authentication\Actions\RegisterUserAction;
use App\Containers\AppSection\Authentication\UI\API\Requests\RegisterUserRequest;
use App\Containers\AppSection\User\UI\API\Transformers\UserTransformer;
use App\Ship\Exceptions\CreateResourceFailedException;
use App\Ship\Parents\Controllers\ApiController;

class RegisterUserController extends ApiController
{
/**
* @param RegisterUserRequest $request
* @return array
* @throws CreateResourceFailedException
* @throws InvalidTransformerException
* @throws IncorrectIdException
*/
public function registerUser(RegisterUserRequest $request): array
{
$user = app(RegisterUserAction::class)->run($request);
$user = app(RegisterUserAction::class)->transactionalRun($request);

return $this->transform($user, UserTransformer::class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
* @apiVersion 1.0.0
* @apiPermission Authenticated ['permissions' => '', 'roles' => '']
*
* @apiHeader {String} accept=application/json
* @apiHeader {String} authorization=Bearer
*
* @apiPermission Authenticated ['permissions' => '', 'roles' => '']
*
* @apiParam {String} verification_url required|url
*
* @apiSuccessExample {json} Success-Response:
Expand All @@ -20,7 +25,8 @@
use App\Containers\AppSection\Authentication\UI\API\Controllers\SendVerificationEmailController;
use Illuminate\Support\Facades\Route;

Route::post('/email/verification-notification', [SendVerificationEmailController::class, 'sendVerificationEmail'])
->name('verification.verify')
->middleware(['auth:api']);
if (config('appSection-authentication.require_email_verification')) {
Route::post('/email/verification-notification', [SendVerificationEmailController::class, 'sendVerificationEmail'])
->middleware(['auth:api']);
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
* @api {POST} /v1/email/verify/:id/:hash Verify Email
* @apiDescription Verify user email
*
* Value of `url` query string in the verification link (sent to the user by email) should be directly used to call the api and verify the user
*
* example of a verification email link sent to the user which is used to verify the use `http://apiato.test/email/verify?url=http://api.apiato.test/v1/email/verify/XbPW7awNkzl83LD6/eaabd911e2e07ede6456d3bd5725c6d4a5c2dc0b?expires=1646913047&signature=232702865b8353c445b39c50397e66db33c74df80e3db5a7c0d46ef94c8ab6a9`
*
* @apiVersion 1.0.0
* @apiPermission none
*
* @apiHeader {String} accept=application/json
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {}
Expand All @@ -18,5 +24,6 @@
use App\Containers\AppSection\Authentication\UI\API\Controllers\VerifyEmailController;
use Illuminate\Support\Facades\Route;

Route::post('email/verify/{id}/{hash}', [VerifyEmailController::class, 'verifyEmail']);

Route::post('email/verify/{id}/{hash}', [VerifyEmailController::class, 'verifyEmail'])
->name('verification.verify')
->middleware('signed');
Loading

0 comments on commit 49d324a

Please sign in to comment.