diff --git a/app/Actions/Service/UpdateServiceApplicationFromApi.php b/app/Actions/Service/UpdateServiceApplicationFromApi.php index 6e1bdccd55..9e7657984e 100644 --- a/app/Actions/Service/UpdateServiceApplicationFromApi.php +++ b/app/Actions/Service/UpdateServiceApplicationFromApi.php @@ -99,6 +99,10 @@ class UpdateServiceApplicationFromApi $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; diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 1286f34282..c39ea3c977 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -55,6 +55,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep', 'is_gzip_enabled', 'is_stripprefix_enabled', + 'is_permanent_redirect_enabled', 'traefik_certificate_resolver', 'is_raw_compose_deployment_enabled', 'is_log_drain_enabled', @@ -79,6 +80,7 @@ class ApplicationsController extends Controller 'is_pr_deployments_public_enabled', 'is_gzip_enabled', 'is_stripprefix_enabled', + 'is_permanent_redirect_enabled', 'is_raw_compose_deployment_enabled', 'is_log_drain_enabled', 'is_gpu_enabled', @@ -229,6 +231,7 @@ class ApplicationsController extends Controller && $application->settings->is_container_label_readonly_enabled && (array_key_exists('is_gzip_enabled', $settings) || array_key_exists('is_stripprefix_enabled', $settings) + || array_key_exists('is_permanent_redirect_enabled', $settings) || array_key_exists('traefik_certificate_resolver', $settings)); $application->settings->fill($settings)->save(); @@ -454,6 +457,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], @@ -654,6 +658,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], @@ -854,6 +859,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], @@ -1025,6 +1031,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], @@ -1192,6 +1199,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], @@ -3192,6 +3200,7 @@ class ApplicationsController extends Controller 'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'], 'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'], 'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'], 'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'], 'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'], diff --git a/app/Http/Controllers/Api/ServiceApplicationsController.php b/app/Http/Controllers/Api/ServiceApplicationsController.php index e97e5e42a9..b336259d63 100644 --- a/app/Http/Controllers/Api/ServiceApplicationsController.php +++ b/app/Http/Controllers/Api/ServiceApplicationsController.php @@ -261,6 +261,7 @@ class ServiceApplicationsController extends Controller 'traefik_certificate_resolver' => new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, description: 'Traefik certificate resolver for this service application. Null uses the server setting, "none" uses no resolver.'), 'is_stripprefix_enabled' => new OA\Property(property: 'is_stripprefix_enabled', type: 'boolean', nullable: true), 'is_force_https_enabled' => new OA\Property(property: 'is_force_https_enabled', type: 'boolean', nullable: true), + 'is_permanent_redirect_enabled' => new OA\Property(property: 'is_permanent_redirect_enabled', type: 'boolean', nullable: true, description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'), 'max_restart_count' => new OA\Property(property: 'max_restart_count', type: 'integer', minimum: 0, nullable: true, description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'), ] ) @@ -336,6 +337,7 @@ class ServiceApplicationsController extends Controller 'traefik_certificate_resolver', 'is_stripprefix_enabled', 'is_force_https_enabled', + 'is_permanent_redirect_enabled', 'max_restart_count', ]; @@ -352,6 +354,7 @@ class ServiceApplicationsController extends Controller 'traefik_certificate_resolver' => ['sometimes', 'nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN], 'is_stripprefix_enabled' => 'sometimes|boolean', 'is_force_https_enabled' => 'sometimes|boolean', + 'is_permanent_redirect_enabled' => 'sometimes|boolean', 'max_restart_count' => 'sometimes|integer|min:0', ]; diff --git a/app/Livewire/Project/Application/Domains.php b/app/Livewire/Project/Application/Domains.php index fe7f99aa3c..bd4768e260 100644 --- a/app/Livewire/Project/Application/Domains.php +++ b/app/Livewire/Project/Application/Domains.php @@ -37,6 +37,8 @@ class Domains extends Component public bool $isForceHttpsEnabled = true; + public bool $isPermanentRedirectEnabled = false; + /** * Per compose-service www/non-www redirect direction. * Keys are wire-safe (dots encoded) — use serviceRedirectWireKey(). @@ -154,6 +156,7 @@ class Domains extends Component 'editingRedirect' => 'string|required|in:both,www,non-www', 'redirect' => 'string|required|in:both,www,non-www', 'isForceHttpsEnabled' => 'boolean', + 'isPermanentRedirectEnabled' => 'boolean', 'serviceRedirects' => 'array', 'serviceRedirects.*' => 'string|in:both,www,non-www', ]; @@ -260,6 +263,18 @@ class Domains extends Component $this->dispatch('success', 'HTTP to HTTPS redirect updated.'); } + public function updatePermanentRedirect(): void + { + $this->authorize('update', $this->application); + $this->validateOnly('isPermanentRedirectEnabled'); + + $this->application->settings->is_permanent_redirect_enabled = $this->isPermanentRedirectEnabled; + $this->saveApplicationSettingsWithAudit($this->application); + $this->resetDefaultLabels(); + $this->dispatch('configurationChanged')->to(ConfigurationChecker::class); + $this->dispatch('success', 'Redirect type updated.'); + } + public function loadDomainState(bool $parseCompose = true): void { $this->application->refresh(); @@ -271,6 +286,7 @@ class Domains extends Component $this->redirect = $this->application->redirect ?? 'both'; } $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); + $this->isPermanentRedirectEnabled = $this->application->isPermanentRedirectEnabled(); $settings = instanceSettings(); $this->dnsValidationEnabled = (bool) data_get($settings, 'is_dns_validation_enabled', true); diff --git a/app/Livewire/Project/Service/Domains.php b/app/Livewire/Project/Service/Domains.php index 7349b63c27..ed56d83efd 100644 --- a/app/Livewire/Project/Service/Domains.php +++ b/app/Livewire/Project/Service/Domains.php @@ -45,6 +45,9 @@ class Domains extends Component /** @var array */ public array $forceHttpsRedirects = []; + /** @var array */ + public array $permanentRedirects = []; + /** Service application id when a pending domain conflict belongs to setServiceRedirect. */ public ?int $pendingRedirectServiceApplicationId = null; @@ -145,6 +148,8 @@ class Domains extends Component 'serviceRedirects.*' => 'string|in:both,www,non-www', 'forceHttpsRedirects' => 'array', 'forceHttpsRedirects.*' => 'boolean', + 'permanentRedirects' => 'array', + 'permanentRedirects.*' => 'boolean', ]; } @@ -239,6 +244,24 @@ class Domains extends Component $this->dispatch('success', 'HTTP to HTTPS redirect updated.'); } + public function updatePermanentRedirect(int $serviceApplicationId, bool $enabled): void + { + $application = $this->service->applications()->findOrFail($serviceApplicationId); + $this->authorize('update', $application); + + $this->permanentRedirects[$serviceApplicationId] = $enabled; + $this->validateOnly("permanentRedirects.{$serviceApplicationId}"); + + $application->is_permanent_redirect_enabled = $enabled; + $changedFields = auditChangedFields($application); + $application->save(); + $this->auditServiceApplicationUpdate($application, $changedFields); + $this->service->parse(); + $this->refreshDomains(); + $this->dispatch('configurationChanged')->to(ConfigurationChecker::class); + $this->dispatch('success', 'Redirect type updated.'); + } + public function loadDomainState(): void { $this->service->loadMissing(['applications', 'server']); @@ -267,6 +290,10 @@ class Domains extends Component ->mapWithKeys(fn (ServiceApplication $app) => [$app->id => $app->isForceHttpsEnabled()]) ->all(); + $this->permanentRedirects = $this->service->applications + ->mapWithKeys(fn (ServiceApplication $app) => [$app->id => $app->isPermanentRedirectEnabled()]) + ->all(); + $this->serviceApps = $this->service->applications ->sortBy(fn (ServiceApplication $app) => strtolower($app->human_name ?: $app->name)) ->values() diff --git a/app/Models/Application.php b/app/Models/Application.php index 2e455bc238..7a33b1d1eb 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -659,6 +659,11 @@ class Application extends BaseModel return data_get($this, 'settings.is_force_https_enabled', false); } + public function isPermanentRedirectEnabled(): bool + { + return (bool) data_get($this, 'settings.is_permanent_redirect_enabled', false); + } + public function isStripprefixEnabled() { return data_get($this, 'settings.is_stripprefix_enabled', true); diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 3c990a68b8..c024ea6c8c 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -31,6 +31,7 @@ use OpenApi\Attributes as OA; 'is_gzip_enabled' => ['type' => 'boolean'], 'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.'], 'is_stripprefix_enabled' => ['type' => 'boolean'], + 'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent (301/308) instead of temporary (302/307) proxy redirects.'], 'connect_to_docker_network' => ['type' => 'boolean'], 'custom_internal_name' => ['type' => 'string', 'nullable' => true], 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true], @@ -86,6 +87,7 @@ class ApplicationSetting extends Model 'is_consistent_container_name_enabled' => 'boolean', 'is_gzip_enabled' => 'boolean', 'is_stripprefix_enabled' => 'boolean', + 'is_permanent_redirect_enabled' => 'boolean', 'connect_to_docker_network' => 'boolean', 'is_env_sorting_enabled' => 'boolean', 'include_coolify_default_variables' => 'boolean', @@ -115,6 +117,7 @@ class ApplicationSetting extends Model 'is_gzip_enabled', 'traefik_certificate_resolver', 'is_stripprefix_enabled', + 'is_permanent_redirect_enabled', 'connect_to_docker_network', 'custom_internal_name', 'custom_container_name_prefix', diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index c6962347c0..e4d6282fe2 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -38,6 +38,7 @@ class ServiceApplication extends BaseModel 'is_gzip_enabled', 'is_stripprefix_enabled', 'is_force_https_enabled', + 'is_permanent_redirect_enabled', 'traefik_certificate_resolver', 'last_online_at', 'is_migrated', @@ -65,6 +66,7 @@ class ServiceApplication extends BaseModel 'domain_port_overrides' => 'array', 'noindex_domains' => 'array', 'is_force_https_enabled' => 'boolean', + 'is_permanent_redirect_enabled' => 'boolean', ]; } @@ -146,6 +148,11 @@ class ServiceApplication extends BaseModel return $this->is_force_https_enabled; } + public function isPermanentRedirectEnabled(): bool + { + return (bool) $this->is_permanent_redirect_enabled; + } + public function type() { return 'service'; diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 57ed374085..6a72c37ba7 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -205,6 +205,7 @@ function sharedDataApplications(?Application $application = null): array 'is_pr_deployments_public_enabled' => 'boolean', 'is_gzip_enabled' => 'boolean', 'is_stripprefix_enabled' => 'boolean', + 'is_permanent_redirect_enabled' => 'boolean', 'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN], 'is_raw_compose_deployment_enabled' => 'boolean', 'is_log_drain_enabled' => 'boolean', @@ -478,6 +479,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('docker_images_to_keep'); $request->offsetUnset('is_gzip_enabled'); $request->offsetUnset('is_stripprefix_enabled'); + $request->offsetUnset('is_permanent_redirect_enabled'); $request->offsetUnset('traefik_certificate_resolver'); $request->offsetUnset('is_raw_compose_deployment_enabled'); $request->offsetUnset('is_log_drain_enabled'); diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 894d4b1ec4..b2fed22add 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -774,7 +774,7 @@ function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool ->contains($normalize($domain)); } -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', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false) +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', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false, bool $is_permanent_redirect_enabled = false) { $labels = collect([]); if ($serviceLabels) { @@ -833,11 +833,12 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_{$loop}.encode=zstd gzip"); } $redirect_schema = $is_force_https_enabled ? $schema : '{scheme}'; + $redirect_code = $is_permanent_redirect_enabled ? ' permanent' : ''; if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { - $labels->push("caddy_{$loop}.redir={$redirect_schema}://www.{$host}{uri}"); + $labels->push("caddy_{$loop}.redir={$redirect_schema}://www.{$host}{uri}{$redirect_code}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { - $labels->push("caddy_{$loop}.redir={$redirect_schema}://{$host_without_www}{uri}"); + $labels->push("caddy_{$loop}.redir={$redirect_schema}://{$host_without_www}{uri}{$redirect_code}"); } if ($is_http_basic_auth_enabled) { // Caddy 2.8 renamed basicauth to basic_auth; see Server::caddySupportsBasicAuthDirective(). @@ -928,7 +929,7 @@ function traefikHostRulesWithoutIpv6Brackets(Collection $labels): Collection ) : $label); } -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', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?string $certificate_resolver = 'letsencrypt') +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', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?string $certificate_resolver = 'letsencrypt', bool $is_permanent_redirect_enabled = false) { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -938,6 +939,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push('traefik.http.middlewares.gzip-sse.compress.excludedcontenttypes=text/event-stream'); } $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); + // Every container declares the shared middleware, so a permanent variant needs its own name. + $redirect_to_https_name = 'redirect-to-https'; + if ($is_permanent_redirect_enabled) { + $redirect_to_https_name = 'redirect-to-https-permanent'; + $labels->push('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.scheme=https'); + $labels->push('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true'); + } + $redirect_permanent = $is_permanent_redirect_enabled ? 'true' : 'false'; $is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; $http_basic_auth_label = "http-basic-auth-{$uuid}"; @@ -1020,12 +1029,12 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $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={$redirect_capture_prefix}{1}://{$redirect_capture_prefix}{2}", - "traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent={$redirect_permanent}", ]; $redirect_to_www = [ "traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)", "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://www.{$redirect_capture_prefix}{2}", - "traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false", + "traefik.http.middlewares.{$to_www_name}.redirectregex.permanent={$redirect_permanent}", ]; if ($schema === 'https') { // Set labels for https @@ -1114,7 +1123,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_noindex) { $httpMiddlewares->push($noindex_name); } - $httpMiddlewares->push('redirect-to-https'); + $httpMiddlewares->push($redirect_to_https_name); } else { $httpMiddlewares = $middlewares; } @@ -1226,6 +1235,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), redirect_direction: $application->redirect, @@ -1244,6 +1254,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), redirect_direction: $application->redirect, @@ -1264,6 +1275,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), redirect_direction: $application->redirect, @@ -1281,6 +1293,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), redirect_direction: $application->redirect, @@ -1312,6 +1325,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, @@ -1330,6 +1344,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, @@ -1349,6 +1364,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, @@ -1365,6 +1381,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 66330d9d09..8493a26bd1 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -2133,6 +2133,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int uuid: $labelUuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -2151,6 +2152,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int uuid: $labelUuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -2171,6 +2173,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int uuid: $labelUuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -2187,6 +2190,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int uuid: $labelUuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -3429,6 +3433,7 @@ function serviceParser(Service $resource): Collection uuid: $uuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -3447,6 +3452,7 @@ function serviceParser(Service $resource): Collection uuid: $uuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -3467,6 +3473,7 @@ function serviceParser(Service $resource): Collection uuid: $uuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), @@ -3483,6 +3490,7 @@ function serviceParser(Service $resource): Collection uuid: $uuid, domains: $fqdns, is_force_https_enabled: $originalResource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 36722b1cd9..b4b92e5a9e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3513,6 +3513,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal uuid: $resource->uuid, domains: $fqdns, is_force_https_enabled: $savedService->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), @@ -3531,6 +3532,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal uuid: $resource->uuid, domains: $fqdns, is_force_https_enabled: $savedService->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), @@ -3551,6 +3553,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal uuid: $resource->uuid, domains: $fqdns, is_force_https_enabled: $savedService->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), @@ -3567,6 +3570,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal uuid: $resource->uuid, domains: $fqdns, is_force_https_enabled: $savedService->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(), serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), @@ -4318,6 +4322,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal generate_unique_uuid: $resource->build_pack === 'dockercompose', image: data_get($service, 'image'), is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), onlyPort: $onlyPort, @@ -4338,6 +4343,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal image: data_get($service, 'image'), service_name: $serviceName, is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), onlyPort: $onlyPort, @@ -4359,6 +4365,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal generate_unique_uuid: $resource->build_pack === 'dockercompose', image: data_get($service, 'image'), is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), onlyPort: $onlyPort, @@ -4377,6 +4384,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal image: data_get($service, 'image'), service_name: $serviceName, is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), onlyPort: $onlyPort, diff --git a/database/migrations/2026_10_19_191912_add_is_permanent_redirect_enabled_to_redirect_settings.php b/database/migrations/2026_10_19_191912_add_is_permanent_redirect_enabled_to_redirect_settings.php new file mode 100644 index 0000000000..946623d31c --- /dev/null +++ b/database/migrations/2026_10_19_191912_add_is_permanent_redirect_enabled_to_redirect_settings.php @@ -0,0 +1,34 @@ +boolean('is_permanent_redirect_enabled')->default(false); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->boolean('is_permanent_redirect_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_permanent_redirect_enabled'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->dropColumn('is_permanent_redirect_enabled'); + }); + } +}; diff --git a/openapi.json b/openapi.json index 28c0ed18db..f837ab1987 100644 --- a/openapi.json +++ b/openapi.json @@ -914,6 +914,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -1513,6 +1517,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -2112,6 +2120,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -2606,6 +2618,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -3080,6 +3096,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -3786,6 +3806,10 @@ "type": "boolean", "description": "Enable path prefix stripping." }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false." + }, "traefik_certificate_resolver": { "type": "string", "nullable": true, @@ -18175,6 +18199,13 @@ "null" ] }, + "is_permanent_redirect_enabled": { + "description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, "max_restart_count": { "description": "Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.", "type": [ @@ -24105,6 +24136,10 @@ "is_stripprefix_enabled": { "type": "boolean" }, + "is_permanent_redirect_enabled": { + "type": "boolean", + "description": "Use permanent (301\/308) instead of temporary (302\/307) proxy redirects." + }, "connect_to_docker_network": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index 71c6507b97..4b952e70ea 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -594,6 +594,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -986,6 +989,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -1378,6 +1384,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -1711,6 +1720,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -2030,6 +2042,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -2493,6 +2508,9 @@ paths: is_stripprefix_enabled: type: boolean description: 'Enable path prefix stripping.' + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' traefik_certificate_resolver: type: string nullable: true @@ -11451,6 +11469,9 @@ paths: type: [boolean, 'null'] is_force_https_enabled: type: [boolean, 'null'] + is_permanent_redirect_enabled: + description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.' + type: [boolean, 'null'] max_restart_count: description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.' type: [integer, 'null'] @@ -15373,6 +15394,9 @@ components: description: 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.' is_stripprefix_enabled: type: boolean + is_permanent_redirect_enabled: + type: boolean + description: 'Use permanent (301/308) instead of temporary (302/307) proxy redirects.' connect_to_docker_network: type: boolean custom_internal_name: diff --git a/resources/views/livewire/project/application/domains.blade.php b/resources/views/livewire/project/application/domains.blade.php index 10f1abf7d5..db316c36f1 100644 --- a/resources/views/livewire/project/application/domains.blade.php +++ b/resources/views/livewire/project/application/domains.blade.php @@ -166,16 +166,28 @@ - @if ($hasHttpsDomains && ! $labelsAreWritable) + @if ($hasRows && ! $labelsAreWritable)
- - -
- Redirect HTTP to HTTPS + +
+ +
+ @endif + + +
+
diff --git a/resources/views/livewire/project/service/domains.blade.php b/resources/views/livewire/project/service/domains.blade.php index 543e1c1318..e40ab95145 100644 --- a/resources/views/livewire/project/service/domains.blade.php +++ b/resources/views/livewire/project/service/domains.blade.php @@ -199,6 +199,18 @@ ]" :disabled="! auth()->user()->can('update', $service)" />
@endif +
+ + + +
diff --git a/tests/Feature/ApplicationDomainsTest.php b/tests/Feature/ApplicationDomainsTest.php index e157269ef8..c0b7a599f3 100644 --- a/tests/Feature/ApplicationDomainsTest.php +++ b/tests/Feature/ApplicationDomainsTest.php @@ -669,6 +669,22 @@ it('shows the HTTP redirect control for HTTPS domains and persists changes', fun expect($this->application->settings->fresh()->is_force_https_enabled)->toBeFalse(); }); +it('persists the redirect type and regenerates permanent redirect labels', function () { + $this->application->update(['fqdn' => 'https://app.example.com']); + + Livewire::test(Domains::class, ['application' => $this->application->fresh()]) + ->assertSet('isPermanentRedirectEnabled', false) + ->assertSee('Redirect type') + ->set('isPermanentRedirectEnabled', true) + ->call('updatePermanentRedirect') + ->assertHasNoErrors(); + + $application = $this->application->fresh(); + + expect($application->settings->is_permanent_redirect_enabled)->toBeTrue() + ->and(base64_decode($application->custom_labels))->toContain('middlewares=redirect-to-https-permanent'); +}); + it('hides the HTTP redirect control for HTTP-only domains', function () { $this->application->update(['fqdn' => 'http://app.example.com']); diff --git a/tests/Feature/ServiceDomainsTest.php b/tests/Feature/ServiceDomainsTest.php index 99b1793318..e1893de8bc 100644 --- a/tests/Feature/ServiceDomainsTest.php +++ b/tests/Feature/ServiceDomainsTest.php @@ -260,6 +260,17 @@ it('shows and persists the HTTP redirect control for HTTPS service applications' expect($this->service->fresh()->docker_compose)->not->toContain('middlewares=redirect-to-https'); }); +it('persists the redirect type for service applications', function () { + Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])]) + ->assertSet("permanentRedirects.{$this->apiApp->id}", false) + ->assertSee('Redirect type') + ->call('updatePermanentRedirect', $this->apiApp->id, true) + ->assertHasNoErrors(); + + expect($this->apiApp->fresh()->is_permanent_redirect_enabled)->toBeTrue() + ->and($this->service->fresh()->docker_compose)->toContain('middlewares=redirect-to-https-permanent'); +}); + it('hides the HTTP redirect control for HTTP-only service applications', function () { $this->apiApp->update(['fqdn' => 'http://api.example.com']); diff --git a/tests/Unit/PermanentRedirectLabelsTest.php b/tests/Unit/PermanentRedirectLabelsTest.php new file mode 100644 index 0000000000..0ca8114a27 --- /dev/null +++ b/tests/Unit/PermanentRedirectLabelsTest.php @@ -0,0 +1,55 @@ +values()->all(); +} + +function permanentRedirectCaddyLabels(bool $permanent): array +{ + return fqdnLabelsForCaddy( + network: 'testnetwork', + uuid: 'testuuid', + domains: collect(['https://example.com', 'https://www.example.com']), + is_force_https_enabled: true, + onlyPort: 3000, + redirect_direction: 'www', + is_permanent_redirect_enabled: $permanent, + )->values()->all(); +} + +test('Traefik redirects are temporary by default', function () { + $labels = permanentRedirectTraefikLabels(false); + + expect($labels) + ->toContain('traefik.http.middlewares.0-testuuid-to-www.redirectregex.permanent=false') + ->toContain('traefik.http.routers.http-0-testuuid.middlewares=redirect-to-https') + ->not->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true'); +}); + +test('Traefik redirects are permanent when enabled and keep the shared HTTPS middleware unchanged', function () { + $labels = permanentRedirectTraefikLabels(true); + + expect($labels) + ->toContain('traefik.http.middlewares.0-testuuid-to-www.redirectregex.permanent=true') + ->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.scheme=https') + ->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true') + ->toContain('traefik.http.routers.http-0-testuuid.middlewares=redirect-to-https-permanent') + ->toContain('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https') + ->not->toContain('traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true'); +}); + +test('Caddy www redirect uses the default temporary code unless permanent is enabled', function () { + expect(permanentRedirectCaddyLabels(false))->toContain('caddy_0.redir=https://www.example.com{uri}') + ->and(permanentRedirectCaddyLabels(true))->toContain('caddy_0.redir=https://www.example.com{uri} permanent'); +});