feat(user-deletion): implement file locking to prevent concurrent user deletions and enhance error handling
This commit is contained in:
parent
364080a447
commit
72f5ae0dc6
1 changed files with 116 additions and 94 deletions
|
|
@ -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,124 +55,141 @@ public function handle()
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logAction("Starting user deletion process for: {$email}");
|
// 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
|
||||||
|
|
||||||
// Phase 1: Show User Overview (outside transaction)
|
if (! $lock->get()) {
|
||||||
if (! $this->showUserOverview()) {
|
$this->error('Another deletion process is already running for this user. Please try again later.');
|
||||||
$this->info('User deletion cancelled.');
|
$this->logAction("Deletion blocked for user {$email}: Another process is already running");
|
||||||
|
|
||||||
return 0;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not dry run, wrap everything in a transaction
|
try {
|
||||||
if (! $this->isDryRun) {
|
$this->logAction("Starting user deletion process for: {$email}");
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
|
// Phase 1: Show User Overview (outside transaction)
|
||||||
|
if (! $this->showUserOverview()) {
|
||||||
|
$this->info('User deletion cancelled.');
|
||||||
|
$lock->release();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not dry run, wrap everything in a transaction
|
||||||
|
if (! $this->isDryRun) {
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Phase 2: Delete Resources
|
||||||
|
if (! $this->skipResources) {
|
||||||
|
if (! $this->deleteResources()) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('User deletion failed at resource deletion phase. All changes rolled back.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Delete Servers
|
||||||
|
if (! $this->deleteServers()) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('User deletion failed at server deletion phase. All changes rolled back.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Handle Teams
|
||||||
|
if (! $this->handleTeams()) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('User deletion failed at team handling phase. All changes rolled back.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Cancel Stripe Subscriptions
|
||||||
|
if (! $this->skipStripe && isCloud()) {
|
||||||
|
if (! $this->cancelStripeSubscriptions()) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: Delete User Profile
|
||||||
|
if (! $this->deleteUserProfile()) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('User deletion failed at final phase. All changes rolled back.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('✅ User deletion completed successfully!');
|
||||||
|
$this->logAction("User deletion completed for: {$email}");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error('An error occurred during user deletion: '.$e->getMessage());
|
||||||
|
$this->logAction("User deletion failed for {$email}: ".$e->getMessage());
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Dry run mode - just run through the phases without transaction
|
||||||
// Phase 2: Delete Resources
|
// Phase 2: Delete Resources
|
||||||
if (! $this->skipResources) {
|
if (! $this->skipResources) {
|
||||||
if (! $this->deleteResources()) {
|
if (! $this->deleteResources()) {
|
||||||
DB::rollBack();
|
$this->info('User deletion would be cancelled at resource deletion phase.');
|
||||||
$this->error('User deletion failed at resource deletion phase. All changes rolled back.');
|
|
||||||
|
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Delete Servers
|
// Phase 3: Delete Servers
|
||||||
if (! $this->deleteServers()) {
|
if (! $this->deleteServers()) {
|
||||||
DB::rollBack();
|
$this->info('User deletion would be cancelled at server deletion phase.');
|
||||||
$this->error('User deletion failed at server deletion phase. All changes rolled back.');
|
|
||||||
|
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Handle Teams
|
// Phase 4: Handle Teams
|
||||||
if (! $this->handleTeams()) {
|
if (! $this->handleTeams()) {
|
||||||
DB::rollBack();
|
$this->info('User deletion would be cancelled at team handling phase.');
|
||||||
$this->error('User deletion failed at team handling phase. All changes rolled back.');
|
|
||||||
|
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: Cancel Stripe Subscriptions
|
// Phase 5: Cancel Stripe Subscriptions
|
||||||
if (! $this->skipStripe && isCloud()) {
|
if (! $this->skipStripe && isCloud()) {
|
||||||
if (! $this->cancelStripeSubscriptions()) {
|
if (! $this->cancelStripeSubscriptions()) {
|
||||||
DB::rollBack();
|
$this->info('User deletion would be cancelled at Stripe cancellation phase.');
|
||||||
$this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.');
|
|
||||||
|
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 6: Delete User Profile
|
// Phase 6: Delete User Profile
|
||||||
if (! $this->deleteUserProfile()) {
|
if (! $this->deleteUserProfile()) {
|
||||||
DB::rollBack();
|
$this->info('User deletion would be cancelled at final phase.');
|
||||||
$this->error('User deletion failed at final phase. All changes rolled back.');
|
|
||||||
|
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the transaction
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
$this->info('✅ User deletion completed successfully!');
|
$this->info('✅ DRY RUN completed successfully! No data was deleted.');
|
||||||
$this->logAction("User deletion completed for: {$email}");
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
$this->error('An error occurred during user deletion: '.$e->getMessage());
|
|
||||||
$this->logAction("User deletion failed for {$email}: ".$e->getMessage());
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Dry run mode - just run through the phases without transaction
|
|
||||||
// Phase 2: Delete Resources
|
|
||||||
if (! $this->skipResources) {
|
|
||||||
if (! $this->deleteResources()) {
|
|
||||||
$this->info('User deletion would be cancelled at resource deletion phase.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Delete Servers
|
return 0;
|
||||||
if (! $this->deleteServers()) {
|
} finally {
|
||||||
$this->info('User deletion would be cancelled at server deletion phase.');
|
// Ensure lock is always released
|
||||||
|
$lock->release();
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 4: Handle Teams
|
|
||||||
if (! $this->handleTeams()) {
|
|
||||||
$this->info('User deletion would be cancelled at team handling phase.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 5: Cancel Stripe Subscriptions
|
|
||||||
if (! $this->skipStripe && isCloud()) {
|
|
||||||
if (! $this->cancelStripeSubscriptions()) {
|
|
||||||
$this->info('User deletion would be cancelled at Stripe cancellation phase.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 6: Delete User Profile
|
|
||||||
if (! $this->deleteUserProfile()) {
|
|
||||||
$this->info('User deletion would be cancelled at final phase.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('✅ DRY RUN completed successfully! No data was deleted.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue