From 65253ca54ebc764c67459f62a06923b0b9aba629 Mon Sep 17 00:00:00 2001 From: Nicolas Bondoux <8930930+Nsbx@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:51:00 +0200 Subject: [PATCH 01/57] Update reactive-resume.yaml --- templates/compose/reactive-resume.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/compose/reactive-resume.yaml b/templates/compose/reactive-resume.yaml index 4c884d7be..8d52a042b 100644 --- a/templates/compose/reactive-resume.yaml +++ b/templates/compose/reactive-resume.yaml @@ -24,6 +24,8 @@ services: - STORAGE_ACCESS_KEY=$SERVICE_USER_MINIO - STORAGE_SECRET_KEY=$SERVICE_PASSWORD_MINIO - STORAGE_USE_SSL=false + - DISABLE_SIGNUPS=$DISABLE_SIGNUPS + - DISABLE_EMAIL_AUTH=$DISABLE_EMAIL_AUTH depends_on: - postgres - minio From 2f87deb10b853513a9accf285eaee535dcce42d1 Mon Sep 17 00:00:00 2001 From: Nicolas Bondoux <8930930+Nsbx@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:54:53 +0200 Subject: [PATCH 02/57] Update reactive-resume.yaml --- templates/compose/reactive-resume.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/reactive-resume.yaml b/templates/compose/reactive-resume.yaml index 8d52a042b..930c52cc8 100644 --- a/templates/compose/reactive-resume.yaml +++ b/templates/compose/reactive-resume.yaml @@ -24,8 +24,8 @@ services: - STORAGE_ACCESS_KEY=$SERVICE_USER_MINIO - STORAGE_SECRET_KEY=$SERVICE_PASSWORD_MINIO - STORAGE_USE_SSL=false - - DISABLE_SIGNUPS=$DISABLE_SIGNUPS - - DISABLE_EMAIL_AUTH=$DISABLE_EMAIL_AUTH + - DISABLE_SIGNUPS=$SERVICE_DISABLE_SIGNUPS + - DISABLE_EMAIL_AUTH=$SERVICE_DISABLE_EMAIL_AUTH depends_on: - postgres - minio From b0863eb5eadfd85530897355923733768b6e3dac Mon Sep 17 00:00:00 2001 From: Astrid Date: Tue, 25 Jun 2024 16:55:29 +0200 Subject: [PATCH 03/57] remove h4 padding on server proxy settings from: ![from](https://astrid.email/u/chrome_9wK3HTTy12.png) to: ![to](https://astrid.email/u/chrome_7m5jXr1aWH.png) --- resources/views/livewire/server/proxy.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index b7050b6ac..7c101050b 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -21,9 +21,9 @@ href="https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies">this. @if ($server->proxyType() === 'TRAEFIK_V2') -

Traefik

+

Traefik

@elseif ($server->proxyType() === 'CADDY') -

Caddy

+

Caddy

@endif @if ( $server->proxy->last_applied_settings && From 490d45e78846f8c42e91ae54fe07cd4f3652a832 Mon Sep 17 00:00:00 2001 From: Astrid Date: Tue, 25 Jun 2024 16:59:41 +0200 Subject: [PATCH 04/57] server settings css changes from: ![from](https://astrid.email/u/chrome_REEIhjc2Yp.png) to: ![to](https://astrid.email/u/chrome_J5XwGaOs84.png) --- resources/views/livewire/server/form.blade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 938e47cbd..d8144e931 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -135,7 +135,7 @@ @if ($server->isFunctional()) -

Settings

+

Settings

@@ -144,8 +144,8 @@
-
-

Sentinel

+
+

Sentinel

@if ($server->isSentinelEnabled()) Restart @endif From ac694b855bb1e23219226ad52fc53a16a7c3cb23 Mon Sep 17 00:00:00 2001 From: Astrid Date: Tue, 25 Jun 2024 14:33:41 +0200 Subject: [PATCH 05/57] change gap of proxy buttons from: ![from](https://astrid.email/u/chrome_ducsHvMI4w.png) to: ![to](https://astrid.email/u/chrome_L4ncORPQtD.png) --- resources/views/livewire/server/proxy/deploy.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php index 32ddbb890..d485b4f9a 100644 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ b/resources/views/livewire/server/proxy/deploy.blade.php @@ -11,7 +11,7 @@ @if (data_get($server, 'proxy.status') === 'running') -
+ @endif @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) -

Subscribe to events

+

Subscribe to events

@if (isDev()) From cbae0845e7e07c2303dd3e3af75522c718f854fe Mon Sep 17 00:00:00 2001 From: Astrid Date: Tue, 25 Jun 2024 17:10:06 +0200 Subject: [PATCH 07/57] h2 instead of h3 as element is child of h2 --- resources/views/livewire/notifications/email.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index aec9d738e..594cf427b 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -103,7 +103,7 @@
@endif @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) -

Subscribe to events

+

Subscribe to events

@if (isDev()) From 7028391e579c7605a1a8e59ca3939496b14cba6a Mon Sep 17 00:00:00 2001 From: Astrid Date: Tue, 25 Jun 2024 17:21:23 +0200 Subject: [PATCH 08/57] remove unused li element? --- resources/views/livewire/project/resource/index.blade.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index cbe9f8a46..f7a795174 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -40,9 +40,6 @@
-
  • - -
  • From 7af151d44edfe9e5220bf4615bbad79cb6e01358 Mon Sep 17 00:00:00 2001 From: Bruce Mak Date: Wed, 26 Jun 2024 00:05:52 +0800 Subject: [PATCH 09/57] add Traditional Chinese translation --- lang/zh-tw.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lang/zh-tw.json diff --git a/lang/zh-tw.json b/lang/zh-tw.json new file mode 100644 index 000000000..63956f7a1 --- /dev/null +++ b/lang/zh-tw.json @@ -0,0 +1,30 @@ +{ + "auth.login": "登入", + "auth.login.azure": "使用 Microsoft 登入", + "auth.login.bitbucket": "使用 Bitbucket 登入", + "auth.login.github": "使用 GitHub 登入", + "auth.login.gitlab": "使用 Gitlab 登入", + "auth.login.google": "使用 Google 登入", + "auth.already_registered": "已經註冊?", + "auth.confirm_password": "確認密碼", + "auth.forgot_password": "忘記密碼", + "auth.forgot_password_send_email": "發送重設密碼電郵", + "auth.register_now": "註冊", + "auth.logout": "登出", + "auth.register": "註冊", + "auth.registration_disabled": "註冊已停用,請聯絡管理員。", + "auth.reset_password": "重設密碼", + "auth.failed": "這些憑證與我們的記錄不符。", + "auth.failed.callback": "無法處理來自登入提供者的回呼。", + "auth.failed.password": "密碼錯誤。", + "auth.failed.email": "找不到該電子郵件地址的使用者。", + "auth.throttle": "登入嘗試次數太多。請在 :seconds 秒後重試。", + "input.name": "名稱", + "input.email": "電子郵件", + "input.password": "密碼", + "input.password.again": "再次輸入密碼", + "input.code": "一次性代碼", + "input.recovery_code": "恢復碼", + "button.save": "儲存", + "repository.url": "例子
    對於公共代碼倉庫,請使用 https://...
    對於私有代碼倉庫,請使用 git@...

    https://github.com/coollabsio/coolify-examples main 分支將被選擇
    https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支將被選擇。
    https://gitea.com/sedlav/expressjs.git main 分支將被選擇。
    https://gitlab.com/andrasbacsai/nodejs-example.git main 分支將被選擇。" +} From ca917d9d2158c95e61b9871c01062a491c8915ae Mon Sep 17 00:00:00 2001 From: Benjamin Rumble Date: Thu, 27 Jun 2024 11:30:28 -0400 Subject: [PATCH 10/57] fix minor typo in backup.blade.php ~add as a database~ -> add a database --- resources/views/livewire/settings/backup.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/settings/backup.blade.php b/resources/views/livewire/settings/backup.blade.php index 50f5f3d28..d517b9516 100644 --- a/resources/views/livewire/settings/backup.blade.php +++ b/resources/views/livewire/settings/backup.blade.php @@ -24,7 +24,7 @@
    @else - To configure automatic backup for your Coolify instance, you first need to add as a database resource + To configure automatic backup for your Coolify instance, you first need to add a database resource into Coolify. Add Database @endif From 70bfd4dd8a3effbd854dba963e528c50d9cdf026 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 28 Jun 2024 11:00:02 +0200 Subject: [PATCH 11/57] fix: show keydbs/dragonflies/clickhouses --- app/Models/Environment.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Models/Environment.php b/app/Models/Environment.php index b2bb51092..fc19c134f 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -27,6 +27,9 @@ class Environment extends Model $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && $this->mariadbs()->count() == 0 && $this->mongodbs()->count() == 0 && $this->services()->count() == 0; From 2dd17cfac5fa2f1502878298eccd041f0c823684 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 28 Jun 2024 12:03:38 +0200 Subject: [PATCH 12/57] fix: force cleanup on busy servers --- app/Jobs/DockerCleanupJob.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index e637fb6d4..e42caeead 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -22,7 +22,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?int $usageBefore = null; - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function handle(): void { @@ -35,9 +37,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue return; } }); - if ($isInprogress) { - throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); - } + // if ($isInprogress) { + // throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); + // } if (! $this->server->isFunctional()) { return; } From e3c4ebb12189ae80acdcff509b715d010e9f25fe Mon Sep 17 00:00:00 2001 From: andrasbacsai Date: Fri, 28 Jun 2024 10:04:28 +0000 Subject: [PATCH 13/57] Fix styling --- app/Jobs/DockerCleanupJob.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index e42caeead..785940ee6 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -22,9 +22,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?int $usageBefore = null; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { From 30b7e831c0ddb15f52d781eb58a43c54e45e8523 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 28 Jun 2024 15:05:37 +0200 Subject: [PATCH 14/57] feat: new app API endpoint --- app/Enums/BuildPackTypes.php | 11 + app/Enums/NewResourceTypes.php | 22 ++ app/Http/Controllers/Api/Applications.php | 235 +++++++++++------- .../Api/{Server.php => Servers.php} | 2 +- app/Models/Server.php | 4 +- bootstrap/helpers/api.php | 117 +++++++++ routes/api.php | 14 +- 7 files changed, 300 insertions(+), 105 deletions(-) create mode 100644 app/Enums/BuildPackTypes.php create mode 100644 app/Enums/NewResourceTypes.php rename app/Http/Controllers/Api/{Server.php => Servers.php} (99%) diff --git a/app/Enums/BuildPackTypes.php b/app/Enums/BuildPackTypes.php new file mode 100644 index 000000000..d4fd505d2 --- /dev/null +++ b/app/Enums/BuildPackTypes.php @@ -0,0 +1,11 @@ +json(serialize_api_response($applications)); } + public function create_application(Request $request) + { + + ray()->clearAll(); + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'instant_deploy']; + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + if (! $request->isJson()) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'type' => ['required', Rule::enum(NewResourceTypes::class)], + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $fqdn = $request->domains; + $type = $request->type; + $instantDeploy = $request->instant_deploy; + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['error' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['error' => 'Environment not found.'], 404); + } + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['error' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['error' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['error' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($type === 'public') { + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = new Application(); + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + + $application->fill($request->all()); + + $application->fqdn = $fqdn; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serialize_api_response($application)); + } + + return response()->json('Application created')->setStatusCode(201); + + } + public function application_by_uuid(Request $request) { $teamId = get_team_id_from_token(); @@ -99,63 +227,20 @@ class Applications extends Controller ], 404); } $server = $application->destination->server; - $allowedFields = ['name', 'description', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'redirect']; + $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'redirect']; $validator = customApiValidator($request->all(), [ + sharedDataApplications(), 'name' => 'string|max:255', 'description' => 'string|nullable', - 'domains' => 'string', - 'git_repository' => 'string', - 'git_branch' => 'string', - 'git_commit_sha' => 'string', - 'docker_registry_image_name' => 'string|nullable', - 'docker_registry_image_tag' => 'string|nullable', - 'build_pack' => 'string', 'static_image' => 'string', - 'install_command' => 'string|nullable', - 'build_command' => 'string|nullable', - 'start_command' => 'string|nullable', - 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', - 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', - 'base_directory' => 'string|nullable', - 'publish_directory' => 'string|nullable', - 'health_check_enabled' => 'boolean', - 'health_check_path' => 'string', - 'health_check_port' => 'string|nullable', - 'health_check_host' => 'string', - 'health_check_method' => 'string', - 'health_check_return_code' => 'numeric', - 'health_check_scheme' => 'string', - 'health_check_response_text' => 'string|nullable', - 'health_check_interval' => 'numeric', - 'health_check_timeout' => 'numeric', - 'health_check_retries' => 'numeric', - 'health_check_start_period' => 'numeric', - 'limits_memory' => 'string', - 'limits_memory_swap' => 'string', - 'limits_memory_swappiness' => 'numeric', - 'limits_memory_reservation' => 'string', - 'limits_cpus' => 'string', - 'limits_cpuset' => 'string|nullable', - 'limits_cpu_shares' => 'numeric', - 'custom_labels' => 'string|nullable', - 'custom_docker_run_options' => 'string|nullable', - 'post_deployment_command' => 'string|nullable', - 'post_deployment_command_container' => 'string', - 'pre_deployment_command' => 'string|nullable', - 'pre_deployment_command_container' => 'string', 'watch_paths' => 'string|nullable', - 'manual_webhook_secret_github' => 'string|nullable', - 'manual_webhook_secret_gitlab' => 'string|nullable', - 'manual_webhook_secret_bitbucket' => 'string|nullable', - 'manual_webhook_secret_gitea' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose' => 'string|nullable', 'docker_compose_raw' => 'string|nullable', // 'docker_compose_domains' => 'string|nullable', // must be like: "{\"api\":{\"domain\":\"http:\\/\\/b8sos8k.127.0.0.1.sslip.io\"}}" 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', - 'redirect' => Rule::enum(RedirectTypes::class), ]); // Validate ports_exposes @@ -172,42 +257,9 @@ class Applications extends Controller } } } - // Validate ports_mappings - if ($request->has('ports_mappings')) { - $ports = []; - foreach (explode(',', $request->ports_mappings) as $portMapping) { - $port = explode(':', $portMapping); - if (in_array($port[0], $ports)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_mappings' => 'The first number before : should be unique between mappings.', - ], - ], 422); - } - $ports[] = $port[0]; - } - } - // Validate custom_labels - if ($request->has('custom_labels')) { - if (! isBase64Encoded($request->custom_labels)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - $customLabels = base64_decode($request->custom_labels); - if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - - } + $return = validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -223,31 +275,22 @@ class Applications extends Controller 'errors' => $errors, ], 422); } + $domains = $request->domains; if ($request->has('domains') && $server->isProxyShouldRun()) { $fqdn = $request->domains; $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceStart(',', '')->trim(); $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; - } - - return str($domain)->trim()->lower(); - }); - if (count($errors) > 0) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } $fqdn = $fqdn->unique()->implode(','); $application->fqdn = $fqdn; $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = base64_encode($customLabels); $request->offsetUnset('domains'); } - $application->fill($request->all()); + + $data = $request->all(); + data_set($data, 'fqdn', $domains); + $application->fill($data); $application->save(); return response()->json(serialize_api_response($application)); diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Servers.php similarity index 99% rename from app/Http/Controllers/Api/Server.php rename to app/Http/Controllers/Api/Servers.php index 1a58da7b0..387c4bd48 100644 --- a/app/Http/Controllers/Api/Server.php +++ b/app/Http/Controllers/Api/Servers.php @@ -9,7 +9,7 @@ use App\Models\Project; use App\Models\Server as ModelsServer; use Illuminate\Http\Request; -class Server extends Controller +class Servers extends Controller { public function servers(Request $request) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 29eedd59f..cd6cc9890 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -496,7 +496,7 @@ $schema://$host { public function checkSentinel() { - ray("Checking sentinel on server: {$this->name}"); + // ray("Checking sentinel on server: {$this->name}"); if ($this->isSentinelEnabled()) { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); @@ -505,7 +505,7 @@ $schema://$host { ray('Sentinel is not running, starting it...'); PullSentinelImageJob::dispatch($this); } else { - ray('Sentinel is running'); + // ray('Sentinel is running'); } } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index c278a5045..a0e42772e 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -1,6 +1,11 @@ 'string', + 'git_branch' => 'string', + 'build_pack' => Rule::enum(BuildPackTypes::class), + 'is_static' => 'boolean', + 'domains' => 'string', + 'redirect' => Rule::enum(RedirectTypes::class), + 'git_commit_sha' => 'string', + 'docker_registry_image_name' => 'string|nullable', + 'docker_registry_image_tag' => 'string|nullable', + 'install_command' => 'string|nullable', + 'build_command' => 'string|nullable', + 'start_command' => 'string|nullable', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', + 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', + 'base_directory' => 'string|nullable', + 'publish_directory' => 'string|nullable', + 'health_check_enabled' => 'boolean', + 'health_check_path' => 'string', + 'health_check_port' => 'string|nullable', + 'health_check_host' => 'string', + 'health_check_method' => 'string', + 'health_check_return_code' => 'numeric', + 'health_check_scheme' => 'string', + 'health_check_response_text' => 'string|nullable', + 'health_check_interval' => 'numeric', + 'health_check_timeout' => 'numeric', + 'health_check_retries' => 'numeric', + 'health_check_start_period' => 'numeric', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'custom_labels' => 'string|nullable', + 'custom_docker_run_options' => 'string|nullable', + 'post_deployment_command' => 'string|nullable', + 'post_deployment_command_container' => 'string', + 'pre_deployment_command' => 'string|nullable', + 'pre_deployment_command_container' => 'string', + 'manual_webhook_secret_github' => 'string|nullable', + 'manual_webhook_secret_gitlab' => 'string|nullable', + 'manual_webhook_secret_bitbucket' => 'string|nullable', + 'manual_webhook_secret_gitea' => 'string|nullable', + ]; +} + +function validateDataApplications(Request $request, Server $server) +{ + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + ray(filter_var($domain, FILTER_VALIDATE_URL)); + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + } +} diff --git a/routes/api.php b/routes/api.php index 7aca146ba..f4d9d786e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,7 +4,7 @@ use App\Http\Controllers\Api\Applications; use App\Http\Controllers\Api\Deploy; use App\Http\Controllers\Api\EnvironmentVariables; use App\Http\Controllers\Api\Resources; -use App\Http\Controllers\Api\Server; +use App\Http\Controllers\Api\Servers; use App\Http\Controllers\Api\Team; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -36,13 +36,15 @@ Route::group([ Route::get('/deployments', [Deploy::class, 'deployments']); Route::get('/deployments/{uuid}', [Deploy::class, 'deployment_by_uuid']); - Route::get('/servers', [Server::class, 'servers']); - Route::get('/servers/{uuid}', [Server::class, 'server_by_uuid']); - Route::get('/servers/domains', [Server::class, 'get_domains_by_server']); + // Add environments endpoints + Route::get('/servers', [Servers::class, 'servers']); + Route::get('/servers/{uuid}', [Servers::class, 'server_by_uuid']); + Route::get('/servers/domains', [Servers::class, 'get_domains_by_server']); Route::get('/resources', [Resources::class, 'resources']); Route::get('/applications', [Applications::class, 'applications']); + Route::post('/applications', [Applications::class, 'create_application']); Route::get('/applications/{uuid}', [Applications::class, 'application_by_uuid']); Route::patch('/applications/{uuid}', [Applications::class, 'update_by_uuid']); @@ -54,12 +56,12 @@ Route::group([ Route::patch('/applications/{uuid}/envs', [Applications::class, 'update_env_by_uuid']); Route::delete('/applications/{uuid}/envs/{env_uuid}', [Applications::class, 'delete_env_by_uuid']); - Route::delete('/envs/{env_uuid}', [EnvironmentVariables::class, 'delete_env_by_uuid']); - Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [Applications::class, 'action_deploy']); Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [Applications::class, 'action_restart']); Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [Applications::class, 'action_stop']); + Route::delete('/envs/{env_uuid}', [EnvironmentVariables::class, 'delete_env_by_uuid']); + Route::get('/teams', [Team::class, 'teams']); Route::get('/teams/current', [Team::class, 'current_team']); Route::get('/teams/current/members', [Team::class, 'current_team_members']); From b86924bc0e5b838c6709350b8bdb733e5e77e7fd Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 30 Jun 2024 11:30:31 +0200 Subject: [PATCH 15/57] feat: private gh deployments through api --- app/Http/Controllers/Api/Applications.php | 194 ++++++++++++++++++++-- app/Models/GithubApp.php | 21 ++- bootstrap/helpers/api.php | 63 ------- 3 files changed, 196 insertions(+), 82 deletions(-) diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/Applications.php index 6fe340176..0e651f476 100644 --- a/app/Http/Controllers/Api/Applications.php +++ b/app/Http/Controllers/Api/Applications.php @@ -9,6 +9,8 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\EnvironmentVariable; +use App\Models\GithubApp; +use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server; use Illuminate\Http\Request; @@ -35,7 +37,7 @@ class Applications extends Controller { ray()->clearAll(); - $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'instant_deploy']; + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy']; $teamId = get_team_id_from_token(); if (is_null($teamId)) { return invalid_token(); @@ -77,10 +79,13 @@ class Applications extends Controller 'errors' => $errors, ], 422); } + $serverUuid = $request->server_uuid; $fqdn = $request->domains; $type = $request->type; $instantDeploy = $request->instant_deploy; + $githubAppUuid = $request->github_app_uuid; + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { return response()->json(['error' => 'Project not found.'], 404); @@ -118,18 +123,12 @@ class Applications extends Controller 'errors' => $validator->errors(), ], 422); } - $return = validateDataApplications($request, $server); + $return = $this->validateDataApplications($request, $server); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } $application = new Application(); - $request->offsetUnset('project_uuid'); - $request->offsetUnset('environment_name'); - $request->offsetUnset('destination_uuid'); - $request->offsetUnset('server_uuid'); - $request->offsetUnset('type'); - $request->offsetUnset('domains'); - $request->offsetUnset('instant_deploy'); + $this->removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); @@ -150,10 +149,110 @@ class Applications extends Controller ); } + return response()->json(serialize_api_response($application)); + } elseif ($type === 'private-gh-app') { + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'github_app_uuid' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); + if (! $githubApp) { + return response()->json(['error' => 'Github App not found.'], 404); + } + $gitRepository = $request->git_repository; + if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { + $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); + } + $application = new Application(); + $this->removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $application->fqdn = $fqdn; + $application->git_repository = $gitRepository; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->source_type = $githubApp->getMorphClass(); + $application->source_id = $githubApp->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serialize_api_response($application)); + } elseif ($type === 'private-deploy-key') { + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'private_key_uuid' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['error' => 'Private Key not found.'], 404); + } + + $application = new Application(); + $this->removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->private_key_id = $privateKey->id; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + return response()->json(serialize_api_response($application)); } - return response()->json('Application created')->setStatusCode(201); + return response()->json(['error' => 'Invalid type.'], 400); } @@ -257,7 +356,7 @@ class Applications extends Controller } } } - $return = validateDataApplications($request, $server); + $return = $this->validateDataApplications($request, $server); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } @@ -711,4 +810,77 @@ class Applications extends Controller ); } + + private function removeUnnecessaryFieldsFromRequest(Request $request) + { + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); + } + + private function validateDataApplications(Request $request, Server $server) + { + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + } + } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index daf902daf..66ecdd967 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -20,6 +20,17 @@ class GithubApp extends BaseModel 'webhook_secret', ]; + protected static function booted(): void + { + static::deleting(function (GithubApp $github_app) { + $applications_count = Application::where('source_id', $github_app->id)->count(); + if ($applications_count > 0) { + throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); + } + $github_app->privateKey()->delete(); + }); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); @@ -30,15 +41,9 @@ class GithubApp extends BaseModel return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get(); } - protected static function booted(): void + public function team() { - static::deleting(function (GithubApp $github_app) { - $applications_count = Application::where('source_id', $github_app->id)->count(); - if ($applications_count > 0) { - throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); - } - $github_app->privateKey()->delete(); - }); + return $this->belongsTo(Team::class); } public function applications() diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index a0e42772e..f5c99dbda 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -2,9 +2,7 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; -use App\Models\Server; use Illuminate\Database\Eloquent\Collection; -use Illuminate\Http\Request; use Illuminate\Validation\Rule; function get_team_id_from_token() @@ -92,64 +90,3 @@ function sharedDataApplications() 'manual_webhook_secret_gitea' => 'string|nullable', ]; } - -function validateDataApplications(Request $request, Server $server) -{ - // Validate ports_mappings - if ($request->has('ports_mappings')) { - $ports = []; - foreach (explode(',', $request->ports_mappings) as $portMapping) { - $port = explode(':', $portMapping); - if (in_array($port[0], $ports)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_mappings' => 'The first number before : should be unique between mappings.', - ], - ], 422); - } - $ports[] = $port[0]; - } - } - // Validate custom_labels - if ($request->has('custom_labels')) { - if (! isBase64Encoded($request->custom_labels)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - $customLabels = base64_decode($request->custom_labels); - if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - - } - } - if ($request->has('domains') && $server->isProxyShouldRun()) { - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - ray(filter_var($domain, FILTER_VALIDATE_URL)); - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; - } - - return str($domain)->trim()->lower(); - }); - if (count($errors) > 0) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - } -} From dbc235d84a5ec7a5b008a19ce1da5e65fb9fbb77 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 1 Jul 2024 11:39:10 +0200 Subject: [PATCH 16/57] fix: check domain on new app via api --- app/Http/Controllers/Api/Applications.php | 93 ++++++++++++++++++++++- app/Models/Server.php | 2 +- app/Models/ServiceApplication.php | 5 ++ bootstrap/helpers/shared.php | 71 +++++++++++++++++ 4 files changed, 166 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/Applications.php index 0e651f476..c73b894ba 100644 --- a/app/Http/Controllers/Api/Applications.php +++ b/app/Http/Controllers/Api/Applications.php @@ -37,7 +37,7 @@ class Applications extends Controller { ray()->clearAll(); - $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy']; + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile']; $teamId = get_team_id_from_token(); if (is_null($teamId)) { return invalid_token(); @@ -94,9 +94,6 @@ class Applications extends Controller if (! $environment) { return response()->json(['error' => 'Environment not found.'], 404); } - if (! $request->has('name')) { - $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); - } $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); if (! $server) { return response()->json(['error' => 'Server not found.'], 404); @@ -110,6 +107,9 @@ class Applications extends Controller } $destination = $destinations->first(); if ($type === 'public') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -151,6 +151,9 @@ class Applications extends Controller return response()->json(serialize_api_response($application)); } elseif ($type === 'private-gh-app') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -204,6 +207,9 @@ class Applications extends Controller return response()->json(serialize_api_response($application)); } elseif ($type === 'private-deploy-key') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } $validator = customApiValidator($request->all(), [ sharedDataApplications(), 'git_repository' => 'string|required', @@ -249,6 +255,75 @@ class Applications extends Controller ); } + return response()->json(serialize_api_response($application)); + } elseif ($type === 'dockerfile') { + if (! $request->has('name')) { + $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'dockerfile' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->dockerfile)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + $this->removeUnnecessaryFieldsFromRequest($request); + + $port = get_port_from_dockerfile($request->dockerfile); + if (! $port) { + $port = 80; + } + + $application = new Application(); + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->ports_exposes = $port; + $application->build_pack = 'dockerfile'; + $application->dockerfile = $dockerFile; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + return response()->json(serialize_api_response($application)); } @@ -826,6 +901,8 @@ class Applications extends Controller private function validateDataApplications(Request $request, Server $server) { + $teamId = get_team_id_from_token(); + // Validate ports_mappings if ($request->has('ports_mappings')) { $ports = []; @@ -881,6 +958,14 @@ class Applications extends Controller 'errors' => $errors, ], 422); } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } } } } diff --git a/app/Models/Server.php b/app/Models/Server.php index cd6cc9890..ea487fee7 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -502,7 +502,7 @@ $schema://$host { $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status !== 'running') { - ray('Sentinel is not running, starting it...'); + // ray('Sentinel is not running, starting it...'); PullSentinelImageJob::dispatch($this); } else { // ray('Sentinel is running'); diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 98c1cf4e7..6690f254e 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -27,6 +27,11 @@ class ServiceApplication extends BaseModel instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a4676cfd4..1d70b674c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -56,6 +56,8 @@ use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +use function PHPUnit\Framework\isEmpty; + function base_configuration_dir(): string { return '/data/coolify'; @@ -2129,6 +2131,75 @@ function ip_match($ip, $cidrs, &$match = null) return false; } +function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null) +{ + if (is_null($teamId)) { + return response()->json(['error' => 'Team ID is required.'], 400); + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $applications = Application::ownedByCurrentTeamAPI($teamId)->get('fqdn'); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get('fqdn'); + $domainFound = false; + foreach ($applications as $app) { + if (is_null($app->fqdn)) { + continue; + } + $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)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + foreach ($serviceApplications as $app) { + if (isEmpty($app->fqdn)) { + continue; + } + $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)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + $settings = InstanceSettings::get(); + 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)) { + return true; + } + } +} function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { From da6f2da3d059d46e3698e8081fe7834f8bd455c4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 1 Jul 2024 16:26:50 +0200 Subject: [PATCH 17/57] feat: lots of api endpoints --- app/Enums/NewDatabaseTypes.php | 15 + app/Events/DatabaseStatusChanged.php | 2 +- ...cations.php => ApplicationsController.php} | 408 +++++++++++++----- .../Controllers/Api/DatabasesController.php | 259 +++++++++++ .../Api/{Deploy.php => DeployController.php} | 58 ++- ...php => EnvironmentVariablesController.php} | 7 +- app/Http/Controllers/Api/Project.php | 44 -- .../Controllers/Api/ProjectController.php | 60 +++ ...{Resources.php => ResourcesController.php} | 11 +- .../Controllers/Api/SecurityController.php | 160 +++++++ .../{Servers.php => ServersController.php} | 52 ++- app/Http/Controllers/Api/Team.php | 74 ---- app/Http/Controllers/Api/TeamController.php | 89 ++++ app/Http/Middleware/ApiAllowed.php | 34 ++ app/Livewire/Settings/Configuration.php | 21 +- app/Models/InstanceSettings.php | 1 + app/Models/StandaloneClickhouse.php | 35 +- app/Models/StandaloneDragonfly.php | 35 +- app/Models/StandaloneKeydb.php | 35 +- app/Models/StandaloneMariadb.php | 35 +- app/Models/StandaloneMongodb.php | 33 +- app/Models/StandaloneMysql.php | 35 +- app/Models/StandalonePostgresql.php | 35 +- app/Models/StandaloneRedis.php | 35 +- bootstrap/helpers/api.php | 53 ++- bootstrap/helpers/databases.php | 168 +++++--- bootstrap/helpers/shared.php | 37 ++ ...1_115528_add_is_api_allowed_and_iplist.php | 24 ++ .../livewire/settings/configuration.blade.php | 17 +- routes/api.php | 128 ++++-- 30 files changed, 1583 insertions(+), 417 deletions(-) create mode 100644 app/Enums/NewDatabaseTypes.php rename app/Http/Controllers/Api/{Applications.php => ApplicationsController.php} (72%) create mode 100644 app/Http/Controllers/Api/DatabasesController.php rename app/Http/Controllers/Api/{Deploy.php => DeployController.php} (73%) rename app/Http/Controllers/Api/{EnvironmentVariables.php => EnvironmentVariablesController.php} (86%) delete mode 100644 app/Http/Controllers/Api/Project.php create mode 100644 app/Http/Controllers/Api/ProjectController.php rename app/Http/Controllers/Api/{Resources.php => ResourcesController.php} (80%) create mode 100644 app/Http/Controllers/Api/SecurityController.php rename app/Http/Controllers/Api/{Servers.php => ServersController.php} (79%) delete mode 100644 app/Http/Controllers/Api/Team.php create mode 100644 app/Http/Controllers/Api/TeamController.php create mode 100644 app/Http/Middleware/ApiAllowed.php create mode 100644 database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php diff --git a/app/Enums/NewDatabaseTypes.php b/app/Enums/NewDatabaseTypes.php new file mode 100644 index 000000000..3563146ff --- /dev/null +++ b/app/Enums/NewDatabaseTypes.php @@ -0,0 +1,15 @@ +user()->id ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + throw new \RuntimeException('User id is null'); } $this->userId = $userId; } diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/ApplicationsController.php similarity index 72% rename from app/Http/Controllers/Api/Applications.php rename to app/Http/Controllers/Api/ApplicationsController.php index c73b894ba..e37de0378 100644 --- a/app/Http/Controllers/Api/Applications.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -13,47 +13,45 @@ use App\Models\GithubApp; use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server; +use App\Models\Service; use Illuminate\Http\Request; use Illuminate\Validation\Rule; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; -class Applications extends Controller +class ApplicationsController extends Controller { public function applications(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $projects = Project::where('team_id', $teamId)->get(); $applications = collect(); $applications->push($projects->pluck('applications')->flatten()); $applications = $applications->flatten(); + $applications = $applications->map(function ($application) { + return serializeApiResponse($application); + }); - return response()->json(serialize_api_response($applications)); + return response()->json([ + 'success' => true, + 'data' => $applications, + ]); } public function create_application(Request $request) { - - ray()->clearAll(); $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } - if (! $request->isJson()) { - return response()->json([ - 'message' => 'Invalid request.', - 'error' => 'Content-Type must be application/json.', - ], 400); - } - // check if request is valid json - if (! json_decode($request->getContent())) { - return response()->json([ - 'message' => 'Invalid request.', - 'error' => 'Invalid JSON.', - ], 400); + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -75,6 +73,7 @@ class Applications extends Controller } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -88,22 +87,22 @@ class Applications extends Controller $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { - return response()->json(['error' => 'Project not found.'], 404); + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); } $environment = $project->environments()->where('name', $request->environment_name)->first(); if (! $environment) { - return response()->json(['error' => 'Environment not found.'], 404); + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); } $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); if (! $server) { - return response()->json(['error' => 'Server not found.'], 404); + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); } $destinations = $server->destinations(); if ($destinations->count() == 0) { - return response()->json(['error' => 'Server has no destinations.'], 400); + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); } if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { - return response()->json(['error' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); } $destination = $destinations->first(); if ($type === 'public') { @@ -114,11 +113,12 @@ class Applications extends Controller sharedDataApplications(), 'git_repository' => 'string|required', 'git_branch' => 'string|required', - 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'build_pack' => [Rule::enum(BuildPackTypes::class)], 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -128,7 +128,7 @@ class Applications extends Controller return $return; } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); @@ -149,7 +149,10 @@ class Applications extends Controller ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'private-gh-app') { if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); @@ -164,6 +167,7 @@ class Applications extends Controller ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -174,14 +178,14 @@ class Applications extends Controller } $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); if (! $githubApp) { - return response()->json(['error' => 'Github App not found.'], 404); + return response()->json(['success' => false, 'message' => 'Github App not found.'], 404); } $gitRepository = $request->git_repository; if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); @@ -205,7 +209,10 @@ class Applications extends Controller ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'private-deploy-key') { if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); @@ -230,11 +237,11 @@ class Applications extends Controller } $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); if (! $privateKey) { - return response()->json(['error' => 'Private Key not found.'], 404); + return response()->json(['success' => false, 'message' => 'Private Key not found.'], 404); } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); $application->fqdn = $fqdn; @@ -255,7 +262,10 @@ class Applications extends Controller ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'dockerfile') { if (! $request->has('name')) { $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); @@ -266,6 +276,7 @@ class Applications extends Controller ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -276,6 +287,7 @@ class Applications extends Controller } if (! isBase64Encoded($request->dockerfile)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'dockerfile' => 'The dockerfile should be base64 encoded.', @@ -285,6 +297,7 @@ class Applications extends Controller $dockerFile = base64_decode($request->dockerfile); if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'dockerfile' => 'The dockerfile should be base64 encoded.', @@ -292,7 +305,7 @@ class Applications extends Controller ], 422); } $dockerFile = base64_decode($request->dockerfile); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $port = get_port_from_dockerfile($request->dockerfile); if (! $port) { @@ -324,42 +337,179 @@ class Applications extends Controller ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-image') { + if (! $request->has('name')) { + $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_registry_image_name' => 'string|required', + 'docker_registry_image_tag' => 'string', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! $request->docker_registry_image_tag) { + $request->offsetSet('docker_registry_image_tag', 'latest'); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->build_pack = 'dockerimage'; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-compose-empty') { + if (! $request->has('name')) { + $request->offsetSet('name', 'service'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_compose' => 'string|required', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->docker_compose)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + if (mb_detect_encoding($dockerCompose, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // $isValid = validateComposeFile($dockerComposeRaw, $server_id); + // if ($isValid !== 'OK') { + // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + // } + + $service = new Service(); + removeUnnecessaryFieldsFromRequest($request); + $service->name = $request->name; + $service->description = $request->description; + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->save(); + + $service->name = "service-$service->uuid"; + $service->parse(isNew: true); + // if ($instantDeploy) { + // $deployment_uuid = new Cuid2(7); + + // queue_application_deployment( + // application: $application, + // deployment_uuid: $deployment_uuid, + // no_questions_asked: true, + // is_api: true, + // ); + // } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($service), + ]); } - return response()->json(['error' => 'Invalid type.'], 400); + return response()->json(['success' => false, 'message' => 'Invalid type.'], 400); } public function application_by_uuid(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } public function delete_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); $cleanup = $request->query->get('cleanup') ?? false; if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($request->collect()->count() == 0) { return response()->json([ + 'success' => false, 'message' => 'Invalid request.', ], 400); } @@ -381,17 +531,21 @@ class Applications extends Controller public function update_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($request->collect()->count() == 0) { return response()->json([ + 'success' => false, 'message' => 'Invalid request.', ], 400); } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { @@ -423,6 +577,7 @@ class Applications extends Controller foreach ($ports as $port) { if (! is_numeric($port)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', @@ -445,6 +600,7 @@ class Applications extends Controller } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -467,15 +623,21 @@ class Applications extends Controller $application->fill($data); $application->save(); - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } public function envs_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -487,17 +649,24 @@ class Applications extends Controller } $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); - return response()->json(serialize_api_response($envs)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($envs), + ]); } public function update_env_by_uuid(Request $request) { - ray()->clearAll(); $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -525,6 +694,7 @@ class Applications extends Controller } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -547,9 +717,10 @@ class Applications extends Controller } $env->save(); - return response()->json(serialize_api_response($env)); + return response()->json(serializeApiResponse($env)); } else { return response()->json([ + 'success' => false, 'message' => 'Environment variable not found.', ], 404); } @@ -568,10 +739,14 @@ class Applications extends Controller } $env->save(); - return response()->json(serialize_api_response($env)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ]); } else { return response()->json([ + 'success' => false, 'message' => 'Environment variable not found.', ], 404); @@ -579,6 +754,7 @@ class Applications extends Controller } return response()->json([ + 'success' => false, 'message' => 'Something went wrong.', ], 500); @@ -586,11 +762,15 @@ class Applications extends Controller public function create_bulk_envs(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -604,6 +784,7 @@ class Applications extends Controller $bulk_data = $request->get('data'); if (! $bulk_data) { return response()->json([ + 'success' => false, 'message' => 'Bulk data is required.', ], 400); } @@ -620,6 +801,7 @@ class Applications extends Controller ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -671,18 +853,18 @@ class Applications extends Controller } return response()->json([ - 'message' => 'Environments updated.', + 'success' => true, + 'data' => serializeApiResponse($env), ]); } public function create_env(Request $request) { - ray()->clearAll(); $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -710,6 +892,7 @@ class Applications extends Controller } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -719,6 +902,7 @@ class Applications extends Controller $env = $application->environment_variables_preview->where('key', $request->key)->first(); if ($env) { return response()->json([ + 'success' => false, 'message' => 'Environment variable already exists. Use PATCH request to update it.', ], 409); } else { @@ -730,7 +914,10 @@ class Applications extends Controller 'is_literal' => $request->is_literal ?? false, ]); - return response()->json(serialize_api_response($env))->setStatusCode(201); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); } } else { $env = $application->environment_variables->where('key', $request->key)->first(); @@ -747,12 +934,16 @@ class Applications extends Controller 'is_literal' => $request->is_literal ?? false, ]); - return response()->json(serialize_api_response($env))->setStatusCode(201); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); } } return response()->json([ + 'success' => false, 'message' => 'Something went wrong.', ], 500); @@ -760,10 +951,9 @@ class Applications extends Controller public function delete_env_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -790,19 +980,19 @@ class Applications extends Controller public function action_deploy(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $force = $request->query->get('force') ?? false; $instant_deploy = $request->query->get('instant_deploy') ?? false; $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } $deployment_uuid = new Cuid2(7); @@ -817,9 +1007,12 @@ class Applications extends Controller return response()->json( [ + 'success' => true, 'message' => 'Deployment request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], ], 200 ); @@ -827,43 +1020,53 @@ class Applications extends Controller public function action_stop(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); $sync = $request->query->get('sync') ?? false; if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } if ($sync) { StopApplication::run($application); - return response()->json(['message' => 'Stopped the application.'], 200); + return response()->json( + [ + 'success' => true, + 'message' => 'Stopped the application.', + ], + ); } else { StopApplication::dispatch($application); - return response()->json(['message' => 'Stopping request queued.'], 200); + return response()->json( + [ + 'success' => true, + 'message' => 'Stopping request queued.', + ], + ); } } public function action_restart(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } $deployment_uuid = new Cuid2(7); @@ -877,31 +1080,25 @@ class Applications extends Controller return response()->json( [ + 'success' => true, 'message' => 'Restart request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], ], - 200 ); } - private function removeUnnecessaryFieldsFromRequest(Request $request) - { - $request->offsetUnset('project_uuid'); - $request->offsetUnset('environment_name'); - $request->offsetUnset('destination_uuid'); - $request->offsetUnset('server_uuid'); - $request->offsetUnset('type'); - $request->offsetUnset('domains'); - $request->offsetUnset('instant_deploy'); - $request->offsetUnset('github_app_uuid'); - $request->offsetUnset('private_key_uuid'); - } - private function validateDataApplications(Request $request, Server $server) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); + + // Default build pack is nixpacks + if (! $request->has('build_pack')) { + $request->offsetSet('build_pack', 'nixpacks'); + } // Validate ports_mappings if ($request->has('ports_mappings')) { @@ -910,6 +1107,7 @@ class Applications extends Controller $port = explode(':', $portMapping); if (in_array($port[0], $ports)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'ports_mappings' => 'The first number before : should be unique between mappings.', @@ -923,6 +1121,7 @@ class Applications extends Controller if ($request->has('custom_labels')) { if (! isBase64Encoded($request->custom_labels)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'custom_labels' => 'The custom_labels should be base64 encoded.', @@ -932,6 +1131,7 @@ class Applications extends Controller $customLabels = base64_decode($request->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'custom_labels' => 'The custom_labels should be base64 encoded.', @@ -954,12 +1154,14 @@ class Applications extends Controller }); if (count($errors) > 0) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); } if (checkIfDomainIsAlreadyUsed($fqdn, $teamId)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'domains' => 'One of the domain is already used.', diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php new file mode 100644 index 000000000..36a5fffaf --- /dev/null +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -0,0 +1,259 @@ +get(); + $databases = collect(); + foreach ($projects as $project) { + $databases = $databases->merge($project->databases()); + } + $databases = $databases->map(function ($database) { + return serializeApiResponse($database); + }); + + return response()->json([ + 'success' => true, + 'data' => $databases, + ]); + } + + public function database_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($database), + ]); + } + + public function create_database(Request $request) + { + $allowedFields = ['type', 'name', 'description', 'image', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'postgres_user', 'postgres_password', 'postgres_db', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'type' => ['required', Rule::enum(NewDatabaseTypes::class)], + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'instant_deploy' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + + if ($request->type === NewDatabaseTypes::POSTGRESQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartPostgresql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MARIADB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMariadb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MYSQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMysql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::REDIS->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartRedis::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::DRAGONFLY->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDragonfly::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::KEYDB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartKeydb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::CLICKHOUSE->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartClickhouse::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MONGODB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMongodb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } + + return response()->json(['success' => false, 'message' => 'Invalid database type requested.'], 400); + } + + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + StopDatabase::dispatch($database); + $database->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Database deletion request queued.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/DeployController.php similarity index 73% rename from app/Http/Controllers/Api/Deploy.php rename to app/Http/Controllers/Api/DeployController.php index d510970dd..76e67548c 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -18,13 +18,13 @@ use App\Models\Tag; use Illuminate\Http\Request; use Visus\Cuid2\Cuid2; -class Deploy extends Controller +class DeployController extends Controller { public function deployments(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $servers = Server::whereTeamId($teamId)->get(); $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([ @@ -38,39 +38,45 @@ class Deploy extends Controller 'status', ])->sortBy('id')->toArray(); - return response()->json(serialize_api_response($deployments_per_server), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($deployments_per_server), + ]); } public function deployment_by_uuid(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['message' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first(); if (! $deployment) { - return response()->json(['message' => 'Deployment not found.'], 404); + return response()->json(['success' => false, 'message' => 'Deployment not found.'], 404); } - return response()->json(serialize_api_response($deployment->makeHidden('logs')), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($deployment->makeHidden('logs')), + ]); } public function deploy(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); $uuids = $request->query->get('uuid'); $tags = $request->query->get('tag'); $force = $request->query->get('force') ?? false; if ($uuids && $tags) { - return response()->json(['message' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($tags) { return $this->by_tags($tags, $teamId, $force); @@ -78,7 +84,7 @@ class Deploy extends Controller return $this->by_uuids($uuids, $teamId, $force); } - return response()->json(['message' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } private function by_uuids(string $uuid, int $teamId, bool $force = false) @@ -87,7 +93,7 @@ class Deploy extends Controller $uuids = collect(array_filter($uuids)); if (count($uuids) === 0) { - return response()->json(['message' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } $deployments = collect(); $payload = collect(); @@ -96,19 +102,22 @@ class Deploy extends Controller if ($resource) { ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); if ($deployment_uuid) { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + $deployments->push(['success' => true, 'message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); } else { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); + $deployments->push(['success' => true, 'message' => $return_message, 'resource_uuid' => $uuid]); } } } if ($deployments->count() > 0) { $payload->put('deployments', $deployments->toArray()); - return response()->json($payload->toArray(), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($payload->toArray()), + ]); } - return response()->json(['message' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + return response()->json(['success' => false, 'message' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } public function by_tags(string $tags, int $team_id, bool $force = false) @@ -117,7 +126,7 @@ class Deploy extends Controller $tags = collect(array_filter($tags)); if (count($tags) === 0) { - return response()->json(['message' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } $message = collect([]); $deployments = collect(); @@ -153,10 +162,13 @@ class Deploy extends Controller $payload->put('details', $deployments->toArray()); } - return response()->json($payload->toArray(), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($payload->toArray()), + ]); } - return response()->json(['message' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + return response()->json(['success' => false, 'message' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } public function deploy_resource($resource, bool $force = false): array @@ -164,7 +176,7 @@ class Deploy extends Controller $message = null; $deployment_uuid = null; if (gettype($resource) !== 'object') { - return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; + return ['success' => false, 'message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } $type = $resource?->getMorphClass(); if ($type === 'App\Models\Application') { @@ -228,6 +240,6 @@ class Deploy extends Controller $message = "Service {$resource->name} started. It could take a while, be patient."; } - return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; + return ['success' => true, 'message' => $message, 'deployment_uuid' => $deployment_uuid]; } } diff --git a/app/Http/Controllers/Api/EnvironmentVariables.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php similarity index 86% rename from app/Http/Controllers/Api/EnvironmentVariables.php rename to app/Http/Controllers/Api/EnvironmentVariablesController.php index d788bdb0c..c54656dc6 100644 --- a/app/Http/Controllers/Api/EnvironmentVariables.php +++ b/app/Http/Controllers/Api/EnvironmentVariablesController.php @@ -6,14 +6,13 @@ use App\Http\Controllers\Controller; use App\Models\EnvironmentVariable; use Illuminate\Http\Request; -class EnvironmentVariables extends Controller +class EnvironmentVariablesController extends Controller { public function delete_env_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $env = EnvironmentVariable::where('uuid', $request->env_uuid)->first(); if (! $env) { diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php deleted file mode 100644 index baaf1eacb..000000000 --- a/app/Http/Controllers/Api/Project.php +++ /dev/null @@ -1,44 +0,0 @@ -select('id', 'name', 'uuid')->get(); - - return response()->json($projects); - } - - public function project_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); - - return response()->json($project); - } - - public function environment_details(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); - - return response()->json($environment); - } -} diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php new file mode 100644 index 000000000..4721b48e1 --- /dev/null +++ b/app/Http/Controllers/Api/ProjectController.php @@ -0,0 +1,60 @@ +select('id', 'name', 'uuid')->get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($projects), + ]); + } + + public function project_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + if (! $project) { + return response()->json(['success' => false, 'message' => 'Project not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($project), + ]); + } + + public function environment_details(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + $environment = $project->environments()->whereName(request()->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($environment), + ]); + } +} diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/ResourcesController.php similarity index 80% rename from app/Http/Controllers/Api/Resources.php rename to app/Http/Controllers/Api/ResourcesController.php index 0d538b62e..47dfc6733 100644 --- a/app/Http/Controllers/Api/Resources.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -6,13 +6,13 @@ use App\Http\Controllers\Controller; use App\Models\Project; use Illuminate\Http\Request; -class Resources extends Controller +class ResourcesController extends Controller { public function resources(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $projects = Project::where('team_id', $teamId)->get(); $resources = collect(); @@ -34,6 +34,9 @@ class Resources extends Controller return $payload; }); - return response()->json($resources); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($resources), + ]); } } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php new file mode 100644 index 000000000..51c6fee26 --- /dev/null +++ b/app/Http/Controllers/Api/SecurityController.php @@ -0,0 +1,160 @@ +get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($keys), + ]); + } + + public function key_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + + if (is_null($key)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function create_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + if ($validator->fails()) { + $errors = $validator->errors(); + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->description) { + $request->offsetSet('description', 'Created by Coolify via API'); + } + $key = PrivateKey::create([ + 'team_id' => $teamId, + 'name' => $request->name, + 'description' => $request->description, + 'private_key' => $request->private_key, + ]); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function update_key(Request $request) + { + $allowedFields = ['name', 'description', 'private_key']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($foundKey)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + $foundKey->update($request->all()); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($foundKey), + ])->setStatusCode(201); + } + + public function delete_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 422); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($key)) { + return response()->json(['success' => false, 'message' => 'Key not found.'], 404); + } + $key->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Key deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Servers.php b/app/Http/Controllers/Api/ServersController.php similarity index 79% rename from app/Http/Controllers/Api/Servers.php rename to app/Http/Controllers/Api/ServersController.php index 387c4bd48..4d9479b7c 100644 --- a/app/Http/Controllers/Api/Servers.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -8,14 +8,15 @@ use App\Models\InstanceSettings; use App\Models\Project; use App\Models\Server as ModelsServer; use Illuminate\Http\Request; +use Stringable; -class Servers extends Controller +class ServersController extends Controller { public function servers(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $server['is_reachable'] = $server->settings->is_reachable; @@ -23,16 +24,22 @@ class Servers extends Controller return $server; }); + $servers = $servers->map(function ($server) { + return serializeApiResponse($server); + }); - return response()->json($servers); + return response()->json([ + 'success' => true, + 'data' => $servers, + ]); } public function server_by_uuid(Request $request) { $with_resources = $request->query('resources'); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); if (is_null($server)) { @@ -60,22 +67,25 @@ class Servers extends Controller $server->load(['settings']); } - return response()->json($server); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($server), + ]); } public function get_domains_by_server(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } - $uuid = $request->query->get('uuid'); + $uuid = $request->get('uuid'); if ($uuid) { $domains = Application::getDomainsByUuid($uuid); return response()->json([ - 'uuid' => $uuid, - 'domains' => $domains, + 'success' => true, + 'data' => serializeApiResponse($domains), ]); } $projects = Project::where('team_id', $teamId)->get(); @@ -86,8 +96,13 @@ class Servers extends Controller foreach ($applications as $application) { $ip = $application->destination->server->ip; $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); }); + if ($ip === 'host.docker.internal') { if ($settings->public_ipv4) { $domains->push([ @@ -122,7 +137,11 @@ class Servers extends Controller if ($service_applications->count() > 0) { foreach ($service_applications as $application) { $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); }); if ($ip === 'host.docker.internal') { if ($settings->public_ipv4) { @@ -162,6 +181,9 @@ class Servers extends Controller ]; })->values(); - return response()->json($domains); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($domains), + ]); } } diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php deleted file mode 100644 index c895f2c1b..000000000 --- a/app/Http/Controllers/Api/Team.php +++ /dev/null @@ -1,74 +0,0 @@ -user()->teams; - - return response()->json($teams); - } - - public function team_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); - } - - return response()->json($team); - } - - public function members_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); - } - - return response()->json($team->members); - } - - public function current_team(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team); - } - - public function current_team_members(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team->members); - } -} diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php new file mode 100644 index 000000000..a256e9caf --- /dev/null +++ b/app/Http/Controllers/Api/TeamController.php @@ -0,0 +1,89 @@ +user()->teams; + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($teams), + ]); + } + + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } + + public function current_team(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function current_team_members(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } +} diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php new file mode 100644 index 000000000..dc0a433e2 --- /dev/null +++ b/app/Http/Middleware/ApiAllowed.php @@ -0,0 +1,34 @@ +clearAll(); + if (isCloud()) { + return $next($request); + } + $settings = InstanceSettings::get(); + if ($settings->is_api_enabled === false) { + return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + } + + if (! isDev()) { + if ($settings->allowed_ips) { + $allowedIps = explode(',', $settings->allowed_ips); + if (! in_array($request->ip(), $allowedIps)) { + return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + } + } + } + + return $next($request); + } +} diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 3b6d7cd72..7439e112f 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -18,7 +18,8 @@ class Configuration extends Component public bool $is_dns_validation_enabled; - // public bool $next_channel; + public bool $is_api_enabled; + protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected Server $server; @@ -30,6 +31,7 @@ class Configuration extends Component 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', 'settings.instance_name' => 'nullable', + 'settings.allowed_ips' => 'nullable', ]; protected $validationAttributes = [ @@ -38,6 +40,7 @@ class Configuration extends Component 'settings.public_port_min' => 'Public port min', 'settings.public_port_max' => 'Public port max', 'settings.custom_dns_servers' => 'Custom DNS servers', + 'settings.allowed_ips' => 'Allowed IPs', ]; public function mount() @@ -45,8 +48,8 @@ class Configuration extends Component $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; - // $this->next_channel = $this->settings->next_channel; $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; + $this->is_api_enabled = $this->settings->is_api_enabled; } public function instantSave() @@ -55,12 +58,7 @@ class Configuration extends Component $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - // if ($this->next_channel) { - // $this->settings->next_channel = false; - // $this->next_channel = false; - // } else { - // $this->settings->next_channel = $this->next_channel; - // } + $this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->save(); $this->dispatch('success', 'Settings updated!'); } @@ -94,6 +92,13 @@ class Configuration extends Component $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + }); + $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); + $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); + $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); if (! $error_show) { diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 38f79ce75..bd3c41a1f 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -17,6 +17,7 @@ class InstanceSettings extends Model implements SendsEmail protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', + 'allowed_ip_ranges' => 'array', ]; public function fqdn(): Attribute diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index e968db18d..673224650 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,6 +13,8 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ class StandaloneClickhouse extends BaseModel return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-clickhouse'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + return $this->externalDbUrl; } else { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index c6718acfe..d78d656c1 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,6 +13,8 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ class StandaloneDragonfly extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-dragonfly'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 142f960aa..7b71bd55f 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,6 +13,8 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url']; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ class StandaloneKeydb extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-keydb'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://{$this->keydb_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://{$this->keydb_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 7e6d2e0d1..00df4fe71 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -13,6 +13,8 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -161,6 +163,13 @@ class StandaloneMariadb extends BaseModel return data_get($this, 'is_log_drain_enabled', false); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mariadb'; @@ -183,12 +192,32 @@ class StandaloneMariadb extends BaseModel ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + return $this->externalDbUrl; } else { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index df895bb34..0863522a8 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,6 +13,8 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -198,17 +200,44 @@ class StandaloneMongodb extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mongodb'; } + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + } + + return null; + } + ); + } + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + return $this->externalDbUrl; } else { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index bd160f877..79e7c37fa 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,6 +13,8 @@ class StandaloneMysql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -157,6 +159,13 @@ class StandaloneMysql extends BaseModel return null; } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mysql'; @@ -184,12 +193,32 @@ class StandaloneMysql extends BaseModel ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + return $this->externalDbUrl; } else { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 114d376e8..1d5276cf3 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,6 +13,8 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -179,17 +181,44 @@ class StandalonePostgresql extends BaseModel return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-postgresql'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + return $this->externalDbUrl; } else { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 022cd8d09..e0f863aca 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,6 +13,8 @@ class StandaloneRedis extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -179,12 +181,39 @@ class StandaloneRedis extends BaseModel return 'standalone-redis'; } - public function get_db_url(bool $useInternal = false): string + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index f5c99dbda..c5083534f 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -3,25 +3,27 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Http\Request; use Illuminate\Validation\Rule; -function get_team_id_from_token() +function getTeamIdFromToken() { $token = auth()->user()->currentAccessToken(); return data_get($token, 'team_id'); } -function invalid_token() +function invalidTokenResponse() { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); + return response()->json(['success' => false, 'message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); } -function serialize_api_response($data) +function serializeApiResponse($data) { if (! $data instanceof Collection) { $data = collect($data); } $data = $data->sortKeys(); + $created_at = data_get($data, 'created_at'); $updated_at = data_get($data, 'updated_at'); if ($created_at) { @@ -33,6 +35,16 @@ function serialize_api_response($data) unset($data['updated_at']); $data['updated_at'] = $updated_at; } + if (data_get($data, 'name')) { + $data = $data->prepend($data['name'], 'name'); + } + if (data_get($data, 'description')) { + $data = $data->prepend($data['description'], 'description'); + } + if (data_get($data, 'uuid')) { + $data = $data->prepend($data['uuid'], 'uuid'); + } + if (data_get($data, 'id')) { $data = $data->prepend($data['id'], 'id'); } @@ -90,3 +102,36 @@ function sharedDataApplications() 'manual_webhook_secret_gitea' => 'string|nullable', ]; } + +function validateIncomingRequest(Request $request) +{ + // check if request is json + if (! $request->isJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } +} + +function removeUnnecessaryFieldsFromRequest(Request $request) +{ + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); +} diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index dba8aa543..ef3f8ac9b 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -19,131 +19,163 @@ function generate_database_name(string $type): string return $type.'-database-'.$cuid; } -function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql +function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql { - // TODO: If another type of destination is added, this will need to be updated. - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + $destination = StandaloneDocker::where('uuid', $destinationUuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandalonePostgresql(); + $database->name = generate_database_name('postgresql'); + $database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environmentId; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandalonePostgresql::create([ - 'name' => generate_database_name('postgresql'), - 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneRedis(); + $database->name = generate_database_name('redis'); + $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneRedis::create([ - 'name' => generate_database_name('redis'), - 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMongodb(); + $database->name = generate_database_name('mongodb'); + $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMongodb::create([ - 'name' => generate_database_name('mongodb'), - 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMysql(); + $database->name = generate_database_name('mysql'); + $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMysql::create([ - 'name' => generate_database_name('mysql'), - 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMariadb(); + $database->name = generate_database_name('mariadb'); + $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); - return StandaloneMariadb::create([ - 'name' => generate_database_name('mariadb'), - 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); + + return $database; } -function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb +function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneKeydb(); + $database->name = generate_database_name('keydb'); + $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneKeydb::create([ - 'name' => generate_database_name('keydb'), - 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly +function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneDragonfly(); + $database->name = generate_database_name('dragonfly'); + $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneDragonfly::create([ - 'name' => generate_database_name('dragonfly'), - 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse +function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneClickhouse(); + $database->name = generate_database_name('clickhouse'); + $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneClickhouse::create([ - 'name' => generate_database_name('clickhouse'), - 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } /** diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1d70b674c..5efc0f9ef 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -538,6 +538,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } +function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId) +{ + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql && $postgresql->team()->id == $teamId) { + return $postgresql->unsetRelation('environment')->unsetRelation('destination'); + } + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis && $redis->team()->id == $teamId) { + return $redis->unsetRelation('environment'); + } + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb && $mongodb->team()->id == $teamId) { + return $mongodb->unsetRelation('environment'); + } + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql && $mysql->team()->id == $teamId) { + return $mysql->unsetRelation('environment'); + } + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb && $mariadb->team()->id == $teamId) { + return $mariadb->unsetRelation('environment'); + } + $keydb = StandaloneKeydb::whereUuid($uuid)->first(); + if ($keydb && $keydb->team()->id == $teamId) { + return $keydb->unsetRelation('environment'); + } + $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); + if ($dragonfly && $dragonfly->team()->id == $teamId) { + return $dragonfly->unsetRelation('environment'); + } + $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); + if ($clickhouse && $clickhouse->team()->id == $teamId) { + return $clickhouse->unsetRelation('environment'); + } + + return null; +} function queryResourcesByUuid(string $uuid) { $resource = null; diff --git a/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php new file mode 100644 index 000000000..b319adb70 --- /dev/null +++ b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php @@ -0,0 +1,24 @@ +boolean('is_api_enabled')->default(true); + $table->text('allowed_ips')->nullable(); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_api_enabled'); + $table->dropColumn('allowed_ips'); + }); + } +}; diff --git a/resources/views/livewire/settings/configuration.blade.php b/resources/views/livewire/settings/configuration.blade.php index b1c399bc3..b5fb49d3e 100644 --- a/resources/views/livewire/settings/configuration.blade.php +++ b/resources/views/livewire/settings/configuration.blade.php @@ -25,7 +25,16 @@
    --}} +

    API

    + +
    + +
    + +

    Advanced

    @if (!is_null(env('AUTOUPDATE', null))) @@ -36,13 +45,5 @@ @endif - {{-- @if ($next_channel) - - @else - - @endif --}}
    diff --git a/routes/api.php b/routes/api.php index f4d9d786e..69eead3ba 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,11 +1,16 @@ json(['message' => 'Feedback sent.'], 200); + return response()->json(['success' => true, 'message' => 'Feedback sent.'], 200); }); Route::group([ 'middleware' => ['auth:sanctum'], 'prefix' => 'v1', +], function () { + Route::get('/enable', function () { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['success' => false, 'message' => 'You are not allowed to enable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => true]); + + return response()->json(['success' => true, 'message' => 'API enabled.'], 200); + }); + Route::get('/disable', function () { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['success' => false, 'message' => 'You are not allowed to disable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => false]); + + return response()->json(['success' => true, 'message' => 'API disabled.'], 200); + }); + +}); +Route::group([ + 'middleware' => ['auth:sanctum', ApiAllowed::class], + 'prefix' => 'v1', ], function () { Route::get('/version', function () { return response(config('version')); }); - Route::match(['get', 'post'], '/deploy', [Deploy::class, 'deploy']); - Route::get('/deployments', [Deploy::class, 'deployments']); - Route::get('/deployments/{uuid}', [Deploy::class, 'deployment_by_uuid']); - // Add environments endpoints - Route::get('/servers', [Servers::class, 'servers']); - Route::get('/servers/{uuid}', [Servers::class, 'server_by_uuid']); - Route::get('/servers/domains', [Servers::class, 'get_domains_by_server']); + Route::get('/teams', [TeamController::class, 'teams']); + Route::get('/teams/current', [TeamController::class, 'current_team']); + Route::get('/teams/current/members', [TeamController::class, 'current_team_members']); + Route::get('/teams/{id}', [TeamController::class, 'team_by_id']); + Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id']); - Route::get('/resources', [Resources::class, 'resources']); + Route::get('/projects', [ProjectController::class, 'projects']); + Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid']); + Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); - Route::get('/applications', [Applications::class, 'applications']); - Route::post('/applications', [Applications::class, 'create_application']); + Route::get('/security/keys', [SecurityController::class, 'keys']); + Route::post('/security/keys', [SecurityController::class, 'create_key']); - Route::get('/applications/{uuid}', [Applications::class, 'application_by_uuid']); - Route::patch('/applications/{uuid}', [Applications::class, 'update_by_uuid']); - Route::delete('/applications/{uuid}', [Applications::class, 'delete_by_uuid']); + Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); + Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key']); + Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key']); - Route::get('/applications/{uuid}/envs', [Applications::class, 'envs_by_uuid']); - Route::post('/applications/{uuid}/envs', [Applications::class, 'create_env']); - Route::post('/applications/{uuid}/envs/bulk', [Applications::class, 'create_bulk_envs']); - Route::patch('/applications/{uuid}/envs', [Applications::class, 'update_env_by_uuid']); - Route::delete('/applications/{uuid}/envs/{env_uuid}', [Applications::class, 'delete_env_by_uuid']); + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']); - Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [Applications::class, 'action_deploy']); - Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [Applications::class, 'action_restart']); - Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [Applications::class, 'action_stop']); + Route::get('/deployments', [DeployController::class, 'deployments']); + Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); - Route::delete('/envs/{env_uuid}', [EnvironmentVariables::class, 'delete_env_by_uuid']); + Route::get('/servers', [ServersController::class, 'servers']); + Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid']); + Route::get('/servers/{uuid}/domains', [ServersController::class, 'get_domains_by_server']); - Route::get('/teams', [Team::class, 'teams']); - Route::get('/teams/current', [Team::class, 'current_team']); - Route::get('/teams/current/members', [Team::class, 'current_team_members']); - Route::get('/teams/{id}', [Team::class, 'team_by_id']); - Route::get('/teams/{id}/members', [Team::class, 'members_by_id']); + Route::get('/resources', [ResourcesController::class, 'resources']); + + Route::get('/applications', [ApplicationsController::class, 'applications']); + Route::post('/applications', [ApplicationsController::class, 'create_application']); + + Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); + Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid']); + Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid']); + + Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs_by_uuid']); + Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env']); + Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs']); + Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid']); + Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid']); + + Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy']); + Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart']); + Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop']); + + Route::get('/databases', [DatabasesController::class, 'databases']); + Route::post('/databases', [DatabasesController::class, 'create_database']); + Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); + // Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid']); + Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid']); + + Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid']); - // Route::get('/projects', [Project::class, 'projects']); - //Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']); - //Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']); }); Route::any('/{any}', function () { - return response()->json(['error' => 'Not found.'], 404); + return response()->json(['success' => false, 'message' => 'Not found.', 'docs' => 'https://coolify.io/docs'], 404); })->where('any', '.*'); // Route::middleware(['throttle:5'])->group(function () { From 1249b1ece9cbac10c1e76491568c93fda7db681a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 2 Jul 2024 10:02:43 +0200 Subject: [PATCH 18/57] fix: custom container name will be the container name, not just internal network name --- app/Jobs/ApplicationDeploymentJob.php | 58 +++++++------------ .../project/application/advanced.blade.php | 14 +++-- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index b53c56d1c..229cb2532 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -194,6 +194,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); + if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) { + $this->container_name = $this->application->settings->custom_internal_name; + } ray('New container name: ', $this->container_name); savePrivateKeyToFs($this->server); @@ -1570,23 +1573,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ], ], ]; - if (isset($this->application->settings->custom_internal_name)) { - $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name; - } - // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = '.env'; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = ['.env']; - // } - // } - // if ($this->env_filename) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; - // } - // } if (! is_null($this->env_filename)) { $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } @@ -1697,32 +1683,28 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - // if ($this->build_pack === 'dockerfile') { - // $docker_compose['services'][$this->container_name]['build'] = [ - // 'context' => $this->workdir, - // 'dockerfile' => $this->workdir . $this->dockerfile_location, - // ]; - // } if ($this->pull_request_id === 0) { $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { - $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; - if (count($custom_compose) > 0) { - $ipv4 = data_get($custom_compose, 'ip.0'); - $ipv6 = data_get($custom_compose, 'ip6.0'); - data_forget($custom_compose, 'ip'); - data_forget($custom_compose, 'ip6'); - if ($ipv4 || $ipv6) { - data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + if (! $this->application->settings->custom_internal_name) { + $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + if (count($custom_compose) > 0) { + $ipv4 = data_get($custom_compose, 'ip.0'); + $ipv6 = data_get($custom_compose, 'ip6.0'); + data_forget($custom_compose, 'ip'); + data_forget($custom_compose, 'ip6'); + if ($ipv4 || $ipv6) { + data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + } + if ($ipv4) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; + } + if ($ipv6) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; + } + $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } - if ($ipv4) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; - } - if ($ipv6) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; - } - $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } } else { if (count($custom_compose) > 0) { diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index ff96b228a..19cb64a05 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -31,12 +31,14 @@ helper="The deployed container will have the same name ({{ $application->uuid }}). You will lose the rolling update feature!" instantSave id="application.settings.is_consistent_container_name_enabled" label="Consistent Container Names" /> -
    - - Save - + @if (!$application->settings->is_consistent_container_name_enabled) +
    + + Save + + @endif @if ($application->build_pack === 'dockercompose')

    Network

    Date: Tue, 2 Jul 2024 12:15:58 +0200 Subject: [PATCH 19/57] feat: token permissions feat: handle sensitive data feat: handle read-only data --- .../Api/ApplicationsController.php | 35 +++++--- .../Controllers/Api/DatabasesController.php | 25 +++++- app/Http/Controllers/Api/DeployController.php | 16 +++- app/Http/Controllers/Api/TeamController.php | 27 +++++- app/Http/Kernel.php | 2 + app/Http/Middleware/OnlyRootApiToken.php | 25 ++++++ app/Http/Middleware/ReadOnlyApiToken.php | 28 +++++++ app/Jobs/DatabaseBackupJob.php | 3 +- .../Project/Database/Clickhouse/General.php | 9 +- .../Project/Database/Dragonfly/General.php | 9 +- .../Project/Database/Keydb/General.php | 9 +- .../Project/Database/Mariadb/General.php | 9 +- .../Project/Database/Mongodb/General.php | 9 +- .../Project/Database/Mysql/General.php | 9 +- .../Project/Database/Postgresql/General.php | 9 +- .../Project/Database/Redis/General.php | 9 +- app/Livewire/Security/ApiTokens.php | 42 +++++++++- app/Models/StandaloneClickhouse.php | 13 +-- app/Models/StandaloneDragonfly.php | 9 -- app/Models/StandaloneKeydb.php | 9 -- app/Models/StandaloneMariadb.php | 9 -- app/Models/StandaloneMongodb.php | 9 -- app/Models/StandaloneMysql.php | 9 -- app/Models/StandalonePostgresql.php | 9 -- app/Models/StandaloneRedis.php | 9 -- bootstrap/helpers/api.php | 84 +++++++++++++------ .../livewire/security/api-tokens.blade.php | 58 +++++++++---- routes/api.php | 36 ++++---- 28 files changed, 328 insertions(+), 201 deletions(-) create mode 100644 app/Http/Middleware/OnlyRootApiToken.php create mode 100644 app/Http/Middleware/ReadOnlyApiToken.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index e37de0378..9626b0488 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -21,6 +21,27 @@ use Visus\Cuid2\Cuid2; class ApplicationsController extends Controller { + private function removeSensitiveData($application) + { + $token = auth()->user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($application); + } + $application->makeHidden([ + 'custom_labels', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'private_key_id', + ]); + + return serializeApiResponse($application); + } + public function applications(Request $request) { $teamId = getTeamIdFromToken(); @@ -32,7 +53,7 @@ class ApplicationsController extends Controller $applications->push($projects->pluck('applications')->flatten()); $applications = $applications->flatten(); $applications = $applications->map(function ($application) { - return serializeApiResponse($application); + return $this->removeSensitiveData($application); }); return response()->json([ @@ -484,10 +505,6 @@ class ApplicationsController extends Controller if (! $uuid) { return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } - $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { - return $return; - } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { return response()->json(['success' => false, 'message' => 'Application not found.'], 404); @@ -495,7 +512,7 @@ class ApplicationsController extends Controller return response()->json([ 'success' => true, - 'data' => serializeApiResponse($application), + 'data' => $this->removeSensitiveData($application), ]); } @@ -625,7 +642,7 @@ class ApplicationsController extends Controller return response()->json([ 'success' => true, - 'data' => serializeApiResponse($application), + 'data' => $this->removeSensitiveData($application), ]); } @@ -635,10 +652,6 @@ class ApplicationsController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } - $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { - return $return; - } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 36a5fffaf..24530dca6 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -20,6 +20,27 @@ use Illuminate\Validation\Rule; class DatabasesController extends Controller { + private function removeSensitiveData($database) + { + $token = auth()->user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($database); + } + + $database->makeHidden([ + 'internal_db_url', + 'external_db_url', + 'postgres_password', + 'dragonfly_password', + 'redis_password', + 'mongo_initdb_root_password', + 'keydb_password', + 'clickhouse_admin_password', + ]); + + return serializeApiResponse($database); + } + public function databases(Request $request) { $teamId = getTeamIdFromToken(); @@ -32,7 +53,7 @@ class DatabasesController extends Controller $databases = $databases->merge($project->databases()); } $databases = $databases->map(function ($database) { - return serializeApiResponse($database); + return $this->removeSensitiveData($database); }); return response()->json([ @@ -57,7 +78,7 @@ class DatabasesController extends Controller return response()->json([ 'success' => true, - 'data' => serializeApiResponse($database), + 'data' => $this->removeSensitiveData($database), ]); } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 76e67548c..7add22bd0 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -20,6 +20,20 @@ use Visus\Cuid2\Cuid2; class DeployController extends Controller { + private function removeSensitiveData($deployment) + { + $token = auth()->user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($deployment); + } + + $deployment->makeHidden([ + 'logs', + ]); + + return serializeApiResponse($deployment); + } + public function deployments(Request $request) { $teamId = getTeamIdFromToken(); @@ -61,7 +75,7 @@ class DeployController extends Controller return response()->json([ 'success' => true, - 'data' => serializeApiResponse($deployment->makeHidden('logs')), + 'data' => $this->removeSensitiveData($deployment), ]); } diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index a256e9caf..b7837c785 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -7,17 +7,36 @@ use Illuminate\Http\Request; class TeamController extends Controller { + private function removeSensitiveData($team) + { + $token = auth()->user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($team); + } + $team->makeHidden([ + 'smtp_username', + 'smtp_password', + 'resend_api_key', + 'telegram_token', + ]); + + return serializeApiResponse($team); + } + public function teams(Request $request) { $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } - $teams = auth()->user()->teams; + $teams = auth()->user()->teams->sortBy('id'); + $teams = $teams->map(function ($team) { + return $this->removeSensitiveData($team); + }); return response()->json([ 'success' => true, - 'data' => serializeApiResponse($teams), + 'data' => $teams, ]); } @@ -33,6 +52,7 @@ class TeamController extends Controller if (is_null($team)) { return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); } + $team = $this->removeSensitiveData($team); return response()->json([ 'success' => true, @@ -52,10 +72,11 @@ class TeamController extends Controller if (is_null($team)) { return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); } + $members = $team->members; return response()->json([ 'success' => true, - 'data' => serializeApiResponse($team->members), + 'data' => serializeApiResponse($members), ]); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e29c4a307..5f1731071 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -67,5 +67,7 @@ class Kernel extends HttpKernel 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, + 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]; } diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php new file mode 100644 index 000000000..bea1ec567 --- /dev/null +++ b/app/Http/Middleware/OnlyRootApiToken.php @@ -0,0 +1,25 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + + return response()->json(['success' => false, 'message' => 'You are not allowed to perform this action.'], 403); + } +} diff --git a/app/Http/Middleware/ReadOnlyApiToken.php b/app/Http/Middleware/ReadOnlyApiToken.php new file mode 100644 index 000000000..447a406a6 --- /dev/null +++ b/app/Http/Middleware/ReadOnlyApiToken.php @@ -0,0 +1,28 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + if ($token->can('read-only')) { + return response()->json(['success' => false, 'message' => 'You are not allowed to perform this action.'], 403); + } + + return $next($request); + } +} diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 94e0ac3a3..4afe50d53 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -332,8 +332,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue private function backup_standalone_mongodb(string $databaseWithCollections): void { try { - ray($this->database->toArray()); - $url = $this->database->get_db_url(useInternal: true); + $url = $this->database->internal_db_url; if ($databaseWithCollections === 'all') { $commands[] = 'mkdir -p '.$this->backup_dir; if (str($this->database->image)->startsWith('mongo:4.0')) { diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 875a36141..ffdbe95c3 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -87,13 +85,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d6c4eb2ce..f81f4a2f0 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -44,10 +44,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -102,13 +100,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 381711946..2b78c9f10 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -108,13 +106,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 8b4b35d11..858d7b383 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -52,10 +52,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -114,13 +112,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index ee639ae41..5a5ef8a62 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -50,10 +50,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -115,13 +113,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index fc0767109..58d8e03a8 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -52,10 +52,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -113,13 +111,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 1c5d39055..76bc97901 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -72,10 +72,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -118,13 +116,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index b5c1dd881..a7ce0161a 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -102,13 +100,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index c485a6a3a..ff8679d21 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -10,6 +10,12 @@ class ApiTokens extends Component public $tokens = []; + public bool $viewSensitiveData = false; + + public bool $readOnly = true; + + public array $permissions = ['read-only']; + public function render() { return view('livewire.security.api-tokens'); @@ -17,7 +23,33 @@ class ApiTokens extends Component public function mount() { - $this->tokens = auth()->user()->tokens; + $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); + } + + public function updatedViewSensitiveData() + { + if ($this->viewSensitiveData) { + $this->permissions[] = 'view:sensitive'; + $this->permissions = array_diff($this->permissions, ['*']); + } else { + $this->permissions = array_diff($this->permissions, ['view:sensitive']); + } + if (count($this->permissions) == 0) { + $this->permissions = ['*']; + } + } + + public function updatedReadOnly() + { + if ($this->readOnly) { + $this->permissions[] = 'read-only'; + $this->permissions = array_diff($this->permissions, ['*']); + } else { + $this->permissions = array_diff($this->permissions, ['read-only']); + } + if (count($this->permissions) == 0) { + $this->permissions = ['*']; + } } public function addNewToken() @@ -26,7 +58,13 @@ class ApiTokens extends Component $this->validate([ 'description' => 'required|min:3|max:255', ]); - $token = auth()->user()->createToken($this->description); + // if ($this->viewSensitiveData) { + // $this->permissions[] = 'view:sensitive'; + // } + // if ($this->readOnly) { + // $this->permissions[] = 'read-only'; + // } + $token = auth()->user()->createToken($this->description, $this->permissions); $this->tokens = auth()->user()->tokens; session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 673224650..718fc9927 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -195,7 +195,7 @@ class StandaloneClickhouse extends BaseModel protected function internalDbUrl(): Attribute { return new Attribute( - get: fn () => "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}", + get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}", ); } @@ -204,7 +204,7 @@ class StandaloneClickhouse extends BaseModel return new Attribute( get: function () { if ($this->is_public && $this->public_port) { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; } return null; @@ -212,15 +212,6 @@ class StandaloneClickhouse extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index d78d656c1..b8d16d512 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -212,15 +212,6 @@ class StandaloneDragonfly extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 7b71bd55f..d2963cf02 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -212,15 +212,6 @@ class StandaloneKeydb extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 00df4fe71..b7907f251 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -212,15 +212,6 @@ class StandaloneMariadb extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 0863522a8..0f9f9a426 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -232,15 +232,6 @@ class StandaloneMongodb extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 79e7c37fa..bc4de88ee 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -213,15 +213,6 @@ class StandaloneMysql extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 1d5276cf3..372d79fd8 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -213,15 +213,6 @@ class StandalonePostgresql extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index e0f863aca..64731a28b 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -208,15 +208,6 @@ class StandaloneRedis extends BaseModel ); } - public function get_db_url(bool $useInternal = false) - { - if ($this->is_public && ! $useInternal) { - return $this->externalDbUrl; - } else { - return $this->internalDbUrl; - } - } - public function environment() { return $this->belongsTo(Environment::class); diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index c5083534f..00fccda74 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -19,37 +19,67 @@ function invalidTokenResponse() function serializeApiResponse($data) { - if (! $data instanceof Collection) { - $data = collect($data); - } - $data = $data->sortKeys(); + if ($data instanceof Collection) { + $data = $data->map(function ($d) { + $d = collect($d)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; - $created_at = data_get($data, 'created_at'); - $updated_at = data_get($data, 'updated_at'); - if ($created_at) { - unset($data['created_at']); - $data['created_at'] = $created_at; + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } - } - if ($updated_at) { - unset($data['updated_at']); - $data['updated_at'] = $updated_at; - } - if (data_get($data, 'name')) { - $data = $data->prepend($data['name'], 'name'); - } - if (data_get($data, 'description')) { - $data = $data->prepend($data['description'], 'description'); - } - if (data_get($data, 'uuid')) { - $data = $data->prepend($data['uuid'], 'uuid'); - } + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } - if (data_get($data, 'id')) { - $data = $data->prepend($data['id'], 'id'); - } + return $d; + }); - return $data; + return $data; + } else { + $d = collect($data)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; + + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } + + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } + + return $d; + } } function sharedDataApplications() diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index b9120878d..3a5d4560c 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -3,29 +3,59 @@ API Tokens | Coolify -
    -

    API Tokens

    - +
    +

    API Tokens

    +
    Tokens are created with the current team as scope. You will only have access to this team's resources. +
    -

    Create New Token

    -
    - - Create New Token +

    New Token

    + +
    + + Create New Token +
    +
    + Permissions : +
    + @if ($permissions) + @foreach ($permissions as $permission) + @if ($permission === '*') +
    All (root/admin access), be careful!
    + @else +
    {{ $permission }}
    + @endif + @endforeach + @endif +
    +
    +

    Token Permissions

    +
    + + +
    @if (session()->has('token')) -
    Please copy this token now. For your security, it won't be shown again. +
    Please copy this token now. For your security, it won't be shown + again.
    {{ session('token') }}
    @endif -

    Issued Tokens

    +

    Issued Tokens

    @forelse ($tokens as $token) -
    -
    -
    {{ $token->name }}
    +
    +
    Description: {{ $token->name }}
    +
    Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
    +
    + @if ($token->abilities) + Abilities: + @foreach ($token->abilities as $ability) +
    {{ $ability }}
    + @endforeach + @endif
    + Revoke token diff --git a/routes/api.php b/routes/api.php index 69eead3ba..61b3348b7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,6 +10,8 @@ use App\Http\Controllers\Api\SecurityController; use App\Http\Controllers\Api\ServersController; use App\Http\Controllers\Api\TeamController; use App\Http\Middleware\ApiAllowed; +use App\Http\Middleware\OnlyRootApiToken; +use App\Http\Middleware\ReadOnlyApiToken; use App\Models\InstanceSettings; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -31,7 +33,7 @@ Route::post('/feedback', function (Request $request) { }); Route::group([ - 'middleware' => ['auth:sanctum'], + 'middleware' => ['auth:sanctum', OnlyRootApiToken::class], 'prefix' => 'v1', ], function () { Route::get('/enable', function () { @@ -81,13 +83,13 @@ Route::group([ Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); Route::get('/security/keys', [SecurityController::class, 'keys']); - Route::post('/security/keys', [SecurityController::class, 'create_key']); + Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware([ReadOnlyApiToken::class]); Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); - Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key']); - Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key']); + Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware([ReadOnlyApiToken::class]); + Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware([ReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']); + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware([ReadOnlyApiToken::class]); Route::get('/deployments', [DeployController::class, 'deployments']); Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); @@ -99,29 +101,29 @@ Route::group([ Route::get('/resources', [ResourcesController::class, 'resources']); Route::get('/applications', [ApplicationsController::class, 'applications']); - Route::post('/applications', [ApplicationsController::class, 'create_application']); + Route::post('/applications', [ApplicationsController::class, 'create_application'])->middleware([ReadOnlyApiToken::class]); Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); - Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid']); - Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid']); + Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware([ReadOnlyApiToken::class]); + Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware([ReadOnlyApiToken::class]); Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs_by_uuid']); - Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env']); - Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs']); + Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware([ReadOnlyApiToken::class]); + Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware([ReadOnlyApiToken::class]); Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid']); - Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid']); + Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware([ReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy']); - Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart']); - Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop']); + Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy'])->middleware([ReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart'])->middleware([ReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop'])->middleware([ReadOnlyApiToken::class]); Route::get('/databases', [DatabasesController::class, 'databases']); - Route::post('/databases', [DatabasesController::class, 'create_database']); + Route::post('/databases', [DatabasesController::class, 'create_database'])->middleware([ReadOnlyApiToken::class]); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); // Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid']); - Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid']); + Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware([ReadOnlyApiToken::class]); - Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid']); + Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid'])->middleware([ReadOnlyApiToken::class]); }); From 3c13f1ff61e4b9791e440d86483ad06d36d0463d Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 2 Jul 2024 13:39:44 +0200 Subject: [PATCH 20/57] feat: restart database feat: public dbs stay public after restart feat: patch database conf --- app/Actions/Database/RestartDatabase.php | 29 ++ app/Actions/Database/StartDatabase.php | 57 +++ app/Actions/Database/StopDatabase.php | 2 - app/Actions/Database/StopDatabaseProxy.php | 1 - .../Controllers/Api/DatabasesController.php | 331 ++++++++++++++++-- app/Http/Controllers/Api/DeployController.php | 91 ++--- app/Livewire/Project/Database/Heading.php | 44 +-- .../Project/Database/Postgresql/General.php | 8 - bootstrap/helpers/api.php | 8 + bootstrap/helpers/databases.php | 17 +- .../project/database/heading.blade.php | 20 +- routes/api.php | 3 +- 12 files changed, 471 insertions(+), 140 deletions(-) create mode 100644 app/Actions/Database/RestartDatabase.php create mode 100644 app/Actions/Database/StartDatabase.php diff --git a/app/Actions/Database/RestartDatabase.php b/app/Actions/Database/RestartDatabase.php new file mode 100644 index 000000000..0400d924d --- /dev/null +++ b/app/Actions/Database/RestartDatabase.php @@ -0,0 +1,29 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + StopDatabase::run($database); + + return StartDatabase::run($database); + } +} diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php new file mode 100644 index 000000000..323c52ff9 --- /dev/null +++ b/app/Actions/Database/StartDatabase.php @@ -0,0 +1,57 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + switch ($database->getMorphClass()) { + case 'App\Models\StandalonePostgresql': + $activity = StartPostgresql::run($database); + break; + case 'App\Models\StandaloneRedis': + $activity = StartRedis::run($database); + break; + case 'App\Models\StandaloneMongodb': + $activity = StartMongodb::run($database); + break; + case 'App\Models\StandaloneMysql': + $activity = StartMysql::run($database); + break; + case 'App\Models\StandaloneMariadb': + $activity = StartMariadb::run($database); + break; + case 'App\Models\StandaloneKeydb': + $activity = StartKeydb::run($database); + break; + case 'App\Models\StandaloneDragonfly': + $activity = StartDragonfly::run($database); + break; + case 'App\Models\StandaloneClickhouse': + $activity = StartClickhouse::run($database); + break; + } + if ($database->is_public && $database->public_port) { + StartDatabaseProxy::dispatch($database); + } + + return $activity; + } +} diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 66a32e811..e4903ff35 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -29,7 +29,5 @@ class StopDatabase if ($database->is_public) { StopDatabaseProxy::run($database); } - // TODO: make notification for services - // $database->environment->project->team->notify(new StatusChanged($database)); } } diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 1b262c898..b2092e2ef 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -27,7 +27,6 @@ class StopDatabaseProxy $server = data_get($database, 'service.server'); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); - $database->is_public = false; $database->save(); DatabaseStatusChanged::dispatch(); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 24530dca6..1a5106d7c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -2,15 +2,10 @@ namespace App\Http\Controllers\Api; -use App\Actions\Database\StartClickhouse; -use App\Actions\Database\StartDragonfly; -use App\Actions\Database\StartKeydb; -use App\Actions\Database\StartMariadb; -use App\Actions\Database\StartMongodb; -use App\Actions\Database\StartMysql; -use App\Actions\Database\StartPostgresql; -use App\Actions\Database\StartRedis; +use App\Actions\Database\StartDatabase; +use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabase; +use App\Actions\Database\StopDatabaseProxy; use App\Enums\NewDatabaseTypes; use App\Http\Controllers\Controller; use App\Models\Project; @@ -82,9 +77,114 @@ class DatabasesController extends Controller ]); } + public function update_by_uuid(Request $request) + { + $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_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + 'dragonfly_password' => 'string', + 'redis_password' => 'string', + 'redis_conf' => 'string', + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + 'mariadb_conf' => 'string', + 'mariadb_root_password' => 'string', + 'mariadb_user' => 'string', + 'mariadb_password' => 'string', + 'mariadb_database' => 'string', + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + if ($request->is_public && $request->public_port) { + if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) { + return response()->json(['success' => false, 'message' => 'Public port already used by another database.'], 400); + } + } + + $whatToDoWithDatabaseProxy = null; + if ($request->is_public === false && $database->is_public === true) { + $whatToDoWithDatabaseProxy = 'stop'; + } + if ($request->is_public === true && $request->public_port && $database->is_public === false) { + $whatToDoWithDatabaseProxy = 'start'; + } + + $database->update($request->all()); + + if ($whatToDoWithDatabaseProxy === 'start') { + StartDatabaseProxy::dispatch($database); + } elseif ($whatToDoWithDatabaseProxy === 'stop') { + StopDatabaseProxy::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database updated.', + 'data' => $this->removeSensitiveData($database), + ]); + + } + public function create_database(Request $request) { - $allowedFields = ['type', 'name', 'description', 'image', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'postgres_user', 'postgres_password', 'postgres_db', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares']; + $allowedFields = ['type', 'name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', '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_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -103,9 +203,8 @@ class DatabasesController extends Controller 'environment_name' => 'string|required', 'server_uuid' => 'string|required', 'destination_uuid' => 'string', - 'postgres_user' => 'string', - 'postgres_password' => 'string', - 'postgres_db' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', 'limits_memory' => 'string', 'limits_memory_swap' => 'string', 'limits_memory_swappiness' => 'numeric', @@ -113,6 +212,32 @@ class DatabasesController extends Controller 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + 'dragonfly_password' => 'string', + 'redis_password' => 'string', + 'redis_conf' => 'string', + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + 'mariadb_conf' => 'string', + 'mariadb_root_password' => 'string', + 'mariadb_user' => 'string', + 'mariadb_password' => 'string', + 'mariadb_database' => 'string', + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', 'instant_deploy' => 'boolean', ]); @@ -133,7 +258,9 @@ class DatabasesController extends Controller } $serverUuid = $request->server_uuid; $instantDeploy = $request->instant_deploy ?? false; - + if ($request->is_public && ! $request->public_port) { + $request->offsetSet('is_public', false); + } $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); @@ -154,12 +281,41 @@ class DatabasesController extends Controller return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); } $destination = $destinations->first(); - + if ($request->has('public_port') && $request->is_public) { + if (isPublicPortAlreadyUsed($server, $request->public_port)) { + return response()->json(['success' => false, 'message' => 'Public port already used by another database.'], 400); + } + } if ($request->type === NewDatabaseTypes::POSTGRESQL->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('postgres_conf')) { + if (! isBase64Encoded($request->postgres_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $postgresConf = base64_decode($request->postgres_conf); + if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('postgres_conf', $postgresConf); + } $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartPostgresql::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -169,9 +325,34 @@ class DatabasesController extends Controller ]); } elseif ($request->type === NewDatabaseTypes::MARIADB->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mariadb_conf')) { + if (! isBase64Encoded($request->mariadb_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $mariadbConf = base64_decode($request->mariadb_conf); + if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mariadb_conf', $mariadbConf); + } $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartMariadb::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -181,9 +362,34 @@ class DatabasesController extends Controller ]); } elseif ($request->type === NewDatabaseTypes::MYSQL->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mysql_conf')) { + if (! isBase64Encoded($request->mysql_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $mysqlConf = base64_decode($request->mysql_conf); + if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mysql_conf', $mysqlConf); + } $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartMysql::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -193,9 +399,34 @@ class DatabasesController extends Controller ]); } elseif ($request->type === NewDatabaseTypes::REDIS->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('redis_conf')) { + if (! isBase64Encoded($request->redis_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $redisConf = base64_decode($request->redis_conf); + if (mb_detect_encoding($redisConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('redis_conf', $redisConf); + } $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartRedis::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -207,7 +438,10 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDragonfly::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -217,9 +451,34 @@ class DatabasesController extends Controller ]); } elseif ($request->type === NewDatabaseTypes::KEYDB->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('keydb_conf')) { + if (! isBase64Encoded($request->keydb_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $keydbConf = base64_decode($request->keydb_conf); + if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('keydb_conf', $keydbConf); + } $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartKeydb::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -231,7 +490,10 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartClickhouse::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ @@ -241,9 +503,34 @@ class DatabasesController extends Controller ]); } elseif ($request->type === NewDatabaseTypes::MONGODB->value) { removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mongo_conf')) { + if (! isBase64Encoded($request->mongo_conf)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $mongoConf = base64_decode($request->mongo_conf); + if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mongo_conf', $mongoConf); + } $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartMongodb::dispatch($database); + StartDatabase::dispatch($database); + if ($request->is_public && $request->public_port) { + StartDatabaseProxy::dispatch($database); + } } return response()->json([ diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 7add22bd0..79d98df0c 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -2,14 +2,7 @@ namespace App\Http\Controllers\Api; -use App\Actions\Database\StartClickhouse; -use App\Actions\Database\StartDragonfly; -use App\Actions\Database\StartKeydb; -use App\Actions\Database\StartMariadb; -use App\Actions\Database\StartMongodb; -use App\Actions\Database\StartMysql; -use App\Actions\Database\StartPostgresql; -use App\Actions\Database\StartRedis; +use App\Actions\Database\StartDatabase; use App\Actions\Service\StartService; use App\Http\Controllers\Controller; use App\Models\ApplicationDeploymentQueue; @@ -192,66 +185,28 @@ class DeployController extends Controller if (gettype($resource) !== 'object') { return ['success' => false, 'message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } - $type = $resource?->getMorphClass(); - if ($type === 'App\Models\Application') { - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $resource, - deployment_uuid: $deployment_uuid, - force_rebuild: $force, - ); - $message = "Application {$resource->name} deployment queued."; - } elseif ($type === 'App\Models\StandalonePostgresql') { - StartPostgresql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneRedis') { - StartRedis::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneKeydb') { - StartKeydb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneDragonfly') { - StartDragonfly::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneClickhouse') { - StartClickhouse::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMongodb') { - StartMongodb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMysql') { - StartMysql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMariadb') { - StartMariadb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\Service') { - StartService::run($resource); - $message = "Service {$resource->name} started. It could take a while, be patient."; + switch ($resource?->getMorphClass()) { + case 'App\Models\Application': + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $resource, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + ); + $message = "Application {$resource->name} deployment queued."; + break; + case 'App\Models\Service': + StartService::run($resource); + $message = "Service {$resource->name} started. It could take a while, be patient."; + break; + default: + // Database resource + StartDatabase::dispatch($resource); + $resource->update([ + 'started_at' => now(), + ]); + $message = "Database {$resource->name} started."; + break; } return ['success' => true, 'message' => $message, 'deployment_uuid' => $deployment_uuid]; diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index ae88ac12b..6435f6781 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -2,14 +2,8 @@ namespace App\Livewire\Project\Database; -use App\Actions\Database\StartClickhouse; -use App\Actions\Database\StartDragonfly; -use App\Actions\Database\StartKeydb; -use App\Actions\Database\StartMariadb; -use App\Actions\Database\StartMongodb; -use App\Actions\Database\StartMysql; -use App\Actions\Database\StartPostgresql; -use App\Actions\Database\StartRedis; +use App\Actions\Database\RestartDatabase; +use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; use Livewire\Component; @@ -47,7 +41,6 @@ class Heading extends Component public function check_status($showNotification = false) { GetContainersStatus::run($this->database->destination->server); - // dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); if ($showNotification) { $this->dispatch('success', 'Database status updated.'); @@ -67,32 +60,15 @@ class Heading extends Component $this->check_status(); } + public function restart() + { + $activity = RestartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); + } + public function start() { - if ($this->database->type() === 'standalone-postgresql') { - $activity = StartPostgresql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-redis') { - $activity = StartRedis::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mongodb') { - $activity = StartMongodb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mysql') { - $activity = StartMysql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mariadb') { - $activity = StartMariadb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-keydb') { - $activity = StartKeydb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-dragonfly') { - $activity = StartDragonfly::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-clickhouse') { - $activity = StartClickhouse::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } + $activity = StartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); } } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 76bc97901..eabbbd679 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -27,10 +27,7 @@ class General extends Component public function getListeners() { - $userId = auth()->user()->id; - return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => 'database_stopped', 'refresh', 'save_init_script', 'delete_init_script', @@ -77,11 +74,6 @@ class General extends Component $this->server = data_get($this->database, 'destination.server'); } - public function database_stopped() - { - $this->dispatch('success', 'Database proxy stopped. Database is no longer publicly accessible.'); - } - public function instantSaveAdvanced() { try { diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 00fccda74..e4da4a563 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -151,6 +151,14 @@ function validateIncomingRequest(Request $request) 'error' => 'Invalid JSON.', ], 400); } + // check if valid json is empty + if (empty($request->json()->all())) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Empty JSON.', + ], 400); + } } function removeUnnecessaryFieldsFromRequest(Request $request) diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index ef3f8ac9b..089298956 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -178,9 +178,6 @@ function create_standalone_clickhouse($environment_id, $destination_uuid, ?array return $database; } -/** - * Delete file locally on the filesystem. - */ function delete_backup_locally(?string $filename, Server $server): void { if (empty($filename)) { @@ -188,3 +185,17 @@ function delete_backup_locally(?string $filename, Server $server): void } instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false); } + +function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool +{ + if ($id) { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->where('id', '!=', $id)->first(); + } else { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->first(); + } + if ($foundDatabase) { + return true; + } + + return false; +} diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index 0c0647324..cee1f0520 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -7,7 +7,8 @@