Merge branch 'develop' into dependabot/npm_and_yarn/develop/i18next-chained-backend-5.0.0

This commit is contained in:
mergify[bot]
2026-01-19 18:37:08 +00:00
committed by GitHub
5 changed files with 101 additions and 73 deletions

View File

@@ -1,8 +1,7 @@
<?php
/*
* EnabledMFA.php
* Copyright (c) 2024 james@firefly-iii.org.
* UserHasDisabledMFA.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -17,19 +16,18 @@
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Security;
namespace FireflyIII\Events\Security\User;
use FireflyIII\Events\Event;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Queue\SerializesModels;
use InvalidArgumentException;
class DisabledMFA extends Event
class UserHasDisabledMFA extends Event
{
use SerializesModels;
@@ -39,6 +37,8 @@ class DisabledMFA extends Event
{
if ($user instanceof User) {
$this->user = $user;
return;
}
throw new InvalidArgumentException('User must be an instance of User.');
}
}

View File

@@ -25,22 +25,20 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events\Security;
use Exception;
use FireflyIII\Events\Security\DisabledMFA;
use FireflyIII\Events\Security\EnabledMFA;
use FireflyIII\Events\Security\MFABackupFewLeft;
use FireflyIII\Events\Security\MFABackupNoLeft;
use FireflyIII\Events\Security\MFAManyFailedAttempts;
use FireflyIII\Events\Security\MFANewBackupCodes;
use FireflyIII\Events\Security\MFAUsedBackupCode;
use FireflyIII\Notifications\Security\DisabledMFANotification;
use FireflyIII\Notifications\Security\EnabledMFANotification;
use FireflyIII\Notifications\Security\MFABackupFewLeftNotification;
use FireflyIII\Notifications\Security\MFABackupNoLeftNotification;
use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification;
use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification;
use FireflyIII\Notifications\Security\NewBackupCodesNotification;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class MFAHandler
{
@@ -95,31 +93,6 @@ class MFAHandler
}
}
public function sendMFADisabledMail(DisabledMFA $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new DisabledMFANotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
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')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
public function sendMFAEnabledMail(EnabledMFA $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));

View File

@@ -24,16 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Profile;
use FireflyIII\Support\Facades\Preferences;
use Carbon\Carbon;
use FireflyIII\Events\Security\DisabledMFA;
use FireflyIII\Events\Security\EnabledMFA;
use FireflyIII\Events\Security\MFANewBackupCodes;
use FireflyIII\Events\Security\User\UserHasDisabledMFA;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Http\Requests\ExistingTokenFormRequest;
use FireflyIII\Http\Requests\TokenFormRequest;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
@@ -71,7 +71,7 @@ class MfaController extends Controller
$this->middleware(
static function ($request, $next) {
app('view')->share('title', (string) trans('firefly.profile'));
app('view')->share('title', (string)trans('firefly.profile'));
app('view')->share('mainTitleIcon', 'fa-user');
return $next($request);
@@ -85,7 +85,7 @@ class MfaController extends Controller
}
public function backupCodes(Request $request): Factory|RedirectResponse|View
public function backupCodes(Request $request): Factory | RedirectResponse | View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
@@ -102,14 +102,14 @@ class MfaController extends Controller
return view('profile.mfa.backup-codes-intro');
}
public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View
public function backupCodesPost(ExistingTokenFormRequest $request): Redirector | RedirectResponse | View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$enabledMFA = null !== auth()->user()->mfa_secret;
$enabledMFA = null !== auth()->user()->mfa_secret;
if (false === $enabledMFA) {
request()->session()->flash('info', trans('firefly.mfa_not_enabled'));
@@ -118,18 +118,17 @@ class MfaController extends Controller
// generate recovery codes:
$recovery = app(Recovery::class);
$recoveryCodes = $recovery->lowercase()
->setCount(8) // Generate 8 codes
->setBlocks(2) // Every code must have 2 blocks
->setChars(6) // Each block must have 6 chars
->toArray()
;
->setCount(8) // Generate 8 codes
->setBlocks(2) // Every code must have 2 blocks
->setChars(6) // Each block must have 6 chars
->toArray();
$codes = implode("\r\n", $recoveryCodes);
Preferences::set('mfa_recovery', $recoveryCodes);
Preferences::mark();
// send user notification.
$user = auth()->user();
$user = auth()->user();
Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email));
event(new MFANewBackupCodes($user));
@@ -137,20 +136,20 @@ class MfaController extends Controller
}
public function disableMFA(Request $request): Factory|RedirectResponse|View
public function disableMFA(Request $request): Factory | RedirectResponse | View
{
if (!$this->internalAuth) {
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$enabledMFA = null !== auth()->user()->mfa_secret;
$enabledMFA = null !== auth()->user()->mfa_secret;
if (false === $enabledMFA) {
request()->session()->flash('info', trans('firefly.mfa_already_disabled'));
return redirect(route('profile.index'));
}
$subTitle = (string) trans('firefly.mfa_index_title');
$subTitle = (string)trans('firefly.mfa_index_title');
$subTitleIcon = 'fa-calculator';
return view('profile.mfa.disable-mfa')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]);
@@ -159,7 +158,7 @@ class MfaController extends Controller
/**
* Delete 2FA routine.
*/
public function disableMFAPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse
public function disableMFAPost(ExistingTokenFormRequest $request): Redirector | RedirectResponse
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
@@ -171,15 +170,15 @@ class MfaController extends Controller
$repository = app(UserRepositoryInterface::class);
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
Preferences::delete('temp-mfa-secret');
Preferences::delete('temp-mfa-codes');
$repository->setMFACode($user, null);
Preferences::mark();
session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled'));
session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it'));
session()->flash('success', (string)trans('firefly.pref_two_factor_auth_disabled'));
session()->flash('info', (string)trans('firefly.pref_two_factor_auth_remove_it'));
// also logout current 2FA tokens.
$cookieName = config('google2fa.cookie_name', 'google2fa_token');
@@ -187,7 +186,7 @@ class MfaController extends Controller
// send user notification.
Log::channel('audit')->info(sprintf('User "%s" has disabled MFA', $user->email));
event(new DisabledMFA($user));
event(new UserHasDisabledMFA($user));
return redirect(route('profile.index'));
}
@@ -195,7 +194,7 @@ class MfaController extends Controller
/**
* Enable 2FA screen.
*/
public function enableMFA(Request $request): Redirector|RedirectResponse|View
public function enableMFA(Request $request): Redirector | RedirectResponse | View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
@@ -210,14 +209,14 @@ class MfaController extends Controller
// If FF3 already has a secret, just set the two-factor auth enabled to 1,
// and let the user continue with the existing secret.
if ($enabledMFA) {
session()->flash('info', (string) trans('firefly.2fa_already_enabled'));
session()->flash('info', (string)trans('firefly.2fa_already_enabled'));
return redirect(route('profile.index'));
}
$domain = $this->getDomain();
$secret = Google2FA::generateSecretKey();
$image = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret);
$domain = $this->getDomain();
$secret = Google2FA::generateSecretKey();
$image = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret);
Preferences::set('temp-mfa-secret', $secret);
@@ -232,7 +231,7 @@ class MfaController extends Controller
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function enableMFAPost(TokenFormRequest $request): Redirector|RedirectResponse
public function enableMFAPost(TokenFormRequest $request): Redirector | RedirectResponse
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
@@ -241,10 +240,10 @@ class MfaController extends Controller
}
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
// verify password.
$password = $request->get('password');
$password = $request->get('password');
if (!auth()->validate(['email' => $user->email, 'password' => $password])) {
session()->flash('error', 'Bad user pw, no MFA for you!');
@@ -257,17 +256,17 @@ class MfaController extends Controller
if (is_array($secret)) {
$secret = null;
}
$secret = (string) $secret;
$secret = (string)$secret;
$repository->setMFACode($user, $secret);
Preferences::delete('temp-mfa-secret');
session()->flash('success', (string) trans('firefly.saved_preferences'));
session()->flash('success', (string)trans('firefly.saved_preferences'));
Preferences::mark();
// also save the code so replay attack is prevented.
$mfaCode = $request->get('code');
$mfaCode = $request->get('code');
$this->addToMFAHistory($mfaCode);
// make sure MFA is logged out.
@@ -327,7 +326,7 @@ class MfaController extends Controller
Preferences::set('mfa_history', $newHistory);
}
public function index(): Factory|RedirectResponse|View
public function index(): Factory | RedirectResponse | View
{
if (!$this->internalAuth) {
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
@@ -335,7 +334,7 @@ class MfaController extends Controller
return redirect(route('profile.index'));
}
$subTitle = (string) trans('firefly.mfa_index_title');
$subTitle = (string)trans('firefly.mfa_index_title');
$subTitleIcon = 'fa-calculator';
$enabledMFA = null !== auth()->user()->mfa_secret;

View File

@@ -0,0 +1,57 @@
<?php
/*
* NotifiesUserAboutDisabledMFA.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasDisabledMFA;
use FireflyIII\Notifications\Security\DisabledMFANotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutDisabledMFA implements ShouldQueue
{
public function handle(UserHasDisabledMFA $event): void {
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new DisabledMFANotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
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')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
}

View File

@@ -35,7 +35,6 @@ use FireflyIII\Events\RequestedNewPassword;
use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Events\Security\DisabledMFA;
use FireflyIII\Events\Security\EnabledMFA;
use FireflyIII\Events\Security\MFABackupFewLeft;
use FireflyIII\Events\Security\MFABackupNoLeft;
@@ -186,9 +185,9 @@ class EventServiceProvider extends ServiceProvider
EnabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
],
DisabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
],
// DisabledMFA::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
// ],
MFANewBackupCodes::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
],