Merge pull request #6476 from coollabsio/next

v4.0.0-beta.425
This commit is contained in:
Andras Bacsai
2025-08-28 11:02:54 +02:00
committed by GitHub
30 changed files with 501 additions and 136 deletions

View File

@@ -15,17 +15,15 @@ class CanAccessTerminal
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (! auth()->check()) {
abort(401, 'Authentication required');
}
// Only admins/owners can access terminal functionality
if (! auth()->user()->can('canAccessTerminal')) {
abort(403, 'Access to terminal functionality is restricted to team administrators');
}
return $next($request); return $next($request);
// if (! auth()->check()) {
// abort(401, 'Authentication required');
// }
// // Only admins/owners can access terminal functionality
// if (! auth()->user()->can('canAccessTerminal')) {
// abort(403, 'Access to terminal functionality is restricted to team administrators');
// }
// return $next($request);
} }
} }

View File

@@ -51,9 +51,16 @@ class General extends Component
public $parsedServiceDomains = []; public $parsedServiceDomains = [];
public $domainConflicts = [];
public $showDomainConflictModal = false;
public $forceSaveDomains = false;
protected $listeners = [ protected $listeners = [
'resetDefaultLabels', 'resetDefaultLabels',
'configurationChanged' => '$refresh', 'configurationChanged' => '$refresh',
'confirmDomainUsage',
]; ];
protected function rules(): array protected function rules(): array
@@ -430,7 +437,7 @@ class General extends Component
$server = data_get($this->application, 'destination.server'); $server = data_get($this->application, 'destination.server');
if ($server) { if ($server) {
$fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version); $fqdn = generateUrl(server: $server, random: $this->application->uuid);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->resetDefaultLabels(); $this->resetDefaultLabels();
@@ -485,10 +492,33 @@ class General extends Component
} }
} }
} }
check_domain_usage(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->application->fqdn = $domains->implode(',');
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
} }
return true;
}
public function confirmDomainUsage()
{
$this->forceSaveDomains = true;
$this->showDomainConflictModal = false;
$this->submit();
} }
public function setRedirect() public function setRedirect()
@@ -536,7 +566,9 @@ class General extends Component
$this->application->parseHealthcheckFromDockerfile($this->application->dockerfile); $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(); $this->application->save();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { 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
} }
} }
} }
check_domain_usage(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->application->save();
$this->resetDefaultLabels(); $this->resetDefaultLabels();
} }

View File

@@ -25,6 +25,14 @@ class Previews extends Component
public int $rate_limit_remaining; public int $rate_limit_remaining;
public $domainConflicts = [];
public $showDomainConflictModal = false;
public $forceSaveDomains = false;
public $pendingPreviewId = null;
protected $rules = [ protected $rules = [
'application.previews.*.fqdn' => 'string|nullable', '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) public function save_preview($preview_id)
{ {
try { try {
@@ -63,7 +81,20 @@ class Previews extends Component
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false; $success = false;
} }
check_domain_usage(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) { if (! $preview) {

View File

@@ -60,7 +60,7 @@ class PreviewsCompose extends Component
$random = new Cuid2; $random = new Cuid2;
// Generate a unique domain like main app services do // Generate a unique domain like main app services do
$generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version); $generated_fqdn = generateUrl(server: $server, random: $random);
$preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn);

View File

@@ -133,7 +133,7 @@ class CloneMe extends Component
$uuid = (string) new Cuid2; $uuid = (string) new Cuid2;
$url = $application->fqdn; $url = $application->fqdn;
if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version); $url = generateUrl(server: $this->server, random: $uuid);
} }
$newApplication = $application->replicate([ $newApplication = $application->replicate([

View File

@@ -60,7 +60,7 @@ class DockerImage extends Component
'health_check_enabled' => false, 'health_check_enabled' => false,
]); ]);
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->update([ $application->update([
'name' => 'docker-image-'.$application->uuid, 'name' => 'docker-image-'.$application->uuid,
'fqdn' => $fqdn, 'fqdn' => $fqdn,

View File

@@ -208,7 +208,7 @@ class GithubPrivateRepository extends Component
$application['docker_compose_location'] = $this->docker_compose_location; $application['docker_compose_location'] = $this->docker_compose_location;
$application['base_directory'] = $this->base_directory; $application['base_directory'] = $this->base_directory;
} }
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid); $application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid);

View File

@@ -194,7 +194,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->name = generate_random_name($application->uuid); $application->name = generate_random_name($application->uuid);
$application->save(); $application->save();

View File

@@ -373,7 +373,7 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->isStatic; $application->settings->is_static = $this->isStatic;
$application->settings->save(); $application->settings->save();
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->save(); $application->save();
if ($this->checkCoolifyConfig) { if ($this->checkCoolifyConfig) {

View File

@@ -68,7 +68,7 @@ CMD ["nginx", "-g", "daemon off;"]
'source_type' => GithubApp::class, 'source_type' => GithubApp::class,
]); ]);
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->update([ $application->update([
'name' => 'dockerfile-'.$application->uuid, 'name' => 'dockerfile-'.$application->uuid,
'fqdn' => $fqdn, 'fqdn' => $fqdn,

View File

@@ -12,6 +12,12 @@ class EditDomain extends Component
public ServiceApplication $application; public ServiceApplication $application;
public $domainConflicts = [];
public $showDomainConflictModal = false;
public $forceSaveDomains = false;
protected $rules = [ protected $rules = [
'application.fqdn' => 'nullable', 'application.fqdn' => 'nullable',
'application.required_fqdn' => 'required|boolean', 'application.required_fqdn' => 'required|boolean',
@@ -22,6 +28,13 @@ class EditDomain extends Component
$this->application = ServiceApplication::find($this->applicationId); $this->application = ServiceApplication::find($this->applicationId);
} }
public function confirmDomainUsage()
{
$this->forceSaveDomains = true;
$this->showDomainConflictModal = false;
$this->submit();
}
public function submit() public function submit()
{ {
try { try {
@@ -37,7 +50,20 @@ class EditDomain extends Component
if ($warning) { if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain')); $this->dispatch('warning', __('warning.sslipdomain'));
} }
check_domain_usage(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->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application); updateCompose($this->application);

View File

@@ -23,6 +23,12 @@ class ServiceApplicationView extends Component
public $delete_volumes = true; public $delete_volumes = true;
public $domainConflicts = [];
public $showDomainConflictModal = false;
public $forceSaveDomains = false;
protected $rules = [ protected $rules = [
'application.human_name' => 'nullable', 'application.human_name' => 'nullable',
'application.description' => '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() public function submit()
{ {
try { try {
@@ -145,7 +158,20 @@ class ServiceApplicationView extends Component
if ($warning) { if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain')); $this->dispatch('warning', __('warning.sslipdomain'));
} }
check_domain_usage(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->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application); updateCompose($this->application);

View File

@@ -33,9 +33,6 @@ class ExecuteContainerCommand extends Component
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->containers = collect(); $this->containers = collect();
$this->servers = collect(); $this->servers = collect();

View File

@@ -66,7 +66,7 @@ class ResourceOperations extends Component
$url = $this->resource->fqdn; $url = $this->resource->fqdn;
if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version); $url = generateUrl(server: $server, random: $uuid);
} }
$new_resource = $this->resource->replicate([ $new_resource = $this->resource->replicate([

View File

@@ -35,6 +35,12 @@ class Index extends Component
#[Validate('required|string|timezone')] #[Validate('required|string|timezone')]
public string $instance_timezone; public string $instance_timezone;
public array $domainConflicts = [];
public bool $showDomainConflictModal = false;
public bool $forceSaveDomains = false;
public function render() public function render()
{ {
return view('livewire.settings.index'); 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() public function submit()
{ {
try { try {
@@ -108,7 +121,18 @@ class Index extends Component
} }
} }
if ($this->fqdn) { if ($this->fqdn) {
check_domain_usage(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); $this->instantSave(isSave: false);

View File

@@ -74,7 +74,7 @@ class ApplicationPreview extends BaseModel
public function generate_preview_fqdn() public function generate_preview_fqdn()
{ {
if (empty($this->fqdn) && $this->application->fqdn) { if ($this->application->fqdn) {
if (str($this->application->fqdn)->contains(',')) { if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]); $preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);

View File

@@ -28,7 +28,8 @@ class ServerPolicy
*/ */
public function create(User $user): bool public function create(User $user): bool
{ {
return $user->isAdmin(); // return $user->isAdmin();
return true;
} }
/** /**
@@ -36,7 +37,8 @@ class ServerPolicy
*/ */
public function update(User $user, Server $server): bool public function update(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
return true;
} }
/** /**
@@ -44,7 +46,8 @@ class ServerPolicy
*/ */
public function delete(User $user, Server $server): bool public function delete(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
return true;
} }
/** /**
@@ -68,7 +71,8 @@ class ServerPolicy
*/ */
public function manageProxy(User $user, Server $server): bool public function manageProxy(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
return true;
} }
/** /**
@@ -76,7 +80,8 @@ class ServerPolicy
*/ */
public function manageSentinel(User $user, Server $server): bool public function manageSentinel(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
return true;
} }
/** /**
@@ -84,15 +89,8 @@ class ServerPolicy
*/ */
public function manageCaCertificate(User $user, Server $server): bool public function manageCaCertificate(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
} return true;
/**
* Determine whether the user can view terminal.
*/
public function viewTerminal(User $user, Server $server): bool
{
return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
} }
/** /**
@@ -100,6 +98,7 @@ class ServerPolicy
*/ */
public function viewSecurity(User $user, Server $server): bool public function viewSecurity(User $user, Server $server): bool
{ {
return $user->isAdmin() && $user->teams->contains('id', $server->team_id); // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
return true;
} }
} }

View File

@@ -67,8 +67,7 @@ class AuthServiceProvider extends ServiceProvider
// Register gate for terminal access // Register gate for terminal access
Gate::define('canAccessTerminal', function ($user) { Gate::define('canAccessTerminal', function ($user) {
// return $user->isAdmin() || $user->isOwner(); return $user->isAdmin() || $user->isOwner();
return true;
}); });
} }
} }

View File

@@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) { if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) {
$MINIO_BROWSER_REDIRECT_URL->update([ $MINIO_BROWSER_REDIRECT_URL->update([
'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), 'value' => generateUrl(server: $server, random: 'console-'.$uuid, forceHttps: true),
]); ]);
} }
if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) { if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) {
$MINIO_SERVER_URL->update([ $MINIO_SERVER_URL->update([
'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->service->compose_parsing_version, forceHttps: true), 'value' => generateUrl(server: $server, random: 'minio-'.$uuid, forceHttps: true),
]); ]);
} }
$payload = collect([ $payload = collect([
@@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) { if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) {
$LOGTO_ENDPOINT->update([ $LOGTO_ENDPOINT->update([
'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->service->compose_parsing_version), 'value' => generateUrl(server: $server, random: 'logto-'.$uuid),
]); ]);
} }
if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) { if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) {
$LOGTO_ADMIN_ENDPOINT->update([ $LOGTO_ADMIN_ENDPOINT->update([
'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->service->compose_parsing_version), 'value' => generateUrl(server: $server, random: 'logto-admin-'.$uuid),
]); ]);
} }
$payload = collect([ $payload = collect([

View File

@@ -0,0 +1,139 @@
<?php
use App\Models\Application;
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');
$domains = collect($domains);
} else {
$domains = collect($resource->fqdns);
}
} elseif ($domain) {
$domains = collect([$domain]);
} else {
return ['conflicts' => [], 'hasConflicts' => false];
}
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
return str($domain);
});
// 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) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
$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) {
$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}'",
];
}
}
}
}
// 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) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
$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) {
$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')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$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,
];
}

View File

@@ -1157,79 +1157,6 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
} }
} }
} }
function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
if ($resource) {
if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
$domains = collect($domains);
} else {
$domains = collect($resource->fqdns);
}
} elseif ($domain) {
$domains = collect($domain);
} else {
throw new \RuntimeException('No resource or FQDN provided.');
}
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
return str($domain);
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
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: <br><br>Link: <a class='underline' target='_blank' href='{$app->link()}'>{$app->name}</a>");
}
} elseif ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->link()}'>{$app->name}</a>");
}
}
}
}
$apps = ServiceApplication::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
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: <br><br>Link: <a class='underline' target='_blank' href='{$app->service->link()}'>{$app->service->name}</a>");
}
} elseif ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->service->link()}'>{$app->service->name}</a>");
}
}
}
}
if ($resource) {
$settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
}
}
}
}
function parseCommandsByLineForSudo(Collection $commands, Server $server): array function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{ {

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.424', 'version' => '4.0.0-beta.425',
'helper_version' => '1.0.10', 'helper_version' => '1.0.10',
'realtime_version' => '1.0.10', 'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),

View File

@@ -0,0 +1,91 @@
@props([
'conflicts' => [],
'showModal' => false,
'confirmAction' => 'confirmDomainUsage',
])
@if ($showModal && count($conflicts) > 0)
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
@keydown.escape.window="modalOpen = false; $wire.set('showDomainConflictModal', false)"
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex justify-between items-center pb-3">
<h2 class="pr-8 font-bold">Domain Already In Use</h2>
<button @click="modalOpen = false; $wire.set('showDomainConflictModal', false)"
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-red-600" role="alert">
<p class="font-bold">Warning: Domain Conflict Detected</p>
<p>{{ $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.' }}
</p>
</div>
<div class="mb-4">
<h4 class="mb-2 font-semibold">Conflicting Resources:</h4>
<ul class="space-y-2">
@foreach ($conflicts as $conflict)
<li class="flex items-start text-red-500">
<div>
<strong>{{ $conflict['domain'] }}</strong> is used by
@if ($conflict['resource_type'] === 'instance')
<strong>{{ $conflict['resource_name'] }}</strong>
@else
<a href="{{ $conflict['resource_link'] }}" target="_blank"
class="underline hover:text-red-400">
{{ $conflict['resource_name'] }}
</a>
@endif
({{ $conflict['resource_type'] }})
</div>
</li>
@endforeach
</ul>
</div>
<div class="p-4 mb-4 text-yellow-800 dark:text-yellow-200 border-l-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg"
role="alert">
<p class="font-bold">What will happen if you continue?</p>
@if (isset($consequences))
{{ $consequences }}
@else
<ul class="mt-2 ml-4 list-disc">
<li>Only one resource will be accessible at this domain</li>
<li>The routing behavior will be unpredictable</li>
<li>You may experience service disruptions</li>
<li>SSL certificates might not work correctly</li>
</ul>
@endif
</div>
<div class="flex flex-wrap gap-2 justify-between mt-4">
<x-forms.button @click="modalOpen = false; $wire.set('showDomainConflictModal', false)"
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
Cancel
</x-forms.button>
<x-forms.button wire:click="{{ $confirmAction }}" @click="modalOpen = false" class="w-auto"
isError>
I understand, proceed anyway
</x-forms.button>
</div>
</div>
</div>
</div>
</template>
</div>
@endif

View File

@@ -462,6 +462,12 @@
</div> </div>
</div> </div>
</form> </form>
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage" />
@script @script
<script> <script>
$wire.$on('loadCompose', (isInit = true) => { $wire.$on('loadCompose', (isInit = true) => {

View File

@@ -219,4 +219,19 @@
@endforeach @endforeach
</div> </div>
@endif @endif
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
The preview deployment domain is already in use by other resources. Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>The preview deployment may not be accessible</li>
<li>Conflicts with production or other preview deployments</li>
<li>SSL certificates might not work correctly</li>
<li>Unpredictable routing behavior</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
</div> </div>

View File

@@ -54,7 +54,7 @@
@if ($environment->isEmpty()) @if ($environment->isEmpty())
@can('createAnyResource') @can('createAnyResource')
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add Resource</a>
@else @else
<div <div
class="flex flex-col items-center justify-center p-8 text-center border border-dashed border-neutral-300 dark:border-coolgray-300 rounded-lg"> class="flex flex-col items-center justify-center p-8 text-center border border-dashed border-neutral-300 dark:border-coolgray-300 rounded-lg">

View File

@@ -1,7 +1,21 @@
<div class="w-full">
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2"> <form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
<div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom <div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom
domain, you can add it with a port.</div> domain, you can add it with a port.</div>
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains" id="application.fqdn" <x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button> <x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form> </form>
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal" confirmAction="confirmDomainUsage">
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>Only one service will be accessible at this domain</li>
<li>The routing behavior will be unpredictable</li>
<li>You may experience service disruptions</li>
<li>SSL certificates might not work correctly</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
</div>

View File

@@ -67,4 +67,18 @@
instantSave="instantSaveAdvanced" id="application.is_log_drain_enabled" label="Drain Logs" /> instantSave="instantSaveAdvanced" id="application.is_log_drain_enabled" label="Drain Logs" />
</div> </div>
</form> </form>
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>Only one service will be accessible at this domain</li>
<li>The routing behavior will be unpredictable</li>
<li>You may experience service disruptions</li>
<li>SSL certificates might not work correctly</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
</div> </div>

View File

@@ -79,5 +79,19 @@
</div> </div>
</div> </div>
</form> </form>
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>The Coolify instance domain will conflict with existing resources</li>
<li>SSL certificates might not work correctly</li>
<li>Routing behavior will be unpredictable</li>
<li>You may not be able to access the Coolify dashboard properly</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.424" "version": "4.0.0-beta.425"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.425" "version": "4.0.0-beta.426"
}, },
"helper": { "helper": {
"version": "1.0.10" "version": "1.0.10"