From 5f6ad27329ee6c8e16a97216710c0d1ebf0075bb Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 23:23:25 +0200 Subject: [PATCH] refactor(settings): split into per-tab nested components (Profile, Security) + tab hosts for Users/Sessions/Email Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Livewire/Settings/Email.php | 13 +++ app/Livewire/Settings/Index.php | 90 +----------------- app/Livewire/Settings/Profile.php | 69 ++++++++++++++ app/Livewire/Settings/Security.php | 49 ++++++++++ app/Livewire/Settings/Sessions.php | 13 +++ app/Livewire/Settings/Users.php | 13 +++ lang/de/settings.php | 12 +++ lang/en/settings.php | 12 +++ resources/views/components/icon.blade.php | 1 + .../views/livewire/settings/email.blade.php | 5 + .../views/livewire/settings/index.blade.php | 95 ++----------------- .../views/livewire/settings/profile.blade.php | 55 +++++++++++ .../livewire/settings/security.blade.php | 34 +++++++ .../livewire/settings/sessions.blade.php | 5 + .../views/livewire/settings/users.blade.php | 5 + .../Feature/SettingsFactorManagementTest.php | 6 +- 16 files changed, 300 insertions(+), 177 deletions(-) create mode 100644 app/Livewire/Settings/Email.php create mode 100644 app/Livewire/Settings/Profile.php create mode 100644 app/Livewire/Settings/Security.php create mode 100644 app/Livewire/Settings/Sessions.php create mode 100644 app/Livewire/Settings/Users.php create mode 100644 resources/views/livewire/settings/email.blade.php create mode 100644 resources/views/livewire/settings/profile.blade.php create mode 100644 resources/views/livewire/settings/security.blade.php create mode 100644 resources/views/livewire/settings/sessions.blade.php create mode 100644 resources/views/livewire/settings/users.blade.php diff --git a/app/Livewire/Settings/Email.php b/app/Livewire/Settings/Email.php new file mode 100644 index 0000000..67a5e94 --- /dev/null +++ b/app/Livewire/Settings/Email.php @@ -0,0 +1,13 @@ +name = Auth::user()->name; - $this->email = Auth::user()->email; - if (session('open_recovery_modal')) { $this->tab = 'security'; $this->openRecoveryModal = true; } } - public function updateProfile(): void - { - $data = $this->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore(Auth::id())], - ]); - - Auth::user()->update($data); - $this->audit('profile.update', $data['email']); - $this->dispatch('notify', message: __('settings.profile_saved')); - } - - public function updatePassword(): void - { - $this->validate([ - 'current_password' => ['required', 'current_password'], - 'password' => ['required', 'confirmed', Password::min(10)], - ]); - - Auth::user()->update(['password' => $this->password, 'must_change_password' => false]); - $this->reset('current_password', 'password', 'password_confirmation'); - $this->audit('password.change', Auth::user()->email); - $this->dispatch('notify', message: __('settings.password_changed')); - } - - public function confirmDisableTwoFactor(): void - { - $this->dispatch('openModal', - component: 'modals.confirm-action', - arguments: [ - 'heading' => __('settings.disable_2fa_heading'), - 'body' => __('settings.disable_2fa_body'), - 'confirmLabel' => __('common.disable'), - 'danger' => true, - 'icon' => 'shield', - 'auditAction' => 'two_factor.disable', - 'auditTarget' => Auth::user()?->email, - 'event' => 'twoFactorDisabled', - 'notify' => __('settings.disable_2fa_notify'), - ], - ); - } - - #[On('twoFactorDisabled')] - public function disableTwoFactor(): void - { - // Remove the TOTP factor only — security keys are managed on their own card. If no - // factor remains afterwards, the backup codes are dropped too. - Auth::user()->forceFill([ - 'two_factor_secret' => null, - 'two_factor_confirmed_at' => null, - ])->save(); - - Auth::user()->resetIfNoFactor(); - } - - private function audit(string $action, string $target): void - { - AuditEvent::create([ - 'user_id' => Auth::id(), - 'actor' => Auth::user()->name, - 'action' => $action, - 'target' => $target, - 'ip' => request()->ip(), - ]); - } - public function render() { - return view('livewire.settings.index', [ - 'twoFactorEnabled' => Auth::user()->hasTwoFactorEnabled(), - 'hasTotp' => Auth::user()->hasTotp(), - ])->title(__('settings.title')); + return view('livewire.settings.index')->title(__('settings.title')); } } diff --git a/app/Livewire/Settings/Profile.php b/app/Livewire/Settings/Profile.php new file mode 100644 index 0000000..d61cc4d --- /dev/null +++ b/app/Livewire/Settings/Profile.php @@ -0,0 +1,69 @@ +name = Auth::user()->name; + $this->email = Auth::user()->email; + } + + public function updateProfile(): void + { + $data = $this->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore(Auth::id())], + ]); + + Auth::user()->update($data); + $this->audit('profile.update', $data['email']); + $this->dispatch('notify', message: __('settings.profile_saved')); + } + + public function updatePassword(): void + { + $this->validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', 'confirmed', Password::min(10)], + ]); + + Auth::user()->update(['password' => $this->password, 'must_change_password' => false]); + $this->reset('current_password', 'password', 'password_confirmation'); + $this->audit('password.change', Auth::user()->email); + $this->dispatch('notify', message: __('settings.password_changed')); + } + + private function audit(string $action, string $target): void + { + AuditEvent::create([ + 'user_id' => Auth::id(), + 'actor' => Auth::user()->name, + 'action' => $action, + 'target' => $target, + 'ip' => request()->ip(), + ]); + } + + public function render() + { + return view('livewire.settings.profile'); + } +} diff --git a/app/Livewire/Settings/Security.php b/app/Livewire/Settings/Security.php new file mode 100644 index 0000000..9e4c52e --- /dev/null +++ b/app/Livewire/Settings/Security.php @@ -0,0 +1,49 @@ +dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => __('settings.disable_2fa_heading'), + 'body' => __('settings.disable_2fa_body'), + 'confirmLabel' => __('common.disable'), + 'danger' => true, + 'icon' => 'shield', + 'auditAction' => 'two_factor.disable', + 'auditTarget' => Auth::user()?->email, + 'event' => 'twoFactorDisabled', + 'notify' => __('settings.disable_2fa_notify'), + ], + ); + } + + #[On('twoFactorDisabled')] + public function disableTwoFactor(): void + { + // Remove the TOTP factor only — security keys are managed on their own card. If no + // factor remains afterwards, the backup codes are dropped too. + Auth::user()->forceFill([ + 'two_factor_secret' => null, + 'two_factor_confirmed_at' => null, + ])->save(); + + Auth::user()->resetIfNoFactor(); + } + + public function render() + { + return view('livewire.settings.security', [ + 'twoFactorEnabled' => Auth::user()->hasTwoFactorEnabled(), + 'hasTotp' => Auth::user()->hasTotp(), + ]); + } +} diff --git a/app/Livewire/Settings/Sessions.php b/app/Livewire/Settings/Sessions.php new file mode 100644 index 0000000..a8b491c --- /dev/null +++ b/app/Livewire/Settings/Sessions.php @@ -0,0 +1,13 @@ + 'Profil', 'tab_security' => 'Sicherheit', + 'tab_users' => 'Benutzer', + 'tab_sessions' => 'Sitzungen', + 'tab_email' => 'E-Mail', // Profile panel 'profile_title' => 'Profil', @@ -46,6 +49,15 @@ return [ 'disable_2fa_body' => 'Den Authenticator (TOTP) als Faktor entfernen? Security-Keys bleiben bestehen.', 'disable_2fa_notify' => 'Authenticator entfernt.', + // Stub tabs (filled in 0.9.0) + 'coming_soon' => 'Wird in 0.9.0 ergänzt.', + 'users_title' => 'Benutzer', + 'users_subtitle' => 'Konten und Rollen verwalten', + 'sessions_title' => 'Sitzungen', + 'sessions_subtitle' => 'Aktive Anmeldungen', + 'email_title' => 'E-Mail', + 'email_subtitle' => 'SMTP und Benachrichtigungen', + // Page title 'title' => 'Einstellungen — Clusev', ]; diff --git a/lang/en/settings.php b/lang/en/settings.php index 8937ce5..56124a3 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -11,6 +11,9 @@ return [ // Section nav tabs 'tab_profile' => 'Profile', 'tab_security' => 'Security', + 'tab_users' => 'Users', + 'tab_sessions' => 'Sessions', + 'tab_email' => 'Email', // Profile panel 'profile_title' => 'Profile', @@ -46,6 +49,15 @@ return [ 'disable_2fa_body' => 'Remove the authenticator (TOTP) as a factor? Security keys are kept.', 'disable_2fa_notify' => 'Authenticator removed.', + // Stub tabs (filled in 0.9.0) + 'coming_soon' => 'Coming in 0.9.0.', + 'users_title' => 'Users', + 'users_subtitle' => 'Manage accounts and roles', + 'sessions_title' => 'Sessions', + 'sessions_subtitle' => 'Active sign-ins', + 'email_title' => 'Email', + 'email_subtitle' => 'SMTP and notifications', + // Page title 'title' => 'Settings — Clusev', ]; diff --git a/resources/views/components/icon.blade.php b/resources/views/components/icon.blade.php index 02524b9..2bb8c17 100644 --- a/resources/views/components/icon.blade.php +++ b/resources/views/components/icon.blade.php @@ -32,6 +32,7 @@ 'eye' => '', 'eye-off' => '', 'user-plus' => '', + 'mail' => '', ]; @endphp merge(['class' => $class]) }} viewBox="0 0 24 24" fill="none" stroke="currentColor" diff --git a/resources/views/livewire/settings/email.blade.php b/resources/views/livewire/settings/email.blade.php new file mode 100644 index 0000000..37b971b --- /dev/null +++ b/resources/views/livewire/settings/email.blade.php @@ -0,0 +1,5 @@ +
+ +

{{ __('settings.coming_soon') }}

+
+
diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index 8f59eac..c4fd6cf 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -1,12 +1,13 @@ @php $u = auth()->user(); $initials = strtoupper(mb_substr($u->name, 0, 2)); - $field = 'h-9 w-full rounded-md border border-line bg-inset px-3 font-sans text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none'; - $label = 'mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3'; - $err = 'mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline'; + $twoFactorEnabled = $u->hasTwoFactorEnabled(); $tabs = [ ['key' => 'profile', 'label' => __('settings.tab_profile'), 'icon' => 'settings'], ['key' => 'security', 'label' => __('settings.tab_security'), 'icon' => 'shield'], + ['key' => 'users', 'label' => __('settings.tab_users'), 'icon' => 'user-plus'], + ['key' => 'sessions', 'label' => __('settings.tab_sessions'), 'icon' => 'logout'], + ['key' => 'email', 'label' => __('settings.tab_email'), 'icon' => 'mail'], ]; @endphp @@ -49,88 +50,12 @@ {{-- Content --}} -
- @if ($tab === 'profile') - -
-
- - - @error('name')

{{ $message }}

@enderror -
-
- - - @error('email')

{{ $message }}

@enderror -
-
- - - {{ __('common.save') }} - -
-
-
- @else - -
-
- - - @error('current_password')

{{ $message }}

@enderror -
-
-
- - - @error('password')

{{ $message }}

@enderror -
-
- - -
-
-
- - - {{ __('settings.change_password') }} - -
-
-
- - - @unless ($twoFactorEnabled) -
- -

{{ __('settings.twofa_recommended') }}

-
- @endunless - -
-
- -
-

{{ $hasTotp ? __('settings.twofa_status_on') : __('settings.twofa_status_off') }}

-

{{ $hasTotp ? __('settings.twofa_hint_on') : __('settings.twofa_hint_off') }}

-
-
-
- @if ($twoFactorEnabled) - {{ __('auth.recovery_manage') }} - @endif - @if ($hasTotp) - {{ __('settings.twofa_remove_totp') }} - @else - - {{ __('settings.twofa_setup') }} - - @endif -
-
-
- - +
+ @if ($tab === 'profile') + @elseif ($tab === 'security') + @elseif ($tab === 'users') + @elseif ($tab === 'sessions') + @elseif ($tab === 'email') @endif
diff --git a/resources/views/livewire/settings/profile.blade.php b/resources/views/livewire/settings/profile.blade.php new file mode 100644 index 0000000..ed1185c --- /dev/null +++ b/resources/views/livewire/settings/profile.blade.php @@ -0,0 +1,55 @@ +@php + $field = 'h-9 w-full rounded-md border border-line bg-inset px-3 font-sans text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none'; + $label = 'mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3'; + $err = 'mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline'; +@endphp + +
+ +
+
+ + + @error('name')

{{ $message }}

@enderror +
+
+ + + @error('email')

{{ $message }}

@enderror +
+
+ + + {{ __('common.save') }} + +
+
+
+ + +
+
+ + + @error('current_password')

{{ $message }}

@enderror +
+
+
+ + + @error('password')

{{ $message }}

@enderror +
+
+ + +
+
+
+ + + {{ __('settings.change_password') }} + +
+
+
+
diff --git a/resources/views/livewire/settings/security.blade.php b/resources/views/livewire/settings/security.blade.php new file mode 100644 index 0000000..71906a7 --- /dev/null +++ b/resources/views/livewire/settings/security.blade.php @@ -0,0 +1,34 @@ +
+ + @unless ($twoFactorEnabled) +
+ +

{{ __('settings.twofa_recommended') }}

+
+ @endunless + +
+
+ +
+

{{ $hasTotp ? __('settings.twofa_status_on') : __('settings.twofa_status_off') }}

+

{{ $hasTotp ? __('settings.twofa_hint_on') : __('settings.twofa_hint_off') }}

+
+
+
+ @if ($twoFactorEnabled) + {{ __('auth.recovery_manage') }} + @endif + @if ($hasTotp) + {{ __('settings.twofa_remove_totp') }} + @else + + {{ __('settings.twofa_setup') }} + + @endif +
+
+
+ + +
diff --git a/resources/views/livewire/settings/sessions.blade.php b/resources/views/livewire/settings/sessions.blade.php new file mode 100644 index 0000000..4fb1310 --- /dev/null +++ b/resources/views/livewire/settings/sessions.blade.php @@ -0,0 +1,5 @@ +
+ +

{{ __('settings.coming_soon') }}

+
+
diff --git a/resources/views/livewire/settings/users.blade.php b/resources/views/livewire/settings/users.blade.php new file mode 100644 index 0000000..5b8d646 --- /dev/null +++ b/resources/views/livewire/settings/users.blade.php @@ -0,0 +1,5 @@ +
+ +

{{ __('settings.coming_soon') }}

+
+
diff --git a/tests/Feature/SettingsFactorManagementTest.php b/tests/Feature/SettingsFactorManagementTest.php index 7a0fd16..7974fc9 100644 --- a/tests/Feature/SettingsFactorManagementTest.php +++ b/tests/Feature/SettingsFactorManagementTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature; -use App\Livewire\Settings\Index; +use App\Livewire\Settings\Security; use App\Livewire\Settings\WebauthnKeys; use App\Models\User; use App\Models\WebauthnCredential; @@ -27,7 +27,7 @@ class SettingsFactorManagementTest extends TestCase $user = User::factory()->create(['two_factor_secret' => 'S', 'two_factor_confirmed_at' => now()]); $user->replaceRecoveryCodes(); - Livewire::actingAs($user)->test(Index::class)->call('disableTwoFactor'); + Livewire::actingAs($user)->test(Security::class)->call('disableTwoFactor'); $fresh = $user->fresh(); $this->assertFalse($fresh->hasTotp()); @@ -40,7 +40,7 @@ class SettingsFactorManagementTest extends TestCase WebauthnCredential::create(['user_id' => $user->id, 'name' => 'k', 'credential_id' => 'cid', 'public_key' => '{}', 'sign_count' => 0]); $user->replaceRecoveryCodes(); - Livewire::actingAs($user->fresh())->test(Index::class)->call('disableTwoFactor'); + Livewire::actingAs($user->fresh())->test(Security::class)->call('disableTwoFactor'); $fresh = $user->fresh(); $this->assertFalse($fresh->hasTotp());