diff --git a/app/Http/Middleware/RequireOperatorTwoFactor.php b/app/Http/Middleware/RequireOperatorTwoFactor.php new file mode 100644 index 0000000..69b1c56 --- /dev/null +++ b/app/Http/Middleware/RequireOperatorTwoFactor.php @@ -0,0 +1,40 @@ +user(); + + if ($operator === null || $operator->two_factor_confirmed_at !== null) { + return $next($request); + } + + // The one page that must stay reachable: where two-factor is enrolled. + if (\App\Support\AdminArea::routeIs('admin.settings') || $request->routeIs('admin.logout')) { + return $next($request); + } + + return redirect()->route('admin.settings') + ->with('status', __('admin_settings.two_factor_required')); + } +} diff --git a/app/Livewire/Admin/Settings.php b/app/Livewire/Admin/Settings.php index 9c8e86a..7636732 100644 --- a/app/Livewire/Admin/Settings.php +++ b/app/Livewire/Admin/Settings.php @@ -59,6 +59,7 @@ class Settings extends Component { $this->name = Auth::guard('operator')->user()->name; $this->email = Auth::guard('operator')->user()->email; + $this->requireTwoFactor = AppSettings::bool('console.require_2fa', false); } /** @@ -331,6 +332,29 @@ class Settings extends Component $this->dispatch('notify', message: __($on ? 'admin_settings.console_locked' : 'admin_settings.console_unlocked')); } + /** Whether two-factor is compulsory for every operator on the console. */ + public bool $requireTwoFactor = false; + + /** + * Make two-factor compulsory for every operator, or make it voluntary again. + * + * The lock-out guard, same shape as console.allowed_ips: the page that + * could switch this back off sits behind the switch. + */ + public function saveTwoFactorPolicy(): void + { + $this->authorize('site.manage'); + + if ($this->requireTwoFactor && Auth::guard('operator')->user()?->two_factor_confirmed_at === null) { + $this->addError('requireTwoFactor', __('admin_settings.two_factor_self_first')); + + return; + } + + AppSettings::set('console.require_2fa', $this->requireTwoFactor); + $this->dispatch('notify', message: __('admin_settings.saved')); + } + /** * Ask the host-side agent to update this installation. diff --git a/bootstrap/app.php b/bootstrap/app.php index e776c6a..1f3cff6 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -17,6 +17,7 @@ return Application::configure(basePath: dirname(__DIR__)) 'admin' => \App\Http\Middleware\EnsureAdmin::class, 'admin.host' => \App\Http\Middleware\RestrictAdminHost::class, 'customer.active' => \App\Http\Middleware\EnsureCustomerActive::class, + 'operator.2fa' => \App\Http\Middleware\RequireOperatorTwoFactor::class, ]); // Runs before the whole route stack (incl. `auth`), so /admin on a public diff --git a/lang/de/admin_settings.php b/lang/de/admin_settings.php index 94c9f34..324e0d8 100644 --- a/lang/de/admin_settings.php +++ b/lang/de/admin_settings.php @@ -77,6 +77,14 @@ return [ 'console_unlocked' => 'Einschränkung aufgehoben.', 'console_lock_refused' => 'Nicht eingeschränkt: :ip steht auf keiner Liste, Sie hätten sich damit ausgesperrt.', + // Two-factor policy — console.require_2fa. + 'saved' => 'Gespeichert.', + 'two_factor_title' => 'Zwei-Faktor-Pflicht', + 'two_factor_body' => 'Verbindlich schaltet Zwei-Faktor für jeden Operator scharf — ohne bestätigte Zwei-Faktor-Bestätigung bleibt nur noch diese Seite erreichbar.', + 'two_factor_require' => 'Für alle verbindlich machen', + 'two_factor_required' => 'Zwei-Faktor ist für die Konsole verbindlich. Bitte richten Sie sie hier ein.', + 'two_factor_self_first' => 'Richten Sie zuerst Ihre eigene Zwei-Faktor-Bestätigung ein — sonst sperren Sie sich mit diesem Schalter selbst aus.', + 'update_title' => 'Version & Aktualisierung', 'update_version' => 'Läuft', 'update_source' => 'Quelle', diff --git a/lang/en/admin_settings.php b/lang/en/admin_settings.php index 2c7b2c4..eddad23 100644 --- a/lang/en/admin_settings.php +++ b/lang/en/admin_settings.php @@ -77,6 +77,14 @@ return [ 'console_unlocked' => 'Restriction lifted.', 'console_lock_refused' => 'Not restricted: :ip is on no list, so this would have locked you out.', + // Two-factor policy — console.require_2fa. + 'saved' => 'Saved.', + 'two_factor_title' => 'Two-factor requirement', + 'two_factor_body' => 'Making it compulsory enforces two-factor for every operator — without a confirmed two-factor setup, only this page stays reachable.', + 'two_factor_require' => 'Make it compulsory for everyone', + 'two_factor_required' => 'Two-factor is compulsory for the console. Please set it up here.', + 'two_factor_self_first' => 'Set up your own two-factor confirmation first — otherwise this switch locks you out yourself.', + 'update_title' => 'Version & update', 'update_version' => 'Running', 'update_source' => 'Source', diff --git a/resources/views/livewire/admin/settings.blade.php b/resources/views/livewire/admin/settings.blade.php index 296cd21..782e625 100644 --- a/resources/views/livewire/admin/settings.blade.php +++ b/resources/views/livewire/admin/settings.blade.php @@ -254,6 +254,27 @@ @endif + {{-- Two-factor policy — voluntary by default, the Owner can make it + compulsory. The switch refuses to turn on while the operator flipping + it has no confirmed two-factor themselves: the page that would turn it + back off sits behind the switch. --}} + @if ($canManageSite) +
{{ __('admin_settings.two_factor_body') }}
+ + + @error('requireTwoFactor'){{ $message }}
@enderror +