From 2e0d4328867e312e70e3c204fe112b640b60838e Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 15:56:34 +0200 Subject: [PATCH 01/48] 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 02/48] 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 03/48] 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 04/48] 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 05/48] 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 06/48] 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 07/48] 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 08/48] 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 09/48] 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 10/48] 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 11/48] 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 bfc8a25b726102f79776a900cdb41e793c240316 Mon Sep 17 00:00:00 2001 From: QarthO Date: Fri, 29 Aug 2025 09:09:03 -0400 Subject: [PATCH 12/48] move domain trimming before URL validation --- app/Livewire/Project/Application/General.php | 3 ++- app/Livewire/Project/Service/EditDomain.php | 3 ++- app/Livewire/Project/Service/ServiceApplicationView.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index aa72b7c5f..c55afb589 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -547,9 +547,10 @@ public function submit($showToaster = true) $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 5ce170b99..7c718393d 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -41,9 +41,10 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $warning = sslipDomainWarning($this->application->fqdn); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 3ac12cfe9..e37b6ad86 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -149,9 +149,10 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $warning = sslipDomainWarning($this->application->fqdn); From c0ffda37f285598a448e90f1a10dc174f4abc3ef Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 31 Aug 2025 22:32:30 -0400 Subject: [PATCH 13/48] remove ~ from forbidden characters in git URLs --- app/Rules/ValidGitRepositoryUrl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Rules/ValidGitRepositoryUrl.php b/app/Rules/ValidGitRepositoryUrl.php index 3cbe9246e..c7ea208cc 100644 --- a/app/Rules/ValidGitRepositoryUrl.php +++ b/app/Rules/ValidGitRepositoryUrl.php @@ -31,7 +31,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void $dangerousChars = [ ';', '|', '&', '$', '`', '(', ')', '{', '}', '[', ']', '<', '>', '\n', '\r', '\0', '"', "'", - '\\', '!', '?', '*', '~', '^', '%', '=', '+', + '\\', '!', '?', '*', '^', '%', '=', '+', '#', // Comment character that could hide commands ]; From 758fe18d79be34768084ecced1020ff46a884370 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Sep 2025 13:01:03 -0400 Subject: [PATCH 14/48] oops missed a check --- app/Rules/ValidGitRepositoryUrl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Rules/ValidGitRepositoryUrl.php b/app/Rules/ValidGitRepositoryUrl.php index c7ea208cc..d549961dc 100644 --- a/app/Rules/ValidGitRepositoryUrl.php +++ b/app/Rules/ValidGitRepositoryUrl.php @@ -85,7 +85,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void } // Validate SSH URL format (git@host:user/repo.git) - if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.]+$/', $value)) { + if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.~]+$/', $value)) { $fail('The :attribute is not a valid SSH repository URL.'); return; From 0f030c5e546bcf6e149f85c01fb7e57117c89d0c Mon Sep 17 00:00:00 2001 From: Terijaki <590522+terijaki@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:28:37 +0200 Subject: [PATCH 15/48] Change favicon image type to PNG and SVG Changing to the correct type. Incorrect type can cause issues with certain browsers. --- resources/views/layouts/base.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index ebb134324..af8353078 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -35,9 +35,9 @@ @endphp {{ $name }}{{ $title ?? 'Coolify' }} @env('local') - + @else - + @endenv @vite(['resources/js/app.js', 'resources/css/app.css']) From 844a67a006c3478f6bc31840476fbcee895169cb Mon Sep 17 00:00:00 2001 From: Yihang Wang Date: Thu, 18 Sep 2025 00:40:09 +0800 Subject: [PATCH 16/48] fix: hide sensitive email change fields in team member responses --- app/Http/Controllers/Api/TeamController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index d4b24d8ab..e12d83542 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -179,6 +179,8 @@ public function members_by_id(Request $request) $members = $team->members; $members->makeHidden([ 'pivot', + 'email_change_code', + 'email_change_code_expires_at', ]); return response()->json( @@ -264,6 +266,8 @@ public function current_team_members(Request $request) $team = auth()->user()->currentTeam(); $team->members->makeHidden([ 'pivot', + 'email_change_code', + 'email_change_code_expires_at', ]); return response()->json( From 65f24de101b4764505662534ec83ded4bc40d57e Mon Sep 17 00:00:00 2001 From: ShadowArcanist Date: Fri, 19 Sep 2025 16:26:11 +0530 Subject: [PATCH 17/48] Changed Sentinel metrics color from yellow to blue + cyan (cpu + memory) --- resources/views/layouts/base.blade.php | 9 ++- .../livewire/project/shared/metrics.blade.php | 74 ++++++++++--------- .../views/livewire/server/charts.blade.php | 60 +++++++-------- 3 files changed, 75 insertions(+), 68 deletions(-) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index ebb134324..c074412d3 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -138,7 +138,8 @@ } } let theme = localStorage.theme - let baseColor = '#FCD452' + let cpuColor = '#1e90ff' + let ramColor = '#00ced1' let textColor = '#ffffff' let editorBackground = '#181818' let editorTheme = 'blackboard' @@ -149,12 +150,14 @@ function checkTheme() { theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } if (theme == 'dark') { - baseColor = '#FCD452' + cpuColor = '#1e90ff' + ramColor = '#00ced1' textColor = '#ffffff' editorBackground = '#181818' editorTheme = 'blackboard' } else { - baseColor = 'black' + cpuColor = '#1e90ff' + ramColor = '#00ced1' textColor = '#000000' editorBackground = '#ffffff' editorTheme = null diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index cfe83ded6..d6609d9e6 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -34,6 +34,7 @@ class="pt-5"> const optionsServerCpu = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -68,16 +69,16 @@ class="pt-5"> enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - }, - series: [{ - name: "CPU %", + grid: { + show: true, + borderColor: '', + }, + colors: [cpuColor], + xaxis: { + type: 'datetime', + }, + series: [{ + name: "CPU %", data: [] }], noData: { @@ -101,11 +102,11 @@ class="pt-5"> document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], xaxis: { type: 'datetime', labels: { @@ -143,6 +144,7 @@ class="pt-5"> const optionsServerMemory = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -177,22 +179,22 @@ class="pt-5"> enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - series: [{ - name: "Memory (MB)", + grid: { + show: true, + borderColor: '', + }, + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + series: [{ + name: "Memory (MB)", data: [] }], noData: { @@ -217,11 +219,11 @@ class="pt-5"> document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], xaxis: { type: 'datetime', labels: { diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index b84e0240f..f5a2418fd 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -27,6 +27,7 @@ const optionsServerCpu = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -61,16 +62,16 @@ enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - }, - series: [{ - name: 'CPU %', + grid: { + show: true, + borderColor: '', + }, + colors: [cpuColor], + xaxis: { + type: 'datetime', + }, + series: [{ + name: 'CPU %', data: [] }], noData: { @@ -95,11 +96,11 @@ document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], xaxis: { type: 'datetime', labels: { @@ -138,6 +139,7 @@ const optionsServerMemory = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -172,15 +174,15 @@ enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - labels: { - show: true, + grid: { + show: true, + borderColor: '', + }, + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, style: { colors: textColor, } @@ -212,11 +214,11 @@ document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], xaxis: { type: 'datetime', labels: { From 0ef0247e14ac0f5a808b9a21600070fe0dc3917f Mon Sep 17 00:00:00 2001 From: ShadowArcanist Date: Fri, 19 Sep 2025 22:40:08 +0530 Subject: [PATCH 18/48] Improved metrics graph tooltip to show usage in a better way and added timestamp to the tooltip --- app/Livewire/Project/Shared/Metrics.php | 12 +- resources/css/utilities.css | 17 ++ .../livewire/project/shared/metrics.blade.php | 277 ++++++++++-------- .../views/livewire/server/charts.blade.php | 54 +++- 4 files changed, 222 insertions(+), 138 deletions(-) diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php index fdc35fc0f..9dc944f9d 100644 --- a/app/Livewire/Project/Shared/Metrics.php +++ b/app/Livewire/Project/Shared/Metrics.php @@ -8,7 +8,7 @@ class Metrics extends Component { public $resource; - public $chartId = 'container-cpu'; + public $chartId = 'metrics'; public $data; @@ -33,6 +33,16 @@ public function loadData() try { $cpuMetrics = $this->resource->getCpuMetrics($this->interval); $memoryMetrics = $this->resource->getMemoryMetrics($this->interval); + + // Debug logging + \Log::info('Metrics loadData called', [ + 'chartId' => $this->chartId, + 'cpuMetrics' => $cpuMetrics, + 'memoryMetrics' => $memoryMetrics, + 'cpuEvent' => "refreshChartData-{$this->chartId}-cpu", + 'memoryEvent' => "refreshChartData-{$this->chartId}-memory" + ]); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); diff --git a/resources/css/utilities.css b/resources/css/utilities.css index d09d7f49c..65869e02f 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -10,6 +10,23 @@ @utility apexcharts-xaxistooltip { @apply hidden!; } +@utility apexcharts-tooltip-custom { + @apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm; + min-width: 160px; +} + +@utility apexcharts-tooltip-custom-value { + @apply text-neutral-700 dark:text-neutral-300 mb-1; +} + +@utility apexcharts-tooltip-value-bold { + @apply font-bold text-black dark:text-white; +} + +@utility apexcharts-tooltip-custom-title { + @apply text-xs text-neutral-500 dark:text-neutral-400 font-medium; +} + @utility input-sticky { @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; } diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index d6609d9e6..9b08babb3 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -1,21 +1,20 @@
-
+

Metrics

Basic metrics for your container.
- @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') -
Metrics are not available for Docker Compose applications yet!
- @elseif(!$resource->destination->server->isMetricsEnabled()) -
Metrics are only available for servers with Sentinel & Metrics enabled!
-
Go to Server settings to - enable - it.
- @else - @if (!str($resource->status)->contains('running')) -
Metrics are only available when this resource is running!
+
+ @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') +
Metrics are not available for Docker Compose applications yet!
+ @elseif(!$resource->destination->server->isMetricsEnabled()) +
Metrics are only available for servers with Sentinel & Metrics enabled!
+
Go to Server settings to enable it.
@else - + @if (!str($resource->status)->contains('running')) +
Metrics are only available when this resource is running!
+ @else +
+ @@ -77,63 +76,76 @@ class="pt-5"> xaxis: { type: 'datetime', }, - series: [{ - name: "CPU %", - data: [] - }], - noData: { - text: 'Loading...', - style: { - color: textColor, - } - }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, - legend: { - show: false - } + series: [{ + name: "CPU %", + 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 '
' + + '
CPU: ' + value + '%
' + + '
' + timeString + '
' + + '
'; + } + }, + legend: { + show: false + } } - const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu); - serverCpuChart.render(); - document.addEventListener('livewire:init', () => { - Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { - checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [cpuColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - yaxis: { - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - noData: { - text: 'Loading...', - style: { - color: textColor, - } - } - }); - }); - }); + const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu); + serverCpuChart.render(); + Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { + checkTheme(); + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + yaxis: { + show: true, + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + noData: { + text: 'Loading...', + style: { + color: textColor, + } + } + }); + });

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 19/48] 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 20/48] 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