feat: easily redirect between www-and-non-www domains

This commit is contained in:
Andras Bacsai 2024-06-11 11:32:08 +02:00
parent a125c0032b
commit 7345ccbbee
8 changed files with 132 additions and 15 deletions

View File

@ -77,6 +77,7 @@ class General extends Component
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable', 'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@ -113,6 +114,7 @@ class General extends Component
'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.watch_paths' => 'Watch paths', 'application.watch_paths' => 'Watch paths',
'application.redirect' => 'Redirect',
]; ];
public function mount() public function mount()
{ {
@ -134,7 +136,7 @@ public function mount()
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { 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->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
} }
@ -270,7 +272,7 @@ public function getWildcardDomain()
} }
public function resetDefaultLabels() 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->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
@ -278,6 +280,7 @@ public function resetDefaultLabels()
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile(); $this->loadComposeFile();
} }
$this->dispatch('configurationChanged');
} }
public function checkFqdns($showToaster = true) public function checkFqdns($showToaster = true)
@ -295,9 +298,25 @@ public function checkFqdns($showToaster = true)
$this->application->fqdn = $domains->implode(','); $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.<br><br>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) public function submit($showToaster = true)
{ {
try { try {
$this->set_redirect();
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $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)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
@ -310,7 +329,7 @@ public function submit($showToaster = true)
$this->application->save(); $this->application->save();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { 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->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
} }

View File

@ -46,7 +46,7 @@ public function cloneTo($destination_id)
]); ]);
$new_resource->save(); $new_resource->save();
if ($new_resource->destination->server->proxyType() !== 'NONE') { 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->custom_labels = base64_encode($customLabels);
$new_resource->save(); $new_resource->save();
} }

View File

@ -565,7 +565,7 @@ public function isLogDrainEnabled()
} }
public function isConfigurationChanged(bool $save = false) 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) { if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
} else { } else {
@ -948,7 +948,7 @@ function parseContainerLabels(?ApplicationPreview $preview = null)
$customLabels = base64_decode($this->custom_labels); $customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
ray('custom_labels contains non-ascii characters'); 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->custom_labels = base64_encode($customLabels);
$this->save(); $this->save();

View File

@ -234,7 +234,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
} }
return $payload; 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([]); $labels = collect([]);
if ($serviceLabels) { if ($serviceLabels) {
@ -247,7 +247,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
$host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme(); $schema = $url->getScheme();
$port = $url->getPort(); $port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) { if (is_null($port) && !is_null($onlyPort)) {
@ -266,13 +266,19 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$labels->push("caddy_{$loop}.encode=zstd gzip"); $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()) { if (isDev()) {
// $labels->push("caddy_{$loop}.tls=internal"); // $labels->push("caddy_{$loop}.tls=internal");
} }
} }
return $labels->sort(); 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 = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
@ -283,6 +289,8 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$basic_auth_middleware = null; $basic_auth_middleware = null;
$redirect = false; $redirect = false;
$redirect_middleware = null; $redirect_middleware = null;
if ($serviceLabels) { if ($serviceLabels) {
$basic_auth = $serviceLabels->contains(function ($value) { $basic_auth = $serviceLabels->contains(function ($value) {
return str_contains($value, 'basicauth'); return str_contains($value, 'basicauth');
@ -316,6 +324,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($generate_unique_uuid) { if ($generate_unique_uuid) {
$uuid = new Cuid2(7); $uuid = new Cuid2(7);
} }
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $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.regex=^{$path}/(.*)");
$labels->push("traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1"); $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') { if ($schema === 'https') {
// Set labels for https // Set labels for https
$labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); $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')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-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()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $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')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-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()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $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')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-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()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $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')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-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()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@ -474,7 +528,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled() is_stripprefix_enabled: $application->isStripprefixEnabled(),
redirect_direction: $application->redirect
)); ));
// Add Caddy labels // Add Caddy labels
$labels = $labels->merge(fqdnLabelsForCaddy( $labels = $labels->merge(fqdnLabelsForCaddy(
@ -484,7 +539,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled() is_stripprefix_enabled: $application->isStripprefixEnabled(),
redirect_direction: $application->redirect
)); ));
} }
} else { } else {

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -9,8 +9,8 @@
@endif @endif
</label> </label>
@endif @endif
<select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:text-white' <select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="text-black bg-warning" wire:loading.attr="disabled" name={{ $id }} 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> @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif>
{{ $slot }} {{ $slot }}
</select> </select>

View File

@ -8,7 +8,7 @@
x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" 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="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]" x-cloak>
<div <div
class="flex flex-col 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 lg:flex-row sm:rounded"> 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">
<div <div
class="flex flex-col items-start h-full pb-0 text-xs lg:items-center lg:flex-row lg:pr-6 lg:space-x-5 dark:text-neutral-300 "> class="flex flex-col items-start h-full pb-0 text-xs lg:items-center lg:flex-row lg:pr-6 lg:space-x-5 dark:text-neutral-300 ">
@if (isset($icon)) @if (isset($icon))
@ -23,7 +23,7 @@ class="w-full mb-1 text-base font-bold leading-none -translate-y-1 text-neutral-
<div>{{ $description }}</div> <div>{{ $description }}</div>
</div> </div>
</div> </div>
<button @click="bannerVisible=false"> <button @click="bannerVisible=false" class="pl-6 lg:pl-0">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-full h-full"> stroke-width="1.5" stroke="currentColor" class="w-full h-full">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />

View File

@ -66,6 +66,20 @@
<x-forms.button wire:click="getWildcardDomain">Generate Domain <x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button> </x-forms.button>
</div> </div>
<div class="flex items-end gap-2">
<x-forms.select label="Direction" id="application.redirect" required
helper="You must need to add www and non-www as an A DNS record.">
<option value="both">Allow www & non-www.</option>
<option value="www">Redirect to www.</option>
<option value="non-www">Redirect to non-www.</option>
</x-forms.select>
<x-modal-confirmation action="set_redirect" >
<x-slot:customButton>
<div class="w-[7.2rem]">Set Direction</div>
</x-slot:customButton>
This will reset the container labels. Are you sure?
</x-modal-confirmation>
</div>
@endif @endif
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')