diff --git a/app/Events/Security/UnknownUserAttemptedLogin.php b/app/Events/Security/UnknownUserAttemptedLogin.php new file mode 100644 index 0000000000..4f49d63d2f --- /dev/null +++ b/app/Events/Security/UnknownUserAttemptedLogin.php @@ -0,0 +1,39 @@ +address = $address; + } +} diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php index 75c3e5f937..809bfaad3d 100644 --- a/app/Handlers/Events/AdminEventHandler.php +++ b/app/Handlers/Events/AdminEventHandler.php @@ -25,7 +25,9 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\NewVersionAvailable; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; use FireflyIII\Events\Test\TestNotificationChannel; +use FireflyIII\Notifications\Admin\UnknownUserLoginAttempt; use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\VersionCheckResult; use FireflyIII\Notifications\Notifiables\OwnerNotifiable; @@ -41,6 +43,28 @@ use Illuminate\Support\Facades\Notification; */ class AdminEventHandler { + public function sendLoginAttemptNotification(UnknownUserAttemptedLogin $event): void { + try { + $owner = new OwnerNotifiable(); + Notification::send($owner, new UnknownUserLoginAttempt($event->address)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + + public function sendInvitationNotification(InvitationCreated $event): void { $sendMail = app('fireflyconfig')->get('notification_invite_created', true)->data; diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index fa2b6c922c..f7640be2cd 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -25,9 +25,12 @@ namespace FireflyIII\Http\Controllers\Auth; use Cookie; use FireflyIII\Events\ActuallyLoggedIn; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\Providers\RouteServiceProvider; +use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; @@ -57,6 +60,7 @@ class LoginController extends Controller * Where to redirect users after login. */ protected string $redirectTo = RouteServiceProvider::HOME; + private UserRepositoryInterface $repository; private string $username; @@ -68,6 +72,7 @@ class LoginController extends Controller parent::__construct(); $this->username = 'email'; $this->middleware('guest')->except('logout'); + $this->repository = app(UserRepositoryInterface::class); } /** @@ -122,6 +127,11 @@ class LoginController extends Controller return $this->sendLoginResponse($request); } app('log')->warning('Login attempt failed.'); + $username = (string) $request->get($this->username()); + if(null === $this->repository->findByEmail($username)) { + // send event to owner. + event(new UnknownUserAttemptedLogin($username)); + } // Copied directly from AuthenticatesUsers, but with logging added: // If the login attempt was unsuccessful we will increment the number of attempts diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index bfc3762b08..3597c75351 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -25,7 +25,6 @@ namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use Carbon\Exceptions\InvalidFormatException; -use FireflyIII\Events\NewVersionAvailable; use FireflyIII\Events\RequestedVersionCheckStatus; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; @@ -63,8 +62,8 @@ class HomeController extends Controller */ public function dateRange(Request $request): JsonResponse { - $stringStart = ''; - $stringEnd = ''; + $stringStart = ''; + $stringEnd = ''; try { $stringStart = e((string) $request->get('start')); @@ -99,7 +98,7 @@ class HomeController extends Controller app('log')->debug('Range is now marked as "custom".'); } - $diff = $start->diffInDays($end, true) + 1; + $diff = $start->diffInDays($end, true) + 1; if ($diff > 366) { $request->session()->flash('warning', (string) trans('firefly.warning_much_data', ['days' => (int) $diff])); @@ -154,13 +153,13 @@ class HomeController extends Controller } /** @var Carbon $start */ - $start = session('start', today(config('app.timezone'))->startOfMonth()); + $start = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); - $accounts = $repository->getAccountsById($frontpageArray); - $today = today(config('app.timezone')); - $accounts = $accounts->sortBy('order'); // sort frontpage accounts by order + $end = session('end', today(config('app.timezone'))->endOfMonth()); + $accounts = $repository->getAccountsById($frontpageArray); + $today = today(config('app.timezone')); + $accounts = $accounts->sortBy('order'); // sort frontpage accounts by order app('log')->debug('Frontpage accounts are ', $frontpageArray); @@ -170,14 +169,14 @@ class HomeController extends Controller // collect groups for each transaction. foreach ($accounts as $account) { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->withAccountInformation()->setRange($start, $end)->setLimit(10)->setPage(1); $set = $collector->getExtractedJournals(); $transactions[] = ['transactions' => $set, 'account' => $account]; } /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); event(new RequestedVersionCheckStatus($user)); return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today', 'pageTitle')); @@ -188,11 +187,11 @@ class HomeController extends Controller $subTitle = (string) trans('firefly.welcome_back'); $pageTitle = (string) trans('firefly.main_dashboard_page_title'); - $start = session('start', today(config('app.timezone'))->startOfMonth()); - $end = session('end', today(config('app.timezone'))->endOfMonth()); + $start = session('start', today(config('app.timezone'))->startOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); event(new RequestedVersionCheckStatus($user)); return view('index', compact('subTitle', 'start', 'end', 'pageTitle')); diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index e326325471..513d0b43ee 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -111,9 +111,10 @@ class PreferencesController extends Controller // notification preferences (single value for each): $notifications = []; - die('fix the reference to the available notifications.'); - foreach (config('firefly.available_notifications') as $notification) { - $notifications[$notification] = app('preferences')->get(sprintf('notification_%s', $notification), true)->data; + foreach (config('notifications.notifications.user') as $key => $info) { + if($info['enabled']) { + $notifications[$key] = app('preferences')->get(sprintf('notification_%s', $key), true)->data; + } } ksort($languages); diff --git a/app/Notifications/Admin/UnknownUserLoginAttempt.php b/app/Notifications/Admin/UnknownUserLoginAttempt.php new file mode 100644 index 0000000000..98f718544f --- /dev/null +++ b/app/Notifications/Admin/UnknownUserLoginAttempt.php @@ -0,0 +1,128 @@ +address = $address; + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(OwnerNotifiable $notifiable) + { + return [ + ]; + } + + /** + * Get the mail representation of the notification. + * + * @return MailMessage + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(OwnerNotifiable $notifiable): MailMessage + { + return new MailMessage() + ->markdown('emails.owner.unknown-user', ['address' => $this->address]) + ->subject((string) trans('email.unknown_user_subject')); + } + + /** + * Get the Slack representation of the notification. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable): SlackMessage + { + return new SlackMessage()->content( + (string) trans('email.unknown_user_body', ['address' => $this->address]) + ); + } + + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.unknown_user_message', ['address' => $this->address])) + ->title((string) trans('email.unknown_user_subject')); + } + + public function toNtfy(OwnerNotifiable $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + + // overrule config. + config(['ntfy-notification-channel.server' => $settings['ntfy_server']]); + config(['ntfy-notification-channel.topic' => $settings['ntfy_topic']]); + + if ($settings['ntfy_auth']) { + // overrule auth as well. + config(['ntfy-notification-channel.authentication.enabled' => true]); + config(['ntfy-notification-channel.authentication.username' => $settings['ntfy_user']]); + config(['ntfy-notification-channel.authentication.password' => $settings['ntfy_pass']]); + } + + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.unknown_user_subject')); + $message->body((string) trans('email.unknown_user_message', ['address' => $this->address])); + + return $message; + } + + /** + * Get the notification's delivery channels. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return ReturnsAvailableChannels::returnChannels('owner'); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index bc09ed6fef..7722f48a22 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -47,6 +47,7 @@ use FireflyIII\Events\Security\MFABackupNoLeft; use FireflyIII\Events\Security\MFAManyFailedAttempts; use FireflyIII\Events\Security\MFANewBackupCodes; use FireflyIII\Events\Security\MFAUsedBackupCode; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; use FireflyIII\Events\StoredAccount; use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Events\Test\TestNotificationChannel; @@ -146,6 +147,9 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification', 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite', ], + UnknownUserAttemptedLogin::class => [ + 'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification', + ], // is a Transaction Journal related event. StoredTransactionGroup::class => [ diff --git a/config/notifications.php b/config/notifications.php index 65b53836dd..b1fa9ded3b 100644 --- a/config/notifications.php +++ b/config/notifications.php @@ -31,11 +31,18 @@ return [ ], 'notifications' => [ 'user' => [ - 'some_notification' => [ - 'enabled' => true, - 'email' => '', - 'slack' => '', - ], + 'bill_reminder' => ['enabled' => true, 'configurable' => true], + 'new_access_token' => ['enabled' => true, 'configurable' => true], + 'transaction_creation' => ['enabled' => true, 'configurable' => true], + 'user_login' => ['enabled' => true, 'configurable' => true], + 'rule_action_failures' => ['enabled' => true, 'configurable' => true], + 'new_password' => ['enabled' => true, 'configurable' => false], + 'enabled_mfa' => ['enabled' => true, 'configurable' => false], + 'disabled_mfa' => ['enabled' => true, 'configurable' => false], + 'few_left_mfa' => ['enabled' => true, 'configurable' => false], + 'no_left_mfa' => ['enabled' => true, 'configurable' => false], + 'many_failed_mfa' => ['enabled' => true, 'configurable' => false], + 'new_backup_codes' => ['enabled' => true, 'configurable' => false], ], 'owner' => [ //'invitation_created' => ['enabled' => true], @@ -45,6 +52,7 @@ return [ 'new_version' => ['enabled' => true], 'invite_created' => ['enabled' => true], 'invite_redeemed' => ['enabled' => true], + 'unknown_user_attempt' => ['enabled' => true], ], ], // // notifications diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index be6a1089c5..d6a92270cd 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -61,6 +61,11 @@ return [ 'access_token_created_explanation' => 'With this token, they can access **all** of your financial records through the Firefly III API.', 'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url', + // unknown user login attempt + 'unknown_user_subject' => 'An unknown user tried to log in', + 'unknown_user_body' => 'An unknown user tried to log in to Firefly III. The email address they used was ":address".', + 'unknown_user_message' => 'The email address they used was ":address".', + // registered 'registered_subject' => 'Welcome to Firefly III!', 'registered_subject_admin' => 'A new user has registered', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index ef4cf2575e..02884761c0 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1376,7 +1376,14 @@ return [ 'pref_notification_new_access_token' => 'Alert when a new API access token is created', 'pref_notification_transaction_creation' => 'Alert when a transaction is created automatically', 'pref_notification_user_login' => 'Alert when you login from a new location', - 'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (Slack or Discord only)', + 'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (not over email)', + 'pref_notification_new_password' => 'Your password changed', + 'pref_notification_enabled_mfa' => 'Multi factor authentication is enabled', + 'pref_notification_disabled_mfa' => 'Multi factor authentication is disabled', + 'pref_notification_few_left_mfa' => 'You have just a few backup codes left', + 'pref_notification_no_left_mfa' => 'You have no backup codes left', + 'pref_notification_many_failed_mfa' => 'The multi factor authentication check keeps failing', + 'pref_notification_new_backup_codes' => 'New backup codes have been generated', 'pref_notifications' => 'Notifications', 'pref_notifications_help' => 'Indicate if these are notifications you would like to get. Some notifications may contain sensitive financial information.', 'slack_webhook_url' => 'Slack Webhook URL', @@ -2492,6 +2499,7 @@ return [ 'owner_notification_check_new_version' => 'A new version is available', 'owner_notification_check_invite_created' => 'A user is invited to Firefly III', 'owner_notification_check_invite_redeemed' => 'A user invitation is redeemed', + 'owner_notification_check_unknown_user_attempt' => 'An unknown user tries to login', 'all_invited_users' => 'All invited users', 'save_notification_settings' => 'Save settings', 'notification_settings' => 'Settings for notifications', diff --git a/resources/views/emails/owner/unknown-user.blade.php b/resources/views/emails/owner/unknown-user.blade.php new file mode 100644 index 0000000000..3aaf0e8ade --- /dev/null +++ b/resources/views/emails/owner/unknown-user.blade.php @@ -0,0 +1,3 @@ +@component('mail::message') + {{ trans('email.unknown_user_body', ['address' => $address]) }} +@endcomponent