diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 67731c87d..aa72b7c5f 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -51,9 +51,16 @@ class General extends Component
public $parsedServiceDomains = [];
+ public $domainConflicts = [];
+
+ public $showDomainConflictModal = false;
+
+ public $forceSaveDomains = false;
+
protected $listeners = [
'resetDefaultLabels',
'configurationChanged' => '$refresh',
+ 'confirmDomainUsage',
];
protected function rules(): array
@@ -485,10 +492,33 @@ class General extends Component
}
}
}
- checkDomainUsage(resource: $this->application);
+
+ // Check for domain conflicts if not forcing save
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(resource: $this->application);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+
+ return false;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
+
$this->application->fqdn = $domains->implode(',');
$this->resetDefaultLabels(false);
}
+
+ return true;
+ }
+
+ public function confirmDomainUsage()
+ {
+ $this->forceSaveDomains = true;
+ $this->showDomainConflictModal = false;
+ $this->submit();
}
public function setRedirect()
@@ -536,7 +566,9 @@ class General extends Component
$this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
}
- $this->checkFqdns();
+ if (! $this->checkFqdns()) {
+ return; // Stop if there are conflicts and user hasn't confirmed
+ }
$this->application->save();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
@@ -588,7 +620,20 @@ class General extends Component
}
}
}
- checkDomainUsage(resource: $this->application);
+ // Check for domain conflicts if not forcing save
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(resource: $this->application);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+
+ return;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
+
$this->application->save();
$this->resetDefaultLabels();
}
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index cc3c5ea46..ebfd84489 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -25,6 +25,14 @@ class Previews extends Component
public int $rate_limit_remaining;
+ public $domainConflicts = [];
+
+ public $showDomainConflictModal = false;
+
+ public $forceSaveDomains = false;
+
+ public $pendingPreviewId = null;
+
protected $rules = [
'application.previews.*.fqdn' => 'string|nullable',
];
@@ -49,6 +57,16 @@ class Previews extends Component
}
}
+ public function confirmDomainUsage()
+ {
+ $this->forceSaveDomains = true;
+ $this->showDomainConflictModal = false;
+ if ($this->pendingPreviewId) {
+ $this->save_preview($this->pendingPreviewId);
+ $this->pendingPreviewId = null;
+ }
+ }
+
public function save_preview($preview_id)
{
try {
@@ -63,7 +81,20 @@ class Previews extends Component
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.
$preview->fqdn->{$this->application->destination->server->ip}
Check this documentation for further help.");
$success = false;
}
- checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
+ // Check for domain conflicts if not forcing save
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+ $this->pendingPreviewId = $preview_id;
+
+ return;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
}
if (! $preview) {
diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php
index 1b24dc23a..5ce170b99 100644
--- a/app/Livewire/Project/Service/EditDomain.php
+++ b/app/Livewire/Project/Service/EditDomain.php
@@ -12,6 +12,12 @@ class EditDomain extends Component
public ServiceApplication $application;
+ public $domainConflicts = [];
+
+ public $showDomainConflictModal = false;
+
+ public $forceSaveDomains = false;
+
protected $rules = [
'application.fqdn' => 'nullable',
'application.required_fqdn' => 'required|boolean',
@@ -22,6 +28,13 @@ class EditDomain extends Component
$this->application = ServiceApplication::find($this->applicationId);
}
+ public function confirmDomainUsage()
+ {
+ $this->forceSaveDomains = true;
+ $this->showDomainConflictModal = false;
+ $this->submit();
+ }
+
public function submit()
{
try {
@@ -37,7 +50,20 @@ class EditDomain extends Component
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
- checkDomainUsage(resource: $this->application);
+ // Check for domain conflicts if not forcing save
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(resource: $this->application);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+
+ return;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
+
$this->validate();
$this->application->save();
updateCompose($this->application);
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index 80fe59891..3ac12cfe9 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -23,6 +23,12 @@ class ServiceApplicationView extends Component
public $delete_volumes = true;
+ public $domainConflicts = [];
+
+ public $showDomainConflictModal = false;
+
+ public $forceSaveDomains = false;
+
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -129,6 +135,13 @@ class ServiceApplicationView extends Component
}
}
+ public function confirmDomainUsage()
+ {
+ $this->forceSaveDomains = true;
+ $this->showDomainConflictModal = false;
+ $this->submit();
+ }
+
public function submit()
{
try {
@@ -145,7 +158,20 @@ class ServiceApplicationView extends Component
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
- checkDomainUsage(resource: $this->application);
+ // Check for domain conflicts if not forcing save
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(resource: $this->application);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+
+ return;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
+
$this->validate();
$this->application->save();
updateCompose($this->application);
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 6833492a6..02062e1f7 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -33,9 +33,6 @@ class ExecuteContainerCommand extends Component
public function mount()
{
- if (! auth()->user()->isAdmin()) {
- abort(403);
- }
$this->parameters = get_route_parameters();
$this->containers = collect();
$this->servers = collect();
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 98e5ce8bd..d05433082 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -35,6 +35,12 @@ class Index extends Component
#[Validate('required|string|timezone')]
public string $instance_timezone;
+ public array $domainConflicts = [];
+
+ public bool $showDomainConflictModal = false;
+
+ public bool $forceSaveDomains = false;
+
public function render()
{
return view('livewire.settings.index');
@@ -81,6 +87,13 @@ class Index extends Component
}
}
+ public function confirmDomainUsage()
+ {
+ $this->forceSaveDomains = true;
+ $this->showDomainConflictModal = false;
+ $this->submit();
+ }
+
public function submit()
{
try {
@@ -108,7 +121,18 @@ class Index extends Component
}
}
if ($this->fqdn) {
- checkDomainUsage(domain: $this->fqdn);
+ if (! $this->forceSaveDomains) {
+ $result = checkDomainUsage(domain: $this->fqdn);
+ if ($result['hasConflicts']) {
+ $this->domainConflicts = $result['conflicts'];
+ $this->showDomainConflictModal = true;
+
+ return;
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceSaveDomains = false;
+ }
}
$this->instantSave(isSave: false);
diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php
index 36e044344..b562873b5 100644
--- a/bootstrap/helpers/domains.php
+++ b/bootstrap/helpers/domains.php
@@ -5,6 +5,14 @@ use App\Models\ServiceApplication;
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
+ $conflicts = [];
+
+ // Get the current team for filtering
+ $currentTeam = null;
+ if ($resource) {
+ $currentTeam = $resource->team();
+ }
+
if ($resource) {
if ($resource->getMorphClass() === Application::class && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
@@ -15,8 +23,9 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null,
} elseif ($domain) {
$domains = collect([$domain]);
} else {
- throw new \RuntimeException('No resource or FQDN provided.');
+ return ['conflicts' => [], 'hasConflicts' => false];
}
+
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
@@ -24,7 +33,15 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null,
return str($domain);
});
- $apps = Application::all();
+
+ // Filter applications by team if we have a current team
+ $appsQuery = Application::query();
+ if ($currentTeam) {
+ $appsQuery = $appsQuery->whereHas('environment.project', function ($query) use ($currentTeam) {
+ $query->where('team_id', $currentTeam->id);
+ });
+ }
+ $apps = $appsQuery->get();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
@@ -35,15 +52,35 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null,
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
+ $conflicts[] = [
+ 'domain' => $naked_domain,
+ 'resource_name' => $app->name,
+ 'resource_link' => $app->link(),
+ 'resource_type' => 'application',
+ 'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
+ ];
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
+ $conflicts[] = [
+ 'domain' => $naked_domain,
+ 'resource_name' => $app->name,
+ 'resource_link' => $app->link(),
+ 'resource_type' => 'application',
+ 'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
+ ];
}
}
}
}
- $apps = ServiceApplication::all();
+
+ // Filter service applications by team if we have a current team
+ $serviceAppsQuery = ServiceApplication::query();
+ if ($currentTeam) {
+ $serviceAppsQuery = $serviceAppsQuery->whereHas('service.environment.project', function ($query) use ($currentTeam) {
+ $query->where('team_id', $currentTeam->id);
+ });
+ }
+ $apps = $serviceAppsQuery->get();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
@@ -54,14 +91,27 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null,
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->service->name}");
+ $conflicts[] = [
+ 'domain' => $naked_domain,
+ 'resource_name' => $app->service->name,
+ 'resource_link' => $app->service->link(),
+ 'resource_type' => 'service',
+ 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'",
+ ];
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->service->name}");
+ $conflicts[] = [
+ 'domain' => $naked_domain,
+ 'resource_name' => $app->service->name,
+ 'resource_link' => $app->service->link(),
+ 'resource_type' => 'service',
+ 'message' => "Domain $naked_domain is already in use by service '{$app->service->name}'",
+ ];
}
}
}
}
+
if ($resource) {
$settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
@@ -71,8 +121,19 @@ function checkDomainUsage(ServiceApplication|Application|null $resource = null,
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
- throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
+ $conflicts[] = [
+ 'domain' => $naked_domain,
+ 'resource_name' => 'Coolify Instance',
+ 'resource_link' => '#',
+ 'resource_type' => 'instance',
+ 'message' => "Domain $naked_domain is already in use by this Coolify instance",
+ ];
}
}
}
+
+ return [
+ 'conflicts' => $conflicts,
+ 'hasConflicts' => count($conflicts) > 0,
+ ];
}
diff --git a/resources/views/components/domain-conflict-modal.blade.php b/resources/views/components/domain-conflict-modal.blade.php
new file mode 100644
index 000000000..218a7ef16
--- /dev/null
+++ b/resources/views/components/domain-conflict-modal.blade.php
@@ -0,0 +1,91 @@
+@props([
+ 'conflicts' => [],
+ 'showModal' => false,
+ 'confirmAction' => 'confirmDomainUsage',
+])
+
+@if ($showModal && count($conflicts) > 0)
+
Warning: Domain Conflict Detected
+{{ $slot ?? 'The following domain(s) are already in use by other resources. Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' }} +
+What will happen if you continue?
+ @if (isset($consequences)) + {{ $consequences }} + @else +