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)
+
Warning: Domain Conflict Detected
+{{ $slot ?? 'The following domain(s) are already in use by other resources. Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' }} +
+What will happen if you continue?
+ @if (isset($consequences)) + {{ $consequences }} + @else +