diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 58a5ee267..b993542e2 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -77,6 +77,7 @@ class General extends Component 'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', + 'application.redirect' => 'string|required', ]; protected $validationAttributes = [ 'application.name' => 'name', @@ -113,6 +114,7 @@ class General extends Component 'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.watch_paths' => 'Watch paths', + 'application.redirect' => 'Redirect', ]; public function mount() { @@ -134,7 +136,7 @@ class General extends Component $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + $this->customLabels = str(implode("|coolify|", generateLabelsApplication($this->application)))->replace("|coolify|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); } @@ -270,7 +272,7 @@ class General extends Component } public function resetDefaultLabels() { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + $this->customLabels = str(implode("|coolify|", generateLabelsApplication($this->application)))->replace("|coolify|", "\n"); $this->ports_exposes = $this->application->ports_exposes; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->application->custom_labels = base64_encode($this->customLabels); @@ -278,6 +280,7 @@ class General extends Component if ($this->application->build_pack === 'dockercompose') { $this->loadComposeFile(); } + $this->dispatch('configurationChanged'); } public function checkFqdns($showToaster = true) @@ -295,9 +298,25 @@ class General extends Component $this->application->fqdn = $domains->implode(','); } } + public function set_redirect() + { + try { + $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); + if ($has_www === 0 && $this->application->redirect === 'www') { + $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.

Please add www to your domain list and as an A DNS record (if applicable).'); + return; + } + $this->application->save(); + $this->resetDefaultLabels(); + $this->dispatch('success', 'Redirect updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function submit($showToaster = true) { try { + $this->set_redirect(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { @@ -310,7 +329,7 @@ class General extends Component $this->application->save(); if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + $this->customLabels = str(implode("|coolify|", generateLabelsApplication($this->application)))->replace("|coolify|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); } diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 46f9021e5..ba8acad29 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -46,7 +46,7 @@ class ResourceOperations extends Component ]); $new_resource->save(); if ($new_resource->destination->server->proxyType() !== 'NONE') { - $customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n"); + $customLabels = str(implode("|coolify|", generateLabelsApplication($new_resource)))->replace("|coolify|", "\n"); $new_resource->custom_labels = base64_encode($customLabels); $new_resource->save(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index e606735df..63d24decb 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -565,7 +565,7 @@ class Application extends BaseModel } public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build; + $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -948,7 +948,7 @@ class Application extends BaseModel $customLabels = base64_decode($this->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { ray('custom_labels contains non-ascii characters'); - $customLabels = str(implode("|", generateLabelsApplication($this, $preview)))->replace("|", "\n"); + $customLabels = str(implode("|coolify|", generateLabelsApplication($this, $preview)))->replace("|coolify|", "\n"); } $this->custom_labels = base64_encode($customLabels); $this->save(); diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index fb980c296..895573b58 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -234,7 +234,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) } return $payload; } -function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null) +function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both') { $labels = collect([]); if ($serviceLabels) { @@ -247,7 +247,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); - + $host_without_www = str($host)->replace('www.', ''); $schema = $url->getScheme(); $port = $url->getPort(); if (is_null($port) && !is_null($onlyPort)) { @@ -266,13 +266,19 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, if ($is_gzip_enabled) { $labels->push("caddy_{$loop}.encode=zstd gzip"); } + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + $labels->push("caddy_{$loop}.redir={$schema}://www.{$host}{uri}"); + } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}"); + } if (isDev()) { // $labels->push("caddy_{$loop}.tls=internal"); } } return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both') { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -283,6 +289,8 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $basic_auth_middleware = null; $redirect = false; $redirect_middleware = null; + + if ($serviceLabels) { $basic_auth = $serviceLabels->contains(function ($value) { return str_contains($value, 'basicauth'); @@ -316,6 +324,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($generate_unique_uuid) { $uuid = new Cuid2(7); } + $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); @@ -334,6 +343,19 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.regex=^{$path}/(.*)"); $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1"); } + + $to_www_name = "{$loop}-{$uuid}-to-www"; + $to_non_www_name = "{$loop}-{$uuid}-to-non-www"; + $redirect_to_non_www = [ + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\${1}://\${2}", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false" + ]; + $redirect_to_www = [ + "traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)", + "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\${1}://www.\${2}", + "traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false" + ]; if ($schema === 'https') { // Set labels for https $labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); @@ -360,6 +382,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -378,6 +408,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -422,6 +460,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -440,6 +486,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -474,7 +528,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect )); // Add Caddy labels $labels = $labels->merge(fqdnLabelsForCaddy( @@ -484,7 +539,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect )); } } else { diff --git a/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php b/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php new file mode 100644 index 000000000..21ee4efcc --- /dev/null +++ b/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php @@ -0,0 +1,28 @@ +string('redirect')->enum('www', 'non-www', 'both')->default('both')->after('domain'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('redirect'); + }); + } +}; diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 0a19923fc..02308ceb5 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -9,8 +9,8 @@ @endif @endif - merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' + wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" name={{ $id }} @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif> {{ $slot }} diff --git a/resources/views/components/popup-small.blade.php b/resources/views/components/popup-small.blade.php index ba6839bab..1bd996727 100644 --- a/resources/views/components/popup-small.blade.php +++ b/resources/views/components/popup-small.blade.php @@ -8,7 +8,7 @@ x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" class="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]" x-cloak>
+ class="flex flex-row items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 hover:dark:bg-coolgray-100 lg:p-8 sm:rounded">
@if (isset($icon)) @@ -23,7 +23,7 @@
{{ $description }}
-