New code for email address change in profile. See #857

This commit is contained in:
James Cole
2017-09-26 08:52:16 +02:00
parent ea1d543795
commit 91e96aa4b9
22 changed files with 612 additions and 9 deletions

View File

@@ -0,0 +1,51 @@
<?php
/**
* UserChangedEmail.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
/**
* Class UserChangedEmail
*
* @package FireflyIII\Events
*/
class UserChangedEmail extends Event
{
use SerializesModels;
/** @var string */
public $ipAddress;
/** @var string */
public $newEmail;
/** @var string */
public $oldEmail;
/** @var User */
public $user;
/**
* UserChangedEmail constructor.
*
* @param User $user
* @param string $newEmail
* @param string $oldEmail
* @param string $ipAddress
*/
public function __construct(User $user, string $newEmail, string $oldEmail, string $ipAddress)
{
$this->user = $user;
$this->ipAddress = $ipAddress;
$this->oldEmail = $oldEmail;
$this->newEmail = $newEmail;
}
}

View File

@@ -15,11 +15,15 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\RegisteredUser;
use FireflyIII\Events\RequestedNewPassword;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Mail\ConfirmEmailChangeMail;
use FireflyIII\Mail\RegisteredUser as RegisteredUserMail;
use FireflyIII\Mail\RequestedNewPassword as RequestedNewPasswordMail;
use FireflyIII\Mail\UndoEmailChangeMail;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Log;
use Mail;
use Preferences;
use Swift_TransportException;
/**
@@ -54,6 +58,54 @@ class UserEventHandler
return true;
}
/**
* @param UserChangedEmail $event
*
* @return bool
*/
public function sendEmailChangeConfirmMail(UserChangedEmail $event): bool
{
$newEmail = $event->newEmail;
$oldEmail = $event->oldEmail;
$user = $event->user;
$ipAddress = $event->ipAddress;
$token = Preferences::getForUser($user, 'email_change_confirm_token', 'invalid');
$uri = route('profile.confirm-email-change', [$token->data]);
try {
Mail::to($newEmail)->send(new ConfirmEmailChangeMail($newEmail, $oldEmail, $uri, $ipAddress));
// @codeCoverageIgnoreStart
} catch (Swift_TransportException $e) {
Log::error($e->getMessage());
}
// @codeCoverageIgnoreEnd
return true;
}
/**
* @param UserChangedEmail $event
*
* @return bool
*/
public function sendEmailChangeUndoMail(UserChangedEmail $event): bool
{
$newEmail = $event->newEmail;
$oldEmail = $event->oldEmail;
$user = $event->user;
$ipAddress = $event->ipAddress;
$token = Preferences::getForUser($user, 'email_change_undo_token', 'invalid');
$uri = route('profile.undo-email-change', [$token->data, hash('sha256', $oldEmail)]);
try {
Mail::to($oldEmail)->send(new UndoEmailChangeMail($newEmail, $oldEmail, $uri, $ipAddress));
// @codeCoverageIgnoreStart
} catch (Swift_TransportException $e) {
Log::error($e->getMessage());
}
// @codeCoverageIgnoreEnd
return true;
}
/**
* @param RequestedNewPassword $event
*

View File

@@ -14,6 +14,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Auth;
use FireflyConfig;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\User;
use Illuminate\Cookie\CookieJar;

View File

@@ -13,10 +13,15 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Auth;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Http\Middleware\IsLimitedUser;
use FireflyIII\Http\Requests\DeleteAccountFormRequest;
use FireflyIII\Http\Requests\EmailFormRequest;
use FireflyIII\Http\Requests\ProfileFormRequest;
use FireflyIII\Models\Preference;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Hash;
@@ -48,10 +53,23 @@ class ProfileController extends Controller
return $next($request);
}
);
$this->middleware(IsLimitedUser::class);
$this->middleware(IsLimitedUser::class)->except(['confirmEmailChange', 'undoEmailChange']);
}
/**
* @return View
*/
public function changeEmail()
{
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = strval(trans('firefly.change_your_email'));
$subTitleIcon = 'fa-envelope';
return view('profile.change-email', compact('title', 'subTitle', 'subTitleIcon', 'email'));
}
/**
* @return View
*/
@@ -64,6 +82,37 @@ class ProfileController extends Controller
return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon'));
}
/**
* @param string $token
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws FireflyException
*/
public function confirmEmailChange(string $token)
{
// find preference with this token value.
$set = Preferences::findByName('email_change_confirm_token');
$user = null;
/** @var Preference $preference */
foreach ($set as $preference) {
if ($preference->data === $token) {
$user = $preference->user;
}
}
// update user to clear blocked and blocked_code.
if (is_null($user)) {
throw new FireflyException('Invalid token.');
}
$user->blocked = 0;
$user->blocked_code = '';
$user->save();
// return to login.
Session::flash('success', strval(trans('firefly.login_with_new_email')));
return redirect(route('login'));
}
/**
* @return View
*/
@@ -95,6 +144,49 @@ class ProfileController extends Controller
return view('profile.index', compact('subTitle', 'userId', 'accessToken'));
}
/**
* @param EmailFormRequest $request
* @param UserRepositoryInterface $repository
*
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postChangeEmail(EmailFormRequest $request, UserRepositoryInterface $repository)
{
/** @var User $user */
$user = auth()->user();
$newEmail = $request->string('email');
$oldEmail = $user->email;
if ($newEmail === $user->email) {
Session::flash('error', strval(trans('firefly.email_not_changed')));
return redirect(route('profile.change-email'))->withInput();
}
$existing = $repository->findByEmail($newEmail);
if (!is_null($existing)) {
// force user logout.
$this->guard()->logout();
$request->session()->invalidate();
Session::flash('success', strval(trans('firefly.email_changed')));
return redirect(route('index'));
}
// now actually update user:
$repository->changeEmail($user, $newEmail);
// call event.
$ipAddress = $request->ip();
event(new UserChangedEmail($user, $newEmail, $oldEmail, $ipAddress));
// force user logout.
Auth::guard()->logout();
$request->session()->invalidate();
Session::flash('success', strval(trans('firefly.email_changed')));
return redirect(route('index'));
}
/**
* @param ProfileFormRequest $request
* @param UserRepositoryInterface $repository
@@ -160,6 +252,53 @@ class ProfileController extends Controller
return redirect(route('profile.index'));
}
/**
* @param string $token
* @param string $hash
*
* @throws FireflyException
*/
public function undoEmailChange(string $token, string $hash)
{
// find preference with this token value.
$set = Preferences::findByName('email_change_undo_token');
$user = null;
/** @var Preference $preference */
foreach ($set as $preference) {
if ($preference->data === $token) {
$user = $preference->user;
}
}
if (is_null($user)) {
throw new FireflyException('Invalid token.');
}
// found user.
// which email address to return to?
$set = Preferences::beginsWith($user, 'previous_email_');
$match = null;
foreach ($set as $entry) {
$hashed = hash('sha256', $entry->data);
if ($hashed === $hash) {
$match = $entry->data;
break;
}
}
if (is_null($match)) {
throw new FireflyException('Invalid token.');
}
// change user back
$user->email = $match;
$user->blocked = 0;
$user->blocked_code = '';
$user->save();
// return to login.
Session::flash('success', strval(trans('firefly.login_with_old_email')));
return redirect(route('login'));
}
/**
* @param User $user
* @param string $current

View File

@@ -44,8 +44,13 @@ class Authenticate
return redirect()->guest('login');
}
if (intval(auth()->user()->blocked) === 1) {
$message = strval(trans('firefly.block_account_logout'));
if (auth()->user()->blocked_code === 'email_changed') {
$message = strval(trans('firefly.email_changed_logout'));
}
Session::flash('logoutMessage', $message);
Auth::guard($guard)->logout();
Session::flash('logoutMessage', trans('firefly.block_account_logout'));
return redirect()->guest('login');
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* EmailFormRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
/**
* Class EmailFormRequest
*
*
* @package FireflyIII\Http\Requests
*/
class EmailFormRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged in users
return auth()->check();
}
/**
* @return array
*/
public function rules()
{
// fixed
return [
'email' => 'required|email',
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace FireflyIII\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ConfirmEmailChangeMail extends Mailable
{
use Queueable, SerializesModels;
/** @var string */
public $ipAddress;
/** @var string */
public $newEmail;
/** @var string */
public $oldEmail;
/** @var string */
public $uri;
/**
* ConfirmEmailChangeMail constructor.
*
* @param string $newEmail
* @param string $oldEmail
* @param string $uri
* @param string $ipAddress
*/
public function __construct(string $newEmail, string $oldEmail, string $uri, string $ipAddress)
{
$this->newEmail = $newEmail;
$this->oldEmail = $oldEmail;
$this->uri = $uri;
$this->ipAddress = $ipAddress;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('emails.confirm-email-change-html')->text('emails.confirm-email-change-text')
->subject('Your Firefly III email address has changed.');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace FireflyIII\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class UndoEmailChangeMail extends Mailable
{
use Queueable, SerializesModels;
/** @var string */
public $ipAddress;
/** @var string */
public $newEmail;
/** @var string */
public $oldEmail;
/** @var string */
public $uri;
/**
* UndoEmailChangeMail constructor.
*
* @param string $newEmail
* @param string $oldEmail
* @param string $uri
* @param string $ipAddress
*/
public function __construct(string $newEmail, string $oldEmail, string $uri, string $ipAddress)
{
$this->newEmail = $newEmail;
$this->oldEmail = $oldEmail;
$this->uri = $uri;
$this->ipAddress = $ipAddress;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('emails.undo-email-change-html')->text('emails.undo-email-change-text')
->subject('Your Firefly III email address has changed.');
}
}

View File

@@ -36,26 +36,35 @@ class EventServiceProvider extends ServiceProvider
*/
protected $listen
= [
// new event handlers:
'FireflyIII\Events\RegisteredUser' => // is a User related event.
// is a User related event.
'FireflyIII\Events\RegisteredUser' =>
[
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
],
'FireflyIII\Events\RequestedNewPassword' => [ // is a User related event.
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
// is a User related event.
'FireflyIII\Events\RequestedNewPassword' => [
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
],
'FireflyIII\Events\StoredTransactionJournal' => // is a Transaction Journal related event.
// is a User related event.
'FireflyIII\Events\UserChangedEmail' => [
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
],
// is a Transaction Journal related event.
'FireflyIII\Events\StoredTransactionJournal' =>
[
'FireflyIII\Handlers\Events\StoredJournalEventHandler@scanBills',
'FireflyIII\Handlers\Events\StoredJournalEventHandler@connectToPiggyBank',
'FireflyIII\Handlers\Events\StoredJournalEventHandler@processRules',
],
'FireflyIII\Events\UpdatedTransactionJournal' => // is a Transaction Journal related event.
// is a Transaction Journal related event.
'FireflyIII\Events\UpdatedTransactionJournal' =>
[
'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@scanBills',
'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@processRules',
],
];
/**

View File

@@ -52,6 +52,33 @@ class UserRepository implements UserRepositoryInterface
return true;
}
/**
* @param User $user
* @param string $newEmail
*
* @return bool
*/
public function changeEmail(User $user, string $newEmail): bool
{
$oldEmail = $user->email;
// save old email as pref
Preferences::setForUser($user, 'previous_email_latest', $oldEmail);
Preferences::setForUser($user, 'previous_email_' . date('Y-m-d-H-i-s'), $oldEmail);
// set undo and confirm token:
Preferences::setForUser($user, 'email_change_undo_token', strval(bin2hex(random_bytes(16))));
Preferences::setForUser($user, 'email_change_confirm_token', strval(bin2hex(random_bytes(16))));
// update user
$user->email = $newEmail;
$user->blocked = 1;
$user->blocked_code = 'email_changed';
$user->save();
return true;
}
/**
* @param User $user
* @param string $password
@@ -119,6 +146,16 @@ class UserRepository implements UserRepositoryInterface
return new User;
}
/**
* @param string $email
*
* @return User|null
*/
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
/**
* Return basic user information.
*

View File

@@ -42,6 +42,14 @@ interface UserRepositoryInterface
*/
public function attachRole(User $user, string $role): bool;
/**
* @param User $user
* @param string $newEmail
*
* @return bool
*/
public function changeEmail(User $user, string $newEmail): bool;
/**
* @param User $user
* @param string $password
@@ -80,6 +88,13 @@ interface UserRepositoryInterface
*/
public function find(int $userId): User;
/**
* @param string $email
*
* @return User|null
*/
public function findByEmail(string $email): ?User;
/**
* Return basic user information.
*

View File

@@ -16,6 +16,7 @@ namespace FireflyIII\Support;
use Cache;
use FireflyIII\Models\Preference;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Session;
/**
@@ -25,13 +26,26 @@ use Session;
*/
class Preferences
{
/**
* @param User $user
* @param string $search
*
* @return Collection
*/
public function beginsWith(User $user, string $search): Collection
{
$set = Preference::where('user_id', $user->id)->where('name', 'LIKE', $search . '%')->get();
return $set;
}
/**
* @param $name
*
* @return bool
* @throws \Exception
*/
public function delete($name): bool
public function delete(string $name): bool
{
$fullName = sprintf('preference%s%s', auth()->user()->id, $name);
if (Cache::has($fullName)) {
@@ -42,6 +56,18 @@ class Preferences
return true;
}
/**
* @param string $name
*
* @return Collection
*/
public function findByName(string $name): Collection
{
$set = Preference::where('name', $name)->get();
return $set;
}
/**
* @param $name
* @param null $default