2024-02-16 20:56:38 +00:00
|
|
|
<?php
|
|
|
|
|
|
2024-06-28 13:05:37 +00:00
|
|
|
use App\Enums\BuildPackTypes;
|
|
|
|
|
use App\Enums\RedirectTypes;
|
2024-10-10 07:33:29 +00:00
|
|
|
use App\Enums\StaticImageTypes;
|
2026-07-07 19:20:41 +00:00
|
|
|
use App\Models\Environment;
|
2026-06-01 13:13:04 +00:00
|
|
|
use App\Rules\ValidGitBranch;
|
2026-06-01 13:12:58 +00:00
|
|
|
use App\Support\ValidationPatterns;
|
2024-06-21 14:46:13 +00:00
|
|
|
use Illuminate\Database\Eloquent\Collection;
|
2026-07-07 10:53:34 +00:00
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2026-07-07 19:20:41 +00:00
|
|
|
use Illuminate\Http\JsonResponse;
|
2024-07-01 14:26:50 +00:00
|
|
|
use Illuminate\Http\Request;
|
2026-07-07 18:56:55 +00:00
|
|
|
use Illuminate\Support\Facades\Gate;
|
2026-07-07 19:20:41 +00:00
|
|
|
use Illuminate\Support\Facades\Validator;
|
2024-06-28 13:05:37 +00:00
|
|
|
use Illuminate\Validation\Rule;
|
2024-06-21 14:46:13 +00:00
|
|
|
|
2024-07-01 14:26:50 +00:00
|
|
|
function getTeamIdFromToken()
|
2024-02-16 20:56:38 +00:00
|
|
|
{
|
2026-06-01 13:17:55 +00:00
|
|
|
$user = auth()->user();
|
|
|
|
|
$token = $user?->currentAccessToken();
|
|
|
|
|
$teamId = data_get($token, 'team_id');
|
2024-06-10 20:43:34 +00:00
|
|
|
|
2026-06-01 13:17:55 +00:00
|
|
|
if (! $user || is_null($teamId) || ! $user->teams()->where('teams.id', $teamId)->exists()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $teamId;
|
2024-02-16 20:56:38 +00:00
|
|
|
}
|
2024-07-01 14:26:50 +00:00
|
|
|
function invalidTokenResponse()
|
2024-03-07 11:27:23 +00:00
|
|
|
{
|
2024-07-03 11:13:38 +00:00
|
|
|
return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
|
2024-03-07 11:27:23 +00:00
|
|
|
}
|
2024-06-21 14:46:13 +00:00
|
|
|
|
2024-07-01 14:26:50 +00:00
|
|
|
function serializeApiResponse($data)
|
2024-06-21 14:46:13 +00:00
|
|
|
{
|
2024-07-02 10:15:58 +00:00
|
|
|
if ($data instanceof Collection) {
|
2024-10-31 17:20:11 +00:00
|
|
|
return $data->map(function ($d) {
|
2024-07-02 10:15:58 +00:00
|
|
|
$d = collect($d)->sortKeys();
|
|
|
|
|
$created_at = data_get($d, 'created_at');
|
|
|
|
|
$updated_at = data_get($d, 'updated_at');
|
|
|
|
|
if ($created_at) {
|
|
|
|
|
unset($d['created_at']);
|
|
|
|
|
$d['created_at'] = $created_at;
|
|
|
|
|
}
|
|
|
|
|
if ($updated_at) {
|
|
|
|
|
unset($d['updated_at']);
|
|
|
|
|
$d['updated_at'] = $updated_at;
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'name')) {
|
|
|
|
|
$d = $d->prepend($d['name'], 'name');
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'description')) {
|
|
|
|
|
$d = $d->prepend($d['description'], 'description');
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'uuid')) {
|
|
|
|
|
$d = $d->prepend($d['uuid'], 'uuid');
|
|
|
|
|
}
|
2024-06-21 14:46:13 +00:00
|
|
|
|
2024-07-02 10:15:58 +00:00
|
|
|
if (! is_null(data_get($d, 'id'))) {
|
|
|
|
|
$d = $d->prepend($d['id'], 'id');
|
|
|
|
|
}
|
2024-07-01 14:26:50 +00:00
|
|
|
|
2024-07-02 10:15:58 +00:00
|
|
|
return $d;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
$d = collect($data)->sortKeys();
|
|
|
|
|
$created_at = data_get($d, 'created_at');
|
|
|
|
|
$updated_at = data_get($d, 'updated_at');
|
|
|
|
|
if ($created_at) {
|
|
|
|
|
unset($d['created_at']);
|
|
|
|
|
$d['created_at'] = $created_at;
|
|
|
|
|
}
|
|
|
|
|
if ($updated_at) {
|
|
|
|
|
unset($d['updated_at']);
|
|
|
|
|
$d['updated_at'] = $updated_at;
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'name')) {
|
|
|
|
|
$d = $d->prepend($d['name'], 'name');
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'description')) {
|
|
|
|
|
$d = $d->prepend($d['description'], 'description');
|
|
|
|
|
}
|
|
|
|
|
if (data_get($d, 'uuid')) {
|
|
|
|
|
$d = $d->prepend($d['uuid'], 'uuid');
|
|
|
|
|
}
|
2024-06-21 14:46:13 +00:00
|
|
|
|
2024-07-02 10:15:58 +00:00
|
|
|
if (! is_null(data_get($d, 'id'))) {
|
|
|
|
|
$d = $d->prepend($d['id'], 'id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $d;
|
|
|
|
|
}
|
2024-06-21 14:46:13 +00:00
|
|
|
}
|
2024-06-28 13:05:37 +00:00
|
|
|
|
2026-07-07 10:53:34 +00:00
|
|
|
/**
|
|
|
|
|
* Re-expose a model's `$hidden` sensitive fields when the current API request
|
|
|
|
|
* carries the `read:sensitive` or `root` token ability (set by the
|
|
|
|
|
* ApiSensitiveData middleware).
|
|
|
|
|
*/
|
|
|
|
|
function exposeSensitiveFields(Model $model): Model
|
|
|
|
|
{
|
|
|
|
|
if (request()->attributes->get('can_read_sensitive', false) === true && filled($model->getHidden())) {
|
|
|
|
|
$model->makeVisible($model->getHidden());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 13:05:37 +00:00
|
|
|
function sharedDataApplications()
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'git_repository' => 'string',
|
2026-06-01 13:13:04 +00:00
|
|
|
'git_branch' => ['string', new ValidGitBranch],
|
2024-06-28 13:05:37 +00:00
|
|
|
'build_pack' => Rule::enum(BuildPackTypes::class),
|
|
|
|
|
'is_static' => 'boolean',
|
2026-01-14 14:16:40 +00:00
|
|
|
'is_spa' => 'boolean',
|
|
|
|
|
'is_auto_deploy_enabled' => 'boolean',
|
|
|
|
|
'is_force_https_enabled' => 'boolean',
|
2026-07-07 10:36:35 +00:00
|
|
|
'is_preview_deployments_enabled' => 'boolean',
|
2026-07-14 11:44:45 +00:00
|
|
|
'use_build_secrets' => 'boolean',
|
|
|
|
|
'is_git_submodules_enabled' => 'boolean',
|
|
|
|
|
'is_git_lfs_enabled' => 'boolean',
|
|
|
|
|
'is_git_shallow_clone_enabled' => 'boolean',
|
|
|
|
|
'disable_build_cache' => 'boolean',
|
|
|
|
|
'inject_build_args_to_dockerfile' => 'boolean',
|
|
|
|
|
'include_source_commit_in_build' => 'boolean',
|
|
|
|
|
'is_env_sorting_enabled' => 'boolean',
|
|
|
|
|
'is_pr_deployments_public_enabled' => 'boolean',
|
|
|
|
|
'is_gzip_enabled' => 'boolean',
|
|
|
|
|
'is_stripprefix_enabled' => 'boolean',
|
|
|
|
|
'is_raw_compose_deployment_enabled' => 'boolean',
|
|
|
|
|
'stop_grace_period' => 'nullable|integer|min:'.MIN_STOP_GRACE_PERIOD_SECONDS.'|max:'.MAX_STOP_GRACE_PERIOD_SECONDS,
|
|
|
|
|
'docker_images_to_keep' => 'integer|min:0|max:100',
|
2024-10-10 07:33:29 +00:00
|
|
|
'static_image' => Rule::enum(StaticImageTypes::class),
|
2026-07-02 14:52:07 +00:00
|
|
|
'domains' => ValidationPatterns::applicationDomainRules(),
|
2024-06-28 13:05:37 +00:00
|
|
|
'redirect' => Rule::enum(RedirectTypes::class),
|
2026-03-10 21:22:48 +00:00
|
|
|
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
2026-06-01 13:12:58 +00:00
|
|
|
'docker_registry_image_name' => ValidationPatterns::dockerImageNameRules(),
|
|
|
|
|
'docker_registry_image_tag' => ValidationPatterns::dockerImageTagRules(),
|
|
|
|
|
'install_command' => ValidationPatterns::shellSafeCommandRules(),
|
|
|
|
|
'build_command' => ValidationPatterns::shellSafeCommandRules(),
|
|
|
|
|
'start_command' => ValidationPatterns::shellSafeCommandRules(),
|
2024-06-28 13:05:37 +00:00
|
|
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
|
|
|
|
|
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
|
2025-11-01 12:13:14 +00:00
|
|
|
'custom_network_aliases' => 'string|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'base_directory' => ValidationPatterns::directoryPathRules(),
|
|
|
|
|
'publish_directory' => ValidationPatterns::directoryPathRules(),
|
2024-06-28 13:05:37 +00:00
|
|
|
'health_check_enabled' => 'boolean',
|
2026-02-25 10:38:09 +00:00
|
|
|
'health_check_type' => 'string|in:http,cmd',
|
|
|
|
|
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
2026-03-28 11:01:12 +00:00
|
|
|
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%,;]+$#'],
|
2026-02-25 09:58:29 +00:00
|
|
|
'health_check_port' => 'integer|nullable|min:1|max:65535',
|
|
|
|
|
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
|
|
|
|
'health_check_method' => 'string|in:GET,HEAD,POST,OPTIONS',
|
2024-06-28 13:05:37 +00:00
|
|
|
'health_check_return_code' => 'numeric',
|
2026-02-25 09:58:29 +00:00
|
|
|
'health_check_scheme' => 'string|in:http,https',
|
2024-06-28 13:05:37 +00:00
|
|
|
'health_check_response_text' => 'string|nullable',
|
|
|
|
|
'health_check_interval' => 'numeric',
|
|
|
|
|
'health_check_timeout' => 'numeric',
|
|
|
|
|
'health_check_retries' => 'numeric',
|
|
|
|
|
'health_check_start_period' => 'numeric',
|
|
|
|
|
'limits_memory' => 'string',
|
|
|
|
|
'limits_memory_swap' => 'string',
|
|
|
|
|
'limits_memory_swappiness' => 'numeric',
|
|
|
|
|
'limits_memory_reservation' => 'string',
|
|
|
|
|
'limits_cpus' => 'string',
|
|
|
|
|
'limits_cpuset' => 'string|nullable',
|
|
|
|
|
'limits_cpu_shares' => 'numeric',
|
|
|
|
|
'custom_labels' => 'string|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'custom_docker_run_options' => ValidationPatterns::shellSafeCommandRules(2000),
|
2026-03-18 12:53:01 +00:00
|
|
|
// Security: deployment commands are intentionally arbitrary shell (e.g. "php artisan migrate").
|
|
|
|
|
// Access is gated by API token authentication. Commands run inside the app container, not the host.
|
2024-06-28 13:05:37 +00:00
|
|
|
'post_deployment_command' => 'string|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'post_deployment_command_container' => ValidationPatterns::containerNameRules(),
|
2024-06-28 13:05:37 +00:00
|
|
|
'pre_deployment_command' => 'string|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'pre_deployment_command_container' => ValidationPatterns::containerNameRules(),
|
2024-06-28 13:05:37 +00:00
|
|
|
'manual_webhook_secret_github' => 'string|nullable',
|
|
|
|
|
'manual_webhook_secret_gitlab' => 'string|nullable',
|
|
|
|
|
'manual_webhook_secret_bitbucket' => 'string|nullable',
|
|
|
|
|
'manual_webhook_secret_gitea' => 'string|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'dockerfile_location' => ValidationPatterns::filePathRules(),
|
|
|
|
|
'dockerfile_target_build' => ValidationPatterns::dockerTargetRules(),
|
|
|
|
|
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
2024-07-04 11:45:06 +00:00
|
|
|
'docker_compose' => 'string|nullable',
|
|
|
|
|
'docker_compose_domains' => 'array|nullable',
|
2026-06-01 13:12:58 +00:00
|
|
|
'docker_compose_custom_start_command' => ValidationPatterns::shellSafeCommandRules(),
|
|
|
|
|
'docker_compose_custom_build_command' => ValidationPatterns::shellSafeCommandRules(),
|
2026-01-06 14:50:55 +00:00
|
|
|
'is_container_label_escape_enabled' => 'boolean',
|
2026-04-03 07:05:13 +00:00
|
|
|
'is_preserve_repository_enabled' => 'boolean',
|
2024-06-28 13:05:37 +00:00
|
|
|
];
|
|
|
|
|
}
|
2024-07-01 14:26:50 +00:00
|
|
|
|
2026-07-07 19:20:41 +00:00
|
|
|
function moveResourceToEnvironment(Request $request, $resource, string $resourceType, int $teamId): JsonResponse
|
2026-03-13 14:48:00 +00:00
|
|
|
{
|
|
|
|
|
|
2026-07-07 19:20:41 +00:00
|
|
|
$validator = Validator::make($request->all(), [
|
2026-03-13 14:48:00 +00:00
|
|
|
'environment_uuid' => 'required|string',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Validation failed.',
|
|
|
|
|
'errors' => $validator->errors(),
|
|
|
|
|
], 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$extraFields = array_diff(array_keys($request->all()), ['environment_uuid']);
|
|
|
|
|
if (! empty($extraFields)) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Validation failed.',
|
|
|
|
|
'errors' => collect($extraFields)->mapWithKeys(fn ($field) => [$field => 'This field is not allowed.'])->toArray(),
|
|
|
|
|
], 422);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 19:20:41 +00:00
|
|
|
$newEnvironment = Environment::ownedByCurrentTeamAPI($teamId)
|
2026-03-13 14:48:00 +00:00
|
|
|
->whereUuid($request->environment_uuid)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (! $newEnvironment) {
|
|
|
|
|
return response()->json(['message' => 'Target environment not found or not owned by your team.'], 404);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 18:56:55 +00:00
|
|
|
Gate::authorize('update', $newEnvironment);
|
|
|
|
|
|
2026-03-13 14:48:00 +00:00
|
|
|
if ($resource->environment_id === $newEnvironment->id) {
|
|
|
|
|
return response()->json(['message' => "$resourceType is already in this environment."], 400);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 19:20:41 +00:00
|
|
|
$oldEnvironment = $resource->environment()->with('project')->first();
|
|
|
|
|
|
2026-03-13 14:48:00 +00:00
|
|
|
$resource->update(['environment_id' => $newEnvironment->id]);
|
|
|
|
|
|
2026-07-07 19:20:41 +00:00
|
|
|
auditLog('api.'.str($resourceType)->lower()->value().'.moved', [
|
|
|
|
|
'team_id' => $teamId,
|
|
|
|
|
'resource_uuid' => $resource->uuid,
|
|
|
|
|
'resource_type' => str($resourceType)->lower()->value(),
|
|
|
|
|
'from_project_uuid' => $oldEnvironment?->project?->uuid,
|
|
|
|
|
'from_environment_uuid' => $oldEnvironment?->uuid,
|
|
|
|
|
'to_project_uuid' => $newEnvironment->project->uuid,
|
|
|
|
|
'to_environment_uuid' => $newEnvironment->uuid,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-13 14:48:00 +00:00
|
|
|
return response()->json([
|
|
|
|
|
'message' => "$resourceType moved successfully.",
|
|
|
|
|
'uuid' => $resource->uuid,
|
|
|
|
|
'project_uuid' => $newEnvironment->project->uuid,
|
|
|
|
|
'environment_uuid' => $newEnvironment->uuid,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-01 14:26:50 +00:00
|
|
|
function validateIncomingRequest(Request $request)
|
|
|
|
|
{
|
|
|
|
|
// check if request is json
|
|
|
|
|
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);
|
|
|
|
|
}
|
2024-07-02 11:39:44 +00:00
|
|
|
// check if valid json is empty
|
|
|
|
|
if (empty($request->json()->all())) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Invalid request.',
|
|
|
|
|
'error' => 'Empty JSON.',
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
2024-07-01 14:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeUnnecessaryFieldsFromRequest(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$request->offsetUnset('project_uuid');
|
|
|
|
|
$request->offsetUnset('environment_name');
|
2024-12-17 12:42:16 +00:00
|
|
|
$request->offsetUnset('environment_uuid');
|
2024-07-01 14:26:50 +00:00
|
|
|
$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');
|
2024-09-28 04:11:43 +00:00
|
|
|
$request->offsetUnset('use_build_server');
|
2026-07-14 11:44:45 +00:00
|
|
|
$request->offsetUnset('use_build_secrets');
|
2024-10-10 07:33:29 +00:00
|
|
|
$request->offsetUnset('is_static');
|
2026-01-14 14:16:40 +00:00
|
|
|
$request->offsetUnset('is_spa');
|
|
|
|
|
$request->offsetUnset('is_auto_deploy_enabled');
|
|
|
|
|
$request->offsetUnset('is_force_https_enabled');
|
2026-07-07 10:36:35 +00:00
|
|
|
$request->offsetUnset('is_preview_deployments_enabled');
|
2026-01-14 14:16:40 +00:00
|
|
|
$request->offsetUnset('connect_to_docker_network');
|
2025-08-28 09:21:30 +00:00
|
|
|
$request->offsetUnset('force_domain_override');
|
2025-12-05 20:16:04 +00:00
|
|
|
$request->offsetUnset('autogenerate_domain');
|
2026-01-06 14:50:55 +00:00
|
|
|
$request->offsetUnset('is_container_label_escape_enabled');
|
2026-02-15 19:24:41 +00:00
|
|
|
$request->offsetUnset('is_preserve_repository_enabled');
|
2026-07-07 10:12:20 +00:00
|
|
|
$request->offsetUnset('include_source_commit_in_build');
|
2026-07-14 11:44:45 +00:00
|
|
|
$request->offsetUnset('is_git_submodules_enabled');
|
|
|
|
|
$request->offsetUnset('is_git_lfs_enabled');
|
|
|
|
|
$request->offsetUnset('is_git_shallow_clone_enabled');
|
|
|
|
|
$request->offsetUnset('disable_build_cache');
|
|
|
|
|
$request->offsetUnset('inject_build_args_to_dockerfile');
|
|
|
|
|
$request->offsetUnset('is_env_sorting_enabled');
|
|
|
|
|
$request->offsetUnset('is_pr_deployments_public_enabled');
|
|
|
|
|
$request->offsetUnset('stop_grace_period');
|
|
|
|
|
$request->offsetUnset('docker_images_to_keep');
|
|
|
|
|
$request->offsetUnset('is_gzip_enabled');
|
|
|
|
|
$request->offsetUnset('is_stripprefix_enabled');
|
|
|
|
|
$request->offsetUnset('is_raw_compose_deployment_enabled');
|
2026-01-12 20:42:20 +00:00
|
|
|
$request->offsetUnset('docker_compose_raw');
|
2026-03-29 14:02:05 +00:00
|
|
|
$request->offsetUnset('tags');
|
2024-07-01 14:26:50 +00:00
|
|
|
}
|