diff --git a/app/Enums/BuildPackTypes.php b/app/Enums/BuildPackTypes.php new file mode 100644 index 0000000000..d4fd505d22 --- /dev/null +++ b/app/Enums/BuildPackTypes.php @@ -0,0 +1,11 @@ +user()->id ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + throw new \RuntimeException('User id is null'); } $this->userId = $userId; } diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/Applications.php deleted file mode 100644 index 82fde140c8..0000000000 --- a/app/Http/Controllers/Api/Applications.php +++ /dev/null @@ -1,671 +0,0 @@ -get(); - $applications = collect(); - $applications->push($projects->pluck('applications')->flatten()); - $applications = $applications->flatten(); - - return response()->json(serialize_api_response($applications)); - } - - public function application_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $uuid = $request->route('uuid'); - if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); - } - - return response()->json(serialize_api_response($application)); - } - - public function delete_by_uuid(Request $request) - { - ray()->clearAll(); - $teamId = get_team_id_from_token(); - $cleanup = $request->query->get('cleanup') ?? false; - if (is_null($teamId)) { - return invalid_token(); - } - - if ($request->collect()->count() == 0) { - return response()->json([ - 'message' => 'Invalid request.', - ], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - DeleteResourceJob::dispatch($application, $cleanup); - - return response()->json([ - 'success' => true, - 'message' => 'Application deletion request queued.', - ]); - } - - public function update_by_uuid(Request $request) - { - ray()->clearAll(); - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - - if ($request->collect()->count() == 0) { - return response()->json([ - 'message' => 'Invalid request.', - ], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - $server = $application->destination->server; - $allowedFields = ['name', 'description', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'redirect']; - - $validator = customApiValidator($request->all(), [ - 'name' => 'string|max:255', - 'description' => 'string|nullable', - 'domains' => 'string', - 'git_repository' => 'string', - 'git_branch' => 'string', - 'git_commit_sha' => 'string', - 'docker_registry_image_name' => 'string|nullable', - 'docker_registry_image_tag' => 'string|nullable', - 'build_pack' => 'string', - 'static_image' => 'string', - 'install_command' => 'string|nullable', - 'build_command' => 'string|nullable', - 'start_command' => 'string|nullable', - 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', - 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', - 'base_directory' => 'string|nullable', - 'publish_directory' => 'string|nullable', - 'health_check_enabled' => 'boolean', - 'health_check_path' => 'string', - 'health_check_port' => 'string|nullable', - 'health_check_host' => 'string', - 'health_check_method' => 'string', - 'health_check_return_code' => 'numeric', - 'health_check_scheme' => 'string', - 'health_check_response_text' => 'string|nullable', - 'health_check_interval' => 'numeric', - 'health_check_timeout' => 'numeric', - 'health_check_retries' => 'numeric', - 'health_check_start_period' => 'numeric', - 'limits_memory' => 'string', - 'limits_memory_swap' => 'string', - 'limits_memory_swappiness' => 'numeric', - 'limits_memory_reservation' => 'string', - 'limits_cpus' => 'string', - 'limits_cpuset' => 'string|nullable', - 'limits_cpu_shares' => 'numeric', - 'custom_labels' => 'string|nullable', - 'custom_docker_run_options' => 'string|nullable', - 'post_deployment_command' => 'string|nullable', - 'post_deployment_command_container' => 'string', - 'pre_deployment_command' => 'string|nullable', - 'pre_deployment_command_container' => 'string', - 'watch_paths' => 'string|nullable', - 'manual_webhook_secret_github' => 'string|nullable', - 'manual_webhook_secret_gitlab' => 'string|nullable', - 'manual_webhook_secret_bitbucket' => 'string|nullable', - 'manual_webhook_secret_gitea' => 'string|nullable', - 'docker_compose_location' => 'string', - 'docker_compose' => 'string|nullable', - 'docker_compose_raw' => 'string|nullable', - // 'docker_compose_domains' => 'string|nullable', // must be like: "{\"api\":{\"domain\":\"http:\\/\\/b8sos8k.127.0.0.1.sslip.io\"}}" - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - 'redirect' => Rule::enum(RedirectTypes::class), - ]); - - // Validate ports_exposes - if ($request->has('ports_exposes')) { - $ports = explode(',', $request->ports_exposes); - foreach ($ports as $port) { - if (! is_numeric($port)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', - ], - ], 422); - } - } - } - // Validate ports_mappings - if ($request->has('ports_mappings')) { - $ports = []; - foreach (explode(',', $request->ports_mappings) as $portMapping) { - $port = explode(':', $portMapping); - if (in_array($port[0], $ports)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_mappings' => 'The first number before : should be unique between mappings.', - ], - ], 422); - } - $ports[] = $port[0]; - } - } - // Validate custom_labels - if ($request->has('custom_labels')) { - if (! isBase64Encoded($request->custom_labels)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - $customLabels = base64_decode($request->custom_labels); - if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - - } - } - $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); - } - if ($request->has('domains') && $server->isProxyShouldRun()) { - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; - } - - return str($domain)->trim()->lower(); - }); - if (count($errors) > 0) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - $fqdn = $fqdn->unique()->implode(','); - $application->fqdn = $fqdn; - $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); - $application->custom_labels = base64_encode($customLabels); - $request->offsetUnset('domains'); - } - $application->fill($request->all()); - $application->save(); - - return response()->json(serialize_api_response($application)); - } - - public function envs_by_uuid(Request $request) - { - ray()->clearAll(); - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); - - return response()->json(serialize_api_response($envs)); - } - - public function update_env_by_uuid(Request $request) - { - ray()->clearAll(); - $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); - - if (is_null($teamId)) { - return invalid_token(); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - $validator = customApiValidator($request->all(), [ - 'key' => 'string|required', - 'value' => 'string|nullable', - 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', - 'is_literal' => 'boolean', - ]); - - $extraFields = array_diff(array_keys($request->all()), $allowedFields); - if ($validator->fails() || ! empty($extraFields)) { - $errors = $validator->errors(); - if (! empty($extraFields)) { - foreach ($extraFields as $field) { - $errors->add($field, 'This field is not allowed.'); - } - } - - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - $is_preview = $request->is_preview ?? false; - $is_build_time = $request->is_build_time ?? false; - $is_literal = $request->is_literal ?? false; - if ($is_preview) { - $env = $application->environment_variables_preview->where('key', $request->key)->first(); - if ($env) { - $env->value = $request->value; - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } - if ($env->is_literal != $is_literal) { - $env->is_literal = $is_literal; - } - if ($env->is_preview != $is_preview) { - $env->is_preview = $is_preview; - } - $env->save(); - - return response()->json(serialize_api_response($env)); - } else { - return response()->json([ - 'message' => 'Environment variable not found.', - ], 404); - } - } else { - $env = $application->environment_variables->where('key', $request->key)->first(); - if ($env) { - $env->value = $request->value; - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } - if ($env->is_literal != $is_literal) { - $env->is_literal = $is_literal; - } - if ($env->is_preview != $is_preview) { - $env->is_preview = $is_preview; - } - $env->save(); - - return response()->json(serialize_api_response($env)); - } else { - - return response()->json([ - 'message' => 'Environment variable not found.', - ], 404); - - } - } - - return response()->json([ - 'message' => 'Something went wrong.', - ], 500); - - } - - public function create_bulk_envs(Request $request) - { - ray()->clearAll(); - $teamId = get_team_id_from_token(); - - if (is_null($teamId)) { - return invalid_token(); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - - $bulk_data = $request->get('data'); - if (! $bulk_data) { - return response()->json([ - 'message' => 'Bulk data is required.', - ], 400); - } - $bulk_data = collect($bulk_data)->map(function ($item) { - return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); - }); - foreach ($bulk_data as $item) { - $validator = customApiValidator($item, [ - 'key' => 'string|required', - 'value' => 'string|nullable', - 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', - 'is_literal' => 'boolean', - ]); - if ($validator->fails()) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $validator->errors(), - ], 422); - } - $is_preview = $item->get('is_preview') ?? false; - $is_build_time = $item->get('is_build_time') ?? false; - $is_literal = $item->get('is_literal') ?? false; - if ($is_preview) { - $env = $application->environment_variables_preview->where('key', $item->get('key'))->first(); - if ($env) { - $env->value = $item->get('value'); - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } - if ($env->is_literal != $is_literal) { - $env->is_literal = $is_literal; - } - $env->save(); - } else { - $env = $application->environment_variables()->create([ - 'key' => $item->get('key'), - 'value' => $item->get('value'), - 'is_preview' => $is_preview, - 'is_build_time' => $is_build_time, - 'is_literal' => $is_literal, - ]); - } - } else { - $env = $application->environment_variables->where('key', $item->get('key'))->first(); - if ($env) { - $env->value = $item->get('value'); - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } - if ($env->is_literal != $is_literal) { - $env->is_literal = $is_literal; - } - $env->save(); - } else { - $env = $application->environment_variables()->create([ - 'key' => $item->get('key'), - 'value' => $item->get('value'), - 'is_preview' => $is_preview, - 'is_build_time' => $is_build_time, - 'is_literal' => $is_literal, - ]); - } - } - } - - return response()->json([ - 'message' => 'Environments updated.', - ]); - } - - public function create_env(Request $request) - { - ray()->clearAll(); - $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); - - if (is_null($teamId)) { - return invalid_token(); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found', - ], 404); - } - $validator = customApiValidator($request->all(), [ - 'key' => 'string|required', - 'value' => 'string|nullable', - 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', - 'is_literal' => 'boolean', - ]); - - $extraFields = array_diff(array_keys($request->all()), $allowedFields); - if ($validator->fails() || ! empty($extraFields)) { - $errors = $validator->errors(); - if (! empty($extraFields)) { - foreach ($extraFields as $field) { - $errors->add($field, 'This field is not allowed.'); - } - } - - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - $is_preview = $request->is_preview ?? false; - if ($is_preview) { - $env = $application->environment_variables_preview->where('key', $request->key)->first(); - if ($env) { - return response()->json([ - 'message' => 'Environment variable already exists. Use PATCH request to update it.', - ], 409); - } else { - $env = $application->environment_variables()->create([ - 'key' => $request->key, - 'value' => $request->value, - 'is_preview' => $request->is_preview ?? false, - 'is_build_time' => $request->is_build_time ?? false, - 'is_literal' => $request->is_literal ?? false, - ]); - - return response()->json(serialize_api_response($env))->setStatusCode(201); - } - } else { - $env = $application->environment_variables->where('key', $request->key)->first(); - if ($env) { - return response()->json([ - 'message' => 'Environment variable already exists. Use PATCH request to update it.', - ], 409); - } else { - $env = $application->environment_variables()->create([ - 'key' => $request->key, - 'value' => $request->value, - 'is_preview' => $request->is_preview ?? false, - 'is_build_time' => $request->is_build_time ?? false, - 'is_literal' => $request->is_literal ?? false, - ]); - - return response()->json(serialize_api_response($env))->setStatusCode(201); - - } - } - - return response()->json([ - 'message' => 'Something went wrong.', - ], 500); - - } - - public function delete_env_by_uuid(Request $request) - { - ray()->clearAll(); - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - - if (! $application) { - return response()->json([ - 'success' => false, - 'message' => 'Application not found.', - ], 404); - } - $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first(); - if (! $found_env) { - return response()->json([ - 'success' => false, - 'message' => 'Environment variable not found.', - ], 404); - } - $found_env->delete(); - - return response()->json([ - 'success' => true, - 'message' => 'Environment variable deleted.', - ]); - } - - public function action_deploy(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $force = $request->query->get('force') ?? false; - $instant_deploy = $request->query->get('instant_deploy') ?? false; - $uuid = $request->route('uuid'); - if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); - } - - $deployment_uuid = new Cuid2(7); - - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: $force, - is_api: true, - no_questions_asked: $instant_deploy - ); - - return response()->json( - [ - 'message' => 'Deployment request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), - ], - 200 - ); - } - - public function action_stop(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $uuid = $request->route('uuid'); - $sync = $request->query->get('sync') ?? false; - if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); - } - if ($sync) { - StopApplication::run($application); - - return response()->json(['message' => 'Stopped the application.'], 200); - } else { - StopApplication::dispatch($application); - - return response()->json(['message' => 'Stopping request queued.'], 200); - } - } - - public function action_restart(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $uuid = $request->route('uuid'); - if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); - } - - $deployment_uuid = new Cuid2(7); - - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - restart_only: true, - is_api: true, - ); - - return response()->json( - [ - 'message' => 'Restart request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), - ], - 200 - ); - - } -} diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php new file mode 100644 index 0000000000..e37de03786 --- /dev/null +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -0,0 +1,1173 @@ +get(); + $applications = collect(); + $applications->push($projects->pluck('applications')->flatten()); + $applications = $applications->flatten(); + $applications = $applications->map(function ($application) { + return serializeApiResponse($application); + }); + + return response()->json([ + 'success' => true, + 'data' => $applications, + ]); + } + + public function create_application(Request $request) + { + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'type' => ['required', Rule::enum(NewResourceTypes::class)], + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $serverUuid = $request->server_uuid; + $fqdn = $request->domains; + $type = $request->type; + $instantDeploy = $request->instant_deploy; + $githubAppUuid = $request->github_app_uuid; + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($type === 'public') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => [Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $application->fqdn = $fqdn; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'private-gh-app') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'github_app_uuid' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); + if (! $githubApp) { + return response()->json(['success' => false, 'message' => 'Github App not found.'], 404); + } + $gitRepository = $request->git_repository; + if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { + $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $application->fqdn = $fqdn; + $application->git_repository = $gitRepository; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->source_type = $githubApp->getMorphClass(); + $application->source_id = $githubApp->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'private-deploy-key') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'private_key_uuid' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['success' => false, 'message' => 'Private Key not found.'], 404); + } + + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->private_key_id = $privateKey->id; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'dockerfile') { + if (! $request->has('name')) { + $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'dockerfile' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->dockerfile)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + removeUnnecessaryFieldsFromRequest($request); + + $port = get_port_from_dockerfile($request->dockerfile); + if (! $port) { + $port = 80; + } + + $application = new Application(); + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->ports_exposes = $port; + $application->build_pack = 'dockerfile'; + $application->dockerfile = $dockerFile; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-image') { + if (! $request->has('name')) { + $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_registry_image_name' => 'string|required', + 'docker_registry_image_tag' => 'string', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! $request->docker_registry_image_tag) { + $request->offsetSet('docker_registry_image_tag', 'latest'); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->build_pack = 'dockerimage'; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-compose-empty') { + if (! $request->has('name')) { + $request->offsetSet('name', 'service'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_compose' => 'string|required', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->docker_compose)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + if (mb_detect_encoding($dockerCompose, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // $isValid = validateComposeFile($dockerComposeRaw, $server_id); + // if ($isValid !== 'OK') { + // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + // } + + $service = new Service(); + removeUnnecessaryFieldsFromRequest($request); + $service->name = $request->name; + $service->description = $request->description; + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->save(); + + $service->name = "service-$service->uuid"; + $service->parse(isNew: true); + // if ($instantDeploy) { + // $deployment_uuid = new Cuid2(7); + + // queue_application_deployment( + // application: $application, + // deployment_uuid: $deployment_uuid, + // no_questions_asked: true, + // is_api: true, + // ); + // } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($service), + ]); + } + + return response()->json(['success' => false, 'message' => 'Invalid type.'], 400); + + } + + public function application_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } + + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + $cleanup = $request->query->get('cleanup') ?? false; + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + ], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + DeleteResourceJob::dispatch($application, $cleanup); + + return response()->json([ + 'success' => true, + 'message' => 'Application deletion request queued.', + ]); + } + + public function update_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + ], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + $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', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'redirect']; + + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'static_image' => 'string', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose' => 'string|nullable', + 'docker_compose_raw' => 'string|nullable', + // 'docker_compose_domains' => 'string|nullable', // must be like: "{\"api\":{\"domain\":\"http:\\/\\/b8sos8k.127.0.0.1.sslip.io\"}}" + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]); + + // Validate ports_exposes + if ($request->has('ports_exposes')) { + $ports = explode(',', $request->ports_exposes); + foreach ($ports as $port) { + if (! is_numeric($port)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', + ], + ], 422); + } + } + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $domains = $request->domains; + if ($request->has('domains') && $server->isProxyShouldRun()) { + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = $fqdn->unique()->implode(','); + $application->fqdn = $fqdn; + $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->custom_labels = base64_encode($customLabels); + $request->offsetUnset('domains'); + } + + $data = $request->all(); + data_set($data, 'fqdn', $domains); + $application->fill($data); + $application->save(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } + + public function envs_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($envs), + ]); + } + + public function update_env_by_uuid(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + $is_build_time = $request->is_build_time ?? false; + $is_literal = $request->is_literal ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + $env->save(); + + return response()->json(serializeApiResponse($env)); + } else { + return response()->json([ + 'success' => false, + 'message' => 'Environment variable not found.', + ], 404); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + $env->save(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ]); + } else { + + return response()->json([ + 'success' => false, + 'message' => 'Environment variable not found.', + ], 404); + + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Something went wrong.', + ], 500); + + } + + public function create_bulk_envs(Request $request) + { + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + + $bulk_data = $request->get('data'); + if (! $bulk_data) { + return response()->json([ + 'success' => false, + 'message' => 'Bulk data is required.', + ], 400); + } + $bulk_data = collect($bulk_data)->map(function ($item) { + return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); + }); + foreach ($bulk_data as $item) { + $validator = customApiValidator($item, [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $is_preview = $item->get('is_preview') ?? false; + $is_build_time = $item->get('is_build_time') ?? false; + $is_literal = $item->get('is_literal') ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + ]); + } + } else { + $env = $application->environment_variables->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + ]); + } + } + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ]); + } + + public function create_env(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'success' => false, + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + ]); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + ]); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); + + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Something went wrong.', + ], 500); + + } + + public function delete_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found.', + ], 404); + } + $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first(); + if (! $found_env) { + return response()->json([ + 'success' => false, + 'message' => 'Environment variable not found.', + ], 404); + } + $found_env->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Environment variable deleted.', + ]); + } + + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $force = $request->query->get('force') ?? false; + $instant_deploy = $request->query->get('instant_deploy') ?? false; + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + is_api: true, + no_questions_asked: $instant_deploy + ); + + return response()->json( + [ + 'success' => true, + 'message' => 'Deployment request queued.', + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], + ], + 200 + ); + } + + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + $sync = $request->query->get('sync') ?? false; + if (! $uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); + } + if ($sync) { + StopApplication::run($application); + + return response()->json( + [ + 'success' => true, + 'message' => 'Stopped the application.', + ], + ); + } else { + StopApplication::dispatch($application); + + return response()->json( + [ + 'success' => true, + 'message' => 'Stopping request queued.', + ], + ); + } + } + + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + restart_only: true, + is_api: true, + ); + + return response()->json( + [ + 'success' => true, + 'message' => 'Restart request queued.', + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], + ], + ); + + } + + private function validateDataApplications(Request $request, Server $server) + { + $teamId = getTeamIdFromToken(); + + // Default build pack is nixpacks + if (! $request->has('build_pack')) { + $request->offsetSet('build_pack', 'nixpacks'); + } + + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } +} diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php new file mode 100644 index 0000000000..36a5fffaff --- /dev/null +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -0,0 +1,259 @@ +get(); + $databases = collect(); + foreach ($projects as $project) { + $databases = $databases->merge($project->databases()); + } + $databases = $databases->map(function ($database) { + return serializeApiResponse($database); + }); + + return response()->json([ + 'success' => true, + 'data' => $databases, + ]); + } + + public function database_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($database), + ]); + } + + public function create_database(Request $request) + { + $allowedFields = ['type', 'name', 'description', 'image', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'postgres_user', 'postgres_password', 'postgres_db', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'type' => ['required', Rule::enum(NewDatabaseTypes::class)], + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'instant_deploy' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + + if ($request->type === NewDatabaseTypes::POSTGRESQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartPostgresql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MARIADB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMariadb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MYSQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMysql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::REDIS->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartRedis::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::DRAGONFLY->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDragonfly::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::KEYDB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartKeydb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::CLICKHOUSE->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartClickhouse::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MONGODB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMongodb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } + + return response()->json(['success' => false, 'message' => 'Invalid database type requested.'], 400); + } + + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + StopDatabase::dispatch($database); + $database->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Database deletion request queued.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/DeployController.php similarity index 73% rename from app/Http/Controllers/Api/Deploy.php rename to app/Http/Controllers/Api/DeployController.php index d510970dd0..76e67548cf 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -18,13 +18,13 @@ use Illuminate\Http\Request; use Visus\Cuid2\Cuid2; -class Deploy extends Controller +class DeployController extends Controller { public function deployments(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $servers = Server::whereTeamId($teamId)->get(); $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([ @@ -38,39 +38,45 @@ public function deployments(Request $request) 'status', ])->sortBy('id')->toArray(); - return response()->json(serialize_api_response($deployments_per_server), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($deployments_per_server), + ]); } public function deployment_by_uuid(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['message' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first(); if (! $deployment) { - return response()->json(['message' => 'Deployment not found.'], 404); + return response()->json(['success' => false, 'message' => 'Deployment not found.'], 404); } - return response()->json(serialize_api_response($deployment->makeHidden('logs')), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($deployment->makeHidden('logs')), + ]); } public function deploy(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); $uuids = $request->query->get('uuid'); $tags = $request->query->get('tag'); $force = $request->query->get('force') ?? false; if ($uuids && $tags) { - return response()->json(['message' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($tags) { return $this->by_tags($tags, $teamId, $force); @@ -78,7 +84,7 @@ public function deploy(Request $request) return $this->by_uuids($uuids, $teamId, $force); } - return response()->json(['message' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } private function by_uuids(string $uuid, int $teamId, bool $force = false) @@ -87,7 +93,7 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false) $uuids = collect(array_filter($uuids)); if (count($uuids) === 0) { - return response()->json(['message' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } $deployments = collect(); $payload = collect(); @@ -96,19 +102,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false) if ($resource) { ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); if ($deployment_uuid) { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + $deployments->push(['success' => true, 'message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); } else { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); + $deployments->push(['success' => true, 'message' => $return_message, 'resource_uuid' => $uuid]); } } } if ($deployments->count() > 0) { $payload->put('deployments', $deployments->toArray()); - return response()->json($payload->toArray(), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($payload->toArray()), + ]); } - return response()->json(['message' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + return response()->json(['success' => false, 'message' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } public function by_tags(string $tags, int $team_id, bool $force = false) @@ -117,7 +126,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false) $tags = collect(array_filter($tags)); if (count($tags) === 0) { - return response()->json(['message' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); + return response()->json(['success' => false, 'message' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } $message = collect([]); $deployments = collect(); @@ -153,10 +162,13 @@ public function by_tags(string $tags, int $team_id, bool $force = false) $payload->put('details', $deployments->toArray()); } - return response()->json($payload->toArray(), 200); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($payload->toArray()), + ]); } - return response()->json(['message' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + return response()->json(['success' => false, 'message' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } public function deploy_resource($resource, bool $force = false): array @@ -164,7 +176,7 @@ public function deploy_resource($resource, bool $force = false): array $message = null; $deployment_uuid = null; if (gettype($resource) !== 'object') { - return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; + return ['success' => false, 'message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } $type = $resource?->getMorphClass(); if ($type === 'App\Models\Application') { @@ -228,6 +240,6 @@ public function deploy_resource($resource, bool $force = false): array $message = "Service {$resource->name} started. It could take a while, be patient."; } - return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; + return ['success' => true, 'message' => $message, 'deployment_uuid' => $deployment_uuid]; } } diff --git a/app/Http/Controllers/Api/EnvironmentVariables.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php similarity index 86% rename from app/Http/Controllers/Api/EnvironmentVariables.php rename to app/Http/Controllers/Api/EnvironmentVariablesController.php index d788bdb0c4..c54656dc65 100644 --- a/app/Http/Controllers/Api/EnvironmentVariables.php +++ b/app/Http/Controllers/Api/EnvironmentVariablesController.php @@ -6,14 +6,13 @@ use App\Models\EnvironmentVariable; use Illuminate\Http\Request; -class EnvironmentVariables extends Controller +class EnvironmentVariablesController extends Controller { public function delete_env_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $env = EnvironmentVariable::where('uuid', $request->env_uuid)->first(); if (! $env) { diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php deleted file mode 100644 index baaf1eacb4..0000000000 --- a/app/Http/Controllers/Api/Project.php +++ /dev/null @@ -1,44 +0,0 @@ -select('id', 'name', 'uuid')->get(); - - return response()->json($projects); - } - - public function project_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); - - return response()->json($project); - } - - public function environment_details(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); - - return response()->json($environment); - } -} diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php new file mode 100644 index 0000000000..4721b48e12 --- /dev/null +++ b/app/Http/Controllers/Api/ProjectController.php @@ -0,0 +1,60 @@ +select('id', 'name', 'uuid')->get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($projects), + ]); + } + + public function project_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + if (! $project) { + return response()->json(['success' => false, 'message' => 'Project not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($project), + ]); + } + + public function environment_details(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + $environment = $project->environments()->whereName(request()->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($environment), + ]); + } +} diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/ResourcesController.php similarity index 80% rename from app/Http/Controllers/Api/Resources.php rename to app/Http/Controllers/Api/ResourcesController.php index 0d538b62eb..47dfc67330 100644 --- a/app/Http/Controllers/Api/Resources.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -6,13 +6,13 @@ use App\Models\Project; use Illuminate\Http\Request; -class Resources extends Controller +class ResourcesController extends Controller { public function resources(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $projects = Project::where('team_id', $teamId)->get(); $resources = collect(); @@ -34,6 +34,9 @@ public function resources(Request $request) return $payload; }); - return response()->json($resources); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($resources), + ]); } } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php new file mode 100644 index 0000000000..51c6fee26f --- /dev/null +++ b/app/Http/Controllers/Api/SecurityController.php @@ -0,0 +1,160 @@ +get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($keys), + ]); + } + + public function key_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + + if (is_null($key)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function create_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + if ($validator->fails()) { + $errors = $validator->errors(); + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->description) { + $request->offsetSet('description', 'Created by Coolify via API'); + } + $key = PrivateKey::create([ + 'team_id' => $teamId, + 'name' => $request->name, + 'description' => $request->description, + 'private_key' => $request->private_key, + ]); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function update_key(Request $request) + { + $allowedFields = ['name', 'description', 'private_key']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($foundKey)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + $foundKey->update($request->all()); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($foundKey), + ])->setStatusCode(201); + } + + public function delete_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 422); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($key)) { + return response()->json(['success' => false, 'message' => 'Key not found.'], 404); + } + $key->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Key deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/ServersController.php similarity index 79% rename from app/Http/Controllers/Api/Server.php rename to app/Http/Controllers/Api/ServersController.php index 1a58da7b06..4d9479b7cf 100644 --- a/app/Http/Controllers/Api/Server.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -8,14 +8,15 @@ use App\Models\Project; use App\Models\Server as ModelsServer; use Illuminate\Http\Request; +use Stringable; -class Server extends Controller +class ServersController extends Controller { public function servers(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $server['is_reachable'] = $server->settings->is_reachable; @@ -23,16 +24,22 @@ public function servers(Request $request) return $server; }); + $servers = $servers->map(function ($server) { + return serializeApiResponse($server); + }); - return response()->json($servers); + return response()->json([ + 'success' => true, + 'data' => $servers, + ]); } public function server_by_uuid(Request $request) { $with_resources = $request->query('resources'); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); if (is_null($server)) { @@ -60,22 +67,25 @@ public function server_by_uuid(Request $request) $server->load(['settings']); } - return response()->json($server); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($server), + ]); } public function get_domains_by_server(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } - $uuid = $request->query->get('uuid'); + $uuid = $request->get('uuid'); if ($uuid) { $domains = Application::getDomainsByUuid($uuid); return response()->json([ - 'uuid' => $uuid, - 'domains' => $domains, + 'success' => true, + 'data' => serializeApiResponse($domains), ]); } $projects = Project::where('team_id', $teamId)->get(); @@ -86,8 +96,13 @@ public function get_domains_by_server(Request $request) foreach ($applications as $application) { $ip = $application->destination->server->ip; $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); }); + if ($ip === 'host.docker.internal') { if ($settings->public_ipv4) { $domains->push([ @@ -122,7 +137,11 @@ public function get_domains_by_server(Request $request) if ($service_applications->count() > 0) { foreach ($service_applications as $application) { $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); }); if ($ip === 'host.docker.internal') { if ($settings->public_ipv4) { @@ -162,6 +181,9 @@ public function get_domains_by_server(Request $request) ]; })->values(); - return response()->json($domains); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($domains), + ]); } } diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php deleted file mode 100644 index c895f2c1b3..0000000000 --- a/app/Http/Controllers/Api/Team.php +++ /dev/null @@ -1,74 +0,0 @@ -user()->teams; - - return response()->json($teams); - } - - public function team_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); - } - - return response()->json($team); - } - - public function members_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); - } - - return response()->json($team->members); - } - - public function current_team(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team); - } - - public function current_team_members(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team->members); - } -} diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php new file mode 100644 index 0000000000..a256e9caf9 --- /dev/null +++ b/app/Http/Controllers/Api/TeamController.php @@ -0,0 +1,89 @@ +user()->teams; + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($teams), + ]); + } + + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } + + public function current_team(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function current_team_members(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } +} diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php new file mode 100644 index 0000000000..dc0a433e25 --- /dev/null +++ b/app/Http/Middleware/ApiAllowed.php @@ -0,0 +1,34 @@ +clearAll(); + if (isCloud()) { + return $next($request); + } + $settings = InstanceSettings::get(); + if ($settings->is_api_enabled === false) { + return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + } + + if (! isDev()) { + if ($settings->allowed_ips) { + $allowedIps = explode(',', $settings->allowed_ips); + if (! in_array($request->ip(), $allowedIps)) { + return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + } + } + } + + return $next($request); + } +} diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index e637fb6d47..785940ee63 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -35,9 +35,9 @@ public function handle(): void return; } }); - if ($isInprogress) { - throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); - } + // if ($isInprogress) { + // throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); + // } if (! $this->server->isFunctional()) { return; } diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index fe5a935e92..52f1185812 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -18,9 +18,10 @@ class Configuration extends Component public bool $is_dns_validation_enabled; - // public bool $next_channel; protected string $dynamic_config_path; + public bool $is_api_enabled; + protected Server $server; protected $rules = [ @@ -30,6 +31,7 @@ class Configuration extends Component 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', 'settings.instance_name' => 'nullable', + 'settings.allowed_ips' => 'nullable', ]; protected $validationAttributes = [ @@ -38,6 +40,7 @@ class Configuration extends Component 'settings.public_port_min' => 'Public port min', 'settings.public_port_max' => 'Public port max', 'settings.custom_dns_servers' => 'Custom DNS servers', + 'settings.allowed_ips' => 'Allowed IPs', ]; public function mount() @@ -45,9 +48,9 @@ public function mount() $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; - // $this->next_channel = $this->settings->next_channel; $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; $this->dynamic_config_path = config('coolify.coolify_root_path').'/proxy/dynamic'; + $this->is_api_enabled = $this->settings->is_api_enabled; } public function instantSave() @@ -56,12 +59,7 @@ public function instantSave() $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - // if ($this->next_channel) { - // $this->settings->next_channel = false; - // $this->next_channel = false; - // } else { - // $this->settings->next_channel = $this->next_channel; - // } + $this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->save(); $this->dispatch('success', 'Settings updated!'); } @@ -95,6 +93,13 @@ public function submit() $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + }); + $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); + $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); + $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); if (! $error_show) { diff --git a/app/Models/Environment.php b/app/Models/Environment.php index b2bb51092a..fc19c134ff 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -27,6 +27,9 @@ public function isEmpty() $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && $this->mariadbs()->count() == 0 && $this->mongodbs()->count() == 0 && $this->services()->count() == 0; diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index daf902daf3..66ecdd9670 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -20,6 +20,17 @@ class GithubApp extends BaseModel 'webhook_secret', ]; + protected static function booted(): void + { + static::deleting(function (GithubApp $github_app) { + $applications_count = Application::where('source_id', $github_app->id)->count(); + if ($applications_count > 0) { + throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); + } + $github_app->privateKey()->delete(); + }); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); @@ -30,15 +41,9 @@ public static function private() return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get(); } - protected static function booted(): void + public function team() { - static::deleting(function (GithubApp $github_app) { - $applications_count = Application::where('source_id', $github_app->id)->count(); - if ($applications_count > 0) { - throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); - } - $github_app->privateKey()->delete(); - }); + return $this->belongsTo(Team::class); } public function applications() diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 38f79ce751..bd3c41a1fb 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -17,6 +17,7 @@ class InstanceSettings extends Model implements SendsEmail protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', + 'allowed_ip_ranges' => 'array', ]; public function fqdn(): Attribute diff --git a/app/Models/Server.php b/app/Models/Server.php index b9cc556af1..05d790c609 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -496,16 +496,16 @@ public function checkServerApi() public function checkSentinel() { - ray("Checking sentinel on server: {$this->name}"); + // ray("Checking sentinel on server: {$this->name}"); if ($this->isSentinelEnabled()) { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status !== 'running') { - ray('Sentinel is not running, starting it...'); + // ray('Sentinel is not running, starting it...'); PullSentinelImageJob::dispatch($this); } else { - ray('Sentinel is running'); + // ray('Sentinel is running'); } } } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 98c1cf4e75..6690f254ef 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -27,6 +27,11 @@ public function restart() instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index e968db18d7..6732246501 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,6 +13,8 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ public function team() return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-clickhouse'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + return $this->externalDbUrl; } else { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index c6718acfe8..d78d656c1c 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,6 +13,8 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-dragonfly'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 142f960aab..7b71bd55f3 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,6 +13,8 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url']; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-keydb'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://{$this->keydb_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://{$this->keydb_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 7e6d2e0d1e..00df4fe719 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -13,6 +13,8 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -161,6 +163,13 @@ public function isLogDrainEnabled() return data_get($this, 'is_log_drain_enabled', false); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mariadb'; @@ -183,12 +192,32 @@ public function portsMappingsArray(): Attribute ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + return $this->externalDbUrl; } else { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index df895bb34f..0863522a8f 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,6 +13,8 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -198,17 +200,44 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mongodb'; } + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + } + + return null; + } + ); + } + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + return $this->externalDbUrl; } else { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index bd160f8772..79e7c37fa3 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,6 +13,8 @@ class StandaloneMysql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -157,6 +159,13 @@ public function link() return null; } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mysql'; @@ -184,12 +193,32 @@ public function portsMappingsArray(): Attribute ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + return $this->externalDbUrl; } else { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 114d376e89..1d5276cf33 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,6 +13,8 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -179,17 +181,44 @@ public function team() return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-postgresql'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + return $this->externalDbUrl; } else { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; + return $this->internalDbUrl; } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 022cd8d090..e0f863acac 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,6 +13,8 @@ class StandaloneRedis extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -179,12 +181,39 @@ public function type(): string return 'standalone-redis'; } - public function get_db_url(bool $useInternal = false): string + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function get_db_url(bool $useInternal = false) { if ($this->is_public && ! $useInternal) { - return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return $this->externalDbUrl; } else { - return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; + return $this->internalDbUrl; } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index c278a5045b..c5083534f9 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -1,24 +1,29 @@ user()->currentAccessToken(); return data_get($token, 'team_id'); } -function invalid_token() +function invalidTokenResponse() { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); + return response()->json(['success' => false, 'message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); } -function serialize_api_response($data) +function serializeApiResponse($data) { if (! $data instanceof Collection) { $data = collect($data); } $data = $data->sortKeys(); + $created_at = data_get($data, 'created_at'); $updated_at = data_get($data, 'updated_at'); if ($created_at) { @@ -30,9 +35,103 @@ function serialize_api_response($data) unset($data['updated_at']); $data['updated_at'] = $updated_at; } + if (data_get($data, 'name')) { + $data = $data->prepend($data['name'], 'name'); + } + if (data_get($data, 'description')) { + $data = $data->prepend($data['description'], 'description'); + } + if (data_get($data, 'uuid')) { + $data = $data->prepend($data['uuid'], 'uuid'); + } + if (data_get($data, 'id')) { $data = $data->prepend($data['id'], 'id'); } return $data; } + +function sharedDataApplications() +{ + return [ + 'git_repository' => 'string', + 'git_branch' => 'string', + 'build_pack' => Rule::enum(BuildPackTypes::class), + 'is_static' => 'boolean', + 'domains' => 'string', + 'redirect' => Rule::enum(RedirectTypes::class), + 'git_commit_sha' => 'string', + 'docker_registry_image_name' => 'string|nullable', + 'docker_registry_image_tag' => 'string|nullable', + 'install_command' => 'string|nullable', + 'build_command' => 'string|nullable', + 'start_command' => 'string|nullable', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', + 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', + 'base_directory' => 'string|nullable', + 'publish_directory' => 'string|nullable', + 'health_check_enabled' => 'boolean', + 'health_check_path' => 'string', + 'health_check_port' => 'string|nullable', + 'health_check_host' => 'string', + 'health_check_method' => 'string', + 'health_check_return_code' => 'numeric', + 'health_check_scheme' => 'string', + 'health_check_response_text' => 'string|nullable', + 'health_check_interval' => 'numeric', + 'health_check_timeout' => 'numeric', + 'health_check_retries' => 'numeric', + 'health_check_start_period' => 'numeric', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'custom_labels' => 'string|nullable', + 'custom_docker_run_options' => 'string|nullable', + 'post_deployment_command' => 'string|nullable', + 'post_deployment_command_container' => 'string', + 'pre_deployment_command' => 'string|nullable', + 'pre_deployment_command_container' => 'string', + 'manual_webhook_secret_github' => 'string|nullable', + 'manual_webhook_secret_gitlab' => 'string|nullable', + 'manual_webhook_secret_bitbucket' => 'string|nullable', + 'manual_webhook_secret_gitea' => 'string|nullable', + ]; +} + +function validateIncomingRequest(Request $request) +{ + // check if request is json + if (! $request->isJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } +} + +function removeUnnecessaryFieldsFromRequest(Request $request) +{ + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); +} diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index dba8aa543e..ef3f8ac9b4 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -19,131 +19,163 @@ function generate_database_name(string $type): string return $type.'-database-'.$cuid; } -function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql +function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql { - // TODO: If another type of destination is added, this will need to be updated. - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + $destination = StandaloneDocker::where('uuid', $destinationUuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandalonePostgresql(); + $database->name = generate_database_name('postgresql'); + $database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environmentId; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandalonePostgresql::create([ - 'name' => generate_database_name('postgresql'), - 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneRedis(); + $database->name = generate_database_name('redis'); + $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneRedis::create([ - 'name' => generate_database_name('redis'), - 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMongodb(); + $database->name = generate_database_name('mongodb'); + $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMongodb::create([ - 'name' => generate_database_name('mongodb'), - 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMysql(); + $database->name = generate_database_name('mysql'); + $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMysql::create([ - 'name' => generate_database_name('mysql'), - 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMariadb(); + $database->name = generate_database_name('mariadb'); + $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMariadb::create([ - 'name' => generate_database_name('mariadb'), - 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb +function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneKeydb(); + $database->name = generate_database_name('keydb'); + $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneKeydb::create([ - 'name' => generate_database_name('keydb'), - 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly +function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneDragonfly(); + $database->name = generate_database_name('dragonfly'); + $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneDragonfly::create([ - 'name' => generate_database_name('dragonfly'), - 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse +function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneClickhouse(); + $database->name = generate_database_name('clickhouse'); + $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneClickhouse::create([ - 'name' => generate_database_name('clickhouse'), - 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } /** diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 8b4db0695a..09ee5459ea 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -56,6 +56,8 @@ use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +use function PHPUnit\Framework\isEmpty; + function base_configuration_dir(): string { return config('coolify.coolify_root_path') ?? '/data/coolify'; @@ -536,6 +538,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } +function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId) +{ + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql && $postgresql->team()->id == $teamId) { + return $postgresql->unsetRelation('environment')->unsetRelation('destination'); + } + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis && $redis->team()->id == $teamId) { + return $redis->unsetRelation('environment'); + } + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb && $mongodb->team()->id == $teamId) { + return $mongodb->unsetRelation('environment'); + } + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql && $mysql->team()->id == $teamId) { + return $mysql->unsetRelation('environment'); + } + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb && $mariadb->team()->id == $teamId) { + return $mariadb->unsetRelation('environment'); + } + $keydb = StandaloneKeydb::whereUuid($uuid)->first(); + if ($keydb && $keydb->team()->id == $teamId) { + return $keydb->unsetRelation('environment'); + } + $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); + if ($dragonfly && $dragonfly->team()->id == $teamId) { + return $dragonfly->unsetRelation('environment'); + } + $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); + if ($clickhouse && $clickhouse->team()->id == $teamId) { + return $clickhouse->unsetRelation('environment'); + } + + return null; +} function queryResourcesByUuid(string $uuid) { $resource = null; @@ -2129,6 +2168,75 @@ function ip_match($ip, $cidrs, &$match = null) return false; } +function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null) +{ + if (is_null($teamId)) { + return response()->json(['error' => 'Team ID is required.'], 400); + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $applications = Application::ownedByCurrentTeamAPI($teamId)->get('fqdn'); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get('fqdn'); + $domainFound = false; + foreach ($applications as $app) { + if (is_null($app->fqdn)) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + foreach ($serviceApplications as $app) { + if (isEmpty($app->fqdn)) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + return true; + } + } +} function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { diff --git a/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php new file mode 100644 index 0000000000..b319adb70e --- /dev/null +++ b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php @@ -0,0 +1,24 @@ +boolean('is_api_enabled')->default(true); + $table->text('allowed_ips')->nullable(); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_api_enabled'); + $table->dropColumn('allowed_ips'); + }); + } +}; diff --git a/resources/views/livewire/settings/backup.blade.php b/resources/views/livewire/settings/backup.blade.php index 50f5f3d286..d517b9516f 100644 --- a/resources/views/livewire/settings/backup.blade.php +++ b/resources/views/livewire/settings/backup.blade.php @@ -24,7 +24,7 @@ @else - To configure automatic backup for your Coolify instance, you first need to add as a database resource + To configure automatic backup for your Coolify instance, you first need to add a database resource into Coolify. Add Database @endif diff --git a/resources/views/livewire/settings/configuration.blade.php b/resources/views/livewire/settings/configuration.blade.php index b1c399bc3b..b5fb49d3e3 100644 --- a/resources/views/livewire/settings/configuration.blade.php +++ b/resources/views/livewire/settings/configuration.blade.php @@ -25,7 +25,16 @@ --}} +

API

+ +
+ +
+ +

Advanced

@if (!is_null(env('AUTOUPDATE', null))) @@ -36,13 +45,5 @@ @endif - {{-- @if ($next_channel) - - @else - - @endif --}}
diff --git a/routes/api.php b/routes/api.php index 7aca146ba4..69eead3bab 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,11 +1,16 @@ json(['message' => 'Feedback sent.'], 200); + return response()->json(['success' => true, 'message' => 'Feedback sent.'], 200); }); Route::group([ 'middleware' => ['auth:sanctum'], 'prefix' => 'v1', +], function () { + Route::get('/enable', function () { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['success' => false, 'message' => 'You are not allowed to enable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => true]); + + return response()->json(['success' => true, 'message' => 'API enabled.'], 200); + }); + Route::get('/disable', function () { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['success' => false, 'message' => 'You are not allowed to disable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => false]); + + return response()->json(['success' => true, 'message' => 'API disabled.'], 200); + }); + +}); +Route::group([ + 'middleware' => ['auth:sanctum', ApiAllowed::class], + 'prefix' => 'v1', ], function () { Route::get('/version', function () { return response(config('version')); }); - Route::match(['get', 'post'], '/deploy', [Deploy::class, 'deploy']); - Route::get('/deployments', [Deploy::class, 'deployments']); - Route::get('/deployments/{uuid}', [Deploy::class, 'deployment_by_uuid']); - Route::get('/servers', [Server::class, 'servers']); - Route::get('/servers/{uuid}', [Server::class, 'server_by_uuid']); - Route::get('/servers/domains', [Server::class, 'get_domains_by_server']); + Route::get('/teams', [TeamController::class, 'teams']); + Route::get('/teams/current', [TeamController::class, 'current_team']); + Route::get('/teams/current/members', [TeamController::class, 'current_team_members']); + Route::get('/teams/{id}', [TeamController::class, 'team_by_id']); + Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id']); + + Route::get('/projects', [ProjectController::class, 'projects']); + Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid']); + Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); + + Route::get('/security/keys', [SecurityController::class, 'keys']); + Route::post('/security/keys', [SecurityController::class, 'create_key']); + + Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); + Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key']); + Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key']); + + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']); + + Route::get('/deployments', [DeployController::class, 'deployments']); + Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); + + Route::get('/servers', [ServersController::class, 'servers']); + Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid']); + Route::get('/servers/{uuid}/domains', [ServersController::class, 'get_domains_by_server']); - Route::get('/resources', [Resources::class, 'resources']); + Route::get('/resources', [ResourcesController::class, 'resources']); - Route::get('/applications', [Applications::class, 'applications']); + Route::get('/applications', [ApplicationsController::class, 'applications']); + Route::post('/applications', [ApplicationsController::class, 'create_application']); - Route::get('/applications/{uuid}', [Applications::class, 'application_by_uuid']); - Route::patch('/applications/{uuid}', [Applications::class, 'update_by_uuid']); - Route::delete('/applications/{uuid}', [Applications::class, 'delete_by_uuid']); + Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); + Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid']); + Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid']); - Route::get('/applications/{uuid}/envs', [Applications::class, 'envs_by_uuid']); - Route::post('/applications/{uuid}/envs', [Applications::class, 'create_env']); - Route::post('/applications/{uuid}/envs/bulk', [Applications::class, 'create_bulk_envs']); - Route::patch('/applications/{uuid}/envs', [Applications::class, 'update_env_by_uuid']); - Route::delete('/applications/{uuid}/envs/{env_uuid}', [Applications::class, 'delete_env_by_uuid']); + Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs_by_uuid']); + Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env']); + Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs']); + Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid']); + Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid']); - Route::delete('/envs/{env_uuid}', [EnvironmentVariables::class, 'delete_env_by_uuid']); + Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy']); + Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart']); + Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop']); - Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [Applications::class, 'action_deploy']); - Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [Applications::class, 'action_restart']); - Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [Applications::class, 'action_stop']); + Route::get('/databases', [DatabasesController::class, 'databases']); + Route::post('/databases', [DatabasesController::class, 'create_database']); + Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); + // Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid']); + Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid']); - Route::get('/teams', [Team::class, 'teams']); - Route::get('/teams/current', [Team::class, 'current_team']); - Route::get('/teams/current/members', [Team::class, 'current_team_members']); - Route::get('/teams/{id}', [Team::class, 'team_by_id']); - Route::get('/teams/{id}/members', [Team::class, 'members_by_id']); + Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid']); - // Route::get('/projects', [Project::class, 'projects']); - //Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']); - //Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']); }); Route::any('/{any}', function () { - return response()->json(['error' => 'Not found.'], 404); + return response()->json(['success' => false, 'message' => 'Not found.', 'docs' => 'https://coolify.io/docs'], 404); })->where('any', '.*'); // Route::middleware(['throttle:5'])->group(function () {