feat(user-deletion): implement file locking to prevent concurrent user deletions and enhance error handling

This commit is contained in:
Andras Bacsai 2025-09-29 14:03:49 +02:00
parent 364080a447
commit 72f5ae0dc6

View file

@ -8,6 +8,7 @@
use App\Actions\User\DeleteUserTeams; use App\Actions\User\DeleteUserTeams;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -54,11 +55,24 @@ public function handle()
return 1; return 1;
} }
// Implement file lock to prevent concurrent deletions of the same user
$lockKey = "user_deletion_{$this->user->id}";
$lock = Cache::lock($lockKey, 600); // 10 minute lock
if (! $lock->get()) {
$this->error('Another deletion process is already running for this user. Please try again later.');
$this->logAction("Deletion blocked for user {$email}: Another process is already running");
return 1;
}
try {
$this->logAction("Starting user deletion process for: {$email}"); $this->logAction("Starting user deletion process for: {$email}");
// Phase 1: Show User Overview (outside transaction) // Phase 1: Show User Overview (outside transaction)
if (! $this->showUserOverview()) { if (! $this->showUserOverview()) {
$this->info('User deletion cancelled.'); $this->info('User deletion cancelled.');
$lock->release();
return 0; return 0;
} }
@ -172,6 +186,10 @@ public function handle()
} }
return 0; return 0;
} finally {
// Ensure lock is always released
$lock->release();
}
} }
private function showUserOverview(): bool private function showUserOverview(): bool
@ -683,24 +701,21 @@ private function deleteUserProfile(): bool
private function getSubscriptionMonthlyValue(string $planId): int private function getSubscriptionMonthlyValue(string $planId): int
{ {
// Map plan IDs to monthly values based on config // Try to get pricing from subscription metadata or config
$subscriptionConfigs = config('subscription'); // Since we're using dynamic pricing, return 0 for now
// This could be enhanced by fetching the actual price from Stripe API
foreach ($subscriptionConfigs as $key => $value) { // Check if this is a dynamic pricing plan
if ($value === $planId && str_contains($key, 'stripe_price_id_')) { $dynamicMonthlyPlanId = config('subscription.stripe_price_id_dynamic_monthly');
// Extract price from key pattern: stripe_price_id_basic_monthly -> basic $dynamicYearlyPlanId = config('subscription.stripe_price_id_dynamic_yearly');
$planType = str($key)->after('stripe_price_id_')->before('_')->toString();
// Map to known prices (you may need to adjust these based on your actual pricing) if ($planId === $dynamicMonthlyPlanId || $planId === $dynamicYearlyPlanId) {
return match ($planType) { // For dynamic pricing, we can't determine the exact amount without calling Stripe API
'basic' => 29, // Return 0 to indicate dynamic/usage-based pricing
'pro' => 49, return 0;
'ultimate' => 99,
default => 0
};
}
} }
// For any other plans, return 0 as we don't have hardcoded prices
return 0; return 0;
} }
@ -716,6 +731,13 @@ private function logAction(string $message): void
// Also log to a dedicated user deletion log file // Also log to a dedicated user deletion log file
$logFile = storage_path('logs/user-deletions.log'); $logFile = storage_path('logs/user-deletions.log');
// Ensure the logs directory exists
$logDir = dirname($logFile);
if (! is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = now()->format('Y-m-d H:i:s'); $timestamp = now()->format('Y-m-d H:i:s');
file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX);
} }