mirror of
https://github.com/flarum/framework.git
synced 2025-02-01 01:51:45 +08:00
Improve email changing/confirmation stuff
This commit is contained in:
parent
f4dc1b5d04
commit
b6a8416daf
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateEmailTokensTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('email_tokens', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php namespace Flarum\Core\Events;
|
||||
|
||||
use Flarum\Core\Models\User;
|
||||
|
||||
class UserEmailChangeWasRequested
|
||||
{
|
||||
public $user;
|
||||
|
||||
public $email;
|
||||
|
||||
public function __construct(User $user, $email)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->email = $email;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'])) {
|
||||
|
|
|
@ -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')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
46
framework/core/src/Core/Models/EmailToken.php
Normal file
46
framework/core/src/Core/Models/EmailToken.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
class EmailToken extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_tokens';
|
||||
|
||||
/**
|
||||
* Use a custom primary key for this model.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* Generate a reset token for the specified user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return static
|
||||
*/
|
||||
public static function generate($userId, $email)
|
||||
{
|
||||
$token = new static;
|
||||
|
||||
$token->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');
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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}', [
|
||||
|
|
8
framework/core/views/emails/activateAccount.blade.php
Normal file
8
framework/core/views/emails/activateAccount.blade.php
Normal file
|
@ -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.
|
|
@ -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.
|
||||
|
8
framework/core/views/emails/confirmEmail.blade.php
Normal file
8
framework/core/views/emails/confirmEmail.blade.php
Normal file
|
@ -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.
|
Loading…
Reference in New Issue
Block a user