boolean('force_domain_override'); $dnsCleanup = app(ManagedDnsRecordCleanup::class); $previousDnsHostnames = $dnsCleanup->hostnamesOf($serviceApplication->fresh() ?? $serviceApplication); if (array_key_exists('url', $payload)) { $urlRaw = $payload['url']; if ($urlRaw !== null && ! is_string($urlRaw)) { return response()->json([ 'message' => 'Validation failed.', 'errors' => ['url' => 'The url must be a string.'], ], 422); } $parsed = ServiceComposeUrl::validateUrlString( is_string($urlRaw) ? $urlRaw : null, $forceDomainOverride ); if (count($parsed['errors']) > 0) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $parsed['errors'], ], 422); } if ($parsed['normalized'] !== null) { $containerUrls = str($parsed['normalized']) ->explode(',') ->map(fn ($url) => str(trim((string) $url))->lower()); $result = checkIfDomainIsAlreadyUsedViaAPI($containerUrls, $teamId, $serviceApplication->uuid); if (isset($result['error'])) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [$result['error']], ], 422); } if ($result['hasConflicts'] && ! $forceDomainOverride) { return response()->json([ 'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.', 'conflicts' => $result['conflicts'], 'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.', ], 409); } } $serviceApplication->setEditableUrls($parsed['normalized']); } if (array_key_exists('noindex_domains', $payload)) { // Must run after fqdn is set above: flags are kept only for current domains. $serviceApplication->setNoindexDomains($payload['noindex_domains'] ?? []); } if (array_key_exists('redirect', $payload)) { if (in_array($payload['redirect'], ['www', 'non-www'], true) && ! $this->hasWwwAndNonWwwDomains($serviceApplication->fqdn)) { return response()->json([ 'message' => 'Validation failed.', 'errors' => ['redirect' => ['The www and non-www redirects need both a www and a non-www domain in url.']], ], 422); } $serviceApplication->redirect = $payload['redirect']; } if (array_key_exists('human_name', $payload)) { $serviceApplication->human_name = $payload['human_name']; } if (array_key_exists('description', $payload)) { $serviceApplication->description = $payload['description']; } if (array_key_exists('image', $payload)) { $serviceApplication->image = $payload['image']; } if (array_key_exists('exclude_from_status', $payload)) { $serviceApplication->exclude_from_status = filter_var($payload['exclude_from_status'], FILTER_VALIDATE_BOOLEAN); } if (array_key_exists('is_gzip_enabled', $payload)) { $serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN); } if (array_key_exists('traefik_certificate_resolver', $payload)) { $serviceApplication->traefik_certificate_resolver = filled($payload['traefik_certificate_resolver']) ? $payload['traefik_certificate_resolver'] : null; } if (array_key_exists('is_stripprefix_enabled', $payload)) { $serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN); } if (array_key_exists('is_force_https_enabled', $payload)) { $serviceApplication->is_force_https_enabled = filter_var($payload['is_force_https_enabled'], FILTER_VALIDATE_BOOLEAN); } if (array_key_exists('is_permanent_redirect_enabled', $payload)) { $serviceApplication->is_permanent_redirect_enabled = filter_var($payload['is_permanent_redirect_enabled'], FILTER_VALIDATE_BOOLEAN); } if (array_key_exists('max_restart_count', $payload)) { $serviceApplication->max_restart_count = $payload['max_restart_count']; $serviceApplication->restart_limit_reached = false; } if (array_key_exists('is_log_drain_enabled', $payload)) { $enabled = filter_var($payload['is_log_drain_enabled'], FILTER_VALIDATE_BOOLEAN); $server = $serviceApplication->service->destination->server; if ($enabled && ! $server->isLogDrainEnabled()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [ 'is_log_drain_enabled' => ['Log drain is not enabled on the server for this service.'], ], ], 422); } $serviceApplication->is_log_drain_enabled = $enabled; } $serviceApplication->save(); $serviceApplication->refresh(); $dnsCleanup->queueReleaseOfRemovedHostnames($serviceApplication, $previousDnsHostnames, (int) $teamId); updateCompose($serviceApplication); return null; } private function hasWwwAndNonWwwDomains(?string $fqdn): bool { $hosts = str($fqdn ?? '')->explode(',') ->map(fn (string $url) => strtolower((string) parse_url(trim($url), PHP_URL_HOST))) ->filter(); return $hosts->contains(fn (string $host) => str_starts_with($host, 'www.')) && $hosts->contains(fn (string $host) => ! str_starts_with($host, 'www.')); } }