diff --git a/app/Enums/NewDatabaseTypes.php b/app/Enums/NewDatabaseTypes.php new file mode 100644 index 000000000..3563146ff --- /dev/null +++ b/app/Enums/NewDatabaseTypes.php @@ -0,0 +1,15 @@ +user()->id ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + throw new \RuntimeException('User id is null'); } $this->userId = $userId; } diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/ApplicationsController.php similarity index 72% rename from app/Http/Controllers/Api/Applications.php rename to app/Http/Controllers/Api/ApplicationsController.php index c73b894ba..e37de0378 100644 --- a/app/Http/Controllers/Api/Applications.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -13,47 +13,45 @@ use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server; +use App\Models\Service; use Illuminate\Http\Request; use Illuminate\Validation\Rule; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; -class Applications extends Controller +class ApplicationsController extends Controller { public function applications(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $projects = Project::where('team_id', $teamId)->get(); $applications = collect(); $applications->push($projects->pluck('applications')->flatten()); $applications = $applications->flatten(); + $applications = $applications->map(function ($application) { + return serializeApiResponse($application); + }); - return response()->json(serialize_api_response($applications)); + return response()->json([ + 'success' => true, + 'data' => $applications, + ]); } public function create_application(Request $request) { - - ray()->clearAll(); $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } - if (! $request->isJson()) { - return response()->json([ - 'message' => 'Invalid request.', - 'error' => 'Content-Type must be application/json.', - ], 400); - } - // check if request is valid json - if (! json_decode($request->getContent())) { - return response()->json([ - 'message' => 'Invalid request.', - 'error' => 'Invalid JSON.', - ], 400); + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -75,6 +73,7 @@ public function create_application(Request $request) } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -88,22 +87,22 @@ public function create_application(Request $request) $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { - return response()->json(['error' => 'Project not found.'], 404); + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); } $environment = $project->environments()->where('name', $request->environment_name)->first(); if (! $environment) { - return response()->json(['error' => 'Environment not found.'], 404); + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); } $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); if (! $server) { - return response()->json(['error' => 'Server not found.'], 404); + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); } $destinations = $server->destinations(); if ($destinations->count() == 0) { - return response()->json(['error' => 'Server has no destinations.'], 400); + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); } if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { - return response()->json(['error' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); } $destination = $destinations->first(); if ($type === 'public') { @@ -114,11 +113,12 @@ public function create_application(Request $request) sharedDataApplications(), 'git_repository' => 'string|required', 'git_branch' => 'string|required', - 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'build_pack' => [Rule::enum(BuildPackTypes::class)], 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -128,7 +128,7 @@ public function create_application(Request $request) return $return; } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); @@ -149,7 +149,10 @@ public function create_application(Request $request) ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'private-gh-app') { if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); @@ -164,6 +167,7 @@ public function create_application(Request $request) ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -174,14 +178,14 @@ public function create_application(Request $request) } $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); if (! $githubApp) { - return response()->json(['error' => 'Github App not found.'], 404); + return response()->json(['success' => false, 'message' => 'Github App not found.'], 404); } $gitRepository = $request->git_repository; if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); @@ -205,7 +209,10 @@ public function create_application(Request $request) ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'private-deploy-key') { if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); @@ -230,11 +237,11 @@ public function create_application(Request $request) } $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); if (! $privateKey) { - return response()->json(['error' => 'Private Key not found.'], 404); + return response()->json(['success' => false, 'message' => 'Private Key not found.'], 404); } $application = new Application(); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $application->fill($request->all()); $application->fqdn = $fqdn; @@ -255,7 +262,10 @@ public function create_application(Request $request) ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } elseif ($type === 'dockerfile') { if (! $request->has('name')) { $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); @@ -266,6 +276,7 @@ public function create_application(Request $request) ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -276,6 +287,7 @@ public function create_application(Request $request) } if (! isBase64Encoded($request->dockerfile)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'dockerfile' => 'The dockerfile should be base64 encoded.', @@ -285,6 +297,7 @@ public function create_application(Request $request) $dockerFile = base64_decode($request->dockerfile); if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'dockerfile' => 'The dockerfile should be base64 encoded.', @@ -292,7 +305,7 @@ public function create_application(Request $request) ], 422); } $dockerFile = base64_decode($request->dockerfile); - $this->removeUnnecessaryFieldsFromRequest($request); + removeUnnecessaryFieldsFromRequest($request); $port = get_port_from_dockerfile($request->dockerfile); if (! $port) { @@ -324,42 +337,179 @@ public function create_application(Request $request) ); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-image') { + if (! $request->has('name')) { + $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_registry_image_name' => 'string|required', + 'docker_registry_image_tag' => 'string', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! $request->docker_registry_image_tag) { + $request->offsetSet('docker_registry_image_tag', 'latest'); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->build_pack = 'dockerimage'; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); + } elseif ($type === 'docker-compose-empty') { + if (! $request->has('name')) { + $request->offsetSet('name', 'service'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_compose' => 'string|required', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->docker_compose)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + if (mb_detect_encoding($dockerCompose, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose' => 'The docker_compose should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // $isValid = validateComposeFile($dockerComposeRaw, $server_id); + // if ($isValid !== 'OK') { + // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + // } + + $service = new Service(); + removeUnnecessaryFieldsFromRequest($request); + $service->name = $request->name; + $service->description = $request->description; + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->save(); + + $service->name = "service-$service->uuid"; + $service->parse(isNew: true); + // if ($instantDeploy) { + // $deployment_uuid = new Cuid2(7); + + // queue_application_deployment( + // application: $application, + // deployment_uuid: $deployment_uuid, + // no_questions_asked: true, + // is_api: true, + // ); + // } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($service), + ]); } - return response()->json(['error' => 'Invalid type.'], 400); + return response()->json(['success' => false, 'message' => 'Invalid type.'], 400); } public function application_by_uuid(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } public function delete_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); $cleanup = $request->query->get('cleanup') ?? false; if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($request->collect()->count() == 0) { return response()->json([ + 'success' => false, 'message' => 'Invalid request.', ], 400); } @@ -381,17 +531,21 @@ public function delete_by_uuid(Request $request) public function update_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } if ($request->collect()->count() == 0) { return response()->json([ + 'success' => false, 'message' => 'Invalid request.', ], 400); } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { @@ -423,6 +577,7 @@ public function update_by_uuid(Request $request) foreach ($ports as $port) { if (! is_numeric($port)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', @@ -445,6 +600,7 @@ public function update_by_uuid(Request $request) } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -467,15 +623,21 @@ public function update_by_uuid(Request $request) $application->fill($data); $application->save(); - return response()->json(serialize_api_response($application)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($application), + ]); } public function envs_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -487,17 +649,24 @@ public function envs_by_uuid(Request $request) } $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); - return response()->json(serialize_api_response($envs)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($envs), + ]); } public function update_env_by_uuid(Request $request) { - ray()->clearAll(); $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -525,6 +694,7 @@ public function update_env_by_uuid(Request $request) } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -547,9 +717,10 @@ public function update_env_by_uuid(Request $request) } $env->save(); - return response()->json(serialize_api_response($env)); + return response()->json(serializeApiResponse($env)); } else { return response()->json([ + 'success' => false, 'message' => 'Environment variable not found.', ], 404); } @@ -568,10 +739,14 @@ public function update_env_by_uuid(Request $request) } $env->save(); - return response()->json(serialize_api_response($env)); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ]); } else { return response()->json([ + 'success' => false, 'message' => 'Environment variable not found.', ], 404); @@ -579,6 +754,7 @@ public function update_env_by_uuid(Request $request) } return response()->json([ + 'success' => false, 'message' => 'Something went wrong.', ], 500); @@ -586,11 +762,15 @@ public function update_env_by_uuid(Request $request) public function create_bulk_envs(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -604,6 +784,7 @@ public function create_bulk_envs(Request $request) $bulk_data = $request->get('data'); if (! $bulk_data) { return response()->json([ + 'success' => false, 'message' => 'Bulk data is required.', ], 400); } @@ -620,6 +801,7 @@ public function create_bulk_envs(Request $request) ]); if ($validator->fails()) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); @@ -671,18 +853,18 @@ public function create_bulk_envs(Request $request) } return response()->json([ - 'message' => 'Environments updated.', + 'success' => true, + 'data' => serializeApiResponse($env), ]); } public function create_env(Request $request) { - ray()->clearAll(); $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -710,6 +892,7 @@ public function create_env(Request $request) } return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); @@ -719,6 +902,7 @@ public function create_env(Request $request) $env = $application->environment_variables_preview->where('key', $request->key)->first(); if ($env) { return response()->json([ + 'success' => false, 'message' => 'Environment variable already exists. Use PATCH request to update it.', ], 409); } else { @@ -730,7 +914,10 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, ]); - return response()->json(serialize_api_response($env))->setStatusCode(201); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); } } else { $env = $application->environment_variables->where('key', $request->key)->first(); @@ -747,12 +934,16 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, ]); - return response()->json(serialize_api_response($env))->setStatusCode(201); + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($env), + ])->setStatusCode(201); } } return response()->json([ + 'success' => false, 'message' => 'Something went wrong.', ], 500); @@ -760,10 +951,9 @@ public function create_env(Request $request) public function delete_env_by_uuid(Request $request) { - ray()->clearAll(); - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); @@ -790,19 +980,19 @@ public function delete_env_by_uuid(Request $request) public function action_deploy(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $force = $request->query->get('force') ?? false; $instant_deploy = $request->query->get('instant_deploy') ?? false; $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } $deployment_uuid = new Cuid2(7); @@ -817,9 +1007,12 @@ public function action_deploy(Request $request) return response()->json( [ + 'success' => true, 'message' => 'Deployment request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], ], 200 ); @@ -827,43 +1020,53 @@ public function action_deploy(Request $request) public function action_stop(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); $sync = $request->query->get('sync') ?? false; if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } if ($sync) { StopApplication::run($application); - return response()->json(['message' => 'Stopped the application.'], 200); + return response()->json( + [ + 'success' => true, + 'message' => 'Stopped the application.', + ], + ); } else { StopApplication::dispatch($application); - return response()->json(['message' => 'Stopping request queued.'], 200); + return response()->json( + [ + 'success' => true, + 'message' => 'Stopping request queued.', + ], + ); } } public function action_restart(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $uuid = $request->route('uuid'); if (! $uuid) { - return response()->json(['error' => 'UUID is required.'], 400); + return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); if (! $application) { - return response()->json(['error' => 'Application not found.'], 404); + return response()->json(['success' => false, 'message' => 'Application not found.'], 404); } $deployment_uuid = new Cuid2(7); @@ -877,31 +1080,25 @@ public function action_restart(Request $request) return response()->json( [ + 'success' => true, 'message' => 'Restart request queued.', - 'deployment_uuid' => $deployment_uuid->toString(), - 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + 'data' => [ + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], ], - 200 ); } - private function removeUnnecessaryFieldsFromRequest(Request $request) - { - $request->offsetUnset('project_uuid'); - $request->offsetUnset('environment_name'); - $request->offsetUnset('destination_uuid'); - $request->offsetUnset('server_uuid'); - $request->offsetUnset('type'); - $request->offsetUnset('domains'); - $request->offsetUnset('instant_deploy'); - $request->offsetUnset('github_app_uuid'); - $request->offsetUnset('private_key_uuid'); - } - private function validateDataApplications(Request $request, Server $server) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); + + // Default build pack is nixpacks + if (! $request->has('build_pack')) { + $request->offsetSet('build_pack', 'nixpacks'); + } // Validate ports_mappings if ($request->has('ports_mappings')) { @@ -910,6 +1107,7 @@ private function validateDataApplications(Request $request, Server $server) $port = explode(':', $portMapping); if (in_array($port[0], $ports)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'ports_mappings' => 'The first number before : should be unique between mappings.', @@ -923,6 +1121,7 @@ private function validateDataApplications(Request $request, Server $server) if ($request->has('custom_labels')) { if (! isBase64Encoded($request->custom_labels)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'custom_labels' => 'The custom_labels should be base64 encoded.', @@ -932,6 +1131,7 @@ private function validateDataApplications(Request $request, Server $server) $customLabels = base64_decode($request->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'custom_labels' => 'The custom_labels should be base64 encoded.', @@ -954,12 +1154,14 @@ private function validateDataApplications(Request $request, Server $server) }); if (count($errors) > 0) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => $errors, ], 422); } if (checkIfDomainIsAlreadyUsed($fqdn, $teamId)) { return response()->json([ + 'success' => false, 'message' => 'Validation failed.', 'errors' => [ 'domains' => 'One of the domain is already used.', diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php new file mode 100644 index 000000000..36a5fffaf --- /dev/null +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -0,0 +1,259 @@ +get(); + $databases = collect(); + foreach ($projects as $project) { + $databases = $databases->merge($project->databases()); + } + $databases = $databases->map(function ($database) { + return serializeApiResponse($database); + }); + + return response()->json([ + 'success' => true, + 'data' => $databases, + ]); + } + + public function database_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($database), + ]); + } + + public function create_database(Request $request) + { + $allowedFields = ['type', 'name', 'description', 'image', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'postgres_user', 'postgres_password', 'postgres_db', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'type' => ['required', Rule::enum(NewDatabaseTypes::class)], + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'instant_deploy' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['succes' => false, 'message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['success' => false, 'message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['success' => false, 'message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['success' => false, 'message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + + if ($request->type === NewDatabaseTypes::POSTGRESQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartPostgresql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MARIADB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMariadb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MYSQL->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMysql::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::REDIS->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartRedis::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::DRAGONFLY->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDragonfly::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::KEYDB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartKeydb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::CLICKHOUSE->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartClickhouse::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } elseif ($request->type === NewDatabaseTypes::MONGODB->value) { + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartMongodb::dispatch($database); + } + + return response()->json([ + 'success' => true, + 'message' => 'Database starting queued.', + 'data' => serializeApiResponse($database), + ]); + } + + return response()->json(['success' => false, 'message' => 'Invalid database type requested.'], 400); + } + + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['success' => false, 'message' => 'Database not found.'], 404); + } + StopDatabase::dispatch($database); + $database->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Database deletion request queued.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/DeployController.php similarity index 73% rename from app/Http/Controllers/Api/Deploy.php rename to app/Http/Controllers/Api/DeployController.php index d510970dd..76e67548c 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -18,13 +18,13 @@ use 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 d788bdb0c..c54656dc6 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 baaf1eacb..000000000 --- a/app/Http/Controllers/Api/Project.php +++ /dev/null @@ -1,44 +0,0 @@ -select('id', 'name', 'uuid')->get(); - - return response()->json($projects); - } - - public function project_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); - - return response()->json($project); - } - - public function environment_details(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); - - return response()->json($environment); - } -} diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php new file mode 100644 index 000000000..4721b48e1 --- /dev/null +++ b/app/Http/Controllers/Api/ProjectController.php @@ -0,0 +1,60 @@ +select('id', 'name', 'uuid')->get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($projects), + ]); + } + + public function project_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + if (! $project) { + return response()->json(['success' => false, 'message' => 'Project not found.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($project), + ]); + } + + public function environment_details(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + $environment = $project->environments()->whereName(request()->environment_name)->first(); + if (! $environment) { + return response()->json(['success' => false, 'message' => 'Environment not found.'], 404); + } + $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($environment), + ]); + } +} diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/ResourcesController.php similarity index 80% rename from app/Http/Controllers/Api/Resources.php rename to app/Http/Controllers/Api/ResourcesController.php index 0d538b62e..47dfc6733 100644 --- a/app/Http/Controllers/Api/Resources.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -6,13 +6,13 @@ use App\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 000000000..51c6fee26 --- /dev/null +++ b/app/Http/Controllers/Api/SecurityController.php @@ -0,0 +1,160 @@ +get(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($keys), + ]); + } + + public function key_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + + if (is_null($key)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function create_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + if ($validator->fails()) { + $errors = $validator->errors(); + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->description) { + $request->offsetSet('description', 'Created by Coolify via API'); + } + $key = PrivateKey::create([ + 'team_id' => $teamId, + 'name' => $request->name, + 'description' => $request->description, + 'private_key' => $request->private_key, + ]); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($key), + ]); + } + + public function update_key(Request $request) + { + $allowedFields = ['name', 'description', 'private_key']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'success' => false, + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($foundKey)) { + return response()->json([ + 'success' => false, + 'message' => 'Key not found.', + ], 404); + } + $foundKey->update($request->all()); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($foundKey), + ])->setStatusCode(201); + } + + public function delete_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['success' => false, 'message' => 'UUID is required.'], 422); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($key)) { + return response()->json(['success' => false, 'message' => 'Key not found.'], 404); + } + $key->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Key deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Servers.php b/app/Http/Controllers/Api/ServersController.php similarity index 79% rename from app/Http/Controllers/Api/Servers.php rename to app/Http/Controllers/Api/ServersController.php index 387c4bd48..4d9479b7c 100644 --- a/app/Http/Controllers/Api/Servers.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -8,14 +8,15 @@ use App\Models\Project; use App\Models\Server as ModelsServer; use Illuminate\Http\Request; +use Stringable; -class Servers extends Controller +class ServersController extends Controller { public function servers(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $server['is_reachable'] = $server->settings->is_reachable; @@ -23,16 +24,22 @@ 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 c895f2c1b..000000000 --- a/app/Http/Controllers/Api/Team.php +++ /dev/null @@ -1,74 +0,0 @@ -user()->teams; - - return response()->json($teams); - } - - public function team_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); - } - - return response()->json($team); - } - - public function members_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); - } - - return response()->json($team->members); - } - - public function current_team(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team); - } - - public function current_team_members(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team->members); - } -} diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php new file mode 100644 index 000000000..a256e9caf --- /dev/null +++ b/app/Http/Controllers/Api/TeamController.php @@ -0,0 +1,89 @@ +user()->teams; + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($teams), + ]); + } + + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } + + public function current_team(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team), + ]); + } + + public function current_team_members(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json([ + 'success' => true, + 'data' => serializeApiResponse($team->members), + ]); + } +} diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php new file mode 100644 index 000000000..dc0a433e2 --- /dev/null +++ b/app/Http/Middleware/ApiAllowed.php @@ -0,0 +1,34 @@ +clearAll(); + if (isCloud()) { + return $next($request); + } + $settings = InstanceSettings::get(); + if ($settings->is_api_enabled === false) { + return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + } + + if (! isDev()) { + if ($settings->allowed_ips) { + $allowedIps = explode(',', $settings->allowed_ips); + if (! in_array($request->ip(), $allowedIps)) { + return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + } + } + } + + return $next($request); + } +} diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 3b6d7cd72..7439e112f 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -18,7 +18,8 @@ class Configuration extends Component public bool $is_dns_validation_enabled; - // public bool $next_channel; + public bool $is_api_enabled; + protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected Server $server; @@ -30,6 +31,7 @@ class Configuration extends Component 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', 'settings.instance_name' => 'nullable', + 'settings.allowed_ips' => 'nullable', ]; protected $validationAttributes = [ @@ -38,6 +40,7 @@ class Configuration extends Component 'settings.public_port_min' => 'Public port min', 'settings.public_port_max' => 'Public port max', 'settings.custom_dns_servers' => 'Custom DNS servers', + 'settings.allowed_ips' => 'Allowed IPs', ]; public function mount() @@ -45,8 +48,8 @@ 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->is_api_enabled = $this->settings->is_api_enabled; } public function instantSave() @@ -55,12 +58,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!'); } @@ -94,6 +92,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/InstanceSettings.php b/app/Models/InstanceSettings.php index 38f79ce75..bd3c41a1f 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -17,6 +17,7 @@ class InstanceSettings extends Model implements SendsEmail protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', + 'allowed_ip_ranges' => 'array', ]; public function fqdn(): Attribute diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index e968db18d..673224650 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,6 +13,8 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ 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 c6718acfe..d78d656c1 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,6 +13,8 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ 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 142f960aa..7b71bd55f 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,6 +13,8 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url']; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -178,17 +180,44 @@ 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 7e6d2e0d1..00df4fe71 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -13,6 +13,8 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -161,6 +163,13 @@ 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 df895bb34..0863522a8 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,6 +13,8 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -198,17 +200,44 @@ 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 bd160f877..79e7c37fa 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,6 +13,8 @@ class StandaloneMysql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -157,6 +159,13 @@ 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 114d376e8..1d5276cf3 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,6 +13,8 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -179,17 +181,44 @@ 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 022cd8d09..e0f863aca 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,6 +13,8 @@ class StandaloneRedis extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -179,12 +181,39 @@ 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 f5c99dbda..c5083534f 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -3,25 +3,27 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Http\Request; use Illuminate\Validation\Rule; -function get_team_id_from_token() +function getTeamIdFromToken() { $token = auth()->user()->currentAccessToken(); return data_get($token, 'team_id'); } -function invalid_token() +function invalidTokenResponse() { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); + return response()->json(['success' => false, 'message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); } -function serialize_api_response($data) +function serializeApiResponse($data) { if (! $data instanceof Collection) { $data = collect($data); } $data = $data->sortKeys(); + $created_at = data_get($data, 'created_at'); $updated_at = data_get($data, 'updated_at'); if ($created_at) { @@ -33,6 +35,16 @@ function serialize_api_response($data) unset($data['updated_at']); $data['updated_at'] = $updated_at; } + if (data_get($data, 'name')) { + $data = $data->prepend($data['name'], 'name'); + } + if (data_get($data, 'description')) { + $data = $data->prepend($data['description'], 'description'); + } + if (data_get($data, 'uuid')) { + $data = $data->prepend($data['uuid'], 'uuid'); + } + if (data_get($data, 'id')) { $data = $data->prepend($data['id'], 'id'); } @@ -90,3 +102,36 @@ function sharedDataApplications() 'manual_webhook_secret_gitea' => 'string|nullable', ]; } + +function validateIncomingRequest(Request $request) +{ + // check if request is json + if (! $request->isJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } +} + +function removeUnnecessaryFieldsFromRequest(Request $request) +{ + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); +} diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index dba8aa543..ef3f8ac9b 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -19,131 +19,163 @@ function generate_database_name(string $type): string return $type.'-database-'.$cuid; } -function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql +function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql { - // TODO: If another type of destination is added, this will need to be updated. - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + $destination = StandaloneDocker::where('uuid', $destinationUuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandalonePostgresql(); + $database->name = generate_database_name('postgresql'); + $database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environmentId; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandalonePostgresql::create([ - 'name' => generate_database_name('postgresql'), - 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneRedis(); + $database->name = generate_database_name('redis'); + $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneRedis::create([ - 'name' => generate_database_name('redis'), - 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMongodb(); + $database->name = generate_database_name('mongodb'); + $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMongodb::create([ - 'name' => generate_database_name('mongodb'), - 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMysql(); + $database->name = generate_database_name('mysql'); + $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMysql::create([ - 'name' => generate_database_name('mysql'), - 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMariadb(); + $database->name = generate_database_name('mariadb'); + $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); - return StandaloneMariadb::create([ - 'name' => generate_database_name('mariadb'), - 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); + + return $database; } -function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb +function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneKeydb(); + $database->name = generate_database_name('keydb'); + $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneKeydb::create([ - 'name' => generate_database_name('keydb'), - 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly +function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneDragonfly(); + $database->name = generate_database_name('dragonfly'); + $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneDragonfly::create([ - 'name' => generate_database_name('dragonfly'), - 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse +function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneClickhouse(); + $database->name = generate_database_name('clickhouse'); + $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneClickhouse::create([ - 'name' => generate_database_name('clickhouse'), - 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } /** diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1d70b674c..5efc0f9ef 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -538,6 +538,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } +function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId) +{ + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql && $postgresql->team()->id == $teamId) { + return $postgresql->unsetRelation('environment')->unsetRelation('destination'); + } + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis && $redis->team()->id == $teamId) { + return $redis->unsetRelation('environment'); + } + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb && $mongodb->team()->id == $teamId) { + return $mongodb->unsetRelation('environment'); + } + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql && $mysql->team()->id == $teamId) { + return $mysql->unsetRelation('environment'); + } + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb && $mariadb->team()->id == $teamId) { + return $mariadb->unsetRelation('environment'); + } + $keydb = StandaloneKeydb::whereUuid($uuid)->first(); + if ($keydb && $keydb->team()->id == $teamId) { + return $keydb->unsetRelation('environment'); + } + $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); + if ($dragonfly && $dragonfly->team()->id == $teamId) { + return $dragonfly->unsetRelation('environment'); + } + $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); + if ($clickhouse && $clickhouse->team()->id == $teamId) { + return $clickhouse->unsetRelation('environment'); + } + + return null; +} function queryResourcesByUuid(string $uuid) { $resource = null; diff --git a/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php new file mode 100644 index 000000000..b319adb70 --- /dev/null +++ b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php @@ -0,0 +1,24 @@ +boolean('is_api_enabled')->default(true); + $table->text('allowed_ips')->nullable(); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_api_enabled'); + $table->dropColumn('allowed_ips'); + }); + } +}; diff --git a/resources/views/livewire/settings/configuration.blade.php b/resources/views/livewire/settings/configuration.blade.php index b1c399bc3..b5fb49d3e 100644 --- a/resources/views/livewire/settings/configuration.blade.php +++ b/resources/views/livewire/settings/configuration.blade.php @@ -25,7 +25,16 @@ --}} +

API

+ +
+ +
+ +

Advanced

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