diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index ea2befd3a..9f97dd0d4 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -40,7 +40,7 @@ class CreateNewUser implements CreatesNewUsers $user = User::create([ 'id' => 0, 'name' => $input['name'], - 'email' => strtolower($input['email']), + 'email' => $input['email'], 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); @@ -52,7 +52,7 @@ class CreateNewUser implements CreatesNewUsers } else { $user = User::create([ 'name' => $input['name'], - 'email' => strtolower($input['email']), + 'email' => $input['email'], 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 81824675b..ce2d6d598 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Jobs\CleanupHelperContainersJob; +use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; @@ -72,7 +73,7 @@ class CleanupStuckedResources extends Command $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { echo "Deleting stuck application: {$application->name}\n"; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); } } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; @@ -82,26 +83,35 @@ class CleanupStuckedResources extends Command foreach ($applicationsPreviews as $applicationPreview) { if (! data_get($applicationPreview, 'application')) { echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; - $applicationPreview->delete(); + DeleteResourceJob::dispatch($applicationPreview); } } } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; } + try { + $applicationsPreviews = ApplicationPreview::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($applicationsPreviews as $applicationPreview) { + echo "Deleting stuck application preview: {$applicationPreview->fqdn}\n"; + DeleteResourceJob::dispatch($applicationPreview); + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application: {$e->getMessage()}\n"; + } try { $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($postgresqls as $postgresql) { echo "Deleting stuck postgresql: {$postgresql->name}\n"; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); } } catch (\Throwable $e) { echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n"; } try { - $redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); - foreach ($redis as $redis) { + $rediss = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($rediss as $redis) { echo "Deleting stuck redis: {$redis->name}\n"; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); } } catch (\Throwable $e) { echo "Error in cleaning stuck redis: {$e->getMessage()}\n"; @@ -110,7 +120,7 @@ class CleanupStuckedResources extends Command $keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($keydbs as $keydb) { echo "Deleting stuck keydb: {$keydb->name}\n"; - $keydb->forceDelete(); + DeleteResourceJob::dispatch($keydb); } } catch (\Throwable $e) { echo "Error in cleaning stuck keydb: {$e->getMessage()}\n"; @@ -119,7 +129,7 @@ class CleanupStuckedResources extends Command $dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($dragonflies as $dragonfly) { echo "Deleting stuck dragonfly: {$dragonfly->name}\n"; - $dragonfly->forceDelete(); + DeleteResourceJob::dispatch($dragonfly); } } catch (\Throwable $e) { echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n"; @@ -128,7 +138,7 @@ class CleanupStuckedResources extends Command $clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($clickhouses as $clickhouse) { echo "Deleting stuck clickhouse: {$clickhouse->name}\n"; - $clickhouse->forceDelete(); + DeleteResourceJob::dispatch($clickhouse); } } catch (\Throwable $e) { echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n"; @@ -137,7 +147,7 @@ class CleanupStuckedResources extends Command $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mongodbs as $mongodb) { echo "Deleting stuck mongodb: {$mongodb->name}\n"; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); } } catch (\Throwable $e) { echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n"; @@ -146,7 +156,7 @@ class CleanupStuckedResources extends Command $mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mysqls as $mysql) { echo "Deleting stuck mysql: {$mysql->name}\n"; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); } } catch (\Throwable $e) { echo "Error in cleaning stuck mysql: {$e->getMessage()}\n"; @@ -155,7 +165,7 @@ class CleanupStuckedResources extends Command $mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mariadbs as $mariadb) { echo "Deleting stuck mariadb: {$mariadb->name}\n"; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); } } catch (\Throwable $e) { echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n"; @@ -164,7 +174,7 @@ class CleanupStuckedResources extends Command $services = Service::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($services as $service) { echo "Deleting stuck service: {$service->name}\n"; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); } } catch (\Throwable $e) { echo "Error in cleaning stuck service: {$e->getMessage()}\n"; @@ -217,19 +227,19 @@ class CleanupStuckedResources extends Command foreach ($applications as $application) { if (! data_get($application, 'environment')) { echo 'Application without environment: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } if (! $application->destination()) { echo 'Application without destination: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } if (! data_get($application, 'destination.server')) { echo 'Application without server: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } @@ -242,19 +252,19 @@ class CleanupStuckedResources extends Command foreach ($postgresqls as $postgresql) { if (! data_get($postgresql, 'environment')) { echo 'Postgresql without environment: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } if (! $postgresql->destination()) { echo 'Postgresql without destination: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } if (! data_get($postgresql, 'destination.server')) { echo 'Postgresql without server: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } @@ -267,19 +277,19 @@ class CleanupStuckedResources extends Command foreach ($redis as $redis) { if (! data_get($redis, 'environment')) { echo 'Redis without environment: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } if (! $redis->destination()) { echo 'Redis without destination: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } if (! data_get($redis, 'destination.server')) { echo 'Redis without server: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } @@ -293,19 +303,19 @@ class CleanupStuckedResources extends Command foreach ($mongodbs as $mongodb) { if (! data_get($mongodb, 'environment')) { echo 'Mongodb without environment: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } if (! $mongodb->destination()) { echo 'Mongodb without destination: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } if (! data_get($mongodb, 'destination.server')) { echo 'Mongodb without server: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } @@ -319,19 +329,19 @@ class CleanupStuckedResources extends Command foreach ($mysqls as $mysql) { if (! data_get($mysql, 'environment')) { echo 'Mysql without environment: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } if (! $mysql->destination()) { echo 'Mysql without destination: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } if (! data_get($mysql, 'destination.server')) { echo 'Mysql without server: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } @@ -345,19 +355,19 @@ class CleanupStuckedResources extends Command foreach ($mariadbs as $mariadb) { if (! data_get($mariadb, 'environment')) { echo 'Mariadb without environment: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } if (! $mariadb->destination()) { echo 'Mariadb without destination: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } if (! data_get($mariadb, 'destination.server')) { echo 'Mariadb without server: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } @@ -371,19 +381,19 @@ class CleanupStuckedResources extends Command foreach ($services as $service) { if (! data_get($service, 'environment')) { echo 'Service without environment: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } if (! $service->destination()) { echo 'Service without destination: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } if (! data_get($service, 'server')) { echo 'Service without server: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } @@ -396,7 +406,7 @@ class CleanupStuckedResources extends Command foreach ($serviceApplications as $service) { if (! data_get($service, 'service')) { echo 'ServiceApplication without service: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } @@ -409,7 +419,7 @@ class CleanupStuckedResources extends Command foreach ($serviceDatabases as $service) { if (! data_get($service, 'service')) { echo 'ServiceDatabase without service: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 8aefdad0e..6e8d18f61 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -8,6 +8,7 @@ use App\Jobs\CheckHelperImageJob; use App\Jobs\PullChangelog; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; @@ -19,80 +20,18 @@ use Illuminate\Support\Facades\Http; class Init extends Command { - protected $signature = 'app:init {--force-cloud}'; + protected $signature = 'app:init'; protected $description = 'Cleanup instance related stuffs'; public $servers = null; + public InstanceSettings $settings; + public function handle() { - $this->optimize(); - - if (isCloud() && ! $this->option('force-cloud')) { - echo "Skipping init as we are on cloud and --force-cloud option is not set\n"; - - return; - } - - $this->servers = Server::all(); - if (! isCloud()) { - $this->sendAliveSignal(); - get_public_ips(); - } - - // Backward compatibility - $this->replaceSlashInEnvironmentName(); - $this->restoreCoolifyDbBackup(); - $this->updateUserEmails(); - // - $this->updateTraefikLabels(); - if (! isCloud() || $this->option('force-cloud')) { - $this->cleanupUnusedNetworkFromCoolifyProxy(); - } - - $this->call('cleanup:redis'); - - try { - $this->call('cleanup:names'); - } catch (\Throwable $e) { - echo "Error in cleanup:names command: {$e->getMessage()}\n"; - } - $this->call('cleanup:stucked-resources'); - - try { - $this->pullHelperImage(); - } catch (\Throwable $e) { - // - } - - if (isCloud()) { - try { - $this->cleanupInProgressApplicationDeployments(); - } catch (\Throwable $e) { - echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; - } - - try { - $this->pullTemplatesFromCDN(); - } catch (\Throwable $e) { - echo "Could not pull templates from CDN: {$e->getMessage()}\n"; - } - - try { - $this->pullChangelogFromGitHub(); - } catch (\Throwable $e) { - echo "Could not changelogs from github: {$e->getMessage()}\n"; - } - - return; - } - - try { - $this->cleanupInProgressApplicationDeployments(); - } catch (\Throwable $e) { - echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; - } + Artisan::call('optimize:clear'); + Artisan::call('optimize'); try { $this->pullTemplatesFromCDN(); @@ -105,20 +44,80 @@ class Init extends Command } catch (\Throwable $e) { echo "Could not changelogs from github: {$e->getMessage()}\n"; } + + try { + $this->pullHelperImage(); + } catch (\Throwable $e) { + echo "Error in pullHelperImage command: {$e->getMessage()}\n"; + } + + if (isCloud()) { + return; + } + + $this->settings = instanceSettings(); + $this->servers = Server::all(); + + $do_not_track = data_get($this->settings, 'do_not_track', true); + if ($do_not_track == false) { + $this->sendAliveSignal(); + } + get_public_ips(); + + // Backward compatibility + $this->replaceSlashInEnvironmentName(); + $this->restoreCoolifyDbBackup(); + $this->updateUserEmails(); + // + $this->updateTraefikLabels(); + $this->cleanupUnusedNetworkFromCoolifyProxy(); + + try { + $this->call('cleanup:redis'); + } catch (\Throwable $e) { + echo "Error in cleanup:redis command: {$e->getMessage()}\n"; + } + try { + $this->call('cleanup:names'); + } catch (\Throwable $e) { + echo "Error in cleanup:names command: {$e->getMessage()}\n"; + } + try { + $this->call('cleanup:stucked-resources'); + } catch (\Throwable $e) { + echo "Error in cleanup:stucked-resources command: {$e->getMessage()}\n"; + } + try { + $updatedCount = ApplicationDeploymentQueue::whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS->value, + ApplicationDeploymentStatus::QUEUED->value, + ])->update([ + 'status' => ApplicationDeploymentStatus::FAILED->value, + ]); + + if ($updatedCount > 0) { + echo "Marked {$updatedCount} stuck deployments as failed\n"; + } + } catch (\Throwable $e) { + echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; + } + try { $localhost = $this->servers->where('id', 0)->first(); - $localhost->setupDynamicProxyConfiguration(); + if ($localhost) { + $localhost->setupDynamicProxyConfiguration(); + } } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } - $settings = instanceSettings(); + if (! is_null(config('constants.coolify.autoupdate', null))) { if (config('constants.coolify.autoupdate') == true) { echo "Enabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => true]); + $this->settings->update(['is_auto_update_enabled' => true]); } else { echo "Disabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => false]); + $this->settings->update(['is_auto_update_enabled' => false]); } } } @@ -147,17 +146,11 @@ class Init extends Command } } - private function optimize() - { - Artisan::call('optimize:clear'); - Artisan::call('optimize'); - } - private function updateUserEmails() { try { User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { - $user->update(['email' => strtolower($user->email)]); + $user->update(['email' => $user->email]); }); } catch (\Throwable $e) { echo "Error in updating user emails: {$e->getMessage()}\n"; @@ -173,27 +166,6 @@ class Init extends Command } } - private function cleanupUnnecessaryDynamicProxyConfiguration() - { - foreach ($this->servers as $server) { - try { - if (! $server->isFunctional()) { - continue; - } - if ($server->id === 0) { - continue; - } - $file = $server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $server, false); - } catch (\Throwable $e) { - echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; - } - } - } - private function cleanupUnusedNetworkFromCoolifyProxy() { foreach ($this->servers as $server) { @@ -263,13 +235,6 @@ class Init extends Command { $id = config('app.id'); $version = config('constants.coolify.version'); - $settings = instanceSettings(); - $do_not_track = data_get($settings, 'do_not_track'); - if ($do_not_track == true) { - echo "Do_not_track is enabled\n"; - - return; - } try { Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version"); } catch (\Throwable $e) { @@ -277,23 +242,6 @@ class Init extends Command } } - private function cleanupInProgressApplicationDeployments() - { - // Cleanup any failed deployments - try { - if (isCloud()) { - return; - } - $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); - foreach ($queued_inprogress_deployments as $deployment) { - $deployment->status = ApplicationDeploymentStatus::FAILED->value; - $deployment->save(); - } - } catch (\Throwable $e) { - echo "Error: {$e->getMessage()}\n"; - } - } - private function replaceSlashInEnvironmentName() { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { diff --git a/app/Console/Commands/InitChangelog.php b/app/Console/Commands/InitChangelog.php deleted file mode 100644 index f9eb12f04..000000000 --- a/app/Console/Commands/InitChangelog.php +++ /dev/null @@ -1,98 +0,0 @@ -argument('month') ?: Carbon::now()->format('Y-m'); - - // Validate month format - if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { - $this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)'); - - return self::FAILURE; - } - - $changelogsDir = base_path('changelogs'); - $filePath = $changelogsDir."/{$month}.json"; - - // Create changelogs directory if it doesn't exist - if (! is_dir($changelogsDir)) { - mkdir($changelogsDir, 0755, true); - $this->info("Created changelogs directory: {$changelogsDir}"); - } - - // Check if file already exists - if (file_exists($filePath)) { - if (! $this->confirm("File {$month}.json already exists. Overwrite?")) { - $this->info('Operation cancelled'); - - return self::SUCCESS; - } - } - - // Parse the month for example data - $carbonMonth = Carbon::createFromFormat('Y-m', $month); - $monthName = $carbonMonth->format('F Y'); - $sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month - - // Get version from config - $version = 'v'.config('constants.coolify.version'); - - // Create example changelog structure - $exampleData = [ - 'entries' => [ - [ - 'version' => $version, - 'title' => 'Example Feature Release', - 'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.", - 'published_at' => $sampleDate, - ], - ], - ]; - - // Write the file - $jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - - if (file_put_contents($filePath, $jsonContent) === false) { - $this->error("Failed to create changelog file: {$filePath}"); - - return self::FAILURE; - } - - $this->info("✅ Created changelog file: changelogs/{$month}.json"); - $this->line(" Example entry created for {$monthName}"); - $this->line(' Edit the file to add your actual changelog entries'); - - // Show the file contents - if ($this->option('verbose')) { - $this->newLine(); - $this->line('File contents:'); - $this->line($jsonContent); - } - - return self::SUCCESS; - } -} diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b5a74166a..870cef3d9 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -6,7 +6,14 @@ use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; +use App\Models\StandaloneMariadb; +use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Illuminate\Console\Command; use function Laravel\Prompts\confirm; @@ -103,19 +110,79 @@ class ServicesDelete extends Command private function deleteDatabase() { - $databases = StandalonePostgresql::all(); - if ($databases->count() === 0) { + // Collect all databases from all types with unique identifiers + $allDatabases = collect(); + $databaseOptions = collect(); + + // Add PostgreSQL databases + foreach (StandalonePostgresql::all() as $db) { + $key = "postgresql_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (PostgreSQL)"); + } + + // Add MySQL databases + foreach (StandaloneMysql::all() as $db) { + $key = "mysql_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MySQL)"); + } + + // Add MariaDB databases + foreach (StandaloneMariadb::all() as $db) { + $key = "mariadb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MariaDB)"); + } + + // Add MongoDB databases + foreach (StandaloneMongodb::all() as $db) { + $key = "mongodb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MongoDB)"); + } + + // Add Redis databases + foreach (StandaloneRedis::all() as $db) { + $key = "redis_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (Redis)"); + } + + // Add KeyDB databases + foreach (StandaloneKeydb::all() as $db) { + $key = "keydb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (KeyDB)"); + } + + // Add Dragonfly databases + foreach (StandaloneDragonfly::all() as $db) { + $key = "dragonfly_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (Dragonfly)"); + } + + // Add ClickHouse databases + foreach (StandaloneClickhouse::all() as $db) { + $key = "clickhouse_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (ClickHouse)"); + } + + if ($allDatabases->count() === 0) { $this->error('There are no databases to delete.'); return; } + $databasesToDelete = multiselect( 'What database do you want to delete?', - $databases->pluck('name', 'id')->sortKeys(), + $databaseOptions->sortKeys(), ); - foreach ($databasesToDelete as $database) { - $toDelete = $databases->where('id', $database)->first(); + foreach ($databasesToDelete as $databaseKey) { + $toDelete = $allDatabases->get($databaseKey); if ($toDelete) { $this->info($toDelete); $confirmed = confirm('Are you sure you want to delete all selected resources?'); diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 8872754e5..b940bf394 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Webhook; use App\Enums\ProcessStatus; use App\Http\Controllers\Controller; use App\Jobs\ApplicationPullRequestUpdateJob; +use App\Jobs\DeleteResourceJob; use App\Jobs\GithubAppPermissionJob; use App\Models\Application; use App\Models\ApplicationPreview; @@ -78,6 +79,7 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); + $author_association = data_get($payload, 'pull_request.author_association'); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -170,6 +172,19 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); + + continue; + } + } $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { @@ -226,9 +241,7 @@ class Github extends Controller if ($action === 'closed') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { - $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + DeleteResourceJob::dispatch($found); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -327,6 +340,7 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); + $author_association = data_get($payload, 'pull_request.author_association'); } if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); @@ -400,6 +414,19 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); + + continue; + } + } $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { @@ -452,7 +479,8 @@ class Github extends Controller } ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); - $found->delete(); + + DeleteResourceJob::dispatch($found); $return_payloads->push([ 'application' => $application->name, diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index a6b4dbe9e..4a419a12f 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -78,6 +78,8 @@ class Index extends Component 'new_email' => ['required', 'email', 'unique:users,email'], ]); + $this->new_email = strtolower($this->new_email); + // Skip rate limiting in development mode if (! isDev()) { // Rate limit by current user's email (1 request per 2 minutes) @@ -90,7 +92,7 @@ class Index extends Component } // Rate limit by new email address (3 requests per hour per email) - $newEmailKey = 'email-change:email:'.md5(strtolower($this->new_email)); + $newEmailKey = 'email-change:email:'.md5($this->new_email); if (! RateLimiter::attempt($newEmailKey, 3, function () {}, 3600)) { $this->dispatch('error', 'This email address has received too many verification requests. Please try again later.'); diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 862dc20d8..ed15ab258 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -28,6 +28,9 @@ class Advanced extends Component #[Validate(['boolean'])] public bool $isPreviewDeploymentsEnabled = false; + #[Validate(['boolean'])] + public bool $isPrDeploymentsPublicEnabled = false; + #[Validate(['boolean'])] public bool $isAutoDeployEnabled = true; @@ -91,6 +94,7 @@ class Advanced extends Component $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; $this->application->settings->is_git_shallow_clone_enabled = $this->isGitShallowCloneEnabled; $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; + $this->application->settings->is_pr_deployments_public_enabled = $this->isPrDeploymentsPublicEnabled; $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; $this->application->settings->is_gpu_enabled = $this->isGpuEnabled; @@ -117,6 +121,7 @@ class Advanced extends Component $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; $this->isGitShallowCloneEnabled = $this->application->settings->is_git_shallow_clone_enabled ?? false; $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; + $this->isPrDeploymentsPublicEnabled = $this->application->settings->is_pr_deployments_public_enabled ?? false; $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; $this->isGpuEnabled = $this->application->settings->is_gpu_enabled; $this->gpuDriver = $this->application->settings->gpu_driver; diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index d05081d21..4b03c69e1 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -13,6 +13,7 @@ class ApplicationSetting extends Model 'is_force_https_enabled' => 'boolean', 'is_debug_enabled' => 'boolean', 'is_preview_deployments_enabled' => 'boolean', + 'is_pr_deployments_public_enabled' => 'boolean', 'is_git_submodules_enabled' => 'boolean', 'is_git_lfs_enabled' => 'boolean', 'is_git_shallow_clone_enabled' => 'boolean', diff --git a/app/Models/User.php b/app/Models/User.php index 48651d292..9ab9fefe9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -56,6 +56,22 @@ class User extends Authenticatable implements SendsEmail 'email_change_code_expires_at' => 'datetime', ]; + /** + * Set the email attribute to lowercase. + */ + public function setEmailAttribute($value) + { + $this->attributes['email'] = strtolower($value); + } + + /** + * Set the pending_email attribute to lowercase. + */ + public function setPendingEmailAttribute($value) + { + $this->attributes['pending_email'] = $value ? strtolower($value) : null; + } + protected static function boot() { parent::boot(); diff --git a/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php new file mode 100644 index 000000000..5d84ce42d --- /dev/null +++ b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php @@ -0,0 +1,28 @@ +boolean('is_pr_deployments_public_enabled')->default(false)->after('is_preview_deployments_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_pr_deployments_public_enabled'); + }); + } +}; diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 6dd5c872c..62d4380e9 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -13,6 +13,12 @@ helper="Allow to automatically deploy Preview Deployments for all opened PR's.

Closing a PR will delete Preview Deployments." instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update" :canResource="$application" /> + @if ($isPreviewDeploymentsEnabled) + + @endif @endif