diff --git a/web/app/Http/Controllers/Auth/EmailVerificationController.php b/web/app/Http/Controllers/Auth/EmailVerificationController.php
new file mode 100644
index 0000000..6610666
--- /dev/null
+++ b/web/app/Http/Controllers/Auth/EmailVerificationController.php
@@ -0,0 +1,67 @@
+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);
+ }
+}
diff --git a/web/app/Http/Controllers/Auth/LoginController.php b/web/app/Http/Controllers/Auth/LoginController.php
index ba6ca32..34c011b 100644
--- a/web/app/Http/Controllers/Auth/LoginController.php
+++ b/web/app/Http/Controllers/Auth/LoginController.php
@@ -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
@@ -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;
}
/**
@@ -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(),
@@ -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);
+ }
+ }
}
diff --git a/web/app/Http/Controllers/Auth/RegisterController.php b/web/app/Http/Controllers/Auth/RegisterController.php
index 10a2eed..31d587c 100644
--- a/web/app/Http/Controllers/Auth/RegisterController.php
+++ b/web/app/Http/Controllers/Auth/RegisterController.php
@@ -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,
diff --git a/web/app/Providers/AppServiceProvider.php b/web/app/Providers/AppServiceProvider.php
index 2bc9bca..d026e36 100644
--- a/web/app/Providers/AppServiceProvider.php
+++ b/web/app/Providers/AppServiceProvider.php
@@ -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;
@@ -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) {
@@ -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']);
+ });
}
/**
diff --git a/web/routes/api.php b/web/routes/api.php
index cbcc66d..d9546e1 100644
--- a/web/routes/api.php
+++ b/web/routes/api.php
@@ -1,5 +1,6 @@
'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');
});
diff --git a/web/src/Events/EmailWasVerifiedEvent.php b/web/src/Events/EmailWasVerifiedEvent.php
new file mode 100644
index 0000000..0a9914d
--- /dev/null
+++ b/web/src/Events/EmailWasVerifiedEvent.php
@@ -0,0 +1,26 @@
+user = $user;
+ }
+}
diff --git a/web/src/Factories/UserModelFactory.php b/web/src/Factories/UserModelFactory.php
index 194f0d4..623cf7d 100644
--- a/web/src/Factories/UserModelFactory.php
+++ b/web/src/Factories/UserModelFactory.php
@@ -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
@@ -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',
diff --git a/web/src/Models/UserModel.php b/web/src/Models/UserModel.php
index 75e8b38..9200ea8 100644
--- a/web/src/Models/UserModel.php
+++ b/web/src/Models/UserModel.php
@@ -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;
diff --git a/web/src/Notifications/VerifyEmailNotification.php b/web/src/Notifications/VerifyEmailNotification.php
new file mode 100644
index 0000000..fdba75a
--- /dev/null
+++ b/web/src/Notifications/VerifyEmailNotification.php
@@ -0,0 +1,76 @@
+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('' . __('5 Security Tips') . '')
+ ->line('' . __('DO NOT give your password to anyone!') . '
' .
+ __(
+ 'DO NOT call any phone number for someone clainming to be :app_name support!',
+ ['app_name' => config('app.name')]
+ ) . '
' .
+ __(
+ 'DO NOT send any money to anyone clainming to be a member of :app_name!',
+ ['app_name' => config('app.name')]
+ ) . '
' .
+ __('Enable Two Factor Authentication!') . '
' .
+ __('Make sure you are visiting :app_url', [
+ 'app_url' => $this->configRepository->get('app.url')
+ ]) . '');
+ }
+
+ /**
+ * @param string $token
+ * @return $this
+ */
+ public function setToken(string $token): VerifyEmailNotification
+ {
+ $this->token = $token;
+
+ return $this;
+ }
+}
diff --git a/web/src/Repositories/AbstractEloquentRepository.php b/web/src/Repositories/AbstractEloquentRepository.php
index 138ad08..a34573d 100644
--- a/web/src/Repositories/AbstractEloquentRepository.php
+++ b/web/src/Repositories/AbstractEloquentRepository.php
@@ -2,12 +2,14 @@
namespace TinnyApi\Repositories;
+use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Cache\Repository as CacheRepository;
use Ramsey\Uuid\Uuid;
use TinnyApi\Contracts\BaseRepository;
+use Illuminate\Contracts\Events\Dispatcher as Event;
abstract class AbstractEloquentRepository implements BaseRepository
{
@@ -29,18 +31,32 @@ abstract class AbstractEloquentRepository implements BaseRepository
/**
* @var CacheRepository
*/
- private $cacheRepository;
+ protected $cacheRepository;
+
+ /**
+ * @var Guard
+ */
+ protected $auth;
+
+ /**
+ * @var Event
+ */
+ protected $event;
/**
* EloquentRepository constructor.
*
* @param Model $model
* @param CacheRepository $cacheRepository
+ * @param Guard $auth
+ * @param Event $event
*/
- public function __construct(Model $model, CacheRepository $cacheRepository)
+ public function __construct(Model $model, CacheRepository $cacheRepository, Guard $auth, Event $event)
{
$this->model = $model;
$this->cacheRepository = $cacheRepository;
+ $this->auth = $auth;
+ $this->event = $event;
}
/**
@@ -70,7 +86,7 @@ public function findOneById(string $id): Model
throw (new ModelNotFoundException())->setModel(get_class($this->model));
}
- if (!empty($this->with) || auth()->check()) {
+ if (!empty($this->with) || $this->auth->check()) {
return $this->findOneBy(['id' => $id]);
}
diff --git a/web/src/Repositories/UserEloquentRepository.php b/web/src/Repositories/UserEloquentRepository.php
index 8fde5c1..38a6e9f 100644
--- a/web/src/Repositories/UserEloquentRepository.php
+++ b/web/src/Repositories/UserEloquentRepository.php
@@ -57,7 +57,7 @@ public function update(Model $model, array $data): Model
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
- event(new PasswordReset(auth()->user()));
+ $this->event->dispatch(new PasswordReset($this->auth->user()));
}
return parent::update($model, $data);