diff --git a/resources/views/livewire/profile/delete-user-form.blade.php b/resources/views/livewire/profile/delete-user-form.blade.php index 76966ab5..25365f8a 100644 --- a/resources/views/livewire/profile/delete-user-form.blade.php +++ b/resources/views/livewire/profile/delete-user-form.blade.php @@ -1,25 +1,107 @@ isLoading = true; + $this->checkEligibility(); + $this->generateAccountSummary(); + $this->checkPassword(); + } + + /** + * Check if the user is eligible for account deletion + */ + private function checkEligibility(): void + { + /** @var User $user */ + $user = Auth::user(); + $this->isEligible = $user->backupTasks()->count() === 0 && + $user->remoteServers()->count() === 0 && + $user->backupDestinations()->count() === 0; + } + + /** + * Generate a summary of the user's account data + */ + private function generateAccountSummary(): void + { + /** @var User $user */ + $user = Auth::user(); + $this->accountSummary = [ + 'backupTasks' => $user->backupTasks()->count(), + 'remoteServers' => $user->remoteServers()->count(), + 'backupDestinations' => $user->backupDestinations()->count(), + ]; } + /** + * Check if the user has set a password + */ + private function checkPassword(): void + { + /** @var User $user */ + $user = Auth::user(); + $this->hasPassword = !is_null($user->password); + } + + /** + * Proceed to the eligibility check view + */ + public function proceedToEligibilityCheck(): void + { + $this->currentView = 'eligibility'; + } + + /** + * Proceed to the final confirmation view if eligible + */ + public function proceedToFinalConfirmation(): void + { + if ($this->isEligible) { + $this->currentView = 'final-confirmation'; + } + } + + /** + * Delete the user's account + * + * @param Logout $logout + */ public function deleteUser(Logout $logout): void { $this->validate([ 'password' => ['required', 'string', 'current_password'], - ], [ - 'password.required' => __('Please enter your password.'), - 'password.current_password' => __('The password you have entered is incorrect. Please try again.') ]); tap(Auth::user(), $logout(...))->delete(); @@ -29,126 +111,145 @@ public function deleteUser(Logout $logout): void }; ?>
- - - {{ __('Remove your Account') }} - - - {{ __('Remove your account from the application.') }} - - - heroicon-o-trash - - -
- @if (Auth::user()->backupTasks->count() > 0) -
-
- @svg('heroicon-o-exclamation-triangle', 'h-5 w-5 flex-shrink-0 mr-2') - - {{ __('Your scheduled tasks will not be ran if you remove your account.') }} - +
+ @if ($currentView === 'notice') + + {{ __('Account Deletion') }} + + {{ __('Please review the consequences of account deletion before proceeding.') }} + + heroicon-o-exclamation-triangle + +
+
+ @svg('heroicon-o-exclamation-circle', 'w-8 h-8 text-red-500 dark:text-red-400 mr-3') +

{{ __('Warning: Irreversible Action') }}

+

+ {{ __('Deleting your account is a permanent action. Please consider the following consequences:') }} +

+
    +
  • {{ __('All your personal information will be permanently removed') }}
  • +
  • {{ __('Your backup tasks, remote servers, and backup destinations will be deleted') }}
  • +
  • {{ __('You will lose access to all services associated with this account') }}
  • +
  • {{ __('This action cannot be undone') }}
  • +
- @endif - -

- {{ __('Before removing your account, please take a moment to download any data or information that you wish to retain.') }} -

-

- {{ __('Once your account is removed, all of its resources and data will be permanently removed. Additionally, all backup tasks, backup destinations, and linked servers will be removed from our systems.') }} -

-

- {{ __('These resources, such as S3 buckets or Linux servers, will still exist at their respective services (server hosts, Amazon S3, etc.). However, Vanguard will no longer be able to link to them and perform scheduled backups of your data.') }} -

-
- -
-
-
-
- - {{ __('Proceed') }} - + + @if (!$hasPassword) +
+ @svg('heroicon-o-exclamation-triangle', 'w-6 h-6 inline-block mr-2') + {{ __('You need to set a password before you can delete your account. Please request a password reset.') }}
-
- - - {{ __('Get me out of here!') }} - - + @endif + + @if ($hasPassword) +
+ + @svg('heroicon-o-arrow-right', 'w-5 h-5 mr-2') + {{ __('Proceed to Eligibility Check') }} +
+ @endif + + + @elseif ($currentView === 'eligibility') + + {{ __('Account Deletion Eligibility') }} + + {{ __('We\'ll check if your account is eligible for deletion based on your current data and services.') }} + + heroicon-o-clipboard-document-check + +
+

{{ __('Account Summary') }}

+
    + @foreach (['backupTasks' => 'Backup Tasks', 'remoteServers' => 'Remote Servers', 'backupDestinations' => 'Backup Destinations'] as $key => $label) +
  • + {{ __($label) }} + + {{ $accountSummary[$key] }} + +
  • + @endforeach +
-
-
- - - - {{ __('Remove your Account') }} - - - {{ __('Please read this carefully before confirming this action.') }} - - - heroicon-o-trash - -
-

- {{ __('Are you certain you want to proceed with removing your account?') }} -

- -

- {{ __('When your account is removed, all the data we hold on you will be permanently erased. This includes any backup tasks, backup destinations you have configured with us, and servers you have linked.') }} -

- -

- {{ __('This action cannot be reversed, so please make sure you are certain about this decision.') }} -

- -

- {{ __('You are welcome to re-join at any time.') }} -

- -

- {{ __('If you wish to proceed, please enter your account password to confirm.') }} -

- -
- - - - - - - {{ __('To continue with the removal of your account, please enter the password associated with your account. If you do not know, you can reset your password.') }} - + +
+ @if ($isEligible) +
+ @svg('heroicon-o-check-circle', 'w-6 h-6 inline-block mr-2') + {{ __('Your account is eligible for deletion.') }} +
+ @else +
+ @svg('heroicon-o-x-circle', 'w-6 h-6 inline-block mr-2') + {{ __('Your account is not eligible for deletion. Please remove all associated data and services before proceeding.') }} +
+ @endif
-
-
- - {{ __('Confirm Removal') }} +
+ + @svg('heroicon-o-arrow-left', 'w-5 h-5 mr-2') + {{ __('Go Back') }} + + @if ($isEligible) + + @svg('heroicon-o-arrow-right', 'w-5 h-5 mr-2') + {{ __('Proceed to Final Confirmation') }} + @endif +
+ + + @elseif ($currentView === 'final-confirmation') + + {{ __('Final Account Deletion Confirmation') }} + + {{ __('This is your last chance to reconsider. Once confirmed, your account will be deleted.') }} + + heroicon-o-shield-exclamation + + +
+
+
+ @svg('heroicon-o-exclamation-triangle', 'w-8 h-8 text-red-500 dark:text-red-400 mr-3') +

{{ __('Final Warning') }}

+
+

+ {{ __('You are about to permanently delete your account. This action cannot be undone.') }} +

+
+ +
+ + + +
-
- - {{ __('Cancel') }} + +
+ + @svg('heroicon-o-arrow-left', 'w-5 h-5 mr-2') + {{ __('Go Back') }} + + @svg('heroicon-o-trash', 'w-5 h-5 mr-2') + {{ __('Permanently Delete Account') }} +
-
- - -
+ + + @endif +
diff --git a/tests/Feature/Profile/ProfileTest.php b/tests/Feature/Profile/ProfileTest.php index 3934f9f3..9c5846ab 100644 --- a/tests/Feature/Profile/ProfileTest.php +++ b/tests/Feature/Profile/ProfileTest.php @@ -3,6 +3,8 @@ declare(strict_types=1); use App\Models\BackupDestination; +use App\Models\BackupTask; +use App\Models\RemoteServer; use App\Models\User; use Livewire\Volt\Volt; @@ -103,6 +105,7 @@ $this->actingAs($user); $component = Volt::test('profile.delete-user-form') + ->set('currentView', 'final-confirmation') ->set('password', 'password') ->call('deleteUser'); @@ -120,6 +123,7 @@ $this->actingAs($user); $component = Volt::test('profile.delete-user-form') + ->set('currentView', 'final-confirmation') ->set('password', 'wrong-password') ->call('deleteUser'); @@ -130,6 +134,70 @@ $this->assertNotNull($user->fresh()); }); +test('user cannot proceed to eligibility check if they have no password', function (): void { + $user = User::factory()->create(['password' => null]); + + $this->actingAs($user); + + $testable = Volt::test('profile.delete-user-form'); + + $this->assertFalse($testable->get('hasPassword')); +}); + +test('user can proceed to eligibility check if they have a password', function (): void { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->call('proceedToEligibilityCheck'); + + $this->assertTrue($component->get('hasPassword')); + $this->assertEquals('eligibility', $component->get('currentView')); +}); + +test('user can proceed to final confirmation if eligible', function (): void { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('isEligible', true) + ->call('proceedToFinalConfirmation'); + + $this->assertEquals('final-confirmation', $component->get('currentView')); +}); + +test('user cannot proceed to final confirmation if not eligible', function (): void { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('isEligible', false) + ->call('proceedToFinalConfirmation'); + + $this->assertNotEquals('final-confirmation', $component->get('currentView')); +}); + +test('account summary is generated correctly', function (): void { + $user = User::factory()->create(); + + $backupTask = BackupTask::factory()->create(['user_id' => $user->id]); + $remoteServer = RemoteServer::factory()->create(['user_id' => $user->id]); + $backupDestination = BackupDestination::factory()->create(['user_id' => $user->id]); + + $this->actingAs($user); + + $testable = Volt::test('profile.delete-user-form'); + + $this->assertEquals([ + 'backupTasks' => 1, + 'remoteServers' => 1, + 'backupDestinations' => 1, + ], $testable->get('accountSummary')); +}); + test('the timezone must be valid', function (): void { $user = User::factory()->create();