mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 20:14:45 +00:00
Merge remote-tracking branch 'origin/next' into v5-parallel-inertia-react
This commit is contained in:
commit
ae32ab5d7b
139 files changed
+8677
-545
No files matched your search
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Cloud;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CleanupUnverifiedUsers extends Command
|
||||
{
|
||||
protected $signature = 'cloud:cleanup-unverified-users
|
||||
{--yes : Delete eligible users instead of running a dry run}';
|
||||
|
||||
protected $description = 'Delete unverified users without Stripe subscriptions or defined resources';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! isCloud()) {
|
||||
$this->error('This command can only be run on Coolify Cloud.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$eligibleUsers = $this->eligibleUsers();
|
||||
$eligibleCount = $eligibleUsers->count();
|
||||
|
||||
$this->info("Found {$eligibleCount} ".Str::plural('unverified user', $eligibleCount).' eligible for deletion.');
|
||||
$shouldDelete = (bool) $this->option('yes');
|
||||
|
||||
if (! $shouldDelete) {
|
||||
$this->warn('Dry run only. Use --yes to delete eligible users.');
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
|
||||
if ($eligibleCount > 0) {
|
||||
$progressAction = $shouldDelete ? 'Deleting' : 'Checking';
|
||||
$progressBar = $this->output->createProgressBar($eligibleCount);
|
||||
$progressBar->setFormat("{$progressAction} eligible users: %current%/%max% [%bar%] %percent:3s%%");
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($eligibleUsers->lazyById(100) as $user) {
|
||||
if ($shouldDelete && $user->delete()) {
|
||||
$deletedCount++;
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
}
|
||||
|
||||
if ($shouldDelete) {
|
||||
$this->info("Deleted {$deletedCount} ".Str::plural('unverified user', $deletedCount).'.');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function eligibleUsers(): Builder
|
||||
{
|
||||
return User::query()
|
||||
->where('id', '!=', 0)
|
||||
->whereNull('email_verified_at')
|
||||
->whereDoesntHave('teams', fn (Builder $query) => $query->whereKey(0))
|
||||
->whereDoesntHave('teams.subscription')
|
||||
->whereDoesntHave('teams.servers')
|
||||
->whereDoesntHave('teams', function (Builder $query) {
|
||||
$query->whereHas('projects.applications')
|
||||
->orWhereHas('projects.postgresqls')
|
||||
->orWhereHas('projects.redis')
|
||||
->orWhereHas('projects.mongodbs')
|
||||
->orWhereHas('projects.mysqls')
|
||||
->orWhereHas('projects.mariadbs')
|
||||
->orWhereHas('projects.keydbs')
|
||||
->orWhereHas('projects.dragonflies')
|
||||
->orWhereHas('projects.clickhouses')
|
||||
->orWhereHas('projects.services');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Cloud;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class ExportUsers extends Command
|
||||
{
|
||||
protected $signature = 'cloud:export-users';
|
||||
|
||||
protected $description = 'Export subscribed and unsubscribed verified Coolify Cloud users to separate CSV files';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! isCloud()) {
|
||||
$this->error('This command can only be run on Coolify Cloud.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$backups = Storage::disk('backups');
|
||||
$backups->delete('cloud-users.csv');
|
||||
|
||||
$subscribedPath = $backups->path('cloud-users-subscribed.csv');
|
||||
$unsubscribedPath = $backups->path('cloud-users-unsubscribed.csv');
|
||||
$subscribedOutput = fopen($subscribedPath, 'wb');
|
||||
|
||||
if ($subscribedOutput === false) {
|
||||
$this->error("Unable to open {$subscribedPath} for writing.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$unsubscribedOutput = fopen($unsubscribedPath, 'wb');
|
||||
|
||||
if ($unsubscribedOutput === false) {
|
||||
fclose($subscribedOutput);
|
||||
$this->error("Unable to open {$unsubscribedPath} for writing.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$subscribedCount = 0;
|
||||
$unsubscribedCount = 0;
|
||||
|
||||
try {
|
||||
$header = [
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'lifetime_value_currency',
|
||||
'lifetime_value_amount',
|
||||
'utm_campaign',
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_content',
|
||||
'utm_term',
|
||||
'phone',
|
||||
];
|
||||
|
||||
$this->writeCsvRow($subscribedOutput, $header);
|
||||
$this->writeCsvRow($unsubscribedOutput, $header);
|
||||
|
||||
foreach (User::query()
|
||||
->select(['id', 'email', 'name'])
|
||||
->where('id', '!=', 0)
|
||||
->whereNotNull('email_verified_at')
|
||||
->withExists([
|
||||
'teams as is_subscribed' => fn ($query) => $query
|
||||
->whereRelation('subscription', 'stripe_invoice_paid', true),
|
||||
])
|
||||
->lazyById(500) as $user) {
|
||||
$nameParts = preg_split('/\s+/u', trim((string) $user->name), 2) ?: [];
|
||||
[$firstName, $lastName] = array_pad($nameParts, 2, '');
|
||||
|
||||
$row = [
|
||||
$user->email,
|
||||
$firstName,
|
||||
$lastName,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
if ($user->is_subscribed) {
|
||||
$this->writeCsvRow($subscribedOutput, $row);
|
||||
$subscribedCount++;
|
||||
} else {
|
||||
$this->writeCsvRow($unsubscribedOutput, $row);
|
||||
$unsubscribedCount++;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$this->error("Unable to export users: {$exception->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
} finally {
|
||||
fclose($subscribedOutput);
|
||||
fclose($unsubscribedOutput);
|
||||
}
|
||||
|
||||
$this->info("Exported {$subscribedCount} subscribed verified users to {$subscribedPath}");
|
||||
$this->info("Exported {$unsubscribedCount} unsubscribed verified users to {$unsubscribedPath}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $output
|
||||
* @param array<int, mixed> $fields
|
||||
*/
|
||||
private function writeCsvRow($output, array $fields): void
|
||||
{
|
||||
if (fputcsv($output, $fields, ',', '"', '') === false) {
|
||||
throw new RuntimeException('Unable to write the CSV file.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands\Cloud;
|
||||
|
||||
use App\Jobs\SyncStripeSubscriptionsJob;
|
||||
use App\Actions\Stripe\SyncStripeSubscriptions as SyncStripeSubscriptionsAction;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncStripeSubscriptions extends Command
|
||||
@@ -35,14 +35,18 @@ class SyncStripeSubscriptions extends Command
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$job = new SyncStripeSubscriptionsJob($fix);
|
||||
$fetched = 0;
|
||||
$result = $job->handle(function (int $count) use (&$fetched): void {
|
||||
$fetched = $count;
|
||||
$this->output->write("\r Fetching subscriptions from Stripe... {$fetched}");
|
||||
$progressShown = false;
|
||||
$result = SyncStripeSubscriptionsAction::run($fix, function (string $stage, int $current, ?int $total) use (&$progressShown): void {
|
||||
$progressShown = true;
|
||||
$message = match ($stage) {
|
||||
'checking' => " Checking stale subscriptions against Stripe... {$current}/{$total}",
|
||||
default => " Fetching valid subscriptions from Stripe... {$current}",
|
||||
};
|
||||
|
||||
$this->output->write("\r".str_pad($message, 80));
|
||||
});
|
||||
if ($fetched > 0) {
|
||||
$this->output->write("\r".str_repeat(' ', 60)."\r");
|
||||
if ($progressShown) {
|
||||
$this->output->write("\r".str_repeat(' ', 80)."\r");
|
||||
}
|
||||
|
||||
if (isset($result['error'])) {
|
||||
@@ -63,13 +67,22 @@ class SyncStripeSubscriptions extends Command
|
||||
$this->line(" Team ID: {$discrepancy['team_id']}");
|
||||
$this->line(" Stripe ID: {$discrepancy['stripe_subscription_id']}");
|
||||
$this->line(" Stripe Status: {$discrepancy['stripe_status']}");
|
||||
$resolution = match ($discrepancy['resolution']) {
|
||||
'delete_stale' => 'Delete stale local row',
|
||||
'manual_review' => 'Manual review required',
|
||||
default => 'End local subscription',
|
||||
};
|
||||
$this->line(" Resolution: {$resolution}");
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
if ($fix) {
|
||||
$this->info('All discrepancies have been fixed.');
|
||||
$this->info("Automatic corrections applied: {$result['fixed_count']}");
|
||||
if ($result['manual_review_count'] > 0) {
|
||||
$this->warn("Skipped for manual review: {$result['manual_review_count']}");
|
||||
}
|
||||
} else {
|
||||
$this->comment('Run with --fix to correct these discrepancies.');
|
||||
$this->comment('Run with --fix to apply automatic corrections.');
|
||||
}
|
||||
} else {
|
||||
$this->info('No discrepancies found. All subscriptions are in sync.');
|
||||
@@ -84,6 +97,7 @@ class SyncStripeSubscriptions extends Command
|
||||
$this->line(" - Team ID: {$resub['team_id']} | Email: {$resub['email']}");
|
||||
$this->line(" Old: {$resub['old_stripe_subscription_id']} (cus: {$resub['old_stripe_customer_id']})");
|
||||
$this->line(" New: {$resub['new_stripe_subscription_id']} (cus: {$resub['new_stripe_customer_id']}) [{$resub['new_status']}]");
|
||||
$this->line(' Linked to this team: '.($resub['linked_to_team'] ? 'Yes' : 'No'));
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user