From 0e4e44c358ea80243e24fb67c07fdbd05bbd1a00 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 16 Feb 2015 14:52:53 +1030 Subject: [PATCH] Preliminary email confirmation implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whenever a user registers or changes their email, they are sent an email containing a link which they must click to confirm it. Upon registering, a user won’t be assigned to any groups and therefore won’t have permission to do anything (but they can still log in!) Upon confirming their email for the first time, their account will be assigned to the Member group and thus “activated”. --- src/Flarum/Core/CoreServiceProvider.php | 1 + .../Listeners/EmailConfirmationMailer.php | 36 +++++++++++ .../InvalidConfirmationTokenException.php | 7 +++ .../Core/Support/Seeders/UserTableSeeder.php | 11 ++-- .../Users/Commands/ConfirmEmailCommand.php | 14 +++++ .../Commands/ConfirmEmailCommandHandler.php | 38 +++++++++++ .../Users/Commands/ConfirmEmailValidator.php | 11 ++++ .../Commands/RegisterUserCommandHandler.php | 3 +- .../Core/Users/Events/EmailWasConfirmed.php | 13 ++++ .../Core/Users/Events/UserWasActivated.php | 13 ++++ src/Flarum/Core/Users/User.php | 49 ++++++++++++++- .../2014_01_14_231404_create_users_table.php | 63 ++++++++++--------- src/routes.php | 14 +++-- src/views/emails/confirm.blade.php | 13 ++++ 14 files changed, 241 insertions(+), 45 deletions(-) create mode 100755 src/Flarum/Core/Listeners/EmailConfirmationMailer.php create mode 100644 src/Flarum/Core/Support/Exceptions/InvalidConfirmationTokenException.php create mode 100644 src/Flarum/Core/Users/Commands/ConfirmEmailCommand.php create mode 100644 src/Flarum/Core/Users/Commands/ConfirmEmailCommandHandler.php create mode 100644 src/Flarum/Core/Users/Commands/ConfirmEmailValidator.php create mode 100644 src/Flarum/Core/Users/Events/EmailWasConfirmed.php create mode 100644 src/Flarum/Core/Users/Events/UserWasActivated.php create mode 100644 src/views/emails/confirm.blade.php diff --git a/src/Flarum/Core/CoreServiceProvider.php b/src/Flarum/Core/CoreServiceProvider.php index 78976fcc3..82952bd7b 100644 --- a/src/Flarum/Core/CoreServiceProvider.php +++ b/src/Flarum/Core/CoreServiceProvider.php @@ -32,6 +32,7 @@ class CoreServiceProvider extends ServiceProvider Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\DiscussionMetadataUpdater'); Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\UserMetadataUpdater'); Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\RenamedPostCreator'); + Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\EmailConfirmationMailer'); Post::addType('comment', 'Flarum\Core\Posts\CommentPost'); Post::addType('renamed', 'Flarum\Core\Posts\RenamedPost'); diff --git a/src/Flarum/Core/Listeners/EmailConfirmationMailer.php b/src/Flarum/Core/Listeners/EmailConfirmationMailer.php new file mode 100755 index 000000000..90bdda0b9 --- /dev/null +++ b/src/Flarum/Core/Listeners/EmailConfirmationMailer.php @@ -0,0 +1,36 @@ +mailer = $mailer; + } + + public function whenUserWasRegistered(UserWasRegistered $event) + { + $user = $event->user; + + $data = [ + 'user' => $user, + 'url' => route('flarum.confirm', ['id' => $user->id, 'token' => $user->confirmation_token]) + ]; + + $this->mailer->send('flarum::emails.confirm', $data, function ($message) use ($user) { + $message->to($user->email)->subject('Welcome!'); + }); + } + + public function whenEmailWasChanged(EmailWasChanged $event) + { + + } +} diff --git a/src/Flarum/Core/Support/Exceptions/InvalidConfirmationTokenException.php b/src/Flarum/Core/Support/Exceptions/InvalidConfirmationTokenException.php new file mode 100644 index 000000000..3deb1dd59 --- /dev/null +++ b/src/Flarum/Core/Support/Exceptions/InvalidConfirmationTokenException.php @@ -0,0 +1,7 @@ + $faker->userName, - 'email' => $faker->safeEmail, - 'password' => 'password', - 'join_time' => $faker->dateTimeThisYear, - 'time_zone' => $faker->timezone + 'username' => $faker->userName, + 'email' => $faker->safeEmail, + 'is_confirmed' => true, + 'password' => 'password', + 'join_time' => $faker->dateTimeThisYear ]); // Assign the users to the 'Member' group, and possibly some others. @@ -49,6 +49,7 @@ class UserTableSeeder extends Seeder // Guests can view the forum ['group.2' , 'forum' , 'view'], + ['group.2' , 'forum' , 'register'], // Members can create and reply to discussions + edit their own stuff ['group.3' , 'forum' , 'startDiscussion'], diff --git a/src/Flarum/Core/Users/Commands/ConfirmEmailCommand.php b/src/Flarum/Core/Users/Commands/ConfirmEmailCommand.php new file mode 100644 index 000000000..fc7d6547b --- /dev/null +++ b/src/Flarum/Core/Users/Commands/ConfirmEmailCommand.php @@ -0,0 +1,14 @@ +userId = $userId; + $this->token = $token; + } +} diff --git a/src/Flarum/Core/Users/Commands/ConfirmEmailCommandHandler.php b/src/Flarum/Core/Users/Commands/ConfirmEmailCommandHandler.php new file mode 100644 index 000000000..62b8219e2 --- /dev/null +++ b/src/Flarum/Core/Users/Commands/ConfirmEmailCommandHandler.php @@ -0,0 +1,38 @@ +userRepo = $userRepo; + } + + public function handle($command) + { + $user = $this->userRepo->findOrFail($command->userId); + + $user->confirmEmail($command->token); + + // If the user hasn't yet had their account activated, + if (! $user->join_time) { + $user->activate(); + } + + Event::fire('Flarum.Core.Users.Commands.ConfirmEmail.UserWillBeSaved', [$user, $command]); + + $this->userRepo->save($user); + $this->dispatchEventsFor($user); + + return $user; + } +} diff --git a/src/Flarum/Core/Users/Commands/ConfirmEmailValidator.php b/src/Flarum/Core/Users/Commands/ConfirmEmailValidator.php new file mode 100644 index 000000000..feac12843 --- /dev/null +++ b/src/Flarum/Core/Users/Commands/ConfirmEmailValidator.php @@ -0,0 +1,11 @@ +userRepo->save($user); - $this->userRepo->syncGroups($user, [3]); // default groups $this->dispatchEventsFor($user); return $user; diff --git a/src/Flarum/Core/Users/Events/EmailWasConfirmed.php b/src/Flarum/Core/Users/Events/EmailWasConfirmed.php new file mode 100644 index 000000000..441455a48 --- /dev/null +++ b/src/Flarum/Core/Users/Events/EmailWasConfirmed.php @@ -0,0 +1,13 @@ +user = $user; + } +} diff --git a/src/Flarum/Core/Users/Events/UserWasActivated.php b/src/Flarum/Core/Users/Events/UserWasActivated.php new file mode 100644 index 000000000..40c7b73d6 --- /dev/null +++ b/src/Flarum/Core/Users/Events/UserWasActivated.php @@ -0,0 +1,13 @@ +user = $user; + } +} diff --git a/src/Flarum/Core/Users/User.php b/src/Flarum/Core/Users/User.php index 2ad7cf893..68c6637a4 100755 --- a/src/Flarum/Core/Users/User.php +++ b/src/Flarum/Core/Users/User.php @@ -14,12 +14,13 @@ use Laracasts\Commander\Events\EventGenerator; use Flarum\Core\Entity; use Flarum\Core\Groups\Group; use Flarum\Core\Support\Exceptions\PermissionDeniedException; +use Flarum\Core\Support\Exceptions\InvalidConfirmationTokenException; class User extends Entity implements UserInterface, RemindableInterface { use EventGenerator; use Permissible; - + use UserTrait, RemindableTrait; protected static $rules = [ @@ -35,7 +36,7 @@ class User extends Entity implements UserInterface, RemindableInterface protected $table = 'users'; protected $hidden = ['password']; - + public static function boot() { parent::boot(); @@ -61,12 +62,20 @@ class User extends Entity implements UserInterface, RemindableInterface public function setUsernameAttribute($username) { + if ($username === $this->username) { + return; + } + $this->attributes['username'] = $username; $this->raise(new Events\UserWasRenamed($this)); } public function setEmailAttribute($email) { + if ($email === $this->email) { + return; + } + $this->attributes['email'] = $email; $this->raise(new Events\EmailWasChanged($this)); } @@ -77,6 +86,14 @@ class User extends Entity implements UserInterface, RemindableInterface $this->raise(new Events\PasswordWasChanged($this)); } + public function activate() + { + $this->join_time = time(); + $this->groups()->sync([3]); + + $this->raise(new Events\UserWasActivated($this)); + } + public static function register($username, $email, $password) { $user = new static; @@ -84,13 +101,39 @@ class User extends Entity implements UserInterface, RemindableInterface $user->username = $username; $user->email = $email; $user->password = $password; - $user->join_time = time(); + + $user->refreshConfirmationToken(); $user->raise(new Events\UserWasRegistered($user)); return $user; } + public function validateConfirmationToken($token) + { + return ! $this->is_confirmed + && $token + && $this->confirmation_token === $token; + } + + public function refreshConfirmationToken() + { + $this->is_confirmed = false; + $this->confirmation_token = str_random(30); + } + + public function confirmEmail($token) + { + if (! $this->validateConfirmationToken($token)) { + throw new InvalidConfirmationTokenException; + } + + $this->is_confirmed = true; + $this->confirmation_token = null; + + $this->raise(new Events\EmailWasConfirmed($this)); + } + public function getDates() { return ['join_time', 'last_seen_time', 'read_time']; diff --git a/src/migrations/2014_01_14_231404_create_users_table.php b/src/migrations/2014_01_14_231404_create_users_table.php index 9d7d7e42a..0efe6efef 100644 --- a/src/migrations/2014_01_14_231404_create_users_table.php +++ b/src/migrations/2014_01_14_231404_create_users_table.php @@ -5,37 +5,38 @@ use Illuminate\Database\Migrations\Migration; class CreateUsersTable extends Migration { - /** - * Run the migrations. - * - * @return void - */ - public function up() - { - Schema::create('users', function(Blueprint $table) - { - $table->increments('id'); - $table->string('username'); - $table->string('email'); - $table->string('password'); - $table->string('token'); - $table->dateTime('join_time'); - $table->string('time_zone'); - $table->dateTime('last_seen_time')->nullable(); - $table->dateTime('read_time')->nullable(); - $table->integer('discussions_count')->unsigned()->default(0); - $table->integer('posts_count')->unsigned()->default(0); - }); - } + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('users', function(Blueprint $table) + { + $table->increments('id'); + $table->string('username'); + $table->string('email'); + $table->boolean('is_confirmed')->default(0); + $table->string('confirmation_token')->nullable(); + $table->string('password'); + $table->string('token'); + $table->dateTime('join_time')->nullable(); + $table->dateTime('last_seen_time')->nullable(); + $table->dateTime('read_time')->nullable(); + $table->integer('discussions_count')->unsigned()->default(0); + $table->integer('posts_count')->unsigned()->default(0); + }); + } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::drop('users'); - } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('users'); + } } diff --git a/src/routes.php b/src/routes.php index 76042fd64..961be7629 100755 --- a/src/routes.php +++ b/src/routes.php @@ -1,7 +1,13 @@ with('title', Config::get('flarum::forum_title', 'Flarum Demo Forum')); +Route::get('/', function () { + return View::make('flarum.web::index') + ->with('title', Config::get('flarum::forum_title', 'Flarum Demo Forum')); }); + +Route::get('confirm/{id}/{token}', ['as' => 'flarum.confirm', function ($userId, $token) { + $command = new Flarum\Core\Users\Commands\ConfirmEmailCommand($userId, $token); + + $commandBus = App::make('Laracasts\Commander\CommandBus'); + $commandBus->execute($command); +}]); diff --git a/src/views/emails/confirm.blade.php b/src/views/emails/confirm.blade.php new file mode 100644 index 000000000..6bd5a7fc2 --- /dev/null +++ b/src/views/emails/confirm.blade.php @@ -0,0 +1,13 @@ + + + + + + +

Welcome, {{ $user->username }}

+ +
+ To confirm your email, click here: {{ $url }} +
+ +