diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index c07ac354d..16413d2ad 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -190,6 +190,7 @@ public function applications(Request $request) 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -217,6 +218,35 @@ public function applications(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_public_application(Request $request) @@ -310,6 +340,7 @@ public function create_public_application(Request $request) 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -337,6 +368,35 @@ public function create_public_application(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_private_gh_app_application(Request $request) @@ -430,6 +490,7 @@ public function create_private_gh_app_application(Request $request) 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -457,6 +518,35 @@ public function create_private_gh_app_application(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_private_deploy_key_application(Request $request) @@ -534,6 +624,7 @@ public function create_private_deploy_key_application(Request $request) 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -561,6 +652,35 @@ public function create_private_deploy_key_application(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockerfile_application(Request $request) @@ -635,6 +755,7 @@ public function create_dockerfile_application(Request $request) 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -662,6 +783,35 @@ public function create_dockerfile_application(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockerimage_application(Request $request) @@ -699,6 +849,7 @@ public function create_dockerimage_application(Request $request) 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -726,6 +877,35 @@ public function create_dockerimage_application(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function create_dockercompose_application(Request $request) @@ -746,7 +926,7 @@ private function create_application(Request $request, $type) if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', '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', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', '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', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -1380,7 +1560,7 @@ private function create_application(Request $request, $type) 'domains' => data_get($application, 'domains'), ]))->setStatusCode(201); } elseif ($type === 'dockercompose') { - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw', 'force_domain_override']; $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -1810,6 +1990,7 @@ public function delete_by_uuid(Request $request) 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], ], ) ), @@ -1843,6 +2024,35 @@ public function delete_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 409, + description: 'Domain conflicts detected.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'], + 'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'], + 'conflicts' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'domain' => ['type' => 'string', 'example' => 'example.com'], + 'resource_name' => ['type' => 'string', 'example' => 'My Application'], + 'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'], + 'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'], + 'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''], + ] + ), + ], + ] + ) + ), + ] + ), ] )] public function update_by_uuid(Request $request) @@ -1866,7 +2076,7 @@ public function update_by_uuid(Request $request) $this->authorize('update', $application); $server = $application->destination->server; - $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_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network']; + $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_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override']; $validationRules = [ 'name' => 'string|max:255', @@ -1982,14 +2192,23 @@ public function update_by_uuid(Request $request) 'errors' => $errors, ], 422); } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + // Check for domain conflicts + $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], + 'errors' => ['domains' => $result['error']], ], 422); } + + // If there are conflicts and force is not enabled, return warning + if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) { + return response()->json([ + 'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.', + 'conflicts' => $result['conflicts'], + 'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.', + ], 409); + } } $dockerComposeDomainsJson = collect(); @@ -3102,131 +3321,6 @@ public function action_restart(Request $request) ); } - // #[OA\Post( - // summary: 'Execute Command', - // description: "Execute a command on the application's current container.", - // path: '/applications/{uuid}/execute', - // operationId: 'execute-command-application', - // security: [ - // ['bearerAuth' => []], - // ], - // tags: ['Applications'], - // parameters: [ - // new OA\Parameter( - // name: 'uuid', - // in: 'path', - // description: 'UUID of the application.', - // required: true, - // schema: new OA\Schema( - // type: 'string', - // format: 'uuid', - // ) - // ), - // ], - // requestBody: new OA\RequestBody( - // required: true, - // description: 'Command to execute.', - // content: new OA\MediaType( - // mediaType: 'application/json', - // schema: new OA\Schema( - // type: 'object', - // properties: [ - // 'command' => ['type' => 'string', 'description' => 'Command to execute.'], - // ], - // ), - // ), - // ), - // responses: [ - // new OA\Response( - // response: 200, - // description: "Execute a command on the application's current container.", - // content: [ - // new OA\MediaType( - // mediaType: 'application/json', - // schema: new OA\Schema( - // type: 'object', - // properties: [ - // 'message' => ['type' => 'string', 'example' => 'Command executed.'], - // 'response' => ['type' => 'string'], - // ] - // ) - // ), - // ] - // ), - // new OA\Response( - // response: 401, - // ref: '#/components/responses/401', - // ), - // new OA\Response( - // response: 400, - // ref: '#/components/responses/400', - // ), - // new OA\Response( - // response: 404, - // ref: '#/components/responses/404', - // ), - // ] - // )] - // public function execute_command_by_uuid(Request $request) - // { - // // TODO: Need to review this from security perspective, to not allow arbitrary command execution - // $allowedFields = ['command']; - // $teamId = getTeamIdFromToken(); - // if (is_null($teamId)) { - // return invalidTokenResponse(); - // } - // $uuid = $request->route('uuid'); - // if (! $uuid) { - // return response()->json(['message' => 'UUID is required.'], 400); - // } - // $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - // if (! $application) { - // return response()->json(['message' => 'Application not found.'], 404); - // } - // $return = validateIncomingRequest($request); - // if ($return instanceof \Illuminate\Http\JsonResponse) { - // return $return; - // } - // $validator = customApiValidator($request->all(), [ - // 'command' => 'string|required', - // ]); - - // $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); - // } - - // $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail(); - // $status = getContainerStatus($application->destination->server, $container['Names']); - - // if ($status !== 'running') { - // return response()->json([ - // 'message' => 'Application is not running.', - // ], 400); - // } - - // $commands = collect([ - // executeInDocker($container['Names'], $request->command), - // ]); - - // $res = instant_remote_process(command: $commands, server: $application->destination->server); - - // return response()->json([ - // 'message' => 'Command executed.', - // 'response' => $res, - // ]); - // } - private function validateDataApplications(Request $request, Server $server) { $teamId = getTeamIdFromToken(); @@ -3286,14 +3380,23 @@ private function validateDataApplications(Request $request, Server $server) 'errors' => $errors, ], 422); } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + // Check for domain conflicts + $result = checkIfDomainIsAlreadyUsedViaAPI($fqdn, $teamId, $uuid); + if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], + 'errors' => ['domains' => $result['error']], ], 422); } + + // If there are conflicts and force is not enabled, return warning + if ($result['hasConflicts'] && ! $request->boolean('force_domain_override')) { + return response()->json([ + 'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.', + 'conflicts' => $result['conflicts'], + 'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.', + ], 409); + } } } } diff --git a/app/Http/Middleware/CanAccessTerminal.php b/app/Http/Middleware/CanAccessTerminal.php index dcccd819b..348f389ea 100644 --- a/app/Http/Middleware/CanAccessTerminal.php +++ b/app/Http/Middleware/CanAccessTerminal.php @@ -15,17 +15,15 @@ class CanAccessTerminal */ public function handle(Request $request, Closure $next): Response { + if (! auth()->check()) { + abort(401, 'Authentication required'); + } + + // Only admins/owners can access terminal functionality + if (! auth()->user()->can('canAccessTerminal')) { + abort(403, 'Access to terminal functionality is restricted to team administrators'); + } + return $next($request); - - // if (! auth()->check()) { - // abort(401, 'Authentication required'); - // } - - // // Only admins/owners can access terminal functionality - // if (! auth()->user()->can('canAccessTerminal')) { - // abort(403, 'Access to terminal functionality is restricted to team administrators'); - // } - - // return $next($request); } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 3107ef4cb..aa72b7c5f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -51,9 +51,16 @@ class General extends Component public $parsedServiceDomains = []; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $listeners = [ 'resetDefaultLabels', 'configurationChanged' => '$refresh', + 'confirmDomainUsage', ]; protected function rules(): array @@ -430,7 +437,7 @@ public function getWildcardDomain() $server = data_get($this->application, 'destination.server'); if ($server) { - $fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version); + $fqdn = generateUrl(server: $server, random: $this->application->uuid); $this->application->fqdn = $fqdn; $this->application->save(); $this->resetDefaultLabels(); @@ -485,10 +492,33 @@ public function checkFqdns($showToaster = true) } } } - check_domain_usage(resource: $this->application); + + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return false; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->application->fqdn = $domains->implode(','); $this->resetDefaultLabels(false); } + + return true; + } + + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); } public function setRedirect() @@ -536,7 +566,9 @@ public function submit($showToaster = true) $this->application->parseHealthcheckFromDockerfile($this->application->dockerfile); } - $this->checkFqdns(); + if (! $this->checkFqdns()) { + return; // Stop if there are conflicts and user hasn't confirmed + } $this->application->save(); if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { @@ -588,7 +620,20 @@ public function submit($showToaster = true) } } } - check_domain_usage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->application->save(); $this->resetDefaultLabels(); } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 9164c1475..ebfd84489 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -25,6 +25,14 @@ class Previews extends Component public int $rate_limit_remaining; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + + public $pendingPreviewId = null; + protected $rules = [ 'application.previews.*.fqdn' => 'string|nullable', ]; @@ -49,6 +57,16 @@ public function load_prs() } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + if ($this->pendingPreviewId) { + $this->save_preview($this->pendingPreviewId); + $this->pendingPreviewId = null; + } + } + public function save_preview($preview_id) { try { @@ -63,7 +81,20 @@ public function save_preview($preview_id) $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

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

Check this documentation for further help."); $success = false; } - check_domain_usage(resource: $this->application, domain: $preview->fqdn); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + $this->pendingPreviewId = $preview_id; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } } if (! $preview) { diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index 0317ba7e7..2632509ea 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -60,7 +60,7 @@ public function generate() $random = new Cuid2; // Generate a unique domain like main app services do - $generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version); + $generated_fqdn = generateUrl(server: $server, random: $random); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 3c8c9843d..be9de139f 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -133,7 +133,7 @@ public function clone(string $type) $uuid = (string) new Cuid2; $url = $application->fqdn; if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version); + $url = generateUrl(server: $this->server, random: $uuid); } $newApplication = $application->replicate([ diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 7d68ce068..dbb223de2 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -60,7 +60,7 @@ public function submit() 'health_check_enabled' => false, ]); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ 'name' => 'docker-image-'.$application->uuid, 'fqdn' => $fqdn, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index a7aaa94a4..0f496e6db 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -208,7 +208,7 @@ public function submit() $application['docker_compose_location'] = $this->docker_compose_location; $application['base_directory'] = $this->base_directory; } - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid); diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index d76f7baaa..5ff8f9137 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -194,7 +194,7 @@ public function submit() $application->settings->is_static = $this->is_static; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->name = generate_random_name($application->uuid); $application->save(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 8de998a96..f5978aea1 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -373,7 +373,7 @@ public function submit() $application->settings->is_static = $this->isStatic; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->fqdn = $fqdn; $application->save(); if ($this->checkCoolifyConfig) { diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index ebc9878dc..9cc4fbbe2 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -68,7 +68,7 @@ public function submit() 'source_type' => GithubApp::class, ]); - $fqdn = generateFqdn($destination->server, $application->uuid); + $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ 'name' => 'dockerfile-'.$application->uuid, 'fqdn' => $fqdn, diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index b7f73159e..5ce170b99 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -12,6 +12,12 @@ class EditDomain extends Component public ServiceApplication $application; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $rules = [ 'application.fqdn' => 'nullable', 'application.required_fqdn' => 'required|boolean', @@ -22,6 +28,13 @@ public function mount() $this->application = ServiceApplication::find($this->applicationId); } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -37,7 +50,20 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - check_domain_usage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 5e178374b..3ac12cfe9 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -23,6 +23,12 @@ class ServiceApplicationView extends Component public $delete_volumes = true; + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + protected $rules = [ 'application.human_name' => 'nullable', 'application.description' => 'nullable', @@ -129,6 +135,13 @@ public function convertToDatabase() } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -145,7 +158,20 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - check_domain_usage(resource: $this->application); + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + $this->validate(); $this->application->save(); updateCompose($this->application); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 6833492a6..02062e1f7 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -33,9 +33,6 @@ class ExecuteContainerCommand extends Component public function mount() { - if (! auth()->user()->isAdmin()) { - abort(403); - } $this->parameters = get_route_parameters(); $this->containers = collect(); $this->servers = collect(); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index c9b341eed..28a6380d5 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -66,7 +66,7 @@ public function cloneTo($destination_id) $url = $this->resource->fqdn; if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version); + $url = generateUrl(server: $server, random: $uuid); } $new_resource = $this->resource->replicate([ diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index bce343224..d05433082 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -35,6 +35,12 @@ class Index extends Component #[Validate('required|string|timezone')] public string $instance_timezone; + public array $domainConflicts = []; + + public bool $showDomainConflictModal = false; + + public bool $forceSaveDomains = false; + public function render() { return view('livewire.settings.index'); @@ -81,6 +87,13 @@ public function instantSave($isSave = true) } } + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submit(); + } + public function submit() { try { @@ -108,7 +121,18 @@ public function submit() } } if ($this->fqdn) { - check_domain_usage(domain: $this->fqdn); + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(domain: $this->fqdn); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } } $this->instantSave(isSave: false); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index aa31268f1..721b22216 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -74,7 +74,7 @@ public function persistentStorages() public function generate_preview_fqdn() { - if (empty($this->fqdn) && $this->application->fqdn) { + if ($this->application->fqdn) { if (str($this->application->fqdn)->contains(',')) { $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); $preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]); diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 5cc6b739f..6d2396a7d 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -28,7 +28,8 @@ public function view(User $user, Server $server): bool */ public function create(User $user): bool { - return $user->isAdmin(); + // return $user->isAdmin(); + return true; } /** @@ -36,7 +37,8 @@ public function create(User $user): bool */ public function update(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -44,7 +46,8 @@ public function update(User $user, Server $server): bool */ public function delete(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -68,7 +71,8 @@ public function forceDelete(User $user, Server $server): bool */ public function manageProxy(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -76,7 +80,8 @@ public function manageProxy(User $user, Server $server): bool */ public function manageSentinel(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -84,15 +89,8 @@ public function manageSentinel(User $user, Server $server): bool */ public function manageCaCertificate(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - } - - /** - * Determine whether the user can view terminal. - */ - public function viewTerminal(User $user, Server $server): bool - { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } /** @@ -100,6 +98,7 @@ public function viewTerminal(User $user, Server $server): bool */ public function viewSecurity(User $user, Server $server): bool { - return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); + return true; } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3e76e6976..c017a580e 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -67,8 +67,7 @@ public function boot(): void // Register gate for terminal access Gate::define('canAccessTerminal', function ($user) { - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); }); } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 307c7ed1b..5d0f9a2a7 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -176,4 +176,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('private_key_uuid'); $request->offsetUnset('use_build_server'); $request->offsetUnset('is_static'); + $request->offsetUnset('force_domain_override'); } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index ac707f7ab..f61abc806 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) { $MINIO_BROWSER_REDIRECT_URL->update([ - 'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), + 'value' => generateUrl(server: $server, random: 'console-'.$uuid, forceHttps: true), ]); } if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) { $MINIO_SERVER_URL->update([ - 'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), + 'value' => generateUrl(server: $server, random: 'minio-'.$uuid, forceHttps: true), ]); } $payload = collect([ @@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ENDPOINT->update([ - 'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->service->compose_parsing_version), + 'value' => generateUrl(server: $server, random: 'logto-'.$uuid), ]); } if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) { $LOGTO_ADMIN_ENDPOINT->update([ - 'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->service->compose_parsing_version), + 'value' => generateUrl(server: $server, random: 'logto-admin-'.$uuid), ]); } $payload = collect([ diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php new file mode 100644 index 000000000..5b665890c --- /dev/null +++ b/bootstrap/helpers/domains.php @@ -0,0 +1,237 @@ +team(); + } + + if ($resource) { + if ($resource->getMorphClass() === Application::class && $resource->build_pack === 'dockercompose') { + $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); + $domains = collect($domains); + } else { + $domains = collect($resource->fqdns); + } + } elseif ($domain) { + $domains = collect([$domain]); + } else { + return ['conflicts' => [], 'hasConflicts' => false]; + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + + // Filter applications by team if we have a current team + $appsQuery = Application::query(); + if ($currentTeam) { + $appsQuery = $appsQuery->whereHas('environment.project', function ($query) use ($currentTeam) { + $query->where('team_id', $currentTeam->id); + }); + } + $apps = $appsQuery->get(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_link' => $app->link(), + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; + } + } elseif ($domain) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_link' => $app->link(), + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; + } + } + } + } + + // Filter service applications by team if we have a current team + $serviceAppsQuery = ServiceApplication::query(); + if ($currentTeam) { + $serviceAppsQuery = $serviceAppsQuery->whereHas('service.environment.project', function ($query) use ($currentTeam) { + $query->where('team_id', $currentTeam->id); + }); + } + $apps = $serviceAppsQuery->get(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name, + 'resource_link' => $app->service->link(), + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; + } + } elseif ($domain) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name, + 'resource_link' => $app->service->link(), + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; + } + } + } + } + + if ($resource) { + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => 'Coolify Instance', + 'resource_link' => '#', + 'resource_type' => 'instance', + 'message' => "Domain $naked_domain is already in use by this Coolify instance", + ]; + } + } + } + + return [ + 'conflicts' => $conflicts, + 'hasConflicts' => count($conflicts) > 0, + ]; +} + +function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $teamId = null, ?string $uuid = null) +{ + $conflicts = []; + + if (is_null($teamId)) { + return ['error' => 'Team ID is required.']; + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + + // Check applications within the same team + $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id']); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->with('service:id,name')->get(['fqdn', 'uuid', 'id', 'service_id']); + + if ($uuid) { + $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); + $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); + } + + 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)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->name, + 'resource_uuid' => $app->uuid, + 'resource_type' => 'application', + 'message' => "Domain $naked_domain is already in use by application '{$app->name}'", + ]; + } + } + } + + foreach ($serviceApplications as $app) { + if (str($app->fqdn)->isEmpty()) { + 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)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => $app->service->name ?? 'Unknown Service', + 'resource_uuid' => $app->uuid, + 'resource_type' => 'service', + 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'", + ]; + } + } + } + + // Check instance-level domain + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $conflicts[] = [ + 'domain' => $naked_domain, + 'resource_name' => 'Coolify Instance', + 'resource_uuid' => null, + 'resource_type' => 'instance', + 'message' => "Domain $naked_domain is already in use by this Coolify instance", + ]; + } + } + + return [ + 'conflicts' => $conflicts, + 'hasConflicts' => count($conflicts) > 0, + ]; +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7a9b5df80..e01f4d58b 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1084,152 +1084,6 @@ function check_ip_against_allowlist($ip, $allowlist) return false; } -function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = 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', 'uuid']); - $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); - if ($uuid) { - $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); - $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); - } - $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 (str($app->fqdn)->isEmpty()) { - 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(); - 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) { - if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') { - $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); - $domains = collect($domains); - } else { - $domains = collect($resource->fqdns); - } - } elseif ($domain) { - $domains = collect($domain); - } else { - throw new \RuntimeException('No resource or FQDN provided.'); - } - $domains = $domains->map(function ($domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - - return str($domain); - }); - $apps = Application::all(); - foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - if (data_get($resource, 'uuid')) { - if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

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

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

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

Link: {$app->service->name}"); - } - } - } - } - if ($resource) { - $settings = instanceSettings(); - if (data_get($settings, 'fqdn')) { - $domain = data_get($settings, 'fqdn'); - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); - } - } - } -} function parseCommandsByLineForSudo(Collection $commands, Server $server): array { diff --git a/config/constants.php b/config/constants.php index 770d9ba3f..44b51b978 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.424', + 'version' => '4.0.0-beta.425', 'helper_version' => '1.0.10', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/openapi.json b/openapi.json index 791828aed..ad20633c4 100644 --- a/openapi.json +++ b/openapi.json @@ -357,6 +357,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -385,6 +389,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -709,6 +767,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -737,6 +799,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1061,6 +1177,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -1089,6 +1209,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1342,6 +1516,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -1370,6 +1548,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1606,6 +1838,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -1634,6 +1870,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -1709,6 +1999,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -1737,6 +2031,60 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -2175,6 +2523,10 @@ "connect_to_docker_network": { "type": "boolean", "description": "The flag to connect the service to the predefined Docker network." + }, + "force_domain_override": { + "type": "boolean", + "description": "Force domain usage even if conflicts are detected. Default is false." } }, "type": "object" @@ -2206,6 +2558,60 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "409": { + "description": "Domain conflicts detected.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Domain conflicts detected. Use force_domain_override=true to proceed." + }, + "warning": { + "type": "string", + "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." + }, + "conflicts": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string", + "example": "example.com" + }, + "resource_name": { + "type": "string", + "example": "My Application" + }, + "resource_uuid": { + "type": "string", + "nullable": true, + "example": "abc123-def456" + }, + "resource_type": { + "type": "string", + "enum": [ + "application", + "service", + "instance" + ], + "example": "application" + }, + "message": { + "type": "string", + "example": "Domain example.com is already in use by application 'My Application'" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } } }, "security": [ @@ -5196,6 +5602,190 @@ ] } }, + "\/projects\/{uuid}\/environments": { + "get": { + "tags": [ + "Projects" + ], + "summary": "List Environments", + "description": "List all environments in a project.", + "operationId": "get-environments", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of environments", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Environment" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Project not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Projects" + ], + "summary": "Create Environment", + "description": "Create environment in project.", + "operationId": "create-environment", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Environment created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the environment." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "env123", + "description": "The UUID of the environment." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Project not found." + }, + "409": { + "description": "Environment with this name already exists." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/projects\/{uuid}\/environments\/{environment_name_or_uuid}": { + "delete": { + "tags": [ + "Projects" + ], + "summary": "Delete Environment", + "description": "Delete environment by name or UUID. Environment must be empty.", + "operationId": "delete-environment", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "environment_name_or_uuid", + "in": "path", + "description": "Environment name or UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Environment deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "description": "Environment has resources, so it cannot be deleted." + }, + "404": { + "description": "Project or environment not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/resources": { "get": { "tags": [ @@ -6412,13 +7002,6 @@ "content": { "application\/json": { "schema": { - "required": [ - "server_uuid", - "project_uuid", - "environment_name", - "environment_uuid", - "docker_compose_raw" - ], "properties": { "name": { "type": "string", @@ -8026,6 +8609,9 @@ "is_swarm_worker": { "type": "boolean" }, + "is_terminal_enabled": { + "type": "boolean" + }, "is_usable": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index 3f2fa1c59..ddd814e32 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -262,6 +262,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -276,6 +279,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -515,6 +528,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -529,6 +545,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -768,6 +794,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -782,6 +811,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -968,6 +1007,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -982,6 +1024,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1159,6 +1211,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -1173,6 +1228,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1230,6 +1295,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '201': @@ -1244,6 +1312,16 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -1560,6 +1638,9 @@ paths: connect_to_docker_network: type: boolean description: 'The flag to connect the service to the predefined Docker network.' + force_domain_override: + type: boolean + description: 'Force domain usage even if conflicts are detected. Default is false.' type: object responses: '200': @@ -1576,6 +1657,16 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '409': + description: 'Domain conflicts detected.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' } + warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } + conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } + type: object security: - bearerAuth: [] @@ -3570,6 +3661,124 @@ paths: security: - bearerAuth: [] + '/projects/{uuid}/environments': + get: + tags: + - Projects + summary: 'List Environments' + description: 'List all environments in a project.' + operationId: get-environments + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + responses: + '200': + description: 'List of environments' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Environment' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Project not found.' + security: + - + bearerAuth: [] + post: + tags: + - Projects + summary: 'Create Environment' + description: 'Create environment in project.' + operationId: create-environment + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + requestBody: + description: 'Environment created.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'The name of the environment.' + type: object + responses: + '201': + description: 'Environment created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: env123, description: 'The UUID of the environment.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Project not found.' + '409': + description: 'Environment with this name already exists.' + security: + - + bearerAuth: [] + '/projects/{uuid}/environments/{environment_name_or_uuid}': + delete: + tags: + - Projects + summary: 'Delete Environment' + description: 'Delete environment by name or UUID. Environment must be empty.' + operationId: delete-environment + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + - + name: environment_name_or_uuid + in: path + description: 'Environment name or UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Environment deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + description: 'Environment has resources, so it cannot be deleted.' + '404': + description: 'Project or environment not found.' + security: + - + bearerAuth: [] /resources: get: tags: @@ -4289,12 +4498,6 @@ paths: content: application/json: schema: - required: - - server_uuid - - project_uuid - - environment_name - - environment_uuid - - docker_compose_raw properties: name: type: string @@ -5377,6 +5580,8 @@ components: type: boolean is_swarm_worker: type: boolean + is_terminal_enabled: + type: boolean is_usable: type: boolean logdrain_axiom_api_key: diff --git a/resources/views/components/domain-conflict-modal.blade.php b/resources/views/components/domain-conflict-modal.blade.php new file mode 100644 index 000000000..218a7ef16 --- /dev/null +++ b/resources/views/components/domain-conflict-modal.blade.php @@ -0,0 +1,91 @@ +@props([ + 'conflicts' => [], + 'showModal' => false, + 'confirmAction' => 'confirmDomainUsage', +]) + +@if ($showModal && count($conflicts) > 0) +
+ +
+@endif diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index b833fc7bb..315385593 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -462,6 +462,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" + + + @script