'setDonationAccountId']; /** * Called when balanceHandlingOption is updated. * * @return void */ public function updatedBalanceHandlingOption() { $this->checkDonationLimits(); } /** * Set the donation account ID from ToAccount component. * * @param int|null $accountId * @return void */ public function setDonationAccountId($accountId) { $this->donationAccountId = $accountId; $this->checkDonationLimits(); } /** * Check if the donation would exceed the receiving account's limits. * * @return void */ protected function checkDonationLimits() { // Reset error state $this->donationExceedsLimit = false; $this->donationLimitError = null; // If no donation account selected or no balance to donate, skip check if (!$this->donationAccountId || $this->totalBalance <= 0) { return; } // Get the donation account $donationAccount = \App\Models\Account::find($this->donationAccountId); if (!$donationAccount) { return; } // Clear cache for fresh balance \Cache::forget("account_balance_{$donationAccount->id}"); // Get current balance of the receiving account $currentBalance = $donationAccount->balance; // Calculate the maximum receivable amount // limitMaxTo = limit_max - limit_min (similar to Pay.php logic) $limitMaxTo = $donationAccount->limit_max - $donationAccount->limit_min; // Calculate available budget for receiving $transferBudgetTo = $limitMaxTo - $currentBalance; // Check if donation amount exceeds the receiving account's budget if ($this->totalBalance > $transferBudgetTo) { $this->donationExceedsLimit = true; // Check if the receiving account holder's balance is public $holderType = $donationAccount->accountable_type; $balancePublic = timebank_config('account_info.' . strtolower(class_basename($holderType)) . '.balance_public', false); if ($balancePublic) { $this->donationLimitError = __('The selected account cannot receive this donation amount due to account limits. Please select a different account or delete your balance instead.', [ 'amount' => tbFormat($transferBudgetTo) ]); } else { $this->donationLimitError = __('The selected organization account cannot receive this donation amount due to account limits. Please select a different organization or delete your balance instead.'); } } } /** * Confirm that the user would like to delete their account. * * @return void */ public function confirmUserDeletion() { $this->resetErrorBag(); $this->password = ''; $this->dispatch('confirming-delete-user'); $this->confirmingUserDeletion = true; } /** * Delete the current user. * * @param \Illuminate\Http\Request $request * @param \Laravel\Jetstream\Contracts\DeletesUsers $deleter * @param \Illuminate\Contracts\Auth\StatefulGuard $auth * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse */ public function deleteUser(Request $request, DeletesUsers $deleter, StatefulGuard $auth) { $this->resetErrorBag(); // Get the active profile using helper $profile = getActiveProfile(); if (!$profile) { throw new \Exception('No active profile found.'); } // CRITICAL SECURITY: Validate user has ownership/access to this profile // This prevents IDOR (Insecure Direct Object Reference) attacks where // a user manipulates session data to delete profiles they don't own \App\Helpers\ProfileAuthorizationHelper::authorize($profile); // Check if trying to delete a central bank if ($profile instanceof \App\Models\Bank && isset($profile->level) && $profile->level == 0) { throw ValidationException::withMessages([ 'password' => [__('Central bank (level 0) cannot be deleted. Central banks are essential for currency creation and management.')], ]); } // Check if trying to delete the final admin if ($profile instanceof \App\Models\Admin) { $activeAdminCount = \App\Models\Admin::whereNull('deleted_at')->count(); if ($activeAdminCount <= 1) { throw ValidationException::withMessages([ 'password' => [__('Final administrator cannot be deleted. At least one administrator account must remain active in the system.')], ]); } } // Determine which password to check based on profile type if ($profile instanceof \App\Models\User || $profile instanceof \App\Models\Organization) { // User or Organization: validate against base user password // Organizations don't have their own password (passwordless) $authenticatedUser = Auth::user(); if (! Hash::check($this->password, $authenticatedUser->password)) { throw ValidationException::withMessages([ 'password' => [__('This password does not match our records.')], ]); } } elseif ($profile instanceof \App\Models\Bank || $profile instanceof \App\Models\Admin) { // Bank or Admin: validate against their own password if (! Hash::check($this->password, $profile->password)) { throw ValidationException::withMessages([ 'password' => [__('This password does not match our records.')], ]); } } else { throw new \Exception('Unknown profile type.'); } // Validate balance handling option if donation is selected if ($this->balanceHandlingOption === 'donate' && !$this->donationAccountId) { throw ValidationException::withMessages([ 'donationAccountId' => [__('Please select an organization account to donate your balance to.')], ]); } // Check if donation would exceed limits if ($this->balanceHandlingOption === 'donate' && $this->donationExceedsLimit) { throw ValidationException::withMessages([ 'donationAccountId' => [$this->donationLimitError ?? __('The selected organization account cannot receive this donation amount.')], ]); } // Determine table name based on profile type $profileTable = $profile->getTable(); // Get the profile's updated_at timestamp $time = DB::table($profileTable) ->where('id', $profile->id) ->pluck('updated_at') ->first(); $time = Carbon::parse($time); // Convert the time to a Carbon instance // Pass balance handling options to the deleter $result = $deleter->delete( $profile->fresh(), $this->balanceHandlingOption, $this->donationAccountId ); $this->confirmingUserDeletion = false; if ($result['status'] === 'success') { $result['time'] = $time->translatedFormat('j F Y, H:i'); $result['deletedUser'] = $profile; $result['mail'] = $profile->email; $result['balanceHandlingOption'] = $this->balanceHandlingOption; $result['totalBalance'] = $this->totalBalance; $result['donationAccountId'] = $this->donationAccountId; $result['gracePeriodDays'] = timebank_config('delete_profile.grace_period_days', 30); // Get donation account details if donated if ($this->balanceHandlingOption === 'donate' && $this->donationAccountId) { $donationAccount = \App\Models\Account::find($this->donationAccountId); if ($donationAccount && $donationAccount->accountable) { $result['donationAccountName'] = $donationAccount->name; $result['donationOrganizationName'] = $donationAccount->accountable->name; } } Log::notice('Profile deleted: ' . $result['deletedUser']); Mail::to($profile->email)->queue(new UserDeletedMail($result)); // Handle logout based on profile type if ($profile instanceof \App\Models\User) { // User deletion: logout completely from all guards $auth->logout(); session()->invalidate(); session()->regenerateToken(); // Re-flash the result data after session invalidation session()->flash('result', $result); } else { // Flash result for non-user profiles session()->flash('result', $result); // Non-user profile deletion (Organization/Bank/Admin): // Only logout from the specific guard and switch back to base user $profileType = strtolower(class_basename($profile)); if ($profileType === 'organization') { Auth::guard('organization')->logout(); } elseif ($profileType === 'bank') { Auth::guard('bank')->logout(); } elseif ($profileType === 'admin') { Auth::guard('admin')->logout(); } // Switch back to base user profile $baseUser = Auth::guard('web')->user(); if ($baseUser) { session(['activeProfileType' => 'App\\Models\\User']); session(['activeProfileId' => $baseUser->id]); session(['activeProfileName' => $baseUser->name]); session(['activeProfilePhoto' => $baseUser->profile_photo_path]); session(['active_guard' => 'web']); } } return redirect()->route('goodbye-deleted-user'); } else { // Trigger WireUi error notification $this->notification()->error( $title = __('Deletion Failed'), $description = __('There was an error deleting your profile: ') . $result['message'] ); Log::warning('Profile deletion failed for profile ID: ' . $profile->id . ' (Type: ' . get_class($profile) . ')'); Log::error('Error message: ' . $result['message']); return redirect()->back(); } } /** * Load user accounts and calculate balances. * * @return void */ public function mount() { $this->loadAccounts(); } /** * Load and calculate account balances. * * @return void */ public function loadAccounts() { // Get the active profile using helper (could be User, Organization, Bank, etc.) $profile = getActiveProfile(); // Check if profile is a central bank (level = 0) if ($profile instanceof \App\Models\Bank && isset($profile->level) && $profile->level == 0) { $this->isCentralBank = true; } // Check if profile is the final admin if ($profile instanceof \App\Models\Admin) { $activeAdminCount = \App\Models\Admin::whereNull('deleted_at')->count(); if ($activeAdminCount <= 1) { $this->isFinalAdmin = true; } } // Check if profile exists and has accounts method if (!$profile || !method_exists($profile, 'accounts')) { $this->accounts = collect(); $this->totalBalance = 0; $this->hasNegativeBalance = false; return; } // Get all active, non-removed accounts $userAccounts = $profile->accounts() ->active() ->notRemoved() ->get(); // Clear cache and calculate balances $this->accounts = collect(); $this->totalBalance = 0; $this->hasNegativeBalance = false; foreach ($userAccounts as $account) { // Clear cache for fresh balance Cache::forget("account_balance_{$account->id}"); $balance = $account->balance; $this->accounts->push([ 'id' => $account->id, 'name' => __('messages.' . $account->name . '_account'), 'balance' => $balance, 'balanceFormatted' => tbFormat($balance), ]); $this->totalBalance += $balance; if ($balance < 0) { $this->hasNegativeBalance = true; } } } /** * Render the component. * * @return \Illuminate\View\View */ public function render() { $showBalanceOptions = timebank_config('delete_profile.account_balances.donate_balances_to_organization_account_specified', false); return view('profile.delete-user-form', [ 'showBalanceOptions' => $showBalanceOptions, ]); } }