From 8aa48d7aabd579ee2e2da0ca619da01126f34372 Mon Sep 17 00:00:00 2001 From: WendellAdriel <11641518+WendellAdriel@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:10:30 +0000 Subject: [PATCH] Consolidate password and two-factor settings into a unified security page Co-authored-by: WendellAdriel --- .../Settings/PasswordController.php | 32 --- .../Settings/SecurityController.php | 58 +++++ .../TwoFactorAuthenticationController.php | 37 ---- .../TwoFactorAuthenticationRequest.php | 9 - resources/js/layouts/settings/Layout.svelte | 11 +- resources/js/pages/settings/Password.svelte | 103 --------- resources/js/pages/settings/Security.svelte | 201 ++++++++++++++++++ resources/js/pages/settings/TwoFactor.svelte | 121 ----------- routes/settings.php | 10 +- tests/Feature/Settings/PasswordUpdateTest.php | 62 ------ tests/Feature/Settings/SecurityTest.php | 129 +++++++++++ .../Settings/TwoFactorAuthenticationTest.php | 84 -------- 12 files changed, 394 insertions(+), 463 deletions(-) delete mode 100644 app/Http/Controllers/Settings/PasswordController.php create mode 100644 app/Http/Controllers/Settings/SecurityController.php delete mode 100644 app/Http/Controllers/Settings/TwoFactorAuthenticationController.php delete mode 100644 resources/js/pages/settings/Password.svelte create mode 100644 resources/js/pages/settings/Security.svelte delete mode 100644 resources/js/pages/settings/TwoFactor.svelte delete mode 100644 tests/Feature/Settings/PasswordUpdateTest.php create mode 100644 tests/Feature/Settings/SecurityTest.php delete mode 100644 tests/Feature/Settings/TwoFactorAuthenticationTest.php diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php deleted file mode 100644 index d51afe4..0000000 --- a/app/Http/Controllers/Settings/PasswordController.php +++ /dev/null @@ -1,32 +0,0 @@ -user()->update([ - 'password' => $request->password, - ]); - - return back(); - } -} diff --git a/app/Http/Controllers/Settings/SecurityController.php b/app/Http/Controllers/Settings/SecurityController.php new file mode 100644 index 0000000..15ec8e3 --- /dev/null +++ b/app/Http/Controllers/Settings/SecurityController.php @@ -0,0 +1,58 @@ + Features::canManageTwoFactorAuthentication(), + ]; + + if (Features::canManageTwoFactorAuthentication()) { + $request->ensureStateIsValid(); + + $props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication(); + $props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'); + } + + return Inertia::render('settings/Security', $props); + } + + /** + * Update the user's password. + */ + public function update(PasswordUpdateRequest $request): RedirectResponse + { + $request->user()->update([ + 'password' => $request->password, + ]); + + return back(); + } +} diff --git a/app/Http/Controllers/Settings/TwoFactorAuthenticationController.php b/app/Http/Controllers/Settings/TwoFactorAuthenticationController.php deleted file mode 100644 index 4402383..0000000 --- a/app/Http/Controllers/Settings/TwoFactorAuthenticationController.php +++ /dev/null @@ -1,37 +0,0 @@ -ensureStateIsValid(); - - return Inertia::render('settings/TwoFactor', [ - 'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(), - 'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'), - ]); - } -} diff --git a/app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php b/app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php index 1086eb5..8a4c428 100644 --- a/app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php +++ b/app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php @@ -4,21 +4,12 @@ use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; -use Laravel\Fortify\Features; use Laravel\Fortify\InteractsWithTwoFactorState; class TwoFactorAuthenticationRequest extends FormRequest { use InteractsWithTwoFactorState; - /** - * Determine if the user is authorized to make this request. - */ - public function authorize(): bool - { - return Features::enabled(Features::twoFactorAuthentication()); - } - /** * Get the validation rules that apply to the request. * diff --git a/resources/js/layouts/settings/Layout.svelte b/resources/js/layouts/settings/Layout.svelte index 5deda93..9580e70 100644 --- a/resources/js/layouts/settings/Layout.svelte +++ b/resources/js/layouts/settings/Layout.svelte @@ -8,8 +8,7 @@ import { toUrl } from '@/lib/utils'; import { edit as editAppearance } from '@/routes/appearance'; import { edit as editProfile } from '@/routes/profile'; - import { show } from '@/routes/two-factor'; - import { edit as editPassword } from '@/routes/user-password'; + import { edit as editSecurity } from '@/routes/security'; import type { NavItem } from '@/types'; let { @@ -24,12 +23,8 @@ href: editProfile(), }, { - title: 'Password', - href: editPassword(), - }, - { - title: 'Two-factor auth', - href: show(), + title: 'Security', + href: editSecurity(), }, { title: 'Appearance', diff --git a/resources/js/pages/settings/Password.svelte b/resources/js/pages/settings/Password.svelte deleted file mode 100644 index 42f3825..0000000 --- a/resources/js/pages/settings/Password.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - - - - -

Password settings

- - -
- - -
- {#snippet children({ errors, processing, recentlySuccessful })} -
- - - -
- -
- - - -
- -
- - - -
- -
- - - {#if recentlySuccessful} -

Saved.

- {/if} -
- {/snippet} -
-
-
-
diff --git a/resources/js/pages/settings/Security.svelte b/resources/js/pages/settings/Security.svelte new file mode 100644 index 0000000..6f5fe19 --- /dev/null +++ b/resources/js/pages/settings/Security.svelte @@ -0,0 +1,201 @@ + + + + + +

Security settings

+ + +
+ + +
+ {#snippet children({ errors, processing, recentlySuccessful })} +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + {#if recentlySuccessful} +

Saved.

+ {/if} +
+ {/snippet} +
+
+ + {#if canManageTwoFactor} +
+ + + {#if !twoFactorEnabled} +
+

+ When you enable two-factor authentication, you will + be prompted for a secure pin during login. This pin + can be retrieved from a TOTP-supported application + on your phone. +

+ +
+ {#if twoFactorAuth.hasSetupData()} + + {:else} +
(showSetupModal = true)} + > + {#snippet children({ processing })} + + {/snippet} +
+ {/if} +
+
+ {:else} +
+

+ You will be prompted for a secure, random pin during + login, which you can retrieve from the + TOTP-supported application on your phone. +

+ +
+
+ {#snippet children({ processing })} + + {/snippet} +
+
+ + +
+ {/if} + + +
+ {/if} +
+
diff --git a/resources/js/pages/settings/TwoFactor.svelte b/resources/js/pages/settings/TwoFactor.svelte deleted file mode 100644 index 31761e8..0000000 --- a/resources/js/pages/settings/TwoFactor.svelte +++ /dev/null @@ -1,121 +0,0 @@ - - - - - -

Two-factor authentication settings

- - -
- - - {#if !twoFactorEnabled} -
- Disabled - -

- When you enable two-factor authentication, you will be - prompted for a secure pin during login. This pin can be - retrieved from a TOTP-supported application on your - phone. -

- -
- {#if twoFactorAuth.hasSetupData()} - - {:else} -
(showSetupModal = true)} - > - {#snippet children({ processing })} - - {/snippet} -
- {/if} -
-
- {:else} -
- Enabled - -

- With two-factor authentication enabled, you will be - prompted for a secure, random pin during login, which - you can retrieve from the TOTP-supported application on - your phone. -

- - - -
-
- {#snippet children({ processing })} - - {/snippet} -
-
-
- {/if} - - -
-
-
diff --git a/routes/settings.php b/routes/settings.php index 7e63796..634d7e2 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -1,8 +1,7 @@ group(function () { @@ -15,14 +14,11 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - Route::get('settings/password', [PasswordController::class, 'edit'])->name('user-password.edit'); + Route::get('settings/security', [SecurityController::class, 'edit'])->name('security.edit'); - Route::put('settings/password', [PasswordController::class, 'update']) + Route::put('settings/password', [SecurityController::class, 'update']) ->middleware('throttle:6,1') ->name('user-password.update'); Route::inertia('settings/appearance', 'settings/Appearance')->name('appearance.edit'); - - Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show']) - ->name('two-factor.show'); }); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php deleted file mode 100644 index 2fa7f16..0000000 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ /dev/null @@ -1,62 +0,0 @@ -create(); - - $response = $this - ->actingAs($user) - ->get(route('user-password.edit')); - - $response->assertOk(); - } - - public function test_password_can_be_updated() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->from(route('user-password.edit')) - ->put(route('user-password.update'), [ - 'current_password' => 'password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect(route('user-password.edit')); - - $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); - } - - public function test_correct_password_must_be_provided_to_update_password() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->from(route('user-password.edit')) - ->put(route('user-password.update'), [ - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasErrors('current_password') - ->assertRedirect(route('user-password.edit')); - } -} diff --git a/tests/Feature/Settings/SecurityTest.php b/tests/Feature/Settings/SecurityTest.php new file mode 100644 index 0000000..8b8150e --- /dev/null +++ b/tests/Feature/Settings/SecurityTest.php @@ -0,0 +1,129 @@ +skipUnlessFortifyFeature(Features::twoFactorAuthentication()); + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + + $user = User::factory()->create(); + + $this->actingAs($user) + ->withSession(['auth.password_confirmed_at' => time()]) + ->get(route('security.edit')) + ->assertInertia(fn (Assert $page) => $page + ->component('settings/Security') + ->where('canManageTwoFactor', true) + ->where('twoFactorEnabled', false), + ); + } + + public function test_security_page_requires_password_confirmation_when_enabled() + { + $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); + + $user = User::factory()->create(); + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + + $response = $this->actingAs($user) + ->get(route('security.edit')); + + $response->assertRedirect(route('password.confirm')); + } + + public function test_security_page_does_not_require_password_confirmation_when_disabled() + { + $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); + + $user = User::factory()->create(); + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => false, + ]); + + $this->actingAs($user) + ->get(route('security.edit')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('settings/Security'), + ); + } + + public function test_security_page_renders_without_two_factor_when_feature_is_disabled() + { + $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); + + config(['fortify.features' => []]); + + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('security.edit')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('settings/Security') + ->where('canManageTwoFactor', false) + ->missing('twoFactorEnabled') + ->missing('requiresConfirmation'), + ); + } + + public function test_password_can_be_updated() + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from(route('security.edit')) + ->put(route('user-password.update'), [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('security.edit')); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password() + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from(route('security.edit')) + ->put(route('user-password.update'), [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrors('current_password') + ->assertRedirect(route('security.edit')); + } +} diff --git a/tests/Feature/Settings/TwoFactorAuthenticationTest.php b/tests/Feature/Settings/TwoFactorAuthenticationTest.php deleted file mode 100644 index 8822448..0000000 --- a/tests/Feature/Settings/TwoFactorAuthenticationTest.php +++ /dev/null @@ -1,84 +0,0 @@ -skipUnlessFortifyFeature(Features::twoFactorAuthentication()); - - Features::twoFactorAuthentication([ - 'confirm' => true, - 'confirmPassword' => true, - ]); - - $user = User::factory()->create(); - - $this->actingAs($user) - ->withSession(['auth.password_confirmed_at' => time()]) - ->get(route('two-factor.show')) - ->assertInertia(fn (Assert $page) => $page - ->component('settings/TwoFactor') - ->where('twoFactorEnabled', false), - ); - } - - public function test_two_factor_settings_page_requires_password_confirmation_when_enabled() - { - $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); - - $user = User::factory()->create(); - - Features::twoFactorAuthentication([ - 'confirm' => true, - 'confirmPassword' => true, - ]); - - $response = $this->actingAs($user) - ->get(route('two-factor.show')); - - $response->assertRedirect(route('password.confirm')); - } - - public function test_two_factor_settings_page_does_not_requires_password_confirmation_when_disabled() - { - $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); - - $user = User::factory()->create(); - - Features::twoFactorAuthentication([ - 'confirm' => true, - 'confirmPassword' => false, - ]); - - $this->actingAs($user) - ->get(route('two-factor.show')) - ->assertOk() - ->assertInertia(fn (Assert $page) => $page - ->component('settings/TwoFactor'), - ); - } - - public function test_two_factor_settings_page_returns_forbidden_response_when_two_factor_is_disabled() - { - $this->skipUnlessFortifyFeature(Features::twoFactorAuthentication()); - - config(['fortify.features' => []]); - - $user = User::factory()->create(); - - $this->actingAs($user) - ->withSession(['auth.password_confirmed_at' => time()]) - ->get(route('two-factor.show')) - ->assertForbidden(); - } -}