Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create service to verify user by email token #18

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions web/app/Http/Controllers/Auth/EmailVerificationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Events\Dispatcher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use TinnyApi\Contracts\UserRepository;
use TinnyApi\Events\EmailWasVerifiedEvent;

class EmailVerificationController extends Controller
{
/**
* @var UserRepository
*/
private $userRepository;
/**
* @var Dispatcher
*/
private $event;
/**
* @var EmailWasVerifiedEvent
*/
private $emailWasVerifiedEvent;

/**
* EmailVerificationController constructor.
*
* @param UserRepository $userRepository
* @param Dispatcher $event
* @param EmailWasVerifiedEvent $emailWasVerifiedEvent
*/
public function __construct(
UserRepository $userRepository,
Dispatcher $event,
EmailWasVerifiedEvent $emailWasVerifiedEvent
) {
$this->userRepository = $userRepository;
$this->event = $event;
$this->emailWasVerifiedEvent = $emailWasVerifiedEvent;
}

public function verify($token): JsonResponse
{
try {
$user = $this->userRepository->findOneBy(['email_token_confirmation' => $token]);
} catch (Exception $exception) {
$message = __('Invalid token for email verification');

return $this->respondWithCustomData(['message' => $message], Response::HTTP_BAD_REQUEST);
}

if (!$user->hasVerifiedEmail() && $user->markEmailAsVerified()) {
$this->event->dispatch(new EmailWasVerifiedEvent($user));

$message = __('Email successfully verified');

return $this->respondWithCustomData(['message' => $message], Response::HTTP_OK);
}

$message = __('Invalid token for email verification');

return $this->respondWithCustomData(['message' => $message], Response::HTTP_BAD_REQUEST);
}
}
32 changes: 31 additions & 1 deletion web/app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Notification;
use TinnyApi\Models\UserModel;
use TinnyApi\Notifications\VerifyEmailNotification;
use TinnyApi\Traits\ResponseTrait;

class LoginController extends Controller
Expand All @@ -24,15 +26,22 @@ class LoginController extends Controller
*/
private $cacheRepository;

/**
* @var VerifyEmailNotification
*/
private $verifyEmailNotification;

/**
* Create a new controller instance.
*
* @param CacheRepository $cacheRepository
* @param VerifyEmailNotification $verifyEmailNotification
*/
public function __construct(CacheRepository $cacheRepository)
public function __construct(CacheRepository $cacheRepository, VerifyEmailNotification $verifyEmailNotification)
{
$this->middleware('guest')->except('logout');
$this->cacheRepository = $cacheRepository;
$this->verifyEmailNotification = $verifyEmailNotification;
}

/**
Expand All @@ -55,6 +64,7 @@ protected function sendLoginResponse(Request $request): JsonResponse

try {
$this->checkUserIfIsActive($user, $request);
$this->checkIfUserHasVerifiedEmail($user, $request);
} catch (LockedException $exception) {
return $this->respondWithCustomData([
'message' => $exception->getMessage(),
Expand Down Expand Up @@ -113,4 +123,24 @@ public function logout(Request $request)

return $request->wantsJson() ? $this->respondWithNoContent() : redirect('/');
}

/**
* @param UserModel $user
* @param Request $request
*/
private function checkIfUserHasVerifiedEmail(UserModel $user, Request $request)
{
if (!$user->hasVerifiedEmail()) {
Notification::send($user, $this->verifyEmailNotification->setToken($user->email_token_confirmation));

$this->logout($request);

$message = __(
'We sent a confirmation email to :email. Please follow the instructions to complete your registration.',
['email' => $user->email]
);

throw new LockedException($message);
}
}
}
1 change: 1 addition & 0 deletions web/app/Http/Controllers/Auth/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ protected function create(array $data): Model
return $this->userRepository->store([
'id' => Uuid::uuid4()->toString(),
'name' => $data['name'],
'email_token_confirmation' => Uuid::uuid4()->toString(),
'email' => $data['email'],
'password' => Hash::make($data['password']),
'is_active' => 1,
Expand Down
21 changes: 15 additions & 6 deletions web/app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use TinnyApi\DBConnection\MySQLConnectionFactory as MysqlConnection;
use TinnyApi\DBConnection\SQLiteConnectionFactory as SQLiteConnection;
use TinnyApi\Models\UserModel;
use TinnyApi\Notifications\VerifyEmailNotification;
use TinnyApi\Repositories\UserEloquentRepository;
use TinnyApi\Requests\PasswordUpdateRequest;
use TinnyApi\Requests\UserUpdateRequest;
Expand Down Expand Up @@ -36,7 +37,12 @@ private function registerAllServices()
});

$this->app->singleton(UserRepository::class, function ($app) {
return new UserEloquentRepository(new UserModel(), $app['cache.store']);
return new UserEloquentRepository(
new UserModel(),
$app['cache.store'],
$app['auth.driver'],
$app['events']
);
});

$this->app->singleton(WeakPasswordRule::class, function ($app) {
Expand All @@ -48,16 +54,19 @@ private function registerAllServices()
});

$this->app->singleton(PasswordUpdateRequest::class, function ($app) {
$passwordUpdateRequest = new PasswordUpdateRequest();
$passwordUpdateRequest->setCurrentPasswordRuleInstance($app[CurrentPasswordRule::class]);
$passwordUpdateRequest->setWeakPasswordRuleInstance($app[WeakPasswordRule::class]);

return $passwordUpdateRequest;
return tap(new PasswordUpdateRequest(), function ($passwordUpdateRequest) use ($app) {
$passwordUpdateRequest->setCurrentPasswordRuleInstance($app[CurrentPasswordRule::class]);
$passwordUpdateRequest->setWeakPasswordRuleInstance($app[WeakPasswordRule::class]);
});
});

$this->app->singleton(UserUpdateRequest::class, function () {
return new UserUpdateRequest();
});

$this->app->singleton(VerifyEmailNotification::class, function ($app) {
return new VerifyEmailNotification($app['config']);
});
}

/**
Expand Down
4 changes: 4 additions & 0 deletions web/routes/api.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Controllers\Auth\EmailVerificationController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\UserController;
Expand All @@ -17,6 +18,9 @@
*/

Route::group(['prefix' => 'api/v1', 'middleware' => 'guest'], function () {
Route::post('email/verify/{token}', [EmailVerificationController::class, 'verify'])
->middleware('throttle:hard')
->name('api.email.verify');
Route::post('register', [RegisterController::class, 'register'])->name('api.auth.register');
Route::post('login', [LoginController::class, 'login'])->name('api.auth.login');
});
Expand Down
26 changes: 26 additions & 0 deletions web/src/Events/EmailWasVerifiedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace TinnyApi\Events;

use Illuminate\Queue\SerializesModels;
use Illuminate\Database\Eloquent\Model;

class EmailWasVerifiedEvent
{
use SerializesModels;

/**
* @var Model
*/
private $user;

/**
* Create a new event instance.
*
* @param Model $user
*/
public function __construct(Model $user)
{
$this->user = $user;
}
}
4 changes: 3 additions & 1 deletion web/src/Factories/UserModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
use TinnyApi\Models\UserModel;

class UserModelFactory extends Factory
Expand All @@ -19,8 +20,9 @@ class UserModelFactory extends Factory
public function definition()
{
return [
'id' => 'id',
'id' => 'user_id',
'name' => $this->faker->name,
'email_token_confirmation' => 'email_token',
'email' => $this->faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
Expand Down
3 changes: 2 additions & 1 deletion web/src/Models/UserModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
namespace TinnyApi\Models;

use Database\Factories\UserModelFactory;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class UserModel extends Authenticatable
class UserModel extends Authenticatable implements MustVerifyEmail
{
use HasFactory, Notifiable, HasApiTokens;

Expand Down
76 changes: 76 additions & 0 deletions web/src/Notifications/VerifyEmailNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace TinnyApi\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Config\Repository as ConfigRepository;

class VerifyEmailNotification extends Notification implements ShouldQueue
{
use Queueable;

/**
* @var string
*/
private $token;

/**
* @var ConfigRepository
*/
private $configRepository;

/**
* VerifyEmailNotification constructor.
*
* @param ConfigRepository $configRepository
*/
public function __construct(ConfigRepository $configRepository)
{
$this->configRepository = $configRepository;
$this->onQueue('notifications');
}

public function via(): array
{
return ['mail'];
}

public function toMail($notifiable): MailMessage
{
return (new MailMessage())
->markdown('emails.default')
->success()
->subject(__(':app_name - Confirm your registration', ['app_name' => config('app.name')]))
->greeting(__('Welcome to :app_name', ['app_name' => config('app.name')]))
->line(__('Click the link below to complete verification:'))
->action(__('Verify Email'), url('/user/verify/' . $this->token))
->line('<b>' . __('5 Security Tips') . '</b>')
->line('<small>' . __('DO NOT give your password to anyone!') . '<br>' .
__(
'DO NOT call any phone number for someone clainming to be :app_name support!',
['app_name' => config('app.name')]
) . '<br>' .
__(
'DO NOT send any money to anyone clainming to be a member of :app_name!',
['app_name' => config('app.name')]
) . '<br>' .
__('Enable Two Factor Authentication!') . '<br>' .
__('Make sure you are visiting :app_url', [
'app_url' => $this->configRepository->get('app.url')
]) . '</small>');
}

/**
* @param string $token
* @return $this
*/
public function setToken(string $token): VerifyEmailNotification
{
$this->token = $token;

return $this;
}
}
Loading