diff --git a/js/forum/src/components/change-email-modal.js b/js/forum/src/components/change-email-modal.js index 8cd89d073..99da605d2 100644 --- a/js/forum/src/components/change-email-modal.js +++ b/js/forum/src/components/change-email-modal.js @@ -5,21 +5,39 @@ export default class ChangeEmailModal extends FormModal { constructor(props) { super(props); + this.success = m.prop(false); this.email = m.prop(app.session.user().email()); } view() { + if (this.success()) { + var emailProviderName = this.email().split('@')[1]; + } + var disabled = this.loading(); + return super.view({ className: 'modal-sm change-email-modal', title: 'Change Email', - body: [ - m('div.form-group', [ - m('input.form-control[type=email][name=email][placeholder=Email]', {value: this.email(), onchange: m.withAttr('value', this.email)}) - ]), - m('div.form-group', [ - m('button.btn.btn-primary.btn-block[type=submit]', 'Save Changes') - ]) - ] + body: this.success() + ? [ + m('p.help-text', 'We\'ve sent a confirmation email to ', m('strong', this.email()), '. If it doesn\'t arrive soon, check your spam folder.'), + m('div.form-group', [ + m('a.btn.btn-primary.btn-block', {href: 'http://'+emailProviderName}, 'Go to '+emailProviderName) + ]) + ] + : [ + m('div.form-group', [ + m('input.form-control[type=email][name=email]', { + placeholder: app.session.user().email(), + value: this.email(), + onchange: m.withAttr('value', this.email), + disabled + }) + ]), + m('div.form-group', [ + m('button.btn.btn-primary.btn-block[type=submit]', {disabled}, 'Save Changes') + ]) + ] }); } @@ -33,12 +51,13 @@ export default class ChangeEmailModal extends FormModal { this.loading(true); app.session.user().save({ email: this.email() }).then(() => { - this.hide(); + this.loading(false); + this.success(true); + this.alert(null); + m.redraw(); }, response => { this.loading(false); - this.alert = new Alert({ type: 'warning', message: response.errors.map((error, k) => [error.detail, k < response.errors.length - 1 ? m('br') : '']) }); - m.redraw(); - this.$('[name='+response.errors[0].path+']').select(); + this.handleErrors(response.errors); }); } } diff --git a/js/forum/src/components/login-modal.js b/js/forum/src/components/login-modal.js index e298787b2..cbecd24d1 100644 --- a/js/forum/src/components/login-modal.js +++ b/js/forum/src/components/login-modal.js @@ -3,6 +3,7 @@ import LoadingIndicator from 'flarum/components/loading-indicator'; import ForgotPasswordModal from 'flarum/components/forgot-password-modal'; import SignupModal from 'flarum/components/signup-modal'; import Alert from 'flarum/components/alert'; +import ActionButton from 'flarum/components/action-button'; import icon from 'flarum/helpers/icon'; export default class LoginModal extends FormModal { @@ -29,7 +30,11 @@ export default class LoginModal extends FormModal { ]) ], footer: [ - m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => app.modal.show(new ForgotPasswordModal({email: this.email()}))}, 'Forgot password?')), + m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => { + var email = this.email(); + var props = email.indexOf('@') !== -1 ? {email} : null; + app.modal.show(new ForgotPasswordModal(props)); + }}, 'Forgot password?')), m('p.sign-up-link', [ 'Don\'t have an account? ', m('a[href=javascript:;]', {onclick: () => { @@ -50,12 +55,26 @@ export default class LoginModal extends FormModal { onsubmit(e) { e.preventDefault(); this.loading(true); - app.session.login(this.email(), this.password()).then(() => { + var email = this.email(); + var password = this.password(); + + app.session.login(email, password).then(() => { this.hide(); this.props.callback && this.props.callback(); }, response => { this.loading(false); - this.alert = new Alert({ type: 'warning', message: 'Your login details were incorrect.' }); + if (response && response.code === 'confirm_email') { + var state; + + this.alert(Alert.component({ + message: ['You need to confirm your email before you can log in. We\'ve sent a confirmation email to ', m('strong', response.email), '. If it doesn\'t arrive soon, check your spam folder.'] + })); + } else { + this.alert(Alert.component({ + type: 'warning', + message: 'Your login details were incorrect.' + })); + } m.redraw(); this.ready(); }); diff --git a/migrations/2015_02_24_000000_create_email_tokens_table.php b/migrations/2015_02_24_000000_create_email_tokens_table.php new file mode 100644 index 000000000..2ea56ec32 --- /dev/null +++ b/migrations/2015_02_24_000000_create_email_tokens_table.php @@ -0,0 +1,32 @@ +string('id', 100)->primary(); + $table->integer('user_id')->unsigned(); + $table->string('email', 150); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('email_tokens'); + } +} diff --git a/migrations/2015_02_24_000000_create_users_table.php b/migrations/2015_02_24_000000_create_users_table.php index d44d5ab08..ec637bfc0 100644 --- a/migrations/2015_02_24_000000_create_users_table.php +++ b/migrations/2015_02_24_000000_create_users_table.php @@ -17,8 +17,6 @@ class CreateUsersTable extends Migration $table->increments('id'); $table->string('username', 100)->unique(); $table->string('email', 150)->unique(); - $table->boolean('is_confirmed')->default(0); - $table->string('confirmation_token', 50)->nullable(); $table->boolean('is_activated')->default(0); $table->string('password', 100); $table->text('bio')->nullable(); diff --git a/src/Api/Actions/TokenAction.php b/src/Api/Actions/TokenAction.php index 1836b8cb2..932095c9b 100644 --- a/src/Api/Actions/TokenAction.php +++ b/src/Api/Actions/TokenAction.php @@ -4,6 +4,7 @@ use Flarum\Api\Request; use Flarum\Core\Commands\GenerateAccessTokenCommand; use Flarum\Core\Repositories\UserRepositoryInterface; use Flarum\Core\Exceptions\PermissionDeniedException; +use Flarum\Core\Events\UserEmailChangeWasRequested; use Illuminate\Http\JsonResponse; use Illuminate\Contracts\Bus\Dispatcher; @@ -36,6 +37,11 @@ class TokenAction extends JsonApiAction throw new PermissionDeniedException; } + if (! $user->is_activated) { + event(new UserEmailChangeWasRequested($user, $user->email)); + return new JsonResponse(['code' => 'confirm_email', 'email' => $user->email], 401); + } + $token = $this->bus->dispatch( new GenerateAccessTokenCommand($user->id) ); diff --git a/src/Core/Commands/ConfirmEmailCommand.php b/src/Core/Commands/ConfirmEmailCommand.php index f1ffbf591..b792263a2 100644 --- a/src/Core/Commands/ConfirmEmailCommand.php +++ b/src/Core/Commands/ConfirmEmailCommand.php @@ -2,13 +2,10 @@ class ConfirmEmailCommand { - public $userId; - public $token; - public function __construct($userId, $token) + public function __construct($token) { - $this->userId = $userId; $this->token = $token; } } diff --git a/src/Core/Events/UserEmailChangeWasRequested.php b/src/Core/Events/UserEmailChangeWasRequested.php new file mode 100644 index 000000000..e83e249ab --- /dev/null +++ b/src/Core/Events/UserEmailChangeWasRequested.php @@ -0,0 +1,16 @@ +user = $user; + $this->email = $email; + } +} diff --git a/src/Core/Handlers/Commands/ConfirmEmailCommandHandler.php b/src/Core/Handlers/Commands/ConfirmEmailCommandHandler.php index 76e80ddf9..ad92d5e5e 100644 --- a/src/Core/Handlers/Commands/ConfirmEmailCommandHandler.php +++ b/src/Core/Handlers/Commands/ConfirmEmailCommandHandler.php @@ -3,6 +3,8 @@ use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository; use Flarum\Core\Events\UserWillBeSaved; use Flarum\Core\Support\DispatchesEvents; +use Flarum\Core\Exceptions\InvalidConfirmationTokenException; +use Flarum\Core\Models\EmailToken; class ConfirmEmailCommandHandler { @@ -17,10 +19,14 @@ class ConfirmEmailCommandHandler public function handle($command) { - $user = $this->users->findOrFail($command->userId); + $token = EmailToken::find($command->token)->first(); - $user->assertConfirmationTokenValid($command->token); - $user->confirmEmail(); + if (! $token) { + throw new InvalidConfirmationTokenException; + } + + $user = $token->user; + $user->changeEmail($token->email); if (! $user->is_activated) { $user->activate(); @@ -31,6 +37,8 @@ class ConfirmEmailCommandHandler $user->save(); $this->dispatchEventsFor($user); + $token->delete(); + return $user; } } diff --git a/src/Core/Handlers/Commands/EditUserCommandHandler.php b/src/Core/Handlers/Commands/EditUserCommandHandler.php index 85754f77e..76b19fb74 100644 --- a/src/Core/Handlers/Commands/EditUserCommandHandler.php +++ b/src/Core/Handlers/Commands/EditUserCommandHandler.php @@ -23,11 +23,12 @@ class EditUserCommandHandler $userToEdit->assertCan($user, 'edit'); if (isset($command->data['username'])) { + $userToEdit->assertCan($user, 'rename'); $userToEdit->rename($command->data['username']); } if (isset($command->data['email'])) { - $userToEdit->changeEmail($command->data['email']); + $userToEdit->requestEmailChange($command->data['email']); } if (isset($command->data['password'])) { diff --git a/src/Core/Handlers/Events/EmailConfirmationMailer.php b/src/Core/Handlers/Events/EmailConfirmationMailer.php index c0c11aafa..40ad7912a 100755 --- a/src/Core/Handlers/Events/EmailConfirmationMailer.php +++ b/src/Core/Handlers/Events/EmailConfirmationMailer.php @@ -2,8 +2,9 @@ use Illuminate\Mail\Mailer; use Flarum\Core\Events\UserWasRegistered; -use Flarum\Core\Events\EmailWasChanged; -use Config; +use Flarum\Core\Events\UserEmailChangeWasRequested; +use Flarum\Core; +use Flarum\Core\Models\EmailToken; use Illuminate\Contracts\Events\Dispatcher; class EmailConfirmationMailer @@ -23,28 +24,47 @@ class EmailConfirmationMailer public function subscribe(Dispatcher $events) { $events->listen('Flarum\Core\Events\UserWasRegistered', __CLASS__.'@whenUserWasRegistered'); - $events->listen('Flarum\Core\Events\EmailWasChanged', __CLASS__.'@whenEmailWasChanged'); + $events->listen('Flarum\Core\Events\UserEmailChangeWasRequested', __CLASS__.'@whenUserEmailChangeWasRequested'); } public function whenUserWasRegistered(UserWasRegistered $event) { $user = $event->user; + $data = $this->getPayload($user, $user->email); - $forumTitle = Config::get('flarum::forum_title'); - - $data = [ - 'username' => $user->username, - 'forumTitle' => $forumTitle, - 'url' => route('flarum.forum.confirm', ['id' => $user->id, 'token' => $user->confirmation_token]) - ]; - - $this->mailer->send(['text' => 'flarum::emails.confirm'], $data, function ($message) use ($user) { - $message->to($user->email); - $message->subject('Confirm Your Email Address'); + $this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function ($message) use ($email) { + $message->to($email); + $message->subject('Activate Your New Account'); }); } - public function whenEmailWasChanged(EmailWasChanged $event) + public function whenUserEmailChangeWasRequested(UserEmailChangeWasRequested $event) { + $email = $event->email; + $data = $this->getPayload($event->user, $email); + + $this->mailer->send(['text' => 'flarum::emails.confirmEmail'], $data, function ($message) use ($email) { + $message->to($email); + $message->subject('Confirm Your New Email Address'); + }); + } + + protected function generateToken($user, $email) + { + $token = EmailToken::generate($user->id, $email); + $token->save(); + + return $token; + } + + protected function getPayload($user, $email) + { + $token = $this->generateToken($user, $email); + + return [ + 'username' => $user->username, + 'url' => route('flarum.forum.confirmEmail', ['token' => $token->id]), + 'forumTitle' => Core::config('forum_title') + ]; } } diff --git a/src/Core/Models/EmailToken.php b/src/Core/Models/EmailToken.php new file mode 100644 index 000000000..aeca16d70 --- /dev/null +++ b/src/Core/Models/EmailToken.php @@ -0,0 +1,46 @@ +id = str_random(40); + $token->user_id = $userId; + $token->email = $email; + $token->created_at = time(); + + return $token; + } + + /** + * Define the relationship with the owner of this reset token. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo('Flarum\Core\Models\User'); + } +} diff --git a/src/Core/Models/Model.php b/src/Core/Models/Model.php index 0aae45949..18580ba69 100755 --- a/src/Core/Models/Model.php +++ b/src/Core/Models/Model.php @@ -110,14 +110,19 @@ class Model extends Eloquent */ public function assertValid() { - $validation = $this->makeValidator(); - if ($validation->fails()) { - throw (new ValidationFailureException) - ->setErrors($validation->errors()) - ->setInput($validation->getData()); + $validator = $this->makeValidator(); + if ($validator->fails()) { + $this->throwValidationFailureException($validator); } } + protected function throwValidationFailureException($validator) + { + throw (new ValidationFailureException) + ->setErrors($validator->errors()) + ->setInput($validator->getData()); + } + /** * Make a new validator instance for this model. * diff --git a/src/Core/Models/User.php b/src/Core/Models/User.php index 0bedb6ef9..fc80c87ab 100755 --- a/src/Core/Models/User.php +++ b/src/Core/Models/User.php @@ -13,6 +13,7 @@ use Flarum\Core\Events\UserBioWasChanged; use Flarum\Core\Events\UserAvatarWasChanged; use Flarum\Core\Events\UserWasActivated; use Flarum\Core\Events\UserEmailWasConfirmed; +use Flarum\Core\Events\UserEmailChangeWasRequested; class User extends Model { @@ -94,8 +95,6 @@ class User extends Model $user->password = $password; $user->join_time = time(); - $user->refreshConfirmationToken(); - $user->raise(new UserWasRegistered($user)); return $user; @@ -111,6 +110,7 @@ class User extends Model { if ($username !== $this->username) { $this->username = $username; + $this->raise(new UserWasRenamed($this)); } @@ -127,12 +127,31 @@ class User extends Model { if ($email !== $this->email) { $this->email = $email; + $this->raise(new UserEmailWasChanged($this)); } return $this; } + public function requestEmailChange($email) + { + if ($email !== $this->email) { + $validator = static::$validator->make( + compact('email'), + $this->expandUniqueRules(array_only(static::$rules, 'email')) + ); + + if ($validator->fails()) { + $this->throwValidationFailureException($validator); + } + + $this->raise(new UserEmailChangeWasRequested($this, $email)); + } + + return $this; + } + /** * Change the user's password. * @@ -257,41 +276,12 @@ class User extends Model public function activate() { $this->is_activated = true; - $this->groups()->sync([3]); $this->raise(new UserWasActivated($this)); return $this; } - /** - * Check if a given confirmation token is valid for this user. - * - * @param string $token - * @return boolean - */ - public function assertConfirmationTokenValid($token) - { - if ($this->is_confirmed || - ! $token || - $this->confirmation_token !== $token) { - throw new InvalidConfirmationTokenException; - } - } - - /** - * Generate a new confirmation token for the user. - * - * @return $this - */ - public function refreshConfirmationToken() - { - $this->is_confirmed = false; - $this->confirmation_token = str_random(30); - - return $this; - } - /** * Confirm the user's email. * @@ -461,7 +451,13 @@ class User extends Model */ public function permissions() { - return Permission::whereIn('group_id', array_merge([Group::GUEST_ID], $this->groups->lists('id'))); + $groupIds = [Group::GUEST_ID]; + + if ($this->is_activated) { + $groupIds = array_merge($groupIds, [Group::MEMBER_ID], $this->groups->lists('id')); + } + + return Permission::whereIn('group_id', $groupIds); } /** diff --git a/src/Forum/Actions/ConfirmAction.php b/src/Forum/Actions/ConfirmEmailAction.php similarity index 69% rename from src/Forum/Actions/ConfirmAction.php rename to src/Forum/Actions/ConfirmEmailAction.php index 61eecf602..cfc78c444 100644 --- a/src/Forum/Actions/ConfirmAction.php +++ b/src/Forum/Actions/ConfirmEmailAction.php @@ -5,16 +5,15 @@ use Flarum\Core\Commands\ConfirmEmailCommand; use Flarum\Core\Commands\GenerateAccessTokenCommand; use Flarum\Core\Exceptions\InvalidConfirmationTokenException; -class ConfirmAction extends BaseAction +class ConfirmEmailAction extends BaseAction { use MakesRememberCookie; public function handle(Request $request, $routeParams = []) { try { - $userId = array_get($routeParams, 'id'); $token = array_get($routeParams, 'token'); - $command = new ConfirmEmailCommand($userId, $token); + $command = new ConfirmEmailCommand($token); $user = $this->dispatch($command); } catch (InvalidConfirmationTokenException $e) { return 'Invalid confirmation token'; @@ -24,7 +23,6 @@ class ConfirmAction extends BaseAction $token = $this->dispatch($command); return redirect('/') - ->withCookie($this->makeRememberCookie($token->id)) - ->with('alert', ['type' => 'success', 'message' => 'Thanks for confirming!']); + ->withCookie($this->makeRememberCookie($token->id)); } } diff --git a/src/Forum/routes.php b/src/Forum/routes.php index 19d4ed31f..bf705b2b5 100755 --- a/src/Forum/routes.php +++ b/src/Forum/routes.php @@ -28,9 +28,9 @@ Route::post('login', [ 'uses' => $action('Flarum\Forum\Actions\LoginAction') ]); -Route::get('confirm/{id}/{token}', [ - 'as' => 'flarum.forum.confirm', - 'uses' => $action('Flarum\Forum\Actions\ConfirmAction') +Route::get('confirm/{token}', [ + 'as' => 'flarum.forum.confirmEmail', + 'uses' => $action('Flarum\Forum\Actions\ConfirmEmailAction') ]); Route::get('reset/{token}', [ diff --git a/views/emails/activateAccount.blade.php b/views/emails/activateAccount.blade.php new file mode 100644 index 000000000..10a235f77 --- /dev/null +++ b/views/emails/activateAccount.blade.php @@ -0,0 +1,8 @@ +Hey {{ $username }}! + +Someone (hopefully you!) has signed up to {{ $forumTitle }} with this email address. + +If this was you, simply click the following link and your account will be activated: +{{ $url }} + +If you did not sign up, please ignore this email. diff --git a/views/emails/confirm.blade.php b/views/emails/confirm.blade.php deleted file mode 100644 index 5daff5fef..000000000 --- a/views/emails/confirm.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -Hey {{ $username }}! - -Someone (hopefully you!) has signed up to the forum '{{ $forumTitle }}' with this email address. - -If this was you, simply visit the following link and your account will be activated: -{{ $url }} - -If you did not sign up, please ignore this email. - diff --git a/views/emails/confirmEmail.blade.php b/views/emails/confirmEmail.blade.php new file mode 100644 index 000000000..667366ad3 --- /dev/null +++ b/views/emails/confirmEmail.blade.php @@ -0,0 +1,8 @@ +Hey {{ $username }}! + +Someone (hopefully you!) has changed their email address on {{ $forumTitle }} to this one. + +If this was you, simply click the following link and your email will be confirmed: +{{ $url }} + +If this was not you, please ignore this email.