453 lines
15 KiB
PHP
453 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Livewire\Profile;
|
|
|
|
use App\Mail\UserDeletedMail;
|
|
use Illuminate\Contracts\Auth\StatefulGuard;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Laravel\Jetstream\Contracts\DeletesUsers;
|
|
use Livewire\Component;
|
|
use WireUi\Traits\WireUiActions;
|
|
|
|
|
|
class DeleteUserForm extends Component
|
|
{
|
|
use WireUiActions;
|
|
|
|
|
|
|
|
/**
|
|
* Indicates if user deletion is being confirmed.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $confirmingUserDeletion = false;
|
|
|
|
/**
|
|
* The user's current password.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $password = '';
|
|
|
|
/**
|
|
* Balance handling option selected by user.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $balanceHandlingOption = 'delete';
|
|
|
|
/**
|
|
* Selected organization account ID for balance donation.
|
|
*
|
|
* @var int|null
|
|
*/
|
|
public $donationAccountId = null;
|
|
|
|
/**
|
|
* User's accounts with balances.
|
|
*
|
|
* @var \Illuminate\Support\Collection
|
|
*/
|
|
public $accounts;
|
|
|
|
/**
|
|
* Total balance across all accounts.
|
|
*
|
|
* @var float|int
|
|
*/
|
|
public $totalBalance = 0;
|
|
|
|
/**
|
|
* Whether any account has a negative balance.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $hasNegativeBalance = false;
|
|
|
|
/**
|
|
* Whether the profile is a central bank (level = 0).
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $isCentralBank = false;
|
|
|
|
/**
|
|
* Whether the profile is the final admin.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $isFinalAdmin = false;
|
|
|
|
/**
|
|
* Whether the donation would exceed the receiving account's limit.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $donationExceedsLimit = false;
|
|
|
|
/**
|
|
* Error message for donation limit exceeded.
|
|
*
|
|
* @var string|null
|
|
*/
|
|
public $donationLimitError = null;
|
|
|
|
/**
|
|
* Listener to receive toAccountId from ToAccount component.
|
|
*/
|
|
protected $listeners = ['toAccountId' => '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,
|
|
]);
|
|
}
|
|
}
|