diff --git a/framework/core/js/forum/src/components/forgot-password-modal.js b/framework/core/js/forum/src/components/forgot-password-modal.js new file mode 100644 index 000000000..8da5611f5 --- /dev/null +++ b/framework/core/js/forum/src/components/forgot-password-modal.js @@ -0,0 +1,66 @@ +import Component from 'flarum/component'; +import LoadingIndicator from 'flarum/components/loading-indicator'; +import Alert from 'flarum/components/alert'; +import icon from 'flarum/helpers/icon'; + +export default class ForgotPasswordModal extends Component { + constructor(props) { + super(props); + + this.email = m.prop(); + this.loading = m.prop(false); + this.success = m.prop(false); + } + + view() { + return m('div.modal-dialog.modal-sm.modal-forgot-password', [ + m('div.modal-content', [ + m('button.btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), + m('form', {onsubmit: this.onsubmit.bind(this)}, [ + m('div.modal-header', m('h3.title-control', 'Forgot Password')), + this.props.message ? m('div.modal-alert.alert', this.props.message) : '', + m('div.modal-body', [ + m('div.form-centered', this.success() ? 'Sent!' : [ + m('div.form-group', [ + m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email)}) + ]), + m('div.form-group', [ + m('button.btn.btn-primary.btn-block[type=submit]', 'Recover Password') + ]) + ]) + ]) + ]) + ]), + LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}) + ]) + } + + ready($modal) { + $modal.find('[name=email]').focus(); + } + + hide() { + app.modal.close(); + } + + onsubmit(e) { + e.preventDefault(); + this.loading(true); + + m.request({ + method: 'POST', + url: app.config['api_url']+'/forgot', + data: {email: this.email()}, + background: true + }).then(response => { + this.loading(false); + this.success(true); + m.redraw(); + }, response => { + this.loading(false); + m.redraw(); + app.alerts.dismiss(this.errorAlert); + app.alerts.show(this.errorAlert = new Alert({ type: 'warning', message: 'Invalid credentials.' })); + }); + } +} diff --git a/framework/core/js/forum/src/components/login-modal.js b/framework/core/js/forum/src/components/login-modal.js index 09c1563f9..cc9316b4c 100644 --- a/framework/core/js/forum/src/components/login-modal.js +++ b/framework/core/js/forum/src/components/login-modal.js @@ -1,5 +1,6 @@ import Component from 'flarum/component'; 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 icon from 'flarum/helpers/icon'; @@ -34,7 +35,7 @@ export default class LoginModal extends Component { ]) ]), m('div.modal-footer', [ - m('p.forgot-password-link', m('a[href=javascript:;]', 'Forgot password?')), + m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => app.modal.show(new ForgotPasswordModal())}, 'Forgot password?')), m('p.sign-up-link', [ 'Don\'t have an account? ', m('a[href=javascript:;]', {onclick: () => app.modal.show(new SignupModal())}, 'Sign Up') diff --git a/framework/core/js/lib/components/modal.js b/framework/core/js/lib/components/modal.js index b8746c2c5..b4ee6e9b7 100644 --- a/framework/core/js/lib/components/modal.js +++ b/framework/core/js/lib/components/modal.js @@ -20,6 +20,7 @@ export default class Modal extends Component { this.component = component; m.redraw(true); this.$().modal('show'); + this.ready(); } close() { diff --git a/framework/core/migrations/2015_02_24_000000_create_reset_tokens_table.php b/framework/core/migrations/2015_02_24_000000_create_reset_tokens_table.php new file mode 100644 index 000000000..63c2c5b5d --- /dev/null +++ b/framework/core/migrations/2015_02_24_000000_create_reset_tokens_table.php @@ -0,0 +1,30 @@ +string('id'); + $table->integer('user_id')->unsigned(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('reset_tokens'); + } +} diff --git a/framework/core/migrations/2015_02_24_000000_create_users_table.php b/framework/core/migrations/2015_02_24_000000_create_users_table.php index e6f01ae1c..d44d5ab08 100644 --- a/framework/core/migrations/2015_02_24_000000_create_users_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_users_table.php @@ -14,7 +14,6 @@ class CreateUsersTable extends Migration public function up() { Schema::create('users', function (Blueprint $table) { - $table->increments('id'); $table->string('username', 100)->unique(); $table->string('email', 150)->unique(); diff --git a/framework/core/src/Api/Actions/JsonApiAction.php b/framework/core/src/Api/Actions/JsonApiAction.php index 95a790999..4f3ea10a4 100644 --- a/framework/core/src/Api/Actions/JsonApiAction.php +++ b/framework/core/src/Api/Actions/JsonApiAction.php @@ -3,6 +3,8 @@ use Closure; use Flarum\Api\Request; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Flarum\Core\Exceptions\ValidationFailureException; use Flarum\Core\Exceptions\PermissionDeniedException; @@ -29,7 +31,9 @@ abstract class JsonApiAction implements ActionInterface } return new JsonResponse(['errors' => $errors], 422); } catch (PermissionDeniedException $e) { - return new JsonResponse(null, 401); + return new Response(null, 401); + } catch (ModelNotFoundException $e) { + return new Response(null, 404); } } diff --git a/framework/core/src/Api/Actions/Users/ForgotAction.php b/framework/core/src/Api/Actions/Users/ForgotAction.php new file mode 100644 index 000000000..41b0cd742 --- /dev/null +++ b/framework/core/src/Api/Actions/Users/ForgotAction.php @@ -0,0 +1,38 @@ +users = $users; + $this->bus = $bus; + } + + /** + * Log in and return a token. + * + * @param \Flarum\Api\Request $request + * @return \Flarum\Api\Response + */ + public function respond(Request $request) + { + $email = $request->get('email'); + + $this->bus->dispatch( + new RequestPasswordResetCommand($email) + ); + + return new Response; + } +} diff --git a/framework/core/src/Api/routes.php b/framework/core/src/Api/routes.php index 16c1d47ab..f0b192af0 100644 --- a/framework/core/src/Api/routes.php +++ b/framework/core/src/Api/routes.php @@ -37,6 +37,12 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit 'uses' => $action('Flarum\Api\Actions\TokenAction') ]); + // Send forgot password email + Route::post('forgot', [ + 'as' => 'flarum.api.forgot', + 'uses' => $action('Flarum\Api\Actions\Users\ForgotAction') + ]); + /* |-------------------------------------------------------------------------- | Users @@ -73,11 +79,13 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit 'uses' => $action('Flarum\Api\Actions\Users\DeleteAction') ]); + // Upload avatar Route::post('users/{id}/avatar', [ 'as' => 'flarum.api.users.avatar.upload', 'uses' => $action('Flarum\Api\Actions\Users\UploadAvatarAction') ]); + // Remove avatar Route::delete('users/{id}/avatar', [ 'as' => 'flarum.api.users.avatar.delete', 'uses' => $action('Flarum\Api\Actions\Users\DeleteAvatarAction') diff --git a/framework/core/src/Core/Commands/RequestPasswordResetCommand.php b/framework/core/src/Core/Commands/RequestPasswordResetCommand.php new file mode 100644 index 000000000..448dc2eea --- /dev/null +++ b/framework/core/src/Core/Commands/RequestPasswordResetCommand.php @@ -0,0 +1,11 @@ +email = $email; + } +} diff --git a/framework/core/src/Core/Handlers/Commands/RequestPasswordResetCommandHandler.php b/framework/core/src/Core/Handlers/Commands/RequestPasswordResetCommandHandler.php new file mode 100644 index 000000000..ad636d8f2 --- /dev/null +++ b/framework/core/src/Core/Handlers/Commands/RequestPasswordResetCommandHandler.php @@ -0,0 +1,52 @@ +users = $users; + $this->mailer = $mailer; + } + + public function handle(RequestPasswordResetCommand $command) + { + $user = $this->users->findByEmail($command->email); + + if (! $user) { + throw new ModelNotFoundException; + } + + $token = ResetToken::generate($user->id); + $token->save(); + + $data = [ + 'username' => $user->username, + 'url' => route('flarum.forum.resetPassword', ['token' => $token->id]) + ]; + + $this->mailer->send(['text' => 'flarum::emails.reset'], $data, function ($message) use ($user) { + $message->to($user->email); + $message->subject('Reset Your Password'); + }); + + return $user; + } +} diff --git a/framework/core/src/Core/Models/ResetToken.php b/framework/core/src/Core/Models/ResetToken.php new file mode 100644 index 000000000..964f2799d --- /dev/null +++ b/framework/core/src/Core/Models/ResetToken.php @@ -0,0 +1,44 @@ +id = str_random(40); + $token->user_id = $userId; + + 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/framework/core/src/Core/Repositories/EloquentUserRepository.php b/framework/core/src/Core/Repositories/EloquentUserRepository.php index a5bd0a193..ac584950f 100644 --- a/framework/core/src/Core/Repositories/EloquentUserRepository.php +++ b/framework/core/src/Core/Repositories/EloquentUserRepository.php @@ -45,6 +45,17 @@ class EloquentUserRepository implements UserRepositoryInterface return User::where($field, $identification)->first(); } + /** + * Find a user by email. + * + * @param string $email + * @return \Flarum\Core\Models\User|null + */ + public function findByEmail($email) + { + return User::where('email', $email)->first(); + } + /** * Get the ID of a user with the given username. * diff --git a/framework/core/src/Core/Repositories/UserRepositoryInterface.php b/framework/core/src/Core/Repositories/UserRepositoryInterface.php index e3a0cb752..99c9710b1 100644 --- a/framework/core/src/Core/Repositories/UserRepositoryInterface.php +++ b/framework/core/src/Core/Repositories/UserRepositoryInterface.php @@ -31,6 +31,14 @@ interface UserRepositoryInterface */ public function findByIdentification($identification); + /** + * Find a user by email. + * + * @param string $email + * @return \Flarum\Core\Models\User|null + */ + public function findByEmail($email); + /** * Get the ID of a user with the given username. * diff --git a/framework/core/src/Forum/Actions/ResetPasswordAction.php b/framework/core/src/Forum/Actions/ResetPasswordAction.php new file mode 100644 index 000000000..7344c4128 --- /dev/null +++ b/framework/core/src/Forum/Actions/ResetPasswordAction.php @@ -0,0 +1,16 @@ +with('token', $token->id); + } +} diff --git a/framework/core/src/Forum/Actions/SavePasswordAction.php b/framework/core/src/Forum/Actions/SavePasswordAction.php new file mode 100644 index 000000000..137a9eddc --- /dev/null +++ b/framework/core/src/Forum/Actions/SavePasswordAction.php @@ -0,0 +1,28 @@ +get('token')); + + $password = $request->get('password'); + $confirmation = $request->get('password_confirmation'); + + if (! $password || $password !== $confirmation) { + return redirect()->back(); + } + + $this->dispatch( + new EditUserCommand($token->user_id, $token->user, ['password' => $password]) + ); + + $token->delete(); + + return redirect(''); + } +} diff --git a/framework/core/src/Forum/routes.php b/framework/core/src/Forum/routes.php index 3627a973d..19d4ed31f 100755 --- a/framework/core/src/Forum/routes.php +++ b/framework/core/src/Forum/routes.php @@ -32,3 +32,13 @@ Route::get('confirm/{id}/{token}', [ 'as' => 'flarum.forum.confirm', 'uses' => $action('Flarum\Forum\Actions\ConfirmAction') ]); + +Route::get('reset/{token}', [ + 'as' => 'flarum.forum.resetPassword', + 'uses' => $action('Flarum\Forum\Actions\ResetPasswordAction') +]); + +Route::post('reset', [ + 'as' => 'flarum.forum.savePassword', + 'uses' => $action('Flarum\Forum\Actions\SavePasswordAction') +]); diff --git a/framework/core/views/emails/reset.blade.php b/framework/core/views/emails/reset.blade.php new file mode 100644 index 000000000..75b2bcb2d --- /dev/null +++ b/framework/core/views/emails/reset.blade.php @@ -0,0 +1,3 @@ +Hey {{ $username }}! + +Click here to reset your password: {{ $url }} diff --git a/framework/core/views/reset.blade.php b/framework/core/views/reset.blade.php new file mode 100644 index 000000000..c8bfdc72c --- /dev/null +++ b/framework/core/views/reset.blade.php @@ -0,0 +1,43 @@ + + + + + + Reset Your Password + + + + + +

Reset Your Password

+ + @if (count($errors) > 0) +
+ Whoops! There were some problems with your input.

+ +
+ @endif + +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+
+ +