From 499c11eb6d85b929a87ecedcfcbf243efe186d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Fri, 7 Nov 2025 18:23:08 +0000 Subject: [PATCH] Reorganize setting items --- .../Settings/ProfileController.php | 16 +- resources/js/layouts/settings/layout.tsx | 24 +- resources/js/pages/settings/account.tsx | 376 ++++++++++++++++++ .../js/pages/settings/delete-account.tsx | 26 ++ routes/settings.php | 9 +- tests/Feature/Settings/AccountPageTest.php | 30 ++ .../Settings/DeleteAccountPageTest.php | 27 ++ tests/Feature/Settings/ProfileUpdateTest.php | 4 +- 8 files changed, 492 insertions(+), 20 deletions(-) create mode 100644 resources/js/pages/settings/account.tsx create mode 100644 resources/js/pages/settings/delete-account.tsx create mode 100644 tests/Feature/Settings/AccountPageTest.php create mode 100644 tests/Feature/Settings/DeleteAccountPageTest.php diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php index a6cb7e16..4fee9ee9 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -10,9 +10,23 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Inertia\Inertia; use Inertia\Response; +use Laravel\Fortify\Features; class ProfileController extends Controller { + /** + * Show the combined account settings page. + */ + public function account(Request $request): Response + { + return Inertia::render('settings/account', [ + 'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail, + 'status' => $request->session()->get('status'), + 'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(), + 'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'), + ]); + } + /** * Show the user's profile settings page. */ @@ -37,7 +51,7 @@ class ProfileController extends Controller $request->user()->save(); - return to_route('profile.edit'); + return to_route('account.edit'); } /** diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index c8efaa7b..a8a7f478 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -4,27 +4,16 @@ import { Separator } from '@/components/ui/separator'; import { cn, isSameUrl, resolveUrl } from '@/lib/utils'; import { edit as editAppearance } from '@/routes/appearance'; import { index as categoriesIndex } from '@/actions/App/Http/Controllers/Settings/CategoryController'; -import { edit } from '@/routes/profile'; -import { show } from '@/routes/two-factor'; -import { edit as editPassword } from '@/routes/user-password'; +import { edit as editAccount } from '@/routes/account'; +import { edit as editDeleteAccount } from '@/routes/delete-account'; import { type NavItem } from '@/types'; import { Link } from '@inertiajs/react'; import { type PropsWithChildren } from 'react'; const sidebarNavItems: NavItem[] = [ { - title: 'Profile', - href: edit(), - icon: null, - }, - { - title: 'Password', - href: editPassword(), - icon: null, - }, - { - title: 'Two-Factor Auth', - href: show(), + title: 'Account', + href: editAccount(), icon: null, }, { @@ -37,6 +26,11 @@ const sidebarNavItems: NavItem[] = [ href: editAppearance(), icon: null, }, + { + title: 'Delete Account', + href: editDeleteAccount(), + icon: null, + }, ]; export default function SettingsLayout({ children }: PropsWithChildren) { diff --git a/resources/js/pages/settings/account.tsx b/resources/js/pages/settings/account.tsx new file mode 100644 index 00000000..a57e453a --- /dev/null +++ b/resources/js/pages/settings/account.tsx @@ -0,0 +1,376 @@ +import PasswordController from '@/actions/App/Http/Controllers/Settings/PasswordController'; +import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; +import HeadingSmall from '@/components/heading-small'; +import InputError from '@/components/input-error'; +import TwoFactorRecoveryCodes from '@/components/two-factor-recovery-codes'; +import TwoFactorSetupModal from '@/components/two-factor-setup-modal'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth'; +import AppLayout from '@/layouts/app-layout'; +import SettingsLayout from '@/layouts/settings/layout'; +import { edit as accountEdit } from '@/routes/account'; +import { disable, enable } from '@/routes/two-factor'; +import { send } from '@/routes/verification'; +import { type BreadcrumbItem, type SharedData } from '@/types'; +import { Transition } from '@headlessui/react'; +import { Form, Head, Link, usePage } from '@inertiajs/react'; +import { ShieldBan, ShieldCheck } from 'lucide-react'; +import { useRef, useState } from 'react'; + +const breadcrumbs: BreadcrumbItem[] = [ + { + title: 'Account settings', + href: accountEdit().url, + }, +]; + +export default function Account({ + mustVerifyEmail, + status, + requiresConfirmation = false, + twoFactorEnabled = false, +}: { + mustVerifyEmail: boolean; + status?: string; + requiresConfirmation?: boolean; + twoFactorEnabled?: boolean; +}) { + const { auth } = usePage().props; + const passwordInput = useRef(null); + const currentPasswordInput = useRef(null); + + const { + qrCodeSvg, + hasSetupData, + manualSetupKey, + clearSetupData, + fetchSetupData, + recoveryCodesList, + fetchRecoveryCodes, + errors: twoFactorErrors, + } = useTwoFactorAuth(); + const [showSetupModal, setShowSetupModal] = useState(false); + + return ( + + + + +
+ + +
+ {({ processing, recentlySuccessful, errors }) => ( + <> +
+ + + + + +
+ +
+ + + + + +
+ + {mustVerifyEmail && + auth.user.email_verified_at === null && ( +
+

+ Your email address is + unverified.{' '} + + Click here to resend the + verification email. + +

+ + {status === + 'verification-link-sent' && ( +
+ A new verification link has + been sent to your email + address. +
+ )} +
+ )} + +
+ + + +

+ Saved +

+
+
+ + )} +
+
+ + + +
+ + +
{ + if (errors.password) { + passwordInput.current?.focus(); + } + + if (errors.current_password) { + currentPasswordInput.current?.focus(); + } + }} + className="space-y-6" + > + {({ errors, processing, recentlySuccessful }) => ( + <> +
+ + + + + +
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+ + + +

+ Saved +

+
+
+ + )} +
+
+ + + +
+ + {twoFactorEnabled ? ( +
+ 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. +

+ + + +
+
+ {({ processing }) => ( + + )} +
+
+
+ ) : ( +
+ 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. +

+ +
+ {hasSetupData ? ( + + ) : ( +
+ setShowSetupModal(true) + } + > + {({ processing }) => ( + + )} +
+ )} +
+
+ )} + + setShowSetupModal(false)} + requiresConfirmation={requiresConfirmation} + twoFactorEnabled={twoFactorEnabled} + qrCodeSvg={qrCodeSvg} + manualSetupKey={manualSetupKey} + clearSetupData={clearSetupData} + fetchSetupData={fetchSetupData} + errors={twoFactorErrors} + /> +
+
+
+ ); +} + diff --git a/resources/js/pages/settings/delete-account.tsx b/resources/js/pages/settings/delete-account.tsx new file mode 100644 index 00000000..11928300 --- /dev/null +++ b/resources/js/pages/settings/delete-account.tsx @@ -0,0 +1,26 @@ +import DeleteUser from '@/components/delete-user'; +import AppLayout from '@/layouts/app-layout'; +import SettingsLayout from '@/layouts/settings/layout'; +import { edit as deleteAccountEdit } from '@/routes/delete-account'; +import { type BreadcrumbItem } from '@/types'; +import { Head } from '@inertiajs/react'; + +const breadcrumbs: BreadcrumbItem[] = [ + { + title: 'Delete account', + href: deleteAccountEdit().url, + }, +]; + +export default function DeleteAccount() { + return ( + + + + + + + + ); +} + diff --git a/routes/settings.php b/routes/settings.php index b1567e4d..11bfff7f 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -8,12 +8,13 @@ use Illuminate\Support\Facades\Route; use Inertia\Inertia; Route::middleware('auth')->group(function () { - Route::redirect('settings', '/settings/profile'); + Route::redirect('settings', '/settings/account'); - Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::get('settings/account', [ProfileController::class, 'account'])->name('account.edit'); Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); + Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::get('settings/password', [PasswordController::class, 'edit'])->name('user-password.edit'); Route::put('settings/password', [PasswordController::class, 'update']) @@ -29,6 +30,10 @@ Route::middleware('auth')->group(function () { return Inertia::render('settings/appearance'); })->name('appearance.edit'); + Route::get('settings/delete-account', function () { + return Inertia::render('settings/delete-account'); + })->name('delete-account.edit'); + Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show']) ->name('two-factor.show'); }); diff --git a/tests/Feature/Settings/AccountPageTest.php b/tests/Feature/Settings/AccountPageTest.php new file mode 100644 index 00000000..9bd9db01 --- /dev/null +++ b/tests/Feature/Settings/AccountPageTest.php @@ -0,0 +1,30 @@ +create(); + + $response = $this + ->actingAs($user) + ->get(route('account.edit')); + + $response->assertOk(); +}); + +test('account page contains profile information', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->get(route('account.edit')); + + $response + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('settings/account') + ->has('mustVerifyEmail') + ->has('twoFactorEnabled') + ->has('requiresConfirmation') + ); +}); diff --git a/tests/Feature/Settings/DeleteAccountPageTest.php b/tests/Feature/Settings/DeleteAccountPageTest.php new file mode 100644 index 00000000..7cb083e7 --- /dev/null +++ b/tests/Feature/Settings/DeleteAccountPageTest.php @@ -0,0 +1,27 @@ +create(); + + $response = $this + ->actingAs($user) + ->get(route('delete-account.edit')); + + $response->assertOk(); +}); + +test('delete account page displays delete account component', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->get(route('delete-account.edit')); + + $response + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('settings/delete-account') + ); +}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index 9f49e25f..91745103 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -24,7 +24,7 @@ test('profile information can be updated', function () { $response ->assertSessionHasNoErrors() - ->assertRedirect(route('profile.edit')); + ->assertRedirect(route('account.edit')); $user->refresh(); @@ -45,7 +45,7 @@ test('email verification status is unchanged when the email address is unchanged $response ->assertSessionHasNoErrors() - ->assertRedirect(route('profile.edit')); + ->assertRedirect(route('account.edit')); expect($user->refresh()->email_verified_at)->not->toBeNull(); });