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