From 2e0d4328867e312e70e3c204fe112b640b60838e Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 15:56:34 +0200 Subject: [PATCH 001/376] add backup config info to --- app/Http/Controllers/Api/DatabasesController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 504665f6a..452e24837 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Models\Project; +use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -78,7 +79,17 @@ public function databases(Request $request) foreach ($projects as $project) { $databases = $databases->merge($project->databases()); } - $databases = $databases->map(function ($database) { + + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); + $databases = $databases->map(function ($database) use ($backupConfig) { + $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + + if ($databaseBackupConfig) { + $database->backup_configs = $databaseBackupConfig; + } else { + $database->backup_configs = null; + } + return $this->removeSensitiveData($database); }); From da487f609acfd8966ff8393e3c77dba64f358858 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 20:59:20 +0200 Subject: [PATCH 002/376] implmenet `Get /database/:uuid/backups` api --- .../Controllers/Api/DatabasesController.php | 63 +++++++++++++++++++ routes/api.php | 5 ++ 2 files changed, 68 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 452e24837..de8daa43e 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -96,6 +96,69 @@ public function databases(Request $request) return response()->json($databases); } + #[OA\Get( + summary: 'Get', + description: 'Get database by UUID.', + path: '/databases/{uuid}/backups', + operationId: 'get-database-backups-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all backups for a database', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function database_backup_details_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + + return response()->json($this->removeSensitiveData($backupConfig)); + } + #[OA\Get( summary: 'Get', description: 'Get database by UUID.', diff --git a/routes/api.php b/routes/api.php index 8ac8aef14..409dd393f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,10 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); +Route::get('/test', function () { + return response()->json(['message' => 'test']); +}); + Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], 'prefix' => 'v1', @@ -110,6 +114,7 @@ Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); + Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); From 5dff22d3455146c7a46901da823d6c8a8c3c8d06 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 24 Apr 2025 16:48:08 +0200 Subject: [PATCH 003/376] implement backup config via api --- .../Controllers/Api/DatabasesController.php | 61 ++++++++++++++++++- routes/api.php | 3 - 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index de8daa43e..ab0191581 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -288,6 +288,19 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + // WIP + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], ], ), ) @@ -313,12 +326,14 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } + // this check if the request is a valid json $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -336,6 +351,18 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', + 'save_s3' => 'boolean', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -347,6 +374,7 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -545,7 +573,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -567,7 +595,36 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $database->update($request->all()); + $backupPayload = $request->only($allowedBackupConfigsFields); + $databasePayload = $request->only($allowedFields); + + if ($databasePayload) { + $database->update($databasePayload); + } + + if ($backupPayload && ! $backupConfig) { + if ($database->type() === 'standalone-postgresql') { + $backupPayload['databases_to_backup'] = $database->postgres_db; + } elseif ($database->type() === 'standalone-mysql') { + $backupPayload['databases_to_backup'] = $database->mysql_database; + } elseif ($database->type() === 'standalone-mariadb') { + $backupPayload['databases_to_backup'] = $database->mariadb_database; + } elseif ($database->type() === 'standalone-mongodbs') { + $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; + } + + $backupConfig = ScheduledDatabaseBackup::create([ + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => $teamId, + 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, + ...$backupPayload, + ]); + } + + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + } if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); diff --git a/routes/api.php b/routes/api.php index 409dd393f..326399f30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,9 +23,6 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); -Route::get('/test', function () { - return response()->json(['message' => 'test']); -}); Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], From 2a06a392d5174f278f20cf9533644d1e7fd2c747 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 11:46:02 +0200 Subject: [PATCH 004/376] Implement backup delete --- .../Controllers/Api/DatabasesController.php | 95 +++++++++++++++++++ routes/api.php | 1 + 2 files changed, 96 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ab0191581..a25b07bf2 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1750,6 +1750,101 @@ public function delete_by_uuid(Request $request) ]); } + #[OA\Delete( + summary: 'Delete backup', + description: 'Deletes a backup by its database UUID and backup ID.', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'delete-backup-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['backups'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + required: true, + description: 'UUID of the database to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'backup_id', + in: 'path', + required: true, + description: 'ID of the backup to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'delete_s3', + in: 'query', + required: false, + description: 'Whether to delete the backup from S3', + schema: new OA\Schema(type: 'boolean', default: false) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Backup deleted.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup deleted.'), + ] + ) + ), + new OA\Response( + response: 404, + description: 'Backup not found.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup not found.'), + ] + ) + ), + ] + )] + public function delete_backup_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + $backup = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); + if (! $backup) { + return response()->json(['message' => 'Backup not found.'], 404); + } + $execution = $backup->executions()->where('id', $request->backup_id)->first(); + if (! $execution) { + return response()->json(['message' => 'Execution not found.'], 404); + } + + $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); + + try { + if ($execution->filename) { + deleteBackupsLocally($execution->filename, $database->destination->server); + + if ($deleteS3 && $backup->s3) { + deleteBackupsS3($execution->filename, $backup->s3); + } + } + + $execution->delete(); + + return response()->json([ + 'message' => 'Backup deleted.', + ]); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + } + } + #[OA\Get( summary: 'Start', description: 'Start database. `Post` request is also accepted.', diff --git a/routes/api.php b/routes/api.php index 326399f30..1a1990513 100644 --- a/routes/api.php +++ b/routes/api.php @@ -114,6 +114,7 @@ Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']); From 81180af27d4f5870bd7e4253c7fd3804eeac2afb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:49:14 +0200 Subject: [PATCH 005/376] add ability to get backup now and get all schedule backup --- .../Controllers/Api/DatabasesController.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a25b07bf2..9d007939d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -9,6 +9,7 @@ use App\Actions\Database\StopDatabaseProxy; use App\Enums\NewDatabaseTypes; use App\Http\Controllers\Controller; +use App\Jobs\DatabaseBackupJob; use App\Jobs\DeleteResourceJob; use App\Models\Project; use App\Models\ScheduledDatabaseBackup; @@ -80,12 +81,11 @@ public function databases(Request $request) $databases = $databases->merge($project->databases()); } - $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); - $databases = $databases->map(function ($database) use ($backupConfig) { - $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + $databases = $databases->map(function ($database) { + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->where('database_id', $database->id)->get(); - if ($databaseBackupConfig) { - $database->backup_configs = $databaseBackupConfig; + if ($backupConfig) { + $database->backup_configs = $backupConfig; } else { $database->backup_configs = null; } @@ -98,7 +98,7 @@ public function databases(Request $request) #[OA\Get( summary: 'Get', - description: 'Get database by UUID.', + description: 'Get backups details by database UUID.', path: '/databases/{uuid}/backups', operationId: 'get-database-backups-by-uuid', security: [ @@ -291,6 +291,7 @@ public function database_by_uuid(Request $request) // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], @@ -326,7 +327,7 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -352,6 +353,7 @@ public function update_by_uuid(Request $request) 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', 'enabled' => 'boolean', 'dump_all' => 'boolean', 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', @@ -573,7 +575,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -620,10 +622,18 @@ public function update_by_uuid(Request $request) 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, ...$backupPayload, ]); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($backupPayload && $backupConfig) { $backupConfig->update($backupPayload); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($whatToDoWithDatabaseProxy === 'start') { From 71ff19e746e59619ed2975877ea0754ada07b5cb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:53:23 +0200 Subject: [PATCH 006/376] get all of the backups --- app/Http/Controllers/Api/DatabasesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9d007939d..9c04d1d42 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -154,7 +154,7 @@ public function database_backup_details_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); return response()->json($this->removeSensitiveData($backupConfig)); } From b4119fe012052f5d083c0d849d2f2942eca02f40 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 16:43:05 +0200 Subject: [PATCH 007/376] change the order of update --- .../Controllers/Api/DatabasesController.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9c04d1d42..389983920 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -604,6 +604,15 @@ public function update_by_uuid(Request $request) $database->update($databasePayload); } + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + + if ($request->backup_now) { + dd('test'); + DatabaseBackupJob::dispatch($backupConfig); + } + } + if ($backupPayload && ! $backupConfig) { if ($database->type() === 'standalone-postgresql') { $backupPayload['databases_to_backup'] = $database->postgres_db; @@ -628,14 +637,6 @@ public function update_by_uuid(Request $request) } } - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); } elseif ($whatToDoWithDatabaseProxy === 'stop') { From 166e5ad2271479b8ea6d8d7ea1a849fed85d0aad Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 17:20:48 +0200 Subject: [PATCH 008/376] remove dd --- app/Http/Controllers/Api/DatabasesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 389983920..4f62da8bf 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -608,7 +608,6 @@ public function update_by_uuid(Request $request) $backupConfig->update($backupPayload); if ($request->backup_now) { - dd('test'); DatabaseBackupJob::dispatch($backupConfig); } } From be104cd612cdf3e13523c0077bb4273cb95687a5 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:36:14 +0200 Subject: [PATCH 009/376] feat(api): add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id --- .../Controllers/Api/DatabasesController.php | 153 +++++++++++++++++- routes/api.php | 1 + 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 4f62da8bf..7172e5aae 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -156,7 +156,7 @@ public function database_backup_details_uuid(Request $request) $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); - return response()->json($this->removeSensitiveData($backupConfig)); + return response()->json($backupConfig); } #[OA\Get( @@ -288,7 +288,6 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], - // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], @@ -647,6 +646,156 @@ public function update_by_uuid(Request $request) ]); } + #[OA\Patch( + summary: 'Update', + description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'update-database-backup-config-by-uuid-and-backup-id', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'backup_id', + in: 'path', + description: 'ID of the backup configuration.', + required: true, + schema: new OA\Schema( + type: 'integer', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Database backup configuration data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database backup configuration updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_backup_config_by_uuid_and_backup_id(Request $request) + { + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + // this check if the request is a valid json + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('id', $request->backup_id) + ->first(); + if (! $backupConfig) { + return response()->json(['message' => 'Backup config not found.'], 404); + } + + $extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']); + if (! empty($extraFields)) { + $errors = $validator->errors(); + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $backupConfig->update($request->only($backupConfigFields)); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } + + return response()->json([ + 'message' => 'Database backup configuration updated', + ]); + } + #[OA\Post( summary: 'Create (PostgreSQL)', description: 'Create a new PostgreSQL database.', diff --git a/routes/api.php b/routes/api.php index 1a1990513..a5abe4b98 100644 --- a/routes/api.php +++ b/routes/api.php @@ -113,6 +113,7 @@ Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::patch('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'update_backup_config_by_uuid_and_backup_id'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); From 2bf6a9cb2c324715b19d87e88babfba1ebc7ca30 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:39:36 +0200 Subject: [PATCH 010/376] undo changes to update_by_uuid method --- .../Controllers/Api/DatabasesController.php | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 7172e5aae..4fa42c37d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -326,7 +326,6 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -351,19 +350,6 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', - 'save_s3' => 'boolean', - 'backup_now' => 'boolean|nullable', - 'enabled' => 'boolean', - 'dump_all' => 'boolean', - 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', - 'databases_to_backup' => 'string', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', - 'database_backup_retention_amount_locally' => 'integer|min:0', - 'database_backup_retention_days_locally' => 'integer|min:0', - 'database_backup_retention_max_storage_locally' => 'integer|min:0', - 'database_backup_retention_amount_s3' => 'integer|min:0', - 'database_backup_retention_days_s3' => 'integer|min:0', - 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -375,7 +361,6 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); - $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -574,7 +559,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -596,44 +581,7 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $backupPayload = $request->only($allowedBackupConfigsFields); - $databasePayload = $request->only($allowedFields); - - if ($databasePayload) { - $database->update($databasePayload); - } - - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - - if ($backupPayload && ! $backupConfig) { - if ($database->type() === 'standalone-postgresql') { - $backupPayload['databases_to_backup'] = $database->postgres_db; - } elseif ($database->type() === 'standalone-mysql') { - $backupPayload['databases_to_backup'] = $database->mysql_database; - } elseif ($database->type() === 'standalone-mariadb') { - $backupPayload['databases_to_backup'] = $database->mariadb_database; - } elseif ($database->type() === 'standalone-mongodbs') { - $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; - } - - $backupConfig = ScheduledDatabaseBackup::create([ - 'database_id' => $database->id, - 'database_type' => $database->getMorphClass(), - 'team_id' => $teamId, - 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, - ...$backupPayload, - ]); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } + $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); From 7a110880c1e7bc36b4a841890912799746310945 Mon Sep 17 00:00:00 2001 From: jvdboog <110812872+jvdboog@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:15:42 +0200 Subject: [PATCH 011/376] feat: Improve detection of special network modes --- bootstrap/helpers/shared.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7ce511f2c..4e77b35c3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -614,10 +614,14 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetwork = collect([$resource->uuid]); $services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) { $serviceNetworks = collect(data_get($service, 'networks', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); - // Only add 'networks' key if 'network_mode' is not 'host' - if (! $hasHostNetworkMode) { + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + + // Only add 'networks' key if 'network_mode' is not 'host' or does not start with 'service:' or 'container:' + if (! $hasValidNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -1502,7 +1506,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceNetworks = collect(data_get($service, 'networks', [])); $serviceVariables = collect(data_get($service, 'environment', [])); $serviceLabels = collect(data_get($service, 'labels', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); + + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { @@ -1613,7 +1622,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (! $hasHostNetworkMode) { + if (! $hasValidNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; From 4024d7f59bbf9f4331c5c86d1ed14b1a08ef7af6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:42:22 +0200 Subject: [PATCH 012/376] chore: update coolify version to 4.0.0-beta.424 and nightly version to 4.0.0-beta.425 --- config/constants.php | 2 +- versions.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index da373d13d..770d9ba3f 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.423', + 'version' => '4.0.0-beta.424', 'helper_version' => '1.0.10', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index 8697712a8..6bcde3990 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.423" + "version": "4.0.0-beta.424" }, "nightly": { - "version": "4.0.0-beta.424" + "version": "4.0.0-beta.425" }, "helper": { "version": "1.0.10" From efc12648106942ecd0f7be1a5e703b2d85074e8c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:22:04 +0200 Subject: [PATCH 013/376] fix(parsers): do not modify service names, only for getting fqdns and related envs --- bootstrap/helpers/parsers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 6e2b55ae9..f7041c3da 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -858,8 +858,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($resource->build_pack !== 'dockercompose') { $domains = collect([]); } - $serviceName = str($serviceName)->replace('-', '_')->value(); - $fqdns = data_get($domains, "$serviceName.domain"); + $changedServiceName = str($serviceName)->replace('-', '_')->value(); + $fqdns = data_get($domains, "$changedServiceName.domain"); // Generate SERVICE_FQDN & SERVICE_URL for dockercompose if ($resource->build_pack === 'dockercompose') { foreach ($domains as $forServiceName => $domain) { From 754f78bcfd59044b70960f1cb20560c625a1f2b0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:36:05 +0200 Subject: [PATCH 014/376] fix(compose): temporary allow to edit volumes in apps (compose based) and services --- .../views/livewire/project/shared/storages/all.blade.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/project/shared/storages/all.blade.php b/resources/views/livewire/project/shared/storages/all.blade.php index bd377e3b2..4ed1d1b52 100644 --- a/resources/views/livewire/project/shared/storages/all.blade.php +++ b/resources/views/livewire/project/shared/storages/all.blade.php @@ -3,11 +3,10 @@ @foreach ($resource->persistentStorages as $storage) @if ($resource->type() === 'service') + :resource="$resource" :isFirst="$loop->first" isService='true' /> @else @endif @endforeach From d04dbf910b2e3e865323f1d792aa07643a819336 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Aug 2025 19:37:02 +0000 Subject: [PATCH 015/376] docs: update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7003669db..c9582cd2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ # Changelog ## [unreleased] +### 🐛 Bug Fixes + +- *(parsers)* Do not modify service names, only for getting fqdns and related envs +- *(compose)* Temporary allow to edit volumes in apps (compose based) and services + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- Update coolify version to 4.0.0-beta.424 and nightly version to 4.0.0-beta.425 + +## [4.0.0-beta.423] - 2025-08-27 + ### 🚜 Refactor - *(parsers)* Remove unnecessary hyphen-to-underscore replacement for service names in serviceParser function From 2e85ce0e0e251e38523f9c7a0509f1b355d3e27e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:49:58 +0200 Subject: [PATCH 016/376] refactor(urls): replace generateFqdn with generateUrl for consistent URL generation across applications --- app/Livewire/Project/Application/General.php | 2 +- app/Livewire/Project/Application/PreviewsCompose.php | 2 +- app/Livewire/Project/CloneMe.php | 2 +- app/Livewire/Project/New/DockerImage.php | 2 +- app/Livewire/Project/New/GithubPrivateRepository.php | 2 +- .../Project/New/GithubPrivateRepositoryDeployKey.php | 2 +- app/Livewire/Project/New/PublicGitRepository.php | 2 +- app/Livewire/Project/New/SimpleDockerfile.php | 2 +- app/Livewire/Project/Shared/ResourceOperations.php | 2 +- bootstrap/helpers/docker.php | 8 ++++---- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 3107ef4cb..2c8ebb9e2 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -430,7 +430,7 @@ public function getWildcardDomain() $server = data_get($this->application, 'destination.server'); if ($server) { - $fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version); + $fqdn = generateUrl(server: $server, random: $this->application->uuid); $this->application->fqdn = $fqdn; $this->application->save(); $this->resetDefaultLabels(); diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index 0317ba7e7..2632509ea 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -60,7 +60,7 @@ public function generate() $random = new Cuid2; // Generate a unique domain like main app services do - $generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version); + $generated_fqdn = generateUrl(server: $server, random: $random); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 3c8c9843d..be9de139f 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -133,7 +133,7 @@ public function clone(string $type) $uuid = (string) new Cuid2; $url = $application->fqdn; if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version); + $url = generateUrl(server: $this->server, random: $uuid); } $newApplication = $application->replicate([ diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 7d68ce068..dbb223de2 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -60,7 +60,7 @@ public function submit() 'health_check_enabled' => false, ]); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ 'name' => 'docker-image-'.$application->uuid, 'fqdn' => $fqdn, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index a7aaa94a4..0f496e6db 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -208,7 +208,7 @@ public function submit() $application['docker_compose_location'] = $this->docker_compose_location; $application['base_directory'] = $this->base_directory; } - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid); diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index d76f7baaa..5ff8f9137 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -194,7 +194,7 @@ public function submit() $application->settings->is_static = $this->is_static; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->name = generate_random_name($application->uuid); $application->save(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 8de998a96..f5978aea1 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -373,7 +373,7 @@ public function submit() $application->settings->is_static = $this->isStatic; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->save(); if ($this->checkCoolifyConfig) { diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index ebc9878dc..9cc4fbbe2 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -68,7 +68,7 @@ public function submit() 'source_type' => GithubApp::class, ]); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ 'name' => 'dockerfile-'.$application->uuid, 'fqdn' => $fqdn, diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index c9b341eed..28a6380d5 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -66,7 +66,7 @@ public function cloneTo($destination_id) $url = $this->resource->fqdn; if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version); + $url = generateUrl(server: $server, random: $uuid); } $new_resource = $this->resource->replicate([ diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index ac707f7ab..f61abc806 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) { $MINIO_BROWSER_REDIRECT_URL->update([ - 'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), + 'value' => generateUrl(server: $server, random: 'console-'.$uuid, forceHttps: true), ]); } if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) { $MINIO_SERVER_URL->update([ - 'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), + 'value' => generateUrl(server: $server, random: 'minio-'.$uuid, forceHttps: true), ]); } $payload = collect([ @@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ENDPOINT->update([ - 'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->service->compose_parsing_version), + 'value' => generateUrl(server: $server, random: 'logto-'.$uuid), ]); } if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ADMIN_ENDPOINT->update([ - 'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->service->compose_parsing_version), + 'value' => generateUrl(server: $server, random: 'logto-admin-'.$uuid), ]); } $payload = collect([ From c156d5c705f8874278983e48878eef23cf1714c4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:50:16 +0200 Subject: [PATCH 017/376] chore: update coolify version to 4.0.0-beta.425 and nightly version to 4.0.0-beta.426 --- config/constants.php | 2 +- versions.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index 770d9ba3f..44b51b978 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.424', + 'version' => '4.0.0-beta.425', 'helper_version' => '1.0.10', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index 6bcde3990..41f06d69b 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.424" + "version": "4.0.0-beta.425" }, "nightly": { - "version": "4.0.0-beta.425" + "version": "4.0.0-beta.426" }, "helper": { "version": "1.0.10" From e0aa28ba312520e6d89dcd38573791a9684bd6c0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:00:19 +0200 Subject: [PATCH 018/376] refactor(domains): rename check_domain_usage to checkDomainUsage and update references across the application --- app/Livewire/Project/Application/General.php | 4 +- app/Livewire/Project/Application/Previews.php | 2 +- app/Livewire/Project/Service/EditDomain.php | 2 +- .../Service/ServiceApplicationView.php | 2 +- app/Livewire/Settings/Index.php | 2 +- bootstrap/helpers/domains.php | 78 +++++++++++++++++++ bootstrap/helpers/shared.php | 73 ----------------- 7 files changed, 84 insertions(+), 79 deletions(-) create mode 100644 bootstrap/helpers/domains.php diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 2c8ebb9e2..67731c87d 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -485,7 +485,7 @@ public function checkFqdns($showToaster = true) } } } - check_domain_usage(resource: $this->application); + checkDomainUsage(resource: $this->application); $this->application->fqdn = $domains->implode(','); $this->resetDefaultLabels(false); } @@ -588,7 +588,7 @@ public function submit($showToaster = true) } } } - check_domain_usage(resource: $this->application); + checkDomainUsage(resource: $this->application); $this->application->save(); $this->resetDefaultLabels(); } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 9164c1475..cc3c5ea46 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -63,7 +63,7 @@ public function save_preview($preview_id) $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$preview->fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); $success = false; } - check_domain_usage(resource: $this->application, domain: $preview->fqdn); + checkDomainUsage(resource: $this->application, domain: $preview->fqdn); } if (! $preview) { diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index b7f73159e..1b24dc23a 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -37,7 +37,7 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - check_domain_usage(resource: $this->application); + checkDomainUsage(resource: $this->application); $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 5e178374b..80fe59891 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -145,7 +145,7 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - check_domain_usage(resource: $this->application); + checkDomainUsage(resource: $this->application); $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index bce343224..98e5ce8bd 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -108,7 +108,7 @@ public function submit() } } if ($this->fqdn) { - check_domain_usage(domain: $this->fqdn); + checkDomainUsage(domain: $this->fqdn); } $this->instantSave(isSave: false); diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php new file mode 100644 index 000000000..36e044344 --- /dev/null +++ b/bootstrap/helpers/domains.php @@ -0,0 +1,78 @@ +getMorphClass() === Application::class && $resource->build_pack === 'dockercompose') { + $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); + $domains = collect($domains); + } else { + $domains = collect($resource->fqdns); + } + } elseif ($domain) { + $domains = collect([$domain]); + } else { + throw new \RuntimeException('No resource or FQDN provided.'); + } + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $apps = Application::all(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + } + } elseif ($domain) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + } + } + } + } + $apps = ServiceApplication::all(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + } + } elseif ($domain) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + } + } + } + } + if ($resource) { + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); + } + } + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7a9b5df80..4743a811b 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1157,79 +1157,6 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = } } } -function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) -{ - if ($resource) { - if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') { - $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); - $domains = collect($domains); - } else { - $domains = collect($resource->fqdns); - } - } elseif ($domain) { - $domains = collect($domain); - } else { - throw new \RuntimeException('No resource or FQDN provided.'); - } - $domains = $domains->map(function ($domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - - return str($domain); - }); - $apps = Application::all(); - foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - if (data_get($resource, 'uuid')) { - if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); - } - } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); - } - } - } - } - $apps = ServiceApplication::all(); - foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - if (data_get($resource, 'uuid')) { - if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); - } - } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); - } - } - } - } - if ($resource) { - $settings = instanceSettings(); - if (data_get($settings, 'fqdn')) { - $domain = data_get($settings, 'fqdn'); - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); - } - } - } -} function parseCommandsByLineForSudo(Collection $commands, Server $server): array { From 7fe6a4198d0fc250fe6876f0a18e742a7cef1fc0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:11:56 +0200 Subject: [PATCH 019/376] fix(previews): simplify FQDN generation logic by removing unnecessary empty check --- app/Models/ApplicationPreview.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index aa31268f1..721b22216 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -74,7 +74,7 @@ public function persistentStorages() public function generate_preview_fqdn() { - if (empty($this->fqdn) && $this->application->fqdn) { + if ($this->application->fqdn) { if (str($this->application->fqdn)->contains(',')) { $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); $preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]); From 643343785a40f79f73eb7cd374e563d6f61235cc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:48:24 +0200 Subject: [PATCH 020/376] refactor(auth): simplify access control logic in CanAccessTerminal and ServerPolicy by allowing all users to perform actions --- app/Http/Middleware/CanAccessTerminal.php | 20 +++++++--------- app/Policies/ServerPolicy.php | 29 +++++++++++------------ app/Providers/AuthServiceProvider.php | 3 +-- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/app/Http/Middleware/CanAccessTerminal.php b/app/Http/Middleware/CanAccessTerminal.php index dcccd819b..348f389ea 100644 --- a/app/Http/Middleware/CanAccessTerminal.php +++ b/app/Http/Middleware/CanAccessTerminal.php @@ -15,17 +15,15 @@ class CanAccessTerminal */ public function handle(Request $request, Closure $next): Response { + if (! auth()->check()) { + abort(401, 'Authentication required'); + } + + // Only admins/owners can access terminal functionality + if (! auth()->user()->can('canAccessTerminal')) { + abort(403, 'Access to terminal functionality is restricted to team administrators'); + } + return $next($request); - - // if (! auth()->check()) { - // abort(401, 'Authentication required'); - // } - - // // Only admins/owners can access terminal functionality - // if (! auth()->user()->can('canAccessTerminal')) { - // abort(403, 'Access to terminal functionality is restricted to team administrators'); - // } - - // return $next($request); } } diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 5cc6b739f..6d2396a7d 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -28,7 +28,8 @@ public function view(User $user, Server $server): bool */ public function create(User $user): bool { - return $user->isAdmin(); + // return $user->isAdmin(); + return true; } /** @@ -36,7 +37,8 @@ public function create(User $user): bool */ public function update(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -44,7 +46,8 @@ public function update(User $user, Server $server): bool */ public function delete(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -68,7 +71,8 @@ public function forceDelete(User $user, Server $server): bool */ public function manageProxy(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -76,7 +80,8 @@ public function manageProxy(User $user, Server $server): bool */ public function manageSentinel(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -84,15 +89,8 @@ public function manageSentinel(User $user, Server $server): bool */ public function manageCaCertificate(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - } - - /** - * Determine whether the user can view terminal. - */ - public function viewTerminal(User $user, Server $server): bool - { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -100,6 +98,7 @@ public function viewTerminal(User $user, Server $server): bool */ public function viewSecurity(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3e76e6976..c017a580e 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -67,8 +67,7 @@ public function boot(): void // Register gate for terminal access Gate::define('canAccessTerminal', function ($user) { - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); }); } } From 80499a03d82a6030231fe042ca268b0e89060a16 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:52:41 +0200 Subject: [PATCH 021/376] feat(domains): implement domain conflict detection and user confirmation modal across application components --- app/Livewire/Project/Application/General.php | 51 ++++++++++- app/Livewire/Project/Application/Previews.php | 33 ++++++- app/Livewire/Project/Service/EditDomain.php | 28 +++++- .../Service/ServiceApplicationView.php | 28 +++++- .../Shared/ExecuteContainerCommand.php | 3 - app/Livewire/Settings/Index.php | 26 +++++- bootstrap/helpers/domains.php | 77 ++++++++++++++-- .../domain-conflict-modal.blade.php | 91 +++++++++++++++++++ .../project/application/general.blade.php | 6 ++ .../project/application/previews.blade.php | 15 +++ .../livewire/project/resource/index.blade.php | 2 +- .../project/service/edit-domain.blade.php | 28 ++++-- .../service-application-view.blade.php | 14 +++ .../views/livewire/settings/index.blade.php | 14 +++ 14 files changed, 390 insertions(+), 26 deletions(-) create mode 100644 resources/views/components/domain-conflict-modal.blade.php diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 67731c87d..aa72b7c5f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -51,9 +51,16 @@ class General extends Component public $parsedServiceDomains = []; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $listeners = [ 'resetDefaultLabels', 'configurationChanged' => '$refresh', + 'confirmDomainUsage', ]; protected function rules(): array @@ -485,10 +492,33 @@ public function checkFqdns($showToaster = true) } } } - checkDomainUsage(resource: $this->application); + + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return false; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->application->fqdn = $domains->implode(','); $this->resetDefaultLabels(false); } + + return true; + } + + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); } public function setRedirect() @@ -536,7 +566,9 @@ public function submit($showToaster = true) $this->application->parseHealthcheckFromDockerfile($this->application->dockerfile); } - $this->checkFqdns(); + if (! $this->checkFqdns()) { + return; // Stop if there are conflicts and user hasn't confirmed + } $this->application->save(); if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { @@ -588,7 +620,20 @@ public function submit($showToaster = true) } } } - checkDomainUsage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->application->save(); $this->resetDefaultLabels(); } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index cc3c5ea46..ebfd84489 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -25,6 +25,14 @@ class Previews extends Component public int $rate_limit_remaining; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + + public $pendingPreviewId = null; + protected $rules = [ 'application.previews.*.fqdn' => 'string|nullable', ]; @@ -49,6 +57,16 @@ public function load_prs() } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + if ($this->pendingPreviewId) { + $this->save_preview($this->pendingPreviewId); + $this->pendingPreviewId = null; + } + } + public function save_preview($preview_id) { try { @@ -63,7 +81,20 @@ public function save_preview($preview_id) $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$preview->fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); $success = false; } - checkDomainUsage(resource: $this->application, domain: $preview->fqdn); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + $this->pendingPreviewId = $preview_id; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } } if (! $preview) { diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 1b24dc23a..5ce170b99 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -12,6 +12,12 @@ class EditDomain extends Component public ServiceApplication $application; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $rules = [ 'application.fqdn' => 'nullable', 'application.required_fqdn' => 'required|boolean', @@ -22,6 +28,13 @@ public function mount() $this->application = ServiceApplication::find($this->applicationId); } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -37,7 +50,20 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - checkDomainUsage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 80fe59891..3ac12cfe9 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -23,6 +23,12 @@ class ServiceApplicationView extends Component public $delete_volumes = true; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $rules = [ 'application.human_name' => 'nullable', 'application.description' => 'nullable', @@ -129,6 +135,13 @@ public function convertToDatabase() } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -145,7 +158,20 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - checkDomainUsage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 6833492a6..02062e1f7 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -33,9 +33,6 @@ class ExecuteContainerCommand extends Component public function mount() { - if (! auth()->user()->isAdmin()) { - abort(403); - } $this->parameters = get_route_parameters(); $this->containers = collect(); $this->servers = collect(); diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 98e5ce8bd..d05433082 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -35,6 +35,12 @@ class Index extends Component #[Validate('required|string|timezone')] public string $instance_timezone; + public array $domainConflicts = []; + + public bool $showDomainConflictModal = false; + + public bool $forceSaveDomains = false; + public function render() { return view('livewire.settings.index'); @@ -81,6 +87,13 @@ public function instantSave($isSave = true) } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -108,7 +121,18 @@ public function submit() } } if ($this->fqdn) { - checkDomainUsage(domain: $this->fqdn); + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(domain: $this->fqdn); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } } $this->instantSave(isSave: false); diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index 36e044344..b562873b5 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -5,6 +5,14 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { + $conflicts = []; + + // Get the current team for filtering + $currentTeam = null; + if ($resource) { + $currentTeam = $resource->team(); + } + if ($resource) { if ($resource->getMorphClass() === Application::class && $resource->build_pack === 'dockercompose') { $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); @@ -15,8 +23,9 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, } elseif ($domain) { $domains = collect([$domain]); } else { - throw new \RuntimeException('No resource or FQDN provided.'); + return ['conflicts' => [], 'hasConflicts' => false]; } + $domains = $domains->map(function ($domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -24,7 +33,15 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, return str($domain); }); - $apps = Application::all(); + + // Filter applications by team if we have a current team + $appsQuery = Application::query(); + if ($currentTeam) { + $appsQuery = $appsQuery->whereHas('environment.project', function ($query) use ($currentTeam) { + $query->where('team_id', $currentTeam->id); + }); + } + $apps = $appsQuery->get(); foreach ($apps as $app) { $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { @@ -35,15 +52,35 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_link' => $app->link(), + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_link' => $app->link(), + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; } } } } - $apps = ServiceApplication::all(); + + // Filter service applications by team if we have a current team + $serviceAppsQuery = ServiceApplication::query(); + if ($currentTeam) { + $serviceAppsQuery = $serviceAppsQuery->whereHas('service.environment.project', function ($query) use ($currentTeam) { + $query->where('team_id', $currentTeam->id); + }); + } + $apps = $serviceAppsQuery->get(); foreach ($apps as $app) { $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { @@ -54,14 +91,27 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name, + 'resource_link' => $app->service->link(), + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name, + 'resource_link' => $app->service->link(), + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; } } } } + if ($resource) { $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { @@ -71,8 +121,19 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null, } $naked_domain = str($domain)->value(); if ($domains->contains($naked_domain)) { - throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => 'Coolify Instance', + 'resource_link' => '#', + 'resource_type' => 'instance', + 'message' => "Domain $naked_domain is already in use by this Coolify instance", + ]; } } } + + return [ + 'conflicts' => $conflicts, + 'hasConflicts' => count($conflicts) > 0, + ]; } diff --git a/resources/views/components/domain-conflict-modal.blade.php b/resources/views/components/domain-conflict-modal.blade.php new file mode 100644 index 000000000..218a7ef16 --- /dev/null +++ b/resources/views/components/domain-conflict-modal.blade.php @@ -0,0 +1,91 @@ +@props([ + 'conflicts' => [], + 'showModal' => false, + 'confirmAction' => 'confirmDomainUsage', +]) + +@if ($showModal && count($conflicts) > 0) +
+ +
+@endif diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index b833fc7bb..315385593 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -462,6 +462,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" + + + @script

Memory (MB)

@@ -195,65 +207,80 @@ class="pt-5"> }, series: [{ name: "Memory (MB)", - data: [] - }], - noData: { - text: 'Loading...', - style: { - color: textColor, - } - }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, - legend: { - show: false - } + data: [] + }], + noData: { + text: 'Loading...', + style: { + color: textColor, + } + }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '
' + + '
Memory: ' + value + ' MB
' + + '
' + timeString + '
' + + '
'; + } + }, + legend: { + show: false + } } - const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`), - optionsServerMemory); - serverMemoryChart.render(); - document.addEventListener('livewire:init', () => { - Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { - checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [ramColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - yaxis: { - min: 0, - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - noData: { - text: 'Loading...', - style: { - color: textColor, - } - } - }); - }); - }); + const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`), + optionsServerMemory); + serverMemoryChart.render(); + Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { + checkTheme(); + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + yaxis: { + min: 0, + show: true, + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + noData: { + text: 'Loading...', + style: { + color: textColor, + } + } + }); + }); + @endif @endif + diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index f5a2418fd..2cb8e2c37 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -80,12 +80,27 @@ color: textColor, } }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '
' + + '
CPU: ' + value + '%
' + + '
' + timeString + '
' + + '
'; + } + }, legend: { show: false } @@ -198,12 +213,27 @@ color: textColor, } }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '
' + + '
Memory: ' + value + '%
' + + '
' + timeString + '
' + + '
'; + } + }, legend: { show: false } From 610ef310341d7bc7384349f80f787b0b9ce3c41e Mon Sep 17 00:00:00 2001 From: ShadowArcanist Date: Fri, 19 Sep 2025 22:51:24 +0530 Subject: [PATCH 206/376] Hidden metrics charts grid borders on darkmode (it was too bright on darkmode) --- resources/css/utilities.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 65869e02f..694ad61a3 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -6,6 +6,10 @@ @utility apexcharts-tooltip-title { @apply hidden!; } +@utility apexcharts-grid-borders { + @apply dark:hidden!; +} + @utility apexcharts-xaxistooltip { @apply hidden!; } From a0f4566580eb982705f5ceba3efff8faddddce16 Mon Sep 17 00:00:00 2001 From: ShadowArcanist Date: Fri, 19 Sep 2025 22:55:25 +0530 Subject: [PATCH 207/376] Fixed Memory title on app metrics being larger than CPU title --- resources/views/livewire/project/shared/metrics.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 9b08babb3..84e4595aa 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -148,7 +148,7 @@ class="pt-5"> }); -

Memory (MB)

+

Memory (MB)

-

Memory (MB)

+

Memory Usage

-

Memory (%)

+

Memory Usage