mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(ui): load no favicon from domains behind HTTP basic auth
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6c270a42db
commit
465344f1bc
7 files changed
+70
-5
No files matched your search
@@ -10,6 +10,7 @@ use App\Traits\ReleasesManagedDnsRecords;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
|
||||
class ServiceApplication extends BaseModel
|
||||
{
|
||||
@@ -357,6 +358,33 @@ class ServiceApplication extends BaseModel
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proxy labels of this container in the Compose file add HTTP basic auth
|
||||
* (a Traefik `basicauth` middleware or a Caddy `basic_auth` directive).
|
||||
*/
|
||||
public function hasProxyBasicAuth(): bool
|
||||
{
|
||||
try {
|
||||
$dockerCompose = parseDockerComposeYaml((string) data_get($this->service, 'docker_compose_raw'));
|
||||
} catch (ParseException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$labels = $dockerCompose['services'][$this->name]['labels'] ?? [];
|
||||
if (! is_array($labels)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($labels as $key => $value) {
|
||||
$label = is_int($key) ? (string) $value : (string) $key;
|
||||
if (preg_match('/basic_?auth/i', $label) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required port for this service application.
|
||||
* Extracts port from SERVICE_URL_* or SERVICE_FQDN_* environment variables
|
||||
|
||||
@@ -55,7 +55,8 @@ function resource_logo_url(string $name): string
|
||||
* the service template, or how the application is built. Null when there is no specific logo.
|
||||
*
|
||||
* Applications with a domain also get the favicon URL of their first domain. The browser loads it
|
||||
* and shows it in place of the logo when it loads. Coolify itself never requests it.
|
||||
* and shows it in place of the logo when it loads. Coolify itself never requests it. Applications
|
||||
* behind HTTP basic auth get no favicon: the request makes the browser ask for the credentials.
|
||||
*
|
||||
* @return array{src: string, dark: ?string, fallback: ?string, padded: bool, favicon: ?string}|null
|
||||
*/
|
||||
@@ -93,7 +94,9 @@ function resource_logo(Model $resource): ?array
|
||||
default => $resourceLogo('public-repo'),
|
||||
};
|
||||
|
||||
return [...$logo, 'favicon' => resource_favicon_url($resource->fqdn)];
|
||||
$favicon = $resource->is_http_basic_auth_enabled ? null : resource_favicon_url($resource->fqdn);
|
||||
|
||||
return [...$logo, 'favicon' => $favicon];
|
||||
}
|
||||
|
||||
if (! method_exists($resource, 'type')) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
};
|
||||
$gridClass = 'service-domains-overview-grid';
|
||||
$publicUrl = getFqdnWithoutPort($row['url']);
|
||||
$faviconUrl = $isSuggested ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$faviconUrl = $isSuggested || $application->is_http_basic_auth_enabled ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$rowDirection = $isCompose
|
||||
? ($serviceRedirects[$this->serviceRedirectWireKey($row['service'])] ?? 'both')
|
||||
: $redirect;
|
||||
|
||||
@@ -45,14 +45,15 @@
|
||||
? \Illuminate\Support\Str::headline($row['service_name'])
|
||||
: '-';
|
||||
$publicUrl = getFqdnWithoutPort($row['url']);
|
||||
$isNoindexed = $service->applications->firstWhere('id', $row['service_application_id'])?->isDomainNoindexed($row['url']);
|
||||
$serviceApplication = $service->applications->firstWhere('id', $row['service_application_id']);
|
||||
$isNoindexed = $serviceApplication?->isDomainNoindexed($row['url']);
|
||||
$rowDirection = $serviceRedirects[$row['service_application_id']] ?? 'both';
|
||||
$directionLabel = match ($rowDirection) {
|
||||
'www' => 'Redirect to www',
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Both www and non-www',
|
||||
};
|
||||
$faviconUrl = $isSuggested ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$faviconUrl = $isSuggested || $serviceApplication?->hasProxyBasicAuth() ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$domainKey = hash('sha256', $row['url'].'|'.($row['service_application_id'] ?? ''));
|
||||
$editingParts = \App\Support\DomainUrlParts::split($row['url']);
|
||||
if ($row['has_port_override'] ?? false) {
|
||||
|
||||
@@ -655,6 +655,14 @@ it('lists existing domains as individual rows', function () {
|
||||
expect(substr_count($html, 'this.$wire.updateRedirect('))->toBe(0);
|
||||
});
|
||||
|
||||
it('loads no domain favicon when HTTP basic auth protects the application', function () {
|
||||
$this->application->update(['fqdn' => 'https://example.com', 'is_http_basic_auth_enabled' => true]);
|
||||
|
||||
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
|
||||
->assertSee('https://example.com')
|
||||
->assertDontSee('https://example.com/favicon.ico', false);
|
||||
});
|
||||
|
||||
it('shows the HTTP redirect control for HTTPS domains and persists changes', function () {
|
||||
$this->application->update(['fqdn' => 'https://app.example.com']);
|
||||
|
||||
|
||||
@@ -63,6 +63,16 @@ test('applications with a domain get the favicon of their first domain', functio
|
||||
['https://*.example.com', null],
|
||||
]);
|
||||
|
||||
test('applications behind HTTP basic auth get no favicon, so the browser does not ask for credentials', function () {
|
||||
$application = (new Application)->forceFill([
|
||||
'build_pack' => 'nixpacks',
|
||||
'fqdn' => 'https://app.example.com',
|
||||
'is_http_basic_auth_enabled' => true,
|
||||
]);
|
||||
|
||||
expect(resource_logo($application)['favicon'])->toBeNull();
|
||||
});
|
||||
|
||||
test('resource logo URLs carry the file modification time, so browsers load changed logos', function () {
|
||||
$modifiedAt = filemtime(public_path('svgs/resources/docker-image.svg'));
|
||||
|
||||
|
||||
@@ -164,6 +164,21 @@ it('groups configured domains and shows redirect settings in the table', functio
|
||||
->and(substr_count($html, "id=\"service-domain-group-{$this->apiApp->id}\""))->toBe(1);
|
||||
});
|
||||
|
||||
it('loads no domain favicon when proxy labels add HTTP basic auth to the service', function (string $labels) {
|
||||
$this->service->update([
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n api:\n image: node:alpine\n labels:\n{$labels}",
|
||||
]);
|
||||
|
||||
$html = Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
|
||||
->assertSee('https://api.example.com')
|
||||
->html();
|
||||
|
||||
expect($html)->not->toContain('https://api.example.com/favicon.ico');
|
||||
})->with([
|
||||
'traefik list' => [" - traefik.http.middlewares.auth.basicauth.users=admin:hash\n"],
|
||||
'caddy map' => [" caddy.basic_auth.admin: hash\n"],
|
||||
]);
|
||||
|
||||
it('removes consecutive service domains by stable row identity after indexes change', function () {
|
||||
$this->apiApp->update([
|
||||
'fqdn' => 'https://first.example.com,https://second.example.com,https://third.example.com',
|
||||
|
||||
Reference in new issue
Block a user