From 798c73394d9e7da76bf79d9f7b84a8b3f1358b2e Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 28 Aug 2020 21:58:03 +0200 Subject: [PATCH] Code for #3712 --- app/Events/DetectedNewIPAddress.php | 49 ++++++++++++ app/Handlers/Events/UserEventHandler.php | 77 ++++++++++++++++++- app/Mail/NewIPAddressWarningMail.php | 59 ++++++++++++++ app/Providers/EventServiceProvider.php | 5 ++ .../Authentication/RemoteUserGuard.php | 1 - resources/lang/en_US/email.php | 5 ++ resources/views/v1/emails/new-ip-html.twig | 14 ++++ resources/views/v1/emails/new-ip-text.twig | 8 ++ 8 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 app/Events/DetectedNewIPAddress.php create mode 100644 app/Mail/NewIPAddressWarningMail.php create mode 100644 resources/views/v1/emails/new-ip-html.twig create mode 100644 resources/views/v1/emails/new-ip-text.twig diff --git a/app/Events/DetectedNewIPAddress.php b/app/Events/DetectedNewIPAddress.php new file mode 100644 index 0000000000..7fcd36b701 --- /dev/null +++ b/app/Events/DetectedNewIPAddress.php @@ -0,0 +1,49 @@ +. + */ + +namespace FireflyIII\Events; + + +use FireflyIII\User; +use Illuminate\Queue\SerializesModels; + +/** + * Class DetectedNewIPAddress + */ +class DetectedNewIPAddress extends Event +{ + use SerializesModels; + + public string $ipAddress; + public User $user; + + /** + * Create a new event instance. This event is triggered when a new user registers. + * + * @param User $user + * @param string $ipAddress + */ + public function __construct(User $user, string $ipAddress) + { + $this->ipAddress = $ipAddress; + $this->user = $user; + } +} \ No newline at end of file diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 7a7941392d..1c0e1ea9bd 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -23,11 +23,14 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; +use Carbon\Carbon; use Exception; +use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\UserChangedEmail; use FireflyIII\Mail\ConfirmEmailChangeMail; +use FireflyIII\Mail\NewIPAddressWarningMail; use FireflyIII\Mail\RegisteredUser as RegisteredUserMail; use FireflyIII\Mail\RequestedNewPassword as RequestedNewPasswordMail; use FireflyIII\Mail\UndoEmailChangeMail; @@ -125,6 +128,78 @@ class UserEventHandler return true; } + /** + * @param Login $event + */ + public function storeUserIPAddress(Login $event): void + { + /** @var User $user */ + $user = $event->user; + /** @var array $preference */ + $preference = app('preferences')->getForUser($user, 'login_ip_history', [])->data; + $inArray = false; + $ip = request()->ip(); + Log::debug(sprintf('User logging in from IP address %s', $ip)); + + // update array if in array + foreach ($preference as $index => $row) { + if ($row['ip'] === $ip) { + Log::debug('Found IP in array, refresh time.'); + $preference[$index]['time'] = now(config('app.timezone'))->format('Y-m-d H:i:s'); + $inArray = true; + } + // clean up old entries (6 months) + $carbon = Carbon::createFromFormat('Y-m-d H:i:s', $preference[$index]['time']); + if ($carbon->diffInMonths(today()) > 6) { + Log::debug(sprintf('Entry for %s is very old, remove it.', $row['ip'])); + unset($preference[$index]); + } + } + // add to array if not the case: + if (false === $inArray) { + $preference[] = [ + 'ip' => $ip, + 'time' => now(config('app.timezone'))->format('Y-m-d H:i:s'), + 'notified' => false, + ]; + + + } + $preference = array_values($preference); + app('preferences')->setForUser($user, 'login_ip_history', $preference); + + if (false === $inArray) { + event(new DetectedNewIPAddress($user, $ip)); + } + + } + + /** + * @param DetectedNewIPAddress $event + */ + public function notifyNewIPAddress(DetectedNewIPAddress $event): void + { + $user = $event->user; + $email = $user->email; + $ipAddress = $event->ipAddress; + $list = app('preferences')->getForUser($user, 'login_ip_history', [])->data; + + /** @var array $entry */ + foreach ($list as $index => $entry) { + if (false === $entry['notified']) { + try { + Mail::to($email)->send(new NewIPAddressWarningMail($ipAddress)); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + Log::error($e->getMessage()); + } + } + $list[$index]['notified'] = true; + } + + app('preferences')->setForUser($user, 'login_ip_history', $list); + } + /** * Send email to confirm email change. * @@ -167,7 +242,7 @@ class UserEventHandler $ipAddress = $event->ipAddress; $token = app('preferences')->getForUser($user, 'email_change_undo_token', 'invalid'); $hashed = hash('sha256', sprintf('%s%s', (string) config('app.key'), $oldEmail)); - $uri = route('profile.undo-email-change', [$token->data,$hashed]); + $uri = route('profile.undo-email-change', [$token->data, $hashed]); try { Mail::to($oldEmail)->send(new UndoEmailChangeMail($newEmail, $oldEmail, $uri, $ipAddress)); // @codeCoverageIgnoreStart diff --git a/app/Mail/NewIPAddressWarningMail.php b/app/Mail/NewIPAddressWarningMail.php new file mode 100644 index 0000000000..b66790c851 --- /dev/null +++ b/app/Mail/NewIPAddressWarningMail.php @@ -0,0 +1,59 @@ +. + */ + +namespace FireflyIII\Mail; + + +use Illuminate\Bus\Queueable; +use Illuminate\Mail\Mailable; +use Illuminate\Queue\SerializesModels; +use Laravel\Passport\Client; + +/** + * Class NewIPAddressWarningMail + */ +class NewIPAddressWarningMail extends Mailable +{ + use Queueable, SerializesModels; + + public string $ipAddress; + + /** + * OAuthTokenCreatedMail constructor. + * + * @param string $ipAddress + */ + public function __construct(string $ipAddress) + { + $this->ipAddress = $ipAddress; + } + + /** + * Build the message. + * + * @return $this + */ + public function build(): self + { + return $this->view('emails.new-ip-html')->text('emails.new-ip-text') + ->subject((string) trans('email.login_from_new_ip')); + } +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a5af443779..46f142a9ae 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -24,6 +24,7 @@ namespace FireflyIII\Providers; use Exception; use FireflyIII\Events\AdminRequestedTestMessage; +use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedReportOnJournals; @@ -67,6 +68,10 @@ class EventServiceProvider extends ServiceProvider Login::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin', 'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish', + 'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress', + ], + DetectedNewIPAddress::class => [ + 'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress', ], RequestedVersionCheckStatus::class => [ 'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates', diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php index d93b98bcfa..491922881e 100644 --- a/app/Support/Authentication/RemoteUserGuard.php +++ b/app/Support/Authentication/RemoteUserGuard.php @@ -51,7 +51,6 @@ class RemoteUserGuard implements Guard */ public function __construct(UserProvider $provider, Application $app) { - Log::debug('Constructed RemoteUserGuard'); $this->application = $app; $this->provider = $provider; $this->user = null; diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index 45b7f40521..3dddd0efcc 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -33,6 +33,11 @@ return [ 'admin_test_subject' => 'A test message from your Firefly III installation', 'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.', + // new IP + 'login_from_new_ip' => 'You logged in from an unknown IP address.', + 'new_ip_body' => 'You logged in from a new, unknown IP address:', + 'new_ip_warning' => 'If you didn\'t login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!', + // access token created 'access_token_created_subject' => 'A new access token was created', 'access_token_created_body' => 'Somebody (hopefully you) just created a new Firefly III API Access Token for your user account.', diff --git a/resources/views/v1/emails/new-ip-html.twig b/resources/views/v1/emails/new-ip-html.twig new file mode 100644 index 0000000000..2d0b172226 --- /dev/null +++ b/resources/views/v1/emails/new-ip-html.twig @@ -0,0 +1,14 @@ +{% include 'emails.header-html' %} +

+ {{ trans('email.new_ip_body') }} +

+ +

+ {{ ipAddress }} +

+ +

+ {{ trans('email.new_ip_warning') }} +

+ +{% include 'emails.footer-html' %} diff --git a/resources/views/v1/emails/new-ip-text.twig b/resources/views/v1/emails/new-ip-text.twig new file mode 100644 index 0000000000..9eca2aa6d2 --- /dev/null +++ b/resources/views/v1/emails/new-ip-text.twig @@ -0,0 +1,8 @@ +{% include 'emails.header-text' %} +{{ trans('email.new_ip_body') }} + +{{ ipAddress }} + +{{ trans('email.new_ip_warning') }} + +{% include 'emails.footer-text' %}