diff --git a/.dockerignore b/.dockerignore index 2eba3cb46..0adca0b32 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,3 +24,4 @@ yarn-error.log /.ssh .ignition.json .env.dusk.local +docker/coolify-realtime/node_modules diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index 7017b4897..79dfc3fc0 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -11,7 +11,7 @@ on: - docker/coolify-helper/Dockerfile - docker/coolify-realtime/Dockerfile - docker/testing-host/Dockerfile - - templates/service-templates.json + - templates/* env: GITHUB_REGISTRY: ghcr.io @@ -41,7 +41,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -76,7 +76,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index 7e937d17a..ef247170f 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -8,6 +8,7 @@ on: - docker/coolify-realtime/Dockerfile - docker/coolify-realtime/terminal-server.js - docker/coolify-realtime/package.json + - docker/coolify-realtime/package-lock.json - docker/coolify-realtime/soketi-entrypoint.sh env: diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 97bfd52eb..9654a21b0 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -8,6 +8,7 @@ on: - docker/coolify-realtime/Dockerfile - docker/coolify-realtime/terminal-server.js - docker/coolify-realtime/package.json + - docker/coolify-realtime/package-lock.json - docker/coolify-realtime/soketi-entrypoint.sh env: diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index 6e4d4adc3..1aafc2f0b 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -11,7 +11,7 @@ on: - docker/coolify-helper/Dockerfile - docker/coolify-realtime/Dockerfile - docker/testing-host/Dockerfile - - templates/service-templates.json + - templates/* env: GITHUB_REGISTRY: ghcr.io diff --git a/.gitignore b/.gitignore index 1a021ab3e..dd6b141b9 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ _ide_helper_models.php scripts/load-test/* .ignition.json .env.dusk.local +docker/coolify-realtime/node_modules diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 51c5a1f9e..981b81378 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -9,6 +9,7 @@ use App\Jobs\ApplicationDeploymentJob; use App\Models\Server; use Illuminate\Process\ProcessResult; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; use Spatie\Activitylog\Models\Activity; @@ -124,6 +125,7 @@ class RunRemoteProcess ])); } } catch (\Throwable $e) { + Log::error('Error calling event: '.$e->getMessage()); } } diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 1e8616416..13667e829 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -99,8 +99,8 @@ class StartClickhouse } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php index 73b2f9ac0..869a88521 100644 --- a/app/Actions/Database/StartDatabase.php +++ b/app/Actions/Database/StartDatabase.php @@ -49,7 +49,7 @@ class StartDatabase break; } if ($database->is_public && $database->public_port) { - StartDatabaseProxy::dispatch($database); + StartDatabaseProxy::dispatch($database)->onQueue('high'); } return $activity; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 6274737a1..c72714e1c 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -96,8 +96,8 @@ class StartDragonfly } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 734b28322..bd98258ab 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -107,8 +107,8 @@ class StartKeydb } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index aa6b79126..696dd7ff4 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -101,8 +101,8 @@ class StartMariadb } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 8d0a6b3ca..26a0f82d0 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -25,6 +25,10 @@ class StartMongodb $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + if (isDev()) { + $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + } + $this->commands = [ "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", @@ -117,8 +121,8 @@ class StartMongodb ]; // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 0339501b0..a3694648f 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -101,8 +101,8 @@ class StartMysql } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 5c956acbc..f5e85087f 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -122,8 +122,8 @@ class StartPostgresql ]; } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index e445a246e..7a2d2b34d 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -110,8 +110,8 @@ class StartRedis } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 9e9a62170..0a166d24a 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,7 +2,7 @@ namespace App\Actions\Database; -use App\Events\DatabaseStatusChanged; +use App\Events\DatabaseProxyStopped; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -27,7 +27,11 @@ class StopDatabaseProxy $server = data_get($database, 'service.server'); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); + + $database->is_public = false; $database->save(); - DatabaseStatusChanged::dispatch(); + + DatabaseProxyStopped::dispatch(); + } } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 6c1a53f3a..a08056837 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -30,7 +30,7 @@ class GetContainersStatus $this->containerReplicates = $containerReplicates; $this->server = $server; if (! $this->server->isFunctional()) { - return 'Server is not ready.'; + return 'Server is not functional.'; } $this->applications = $this->server->applications(); $skip_these_applications = collect([]); @@ -107,6 +107,8 @@ class GetContainersStatus $statusFromDb = $preview->status; if ($statusFromDb !== $containerStatus) { $preview->update(['status' => $containerStatus]); + } else { + $preview->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -118,6 +120,8 @@ class GetContainersStatus $statusFromDb = $application->status; if ($statusFromDb !== $containerStatus) { $application->update(['status' => $containerStatus]); + } else { + $application->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -160,7 +164,10 @@ class GetContainersStatus $statusFromDb = $database->status; if ($statusFromDb !== $containerStatus) { $database->update(['status' => $containerStatus]); + } else { + $database->update(['last_online_at' => now()]); } + if ($isPublic) { $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { @@ -171,7 +178,7 @@ class GetContainersStatus })->first(); if (! $foundTcpProxy) { StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); } } } else { @@ -202,6 +209,8 @@ class GetContainersStatus if ($statusFromDb !== $containerStatus) { // ray('Updating status: ' . $containerStatus); $service->update(['status' => $containerStatus]); + } else { + $service->update(['last_online_at' => now()]); } } } diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 9f97dd0d4..ea2befd3a 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -40,7 +40,7 @@ class CreateNewUser implements CreatesNewUsers $user = User::create([ 'id' => 0, 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); @@ -52,7 +52,7 @@ class CreateNewUser implements CreatesNewUsers } else { $user = User::create([ 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index d64804758..51303d87a 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -4,6 +4,7 @@ namespace App\Actions\Proxy; use App\Enums\ProxyTypes; use App\Models\Server; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -88,6 +89,7 @@ class CheckProxy $portsToCheck = []; } } catch (\Exception $e) { + Log::error('Error checking proxy: '.$e->getMessage()); } if (count($portsToCheck) === 0) { return false; diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index fd4dd150c..ba6c23ffc 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -12,11 +12,11 @@ class InstallDocker public function handle(Server $server) { + $dockerVersion = config('constants.docker_install_version'); $supported_os_type = $server->validateOS(); if (! $supported_os_type) { throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - $dockerVersion = '26.0'; $config = base64_encode('{ "log-driver": "json-file", "log-opts": { diff --git a/app/Actions/Server/ResourcesCheck.php b/app/Actions/Server/ResourcesCheck.php new file mode 100644 index 000000000..e6b90ba38 --- /dev/null +++ b/app/Actions/Server/ResourcesCheck.php @@ -0,0 +1,41 @@ +subSeconds($seconds))->update(['status' => 'exited']); + ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php new file mode 100644 index 000000000..1dae03fd9 --- /dev/null +++ b/app/Actions/Server/ServerCheck.php @@ -0,0 +1,269 @@ +server = $server; + try { + if ($this->server->isFunctional() === false) { + return 'Server is not functional.'; + } + + if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { + + if (isset($data)) { + $data = collect($data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + $containerReplicates = null; + $this->isSentinel = true; + + } else { + ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); + // ServerStorageCheckJob::dispatch($this->server); + } + + if (is_null($this->containers)) { + return 'No containers found.'; + } + + if (isset($containerReplicates)) { + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + $this->checkContainers(); + + if ($this->server->isSentinelEnabled() && $this->isSentinel === false) { + CheckAndStartSentinelJob::dispatch($this->server); + } + + if ($this->server->isLogDrainEnabled()) { + $this->checkLogDrainContainer(); + } + + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } else { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } + + private function checkContainers() + { + foreach ($this->containers as $container) { + if ($this->isSentinel) { + $labels = Arr::undot(data_get($container, 'labels')); + } else { + if ($this->server->isSwarm()) { + $labels = Arr::undot(data_get($container, 'Spec.Labels')); + } else { + $labels = Arr::undot(data_get($container, 'Config.Labels')); + } + + } + $managed = data_get($labels, 'coolify.managed'); + if (! $managed) { + continue; + } + $uuid = data_get($labels, 'coolify.name'); + if (! $uuid) { + $uuid = data_get($labels, 'com.docker.compose.service'); + } + + if ($this->isSentinel) { + $containerStatus = data_get($container, 'state'); + $containerHealth = data_get($container, 'health_status'); + } else { + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + } + $containerStatus = "$containerStatus ($containerHealth)"; + + $applicationId = data_get($labels, 'coolify.applicationId'); + $serviceId = data_get($labels, 'coolify.serviceId'); + $databaseId = data_get($labels, 'coolify.databaseId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + + if ($applicationId) { + // Application + if ($pullRequestId != 0) { + if (str($applicationId)->contains('-')) { + $applicationId = str($applicationId)->before('-'); + } + $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + if ($preview) { + $preview->update(['status' => $containerStatus]); + } + } else { + $application = Application::where('id', $applicationId)->first(); + if ($application) { + $application->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + } + } + } elseif (isset($serviceId)) { + // Service + $subType = data_get($labels, 'coolify.service.subType'); + $subId = data_get($labels, 'coolify.service.subId'); + $service = Service::where('id', $serviceId)->first(); + if (! $service) { + continue; + } + if ($subType === 'application') { + $service = ServiceApplication::where('id', $subId)->first(); + } else { + $service = ServiceDatabase::where('id', $subId)->first(); + } + if ($service) { + $service->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + if ($subType === 'database') { + $isPublic = data_get($service, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($service); + } + } + } + } + } else { + // Database + if (is_null($this->databases)) { + $this->databases = $this->server->databases(); + } + $database = $this->databases->where('uuid', $uuid)->first(); + if ($database) { + $database->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + + $isPublic = data_get($database, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($database); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } + } + } + } + } + } +} diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 9c4b0349c..9b87454da 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -4,6 +4,7 @@ namespace App\Actions\Service; use App\Actions\Server\CleanupDocker; use App\Models\Service; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; class DeleteService @@ -39,7 +40,8 @@ class DeleteService if (! empty($commands)) { foreach ($commands as $command) { $result = instant_remote_process([$command], $server, false); - if ($result !== 0) { + if ($result !== null && $result !== 0) { + Log::error('Error deleting volumes: '.$result); } } } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index ccb864e1f..c802fb116 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -10,6 +10,7 @@ use App\Models\Environment; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use App\Models\User; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; @@ -38,9 +39,10 @@ class Init extends Command } // Backward compatibility - $this->disable_metrics(); + // $this->disable_metrics(); $this->replace_slash_in_environment_name(); $this->restore_coolify_db_backup(); + $this->update_user_emails(); // $this->update_traefik_labels(); if (! isCloud() || $this->option('force-cloud')) { @@ -78,17 +80,26 @@ class Init extends Command } } - private function disable_metrics() + // private function disable_metrics() + // { + // if (version_compare('4.0.0-beta.312', config('version'), '<=')) { + // foreach ($this->servers as $server) { + // if ($server->settings->is_metrics_enabled === true) { + // $server->settings->update(['is_metrics_enabled' => false]); + // } + // if ($server->isFunctional()) { + // StopSentinel::dispatch($server)->onQueue('high'); + // } + // } + // } + // } + + private function update_user_emails() { - if (version_compare('4.0.0-beta.312', config('version'), '<=')) { - foreach ($this->servers as $server) { - if ($server->settings->is_metrics_enabled === true) { - $server->settings->update(['is_metrics_enabled' => false]); - } - if ($server->isFunctional()) { - StopSentinel::dispatch($server); - } - } + try { + User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)])); + } catch (\Throwable $e) { + echo "Error in updating user emails: {$e->getMessage()}\n"; } } diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b5a74166a..1e5d5808c 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -96,7 +96,7 @@ class ServicesDelete extends Command if (! $confirmed) { break; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -122,7 +122,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -148,7 +148,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } diff --git a/app/Console/Commands/Weird.php b/app/Console/Commands/Weird.php new file mode 100644 index 000000000..e471a5f96 --- /dev/null +++ b/app/Console/Commands/Weird.php @@ -0,0 +1,58 @@ +error('This command can only be run in development mode'); + + return; + } + $run = $this->option('run'); + if ($run) { + $servers = Server::all(); + foreach ($servers as $server) { + ServerCheck::dispatch($server); + } + + return; + } + $number = $this->option('number'); + for ($i = 0; $i < $number; $i++) { + $uuid = Str::uuid(); + $server = Server::create([ + 'name' => 'localhost-'.$uuid, + 'description' => 'This is a test docker container in development mode', + 'ip' => 'coolify-testing-host', + 'team_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::NONE->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); + $server->settings->update([ + 'is_usable' => true, + 'is_reachable' => true, + ]); + } + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7533e8932..3fb4de60b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,6 +13,7 @@ use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; use App\Jobs\ServerCleanupMux; +use App\Jobs\ServerStorageCheckJob; use App\Jobs\UpdateCoolifyJob; use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; @@ -29,11 +30,17 @@ class Kernel extends ConsoleKernel private InstanceSettings $settings; + private string $updateCheckFrequency; + + private string $instanceTimezone; + protected function schedule(Schedule $schedule): void { - $this->allServers = Server::where('ip', '!=', '1.2.3.4')->get(); + $this->allServers = Server::where('ip', '!=', '1.2.3.4'); $this->settings = instanceSettings(); + $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *'; + $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -41,25 +48,30 @@ class Kernel extends ConsoleKernel // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); + $schedule->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); + // Server Jobs - $this->checkScheduledBackups($schedule); $this->checkResources($schedule); + + $this->checkScheduledBackups($schedule); $this->checkScheduledTasks($schedule); + $schedule->command('uploads:clear')->everyTwoMinutes(); - $schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer(); + $schedule->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $this->scheduleUpdates($schedule); // Server Jobs - $this->checkScheduledBackups($schedule); $this->checkResources($schedule); + $this->pullImages($schedule); + + $this->checkScheduledBackups($schedule); $this->checkScheduledTasks($schedule); $schedule->command('cleanup:database --yes')->daily(); @@ -69,33 +81,32 @@ class Kernel extends ConsoleKernel private function pullImages($schedule): void { - $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true); + $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { $schedule->job(function () use ($server) { CheckAndStartSentinelJob::dispatch($server); - })->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer(); + })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); } } $schedule->job(new CheckHelperImageJob) - ->cron($this->settings->update_check_frequency) - ->timezone($this->settings->instance_timezone) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) ->onOneServer(); } private function scheduleUpdates($schedule): void { - $updateCheckFrequency = $this->settings->update_check_frequency; $schedule->job(new CheckForUpdatesJob) - ->cron($updateCheckFrequency) - ->timezone($this->settings->instance_timezone) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) ->onOneServer(); if ($this->settings->is_auto_update_enabled) { $autoUpdateFrequency = $this->settings->auto_update_frequency; $schedule->job(new UpdateCoolifyJob) ->cron($autoUpdateFrequency) - ->timezone($this->settings->instance_timezone) + ->timezone($this->instanceTimezone) ->onOneServer(); } } @@ -103,23 +114,32 @@ class Kernel extends ConsoleKernel private function checkResources($schedule): void { if (isCloud()) { - $servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false); + $servers = $this->allServers->whereHas('team.subscription')->get(); $own = Team::find(0)->servers; $servers = $servers->merge($own); } else { - $servers = $this->allServers; + $servers = $this->allServers->get(); } + foreach ($servers as $server) { - $lastSentinelUpdate = $server->sentinel_updated_at; $serverTimezone = $server->settings->server_timezone; + + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Check container status every minute if Sentinel does not activated $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); + + // Check storage usage every 10 minutes if Sentinel does not activated + $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); } if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); } else { $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer(); } + // Cleanup multiplexed connections every hour $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer(); @@ -134,14 +154,11 @@ class Kernel extends ConsoleKernel private function checkScheduledBackups($schedule): void { - $scheduled_backups = ScheduledDatabaseBackup::all(); + $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); if ($scheduled_backups->isEmpty()) { return; } foreach ($scheduled_backups as $scheduled_backup) { - if (! $scheduled_backup->enabled) { - continue; - } if (is_null(data_get($scheduled_backup, 'database'))) { $scheduled_backup->delete(); @@ -150,30 +167,26 @@ class Kernel extends ConsoleKernel $server = $scheduled_backup->server(); - if (! $server) { + if (is_null($server)) { continue; } - $serverTimezone = $server->settings->server_timezone; if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } $schedule->job(new DatabaseBackupJob( backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); + ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } private function checkScheduledTasks($schedule): void { - $scheduled_tasks = ScheduledTask::all(); + $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); if ($scheduled_tasks->isEmpty()) { return; } foreach ($scheduled_tasks as $scheduled_task) { - if ($scheduled_task->enabled === false) { - continue; - } $service = $scheduled_task->service; $application = $scheduled_task->application; @@ -197,14 +210,13 @@ class Kernel extends ConsoleKernel if (! $server) { continue; } - $serverTimezone = $server->settings->server_timezone ?: config('app.timezone'); if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; } $schedule->job(new ScheduledTaskJob( task: $scheduled_task - ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); + ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php new file mode 100644 index 000000000..b457dc6a0 --- /dev/null +++ b/app/Events/DatabaseProxyStopped.php @@ -0,0 +1,35 @@ +currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php index a94bc2272..913b21bc2 100644 --- a/app/Events/DatabaseStatusChanged.php +++ b/app/Events/DatabaseStatusChanged.php @@ -7,27 +7,29 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class DatabaseStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public ?string $userId = null; + public $userId = null; public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { return false; } + $this->userId = $userId; } public function broadcastOn(): ?array { - if ($this->userId) { + if (! is_null($this->userId)) { return [ new PrivateChannel("user.{$this->userId}"), ]; diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php new file mode 100644 index 000000000..c8b5547f6 --- /dev/null +++ b/app/Events/ScheduledTaskDone.php @@ -0,0 +1,34 @@ +user()->currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index a86a8b02d..3950022e1 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -7,6 +7,7 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class ServiceStatusChanged implements ShouldBroadcast { @@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { return false; diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index dee75578f..f0eeb56d8 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1203,7 +1203,7 @@ class ApplicationsController extends Controller $service->name = "service-$service->uuid"; $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); } return response()->json(serializeApiResponse([ @@ -1358,7 +1358,7 @@ class ApplicationsController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Application deletion request queued.', @@ -2482,7 +2482,7 @@ class ApplicationsController extends Controller if (! $application) { return response()->json(['message' => 'Application not found.'], 404); } - StopApplication::dispatch($application); + StopApplication::dispatch($application)->onQueue('high'); return response()->json( [ diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ce658d2a2..eaa542a83 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -497,9 +497,9 @@ class DatabasesController extends Controller $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { - StartDatabaseProxy::dispatch($database); + StartDatabaseProxy::dispatch($database)->onQueue('high'); } elseif ($whatToDoWithDatabaseProxy === 'stop') { - StopDatabaseProxy::dispatch($database); + StopDatabaseProxy::dispatch($database)->onQueue('high'); } return response()->json([ @@ -1151,7 +1151,7 @@ class DatabasesController extends Controller } $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); $payload = [ @@ -1206,7 +1206,7 @@ class DatabasesController extends Controller } $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1264,7 +1264,7 @@ class DatabasesController extends Controller } $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1320,7 +1320,7 @@ class DatabasesController extends Controller } $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1357,7 +1357,7 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } return response()->json(serializeApiResponse([ @@ -1406,7 +1406,7 @@ class DatabasesController extends Controller } $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1442,7 +1442,7 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1500,7 +1500,7 @@ class DatabasesController extends Controller } $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1593,7 +1593,7 @@ class DatabasesController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Database deletion request queued.', @@ -1666,7 +1666,7 @@ class DatabasesController extends Controller if (str($database->status)->contains('running')) { return response()->json(['message' => 'Database is already running.'], 400); } - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); return response()->json( [ @@ -1742,7 +1742,7 @@ class DatabasesController extends Controller if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { return response()->json(['message' => 'Database is already stopped.'], 400); } - StopDatabase::dispatch($database); + StopDatabase::dispatch($database)->onQueue('high'); return response()->json( [ @@ -1815,7 +1815,7 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } - RestartDatabase::dispatch($database); + RestartDatabase::dispatch($database)->onQueue('high'); return response()->json( [ diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 666dc55a5..59b199d87 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -307,7 +307,7 @@ class DeployController extends Controller break; default: // Database resource - StartDatabase::dispatch($resource); + StartDatabase::dispatch($resource)->onQueue('high'); $resource->update([ 'started_at' => now(), ]); diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 491179d5d..b69028b70 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -422,7 +422,7 @@ class ProjectController extends Controller if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } - if ($project->resource_count() > 0) { + if (! $project->isEmpty()) { return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400); } diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index af4e008ef..024ef35fa 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -538,7 +538,7 @@ class ServersController extends Controller 'is_build_server' => $request->is_build_server, ]); if ($request->instant_validate) { - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); } return response()->json([ @@ -651,7 +651,7 @@ class ServersController extends Controller ]); } if ($request->instant_validate) { - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); } return response()->json(serializeApiResponse($server))->setStatusCode(201); @@ -787,7 +787,7 @@ class ServersController extends Controller if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); return response()->json(['message' => 'Validation started.']); } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index bf90322e2..bdb5612ad 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -342,7 +342,7 @@ class ServicesController extends Controller } $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); } $domains = $service->applications()->get()->pluck('fqdn')->sort(); $domains = $domains->map(function ($domain) { @@ -487,7 +487,7 @@ class ServicesController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Service deletion request queued.', @@ -1076,7 +1076,7 @@ class ServicesController extends Controller if (str($service->status())->contains('running')) { return response()->json(['message' => 'Service is already running.'], 400); } - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); return response()->json( [ @@ -1154,7 +1154,7 @@ class ServicesController extends Controller if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { return response()->json(['message' => 'Service is already stopped.'], 400); } - StopService::dispatch($service); + StopService::dispatch($service)->onQueue('high'); return response()->json( [ @@ -1229,7 +1229,7 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - RestartService::dispatch($service); + RestartService::dispatch($service)->onQueue('high'); return response()->json( [ diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 581118e16..9f1e4eeb8 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -110,13 +110,19 @@ class Controller extends BaseController return redirect()->route('login')->with('error', 'Invalid credentials.'); } - public function accept_invitation() + public function acceptInvitation() { $resetPassword = request()->query('reset-password'); $invitationUuid = request()->route('uuid'); + $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); $user = User::whereEmail($invitation->email)->firstOrFail(); + + if (Auth::id() !== $user->id) { + abort(400, 'You are not allowed to accept this invitation.'); + } $invitationValid = $invitation->isValid(); + if ($invitationValid) { if ($resetPassword) { $user->update([ @@ -131,14 +137,12 @@ class Controller extends BaseController } $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $invitation->delete(); - if (auth()->user()?->id !== $user->id) { - return redirect()->route('login'); - } + refreshSession($invitation->team); return redirect()->route('team.index'); } else { - abort(401); + abort(400, 'Invitation expired.'); } } @@ -146,10 +150,10 @@ class Controller extends BaseController { $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(auth()->user())) { + if (is_null(Auth::user())) { return redirect()->route('login'); } - if (auth()->user()->id !== $user->id) { + if (Auth::id() !== $user->id) { abort(401); } $invitation->delete(); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c80b4a7db..5ceed332a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -230,7 +230,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - if (! $this->server->isFunctional()) { + if ($this->server->isFunctional() === false) { $this->application_deployment_queue->addLogEntry('Server is not functional.'); $this->fail('Server is not functional.'); @@ -1836,7 +1836,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->pull_request_id === 0) { - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { if (! $this->application->settings->custom_internal_name) { $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index acbe82338..84f14ed02 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -3,14 +3,15 @@ namespace App\Jobs; use App\Models\TeamInvitation; -use App\Models\Waitlist; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { @@ -18,34 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho public function __construct() {} - // public function uniqueId(): string - // { - // return $this->container_name; - // } + public function middleware(): array + { + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; + } public function handle(): void { try { - // $this->cleanup_waitlist(); + $this->cleanupInvitationLink(); } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - } - try { - $this->cleanup_invitation_link(); - } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); + Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); } } - private function cleanup_waitlist() - { - $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get(); - foreach ($waitlist as $item) { - $item->delete(); - } - } - - private function cleanup_invitation_link() + private function cleanupInvitationLink() { $invitation = TeamInvitation::all(); foreach ($invitation as $item) { diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 900bae99c..0d7e63dd2 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; @@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?string $usageBefore = null; + public function middleware(): array + { + return [(new WithoutOverlapping($this->server->id))->dontRelease()]; + } + public function __construct(public Server $server, public bool $manualCleanup = false) {} public function handle(): void diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 24f8d1e6b..9822ca071 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -360,7 +360,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue private function checkLogDrainContainer() { if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { - StartLogDrain::dispatch($this->server); + StartLogDrain::dispatch($this->server)->onQueue('high'); } } } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 827641e8c..7bfc29af3 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Events\ScheduledTaskDone; use App\Models\Application; use App\Models\ScheduledTask; use App\Models\ScheduledTaskExecution; @@ -19,7 +20,7 @@ class ScheduledTaskJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public ?Team $team = null; + public Team $team; public Server $server; @@ -47,7 +48,7 @@ class ScheduledTaskJob implements ShouldQueue } else { throw new \RuntimeException('ScheduledTaskJob failed: No resource found.'); } - $this->team = Team::find($task->team_id); + $this->team = Team::findOrFail($task->team_id); $this->server_timezone = $this->getServerTimezone(); } @@ -125,6 +126,7 @@ class ScheduledTaskJob implements ShouldQueue // send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage()); throw $e; } finally { + ScheduledTaskDone::dispatch($this->team->id); } } } diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index f949f4ec0..c584f493d 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -13,6 +13,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue @@ -25,6 +26,11 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public $containers; + public function middleware(): array + { + return [(new WithoutOverlapping($this->server->id))->dontRelease()]; + } + public function __construct(public Server $server) {} public function handle() @@ -39,7 +45,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue if (is_null($this->containers)) { return 'No containers found.'; } - ServerStorageCheckJob::dispatch($this->server); GetContainersStatus::run($this->server, $this->containers, $containerReplicates); if ($this->server->isSentinelEnabled()) { @@ -89,10 +94,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue if ($foundLogDrainContainer) { $status = data_get($foundLogDrainContainer, 'State.Status'); if ($status !== 'running') { - StartLogDrain::dispatch($this->server); + StartLogDrain::dispatch($this->server)->onQueue('high'); } } else { - StartLogDrain::dispatch($this->server); + StartLogDrain::dispatch($this->server)->onQueue('high'); } } } diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php new file mode 100644 index 000000000..3e8e60a31 --- /dev/null +++ b/app/Jobs/ServerCheckNewJob.php @@ -0,0 +1,34 @@ +server); + ResourcesCheck::dispatch($this->server); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 084b6bf81..aa82c6dad 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -30,8 +30,7 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue try { $servers = $this->team->servers; $servers_count = $servers->count(); - $limit = data_get($this->team->limits, 'serverLimit', 2); - $number_of_servers_to_disable = $servers_count - $limit; + $number_of_servers_to_disable = $servers_count - $this->team->limits; if ($number_of_servers_to_disable > 0) { $servers = $servers->sortbyDesc('created_at'); $servers_to_disable = $servers->take($number_of_servers_to_disable); diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index cc838c77f..0723ffcee 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -30,8 +30,8 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue public function handle() { try { - if (! $this->server->isFunctional()) { - return 'Server is not ready.'; + if ($this->server->isFunctional() === false) { + return 'Server is not functional.'; } $team = data_get($this->server, 'team'); $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold'); diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 16cd9152e..2579c3db2 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -3,16 +3,21 @@ namespace App\Livewire\Admin; use App\Models\User; +use Illuminate\Container\Attributes\Auth as AttributesAuth; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Livewire\Component; class Index extends Component { - public $active_subscribers = []; + public int $activeSubscribers; - public $inactive_subscribers = []; + public int $inactiveSubscribers; - public $search = ''; + public Collection $foundUsers; + + public string $search = ''; public function mount() { @@ -20,7 +25,7 @@ class Index extends Component return redirect()->route('dashboard'); } - if (auth()->user()->id !== 0) { + if (Auth::id() !== 0) { return redirect()->route('dashboard'); } $this->getSubscribers(); @@ -29,50 +34,32 @@ class Index extends Component public function submitSearch() { if ($this->search !== '') { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { + $this->foundUsers = User::where(function ($query) { $query->where('name', 'like', "%{$this->search}%") ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { - $query->where('name', 'like', "%{$this->search}%") - ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - } else { - $this->getSubscribers(); + })->get(); } } public function getSubscribers() { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { + $this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { + })->count(); + $this->activeSubscribers = User::whereHas('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); + })->count(); } public function switchUser(int $user_id) { - if (auth()->user()->id !== 0) { + if (AttributesAuth::id() !== 0) { return redirect()->route('dashboard'); } $user = User::find($user_id); $team_to_switch_to = $user->teams->first(); Cache::forget("team:{$user->id}"); - auth()->login($user); + Auth::login($user); refreshSession($team_to_switch_to); return redirect(request()->header('Referer')); diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index d18a7689e..69ba19e40 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -16,28 +16,28 @@ class Dashboard extends Component public Collection $servers; - public Collection $private_keys; + public Collection $privateKeys; - public $deployments_per_server; + public array $deploymentsPerServer = []; public function mount() { - $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); + $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); - $this->get_deployments(); + $this->loadDeployments(); } - public function cleanup_queue() + public function cleanupQueue() { Artisan::queue('cleanup:deployment-queue', [ '--team-id' => currentTeam()->id, ]); } - public function get_deployments() + public function loadDeployments() { - $this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ + $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ 'id', 'application_id', 'application_name', diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php deleted file mode 100644 index a1a0a7b94..000000000 --- a/app/Livewire/Destination/Form.php +++ /dev/null @@ -1,46 +0,0 @@ - 'required', - 'destination.network' => 'required', - 'destination.server.ip' => 'required', - ]; - - protected $validationAttributes = [ - 'destination.name' => 'name', - 'destination.network' => 'network', - 'destination.server.ip' => 'IP Address/Domain', - ]; - - public function submit() - { - $this->validate(); - $this->destination->save(); - } - - public function delete() - { - try { - if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) { - if ($this->destination->attachedTo()) { - return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); - } - instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); - instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); - } - $this->destination->delete(); - - return redirect()->route('destination.all'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Destination/Index.php b/app/Livewire/Destination/Index.php new file mode 100644 index 000000000..a3df3fd56 --- /dev/null +++ b/app/Livewire/Destination/Index.php @@ -0,0 +1,23 @@ +servers = Server::isUsable()->get(); + } + + public function render() + { + return view('livewire.destination.index'); + } +} diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 4fc938df8..f86f42e34 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -3,111 +3,91 @@ namespace App\Livewire\Destination\New; use App\Models\Server; -use App\Models\StandaloneDocker as ModelsStandaloneDocker; +use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Visus\Cuid2\Cuid2; class Docker extends Component { + #[Locked] + public $servers; + + #[Locked] + public Server $selectedServer; + + #[Validate(['required', 'string'])] public string $name; + #[Validate(['required', 'string'])] public string $network; - public ?Collection $servers = null; + #[Validate(['required', 'string'])] + public string $serverId; - public Server $server; + #[Validate(['required', 'boolean'])] + public bool $isSwarm = false; - public ?int $server_id = null; - - public bool $is_swarm = false; - - protected $rules = [ - 'name' => 'required|string', - 'network' => 'required|string', - 'server_id' => 'required|integer', - 'is_swarm' => 'boolean', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'network' => 'network', - 'server_id' => 'server', - 'is_swarm' => 'swarm', - ]; - - public function mount() + public function mount(?string $server_id = null) { - if (is_null($this->servers)) { - $this->servers = Server::isReachable()->get(); - } - if (request()->query('server_id')) { - $this->server_id = request()->query('server_id'); + $this->network = new Cuid2; + $this->servers = Server::isUsable()->get(); + if ($server_id) { + $this->selectedServer = $this->servers->find($server_id); + $this->serverId = $this->selectedServer->id; } else { - if ($this->servers->count() > 0) { - $this->server_id = $this->servers->first()->id; - } - } - if (request()->query('network_name')) { - $this->network = request()->query('network_name'); - } else { - $this->network = new Cuid2; - } - if ($this->servers->count() > 0) { - $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->first(); + $this->serverId = $this->selectedServer->id; } + $this->generateName(); } - public function generate_name() + public function updatedServerId() { - $this->server = Server::find($this->server_id); - $this->name = str("{$this->server->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->find($this->serverId); + $this->generateName(); + } + + public function generateName() + { + $name = data_get($this->selectedServer, 'name', new Cuid2); + $this->name = str("{$name}-{$this->network}")->kebab(); } public function submit() { - $this->validate(); try { - $this->server = Server::find($this->server_id); - if ($this->is_swarm) { - $found = $this->server->swarmDockers()->where('network', $this->network)->first(); + $this->validate(); + if ($this->isSwarm) { + $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { $docker = SwarmDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } else { - $found = $this->server->standaloneDockers()->where('network', $this->network)->first(); + $found = $this->selectedServer->standaloneDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { - $docker = ModelsStandaloneDocker::create([ + $docker = StandaloneDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } - $this->createNetworkAndAttachToProxy(); - - return redirect()->route('destination.show', $docker->uuid); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer); + instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false); + $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); } } - - private function createNetworkAndAttachToProxy() - { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 37583a944..5c4d6c170 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -5,71 +5,91 @@ namespace App\Livewire\Destination; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { - public Server $server; + #[Locked] + public $destination; - public Collection|array $networks = []; + #[Validate(['string', 'required'])] + public string $name; - private function createNetworkAndAttachToProxy() + #[Validate(['string', 'required'])] + public string $network; + + #[Validate(['string', 'required'])] + public string $serverIp; + + public function mount(string $destination_uuid) { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } + try { + $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ?? + SwarmDocker::whereUuid($destination_uuid)->firstOrFail(); - public function add($name) - { - if ($this->server->isSwarm()) { - $found = $this->server->swarmDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; - } else { - SwarmDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $this->name, - 'server_id' => $this->server->id, - ]); + $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) { + if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) { + $this->destination = $destination; + $this->syncData(); + } + }); + if ($ownedByTeam === false) { + return redirect()->route('destination.index'); } - } else { - $found = $this->server->standaloneDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; - } else { - StandaloneDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $name, - 'server_id' => $this->server->id, - ]); - } - $this->createNetworkAndAttachToProxy(); + $this->destination = $destination; + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function scan() + public function syncData(bool $toModel = false) { - if ($this->server->isSwarm()) { - $alreadyAddedNetworks = $this->server->swarmDockers; + if ($toModel) { + $this->validate(); + $this->destination->name = $this->name; + $this->destination->network = $this->network; + $this->destination->server->ip = $this->serverIp; + $this->destination->save(); } else { - $alreadyAddedNetworks = $this->server->standaloneDockers; + $this->name = $this->destination->name; + $this->network = $this->destination->network; + $this->serverIp = $this->destination->server->ip; } - $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); - $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { - return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; - })->filter(function ($network) use ($alreadyAddedNetworks) { - return ! $alreadyAddedNetworks->contains('network', $network['Name']); - }); - if ($this->networks->count() === 0) { - $this->dispatch('success', 'No new destinations found on this server.'); + } - return; + public function submit() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Destination saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('success', 'Scan done.'); + } + + public function delete() + { + try { + if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) { + if ($this->destination->attachedTo()) { + return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); + } + instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); + instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); + } + $this->destination->delete(); + + return redirect()->route('destination.index'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.destination.show'); } } diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 934e81661..f51527fbe 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -5,55 +5,39 @@ namespace App\Livewire; use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Route; +use Livewire\Attributes\Validate; use Livewire\Component; class Help extends Component { use WithRateLimiting; + #[Validate(['required', 'min:10', 'max:1000'])] public string $description; + #[Validate(['required', 'min:3'])] public string $subject; - public ?string $path = null; - - protected $rules = [ - 'description' => 'required|min:10', - 'subject' => 'required|min:3', - ]; - - public function mount() - { - $this->path = Route::current()?->uri() ?? null; - if (isDev()) { - $this->description = "I'm having trouble with {$this->path}"; - $this->subject = "Help with {$this->path}"; - } - } - public function submit() { try { - $this->rateLimit(3, 30); $this->validate(); - $debug = "Route: {$this->path}"; + $this->rateLimit(3, 30); + + $settings = instanceSettings(); $mail = new MailMessage; $mail->view( 'emails.help', [ 'description' => $this->description, - 'debug' => $debug, ] ); $mail->subject("[HELP]: {$this->subject}"); - $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); - if (! $type) { + + // Sending feedback through Cloud API + if ($type === false) { $url = 'https://app.coolify.io/api/feedback'; - if (isDev()) { - $url = 'http://localhost:80/api/feedback'; - } Http::post($url, [ 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', ]); diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index 10ba0c86a..cc5d78f60 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -3,6 +3,7 @@ namespace App\Livewire; use App\Models\InstanceSettings; +use Illuminate\Container\Attributes\Auth as AttributesAuth; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -31,7 +32,7 @@ class NavbarDeleteTeam extends Component $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === auth()->user()->id) { + if ($user->id === AttributesAuth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index db8594558..7a177a227 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -4,60 +4,124 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Discord extends Component { public Team $team; - protected $rules = [ - 'team.discord_enabled' => 'nullable|boolean', - 'team.discord_webhook_url' => 'required|url', - 'team.discord_notifications_test' => 'nullable|boolean', - 'team.discord_notifications_deployments' => 'nullable|boolean', - 'team.discord_notifications_status_changes' => 'nullable|boolean', - 'team.discord_notifications_database_backups' => 'nullable|boolean', - 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.discord_notifications_server_disk_usage' => 'nullable|boolean', - ]; + #[Validate(['boolean'])] + public bool $discordEnabled = false; - protected $validationAttributes = [ - 'team.discord_webhook_url' => 'Discord Webhook', - ]; + #[Validate(['url', 'nullable'])] + public ?string $discordWebhookUrl = null; + + #[Validate(['boolean'])] + public bool $discordNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsScheduledTasks = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->discord_enabled = $this->discordEnabled; + $this->team->discord_webhook_url = $this->discordWebhookUrl; + $this->team->discord_notifications_test = $this->discordNotificationsTest; + $this->team->discord_notifications_deployments = $this->discordNotificationsDeployments; + $this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges; + $this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups; + $this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks; + $this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->discordEnabled = $this->team->discord_enabled; + $this->discordWebhookUrl = $this->team->discord_webhook_url; + $this->discordNotificationsTest = $this->team->discord_notifications_test; + $this->discordNotificationsDeployments = $this->team->discord_notifications_deployments; + $this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes; + $this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups; + $this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks; + $this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage; + } + } + + public function instantSaveDiscordEnabled() + { + try { + $this->validate([ + 'discordWebhookUrl' => 'required', + ], [ + 'discordWebhookUrl.required' => 'Discord Webhook URL is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->discordEnabled = false; + + return handleError($e, $this); + } } public function instantSave() { try { - $this->submit(); - } catch (\Throwable) { - $this->team->discord_enabled = false; - $this->validate(); + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 08415731d..28267331a 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -5,75 +5,133 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; use Illuminate\Support\Facades\RateLimiter; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Email extends Component { public Team $team; + #[Locked] public string $emails; - public bool $sharedEmailEnabled = false; + #[Validate(['boolean'])] + public bool $smtpEnabled = false; - protected $rules = [ - 'team.smtp_enabled' => 'nullable|boolean', - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_recipients' => 'nullable', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - 'team.smtp_notifications_test' => 'nullable|boolean', - 'team.smtp_notifications_deployments' => 'nullable|boolean', - 'team.smtp_notifications_status_changes' => 'nullable|boolean', - 'team.smtp_notifications_database_backups' => 'nullable|boolean', - 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.smtp_notifications_server_disk_usage' => 'nullable|boolean', - 'team.use_instance_email_settings' => 'boolean', - 'team.resend_enabled' => 'nullable|boolean', - 'team.resend_api_key' => 'nullable', - ]; + #[Validate(['boolean'])] + public bool $useInstanceEmailSettings = false; - protected $validationAttributes = [ - 'team.smtp_from_address' => 'From Address', - 'team.smtp_from_name' => 'From Name', - 'team.smtp_recipients' => 'Recipients', - 'team.smtp_host' => 'Host', - 'team.smtp_port' => 'Port', - 'team.smtp_encryption' => 'Encryption', - 'team.smtp_username' => 'Username', - 'team.smtp_password' => 'Password', - 'team.smtp_timeout' => 'Timeout', - 'team.resend_enabled' => 'Resend Enabled', - 'team.resend_api_key' => 'Resend API Key', - ]; + #[Validate(['nullable', 'email'])] + public ?string $smtpFromAddress = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpFromName = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpRecipients = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpHost = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpEncryption = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpUsername = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpPassword = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpTimeout = null; + + #[Validate(['boolean'])] + public bool $smtpNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsScheduledTasks = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsServerDiskUsage = false; + + #[Validate(['boolean'])] + public bool $resendEnabled; + + #[Validate(['nullable', 'string'])] + public ?string $resendApiKey = null; public function mount() - { - $this->team = auth()->user()->currentTeam(); - ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; - $this->emails = auth()->user()->email; - } - - public function submitFromFields() { try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - ]); - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->team = auth()->user()->currentTeam(); + $this->emails = auth()->user()->email; + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->smtp_enabled = $this->smtpEnabled; + $this->team->smtp_from_address = $this->smtpFromAddress; + $this->team->smtp_from_name = $this->smtpFromName; + $this->team->smtp_host = $this->smtpHost; + $this->team->smtp_port = $this->smtpPort; + $this->team->smtp_encryption = $this->smtpEncryption; + $this->team->smtp_username = $this->smtpUsername; + $this->team->smtp_password = $this->smtpPassword; + $this->team->smtp_timeout = $this->smtpTimeout; + $this->team->smtp_recipients = $this->smtpRecipients; + $this->team->smtp_notifications_test = $this->smtpNotificationsTest; + $this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments; + $this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges; + $this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups; + $this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks; + $this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage; + $this->team->use_instance_email_settings = $this->useInstanceEmailSettings; + $this->team->resend_enabled = $this->resendEnabled; + $this->team->resend_api_key = $this->resendApiKey; + $this->team->save(); + refreshSession(); + } else { + $this->smtpEnabled = $this->team->smtp_enabled; + $this->smtpFromAddress = $this->team->smtp_from_address; + $this->smtpFromName = $this->team->smtp_from_name; + $this->smtpHost = $this->team->smtp_host; + $this->smtpPort = $this->team->smtp_port; + $this->smtpEncryption = $this->team->smtp_encryption; + $this->smtpUsername = $this->team->smtp_username; + $this->smtpPassword = $this->team->smtp_password; + $this->smtpTimeout = $this->team->smtp_timeout; + $this->smtpRecipients = $this->team->smtp_recipients; + $this->smtpNotificationsTest = $this->team->smtp_notifications_test; + $this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments; + $this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes; + $this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups; + $this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks; + $this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage; + $this->useInstanceEmailSettings = $this->team->use_instance_email_settings; + $this->resendEnabled = $this->team->resend_enabled; + $this->resendApiKey = $this->team->resend_api_key; + } + } + public function sendTestNotification() { try { @@ -98,38 +156,44 @@ class Email extends Component public function instantSaveInstance() { try { - if (! $this->sharedEmailEnabled) { - throw new \Exception('Not allowed to change settings. Please upgrade your subscription.'); - } - $this->team->smtp_enabled = false; - $this->team->resend_enabled = false; - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = false; + $this->resendEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function instantSaveSmtpEnabled() + { + try { + $this->validate([ + 'smtpHost' => 'required', + 'smtpPort' => 'required|numeric', + ], [ + 'smtpHost.required' => 'SMTP Host is required.', + 'smtpPort.required' => 'SMTP Port is required.', + ]); + $this->resendEnabled = false; + $this->saveModel(); + } catch (\Throwable $e) { + $this->smtpEnabled = false; + + return handleError($e, $this); + } + } + public function instantSaveResend() { try { - $this->team->smtp_enabled = false; - $this->submitResend(); + $this->validate([ + ], [ + 'resendApiKey.required' => 'Resend API Key is required.', + ]); + $this->smtpEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; - - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->team->resend_enabled = false; - $this->submit(); - } catch (\Throwable $e) { - $this->team->smtp_enabled = false; + $this->resendEnabled = false; return handleError($e, $this); } @@ -137,7 +201,7 @@ class Email extends Component public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } @@ -146,43 +210,8 @@ class Email extends Component { try { $this->resetErrorBag(); - if (! $this->team->use_instance_email_settings) { - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required|numeric', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - ]); - } - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; - - return handleError($e, $this); - } - } - - public function submitResend() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.resend_api_key' => 'required', - ]); - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->team->resend_enabled = false; - return handleError($e, $this); } } @@ -190,35 +219,28 @@ class Email extends Component public function copyFromInstanceSettings() { $settings = instanceSettings(); + if ($settings->smtp_enabled) { - $team = currentTeam(); - $team->update([ - 'smtp_enabled' => $settings->smtp_enabled, - 'smtp_from_address' => $settings->smtp_from_address, - 'smtp_from_name' => $settings->smtp_from_name, - 'smtp_recipients' => $settings->smtp_recipients, - 'smtp_host' => $settings->smtp_host, - 'smtp_port' => $settings->smtp_port, - 'smtp_encryption' => $settings->smtp_encryption, - 'smtp_username' => $settings->smtp_username, - 'smtp_password' => $settings->smtp_password, - 'smtp_timeout' => $settings->smtp_timeout, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = true; + $this->smtpFromAddress = $settings->smtp_from_address; + $this->smtpFromName = $settings->smtp_from_name; + $this->smtpRecipients = $settings->smtp_recipients; + $this->smtpHost = $settings->smtp_host; + $this->smtpPort = $settings->smtp_port; + $this->smtpEncryption = $settings->smtp_encryption; + $this->smtpUsername = $settings->smtp_username; + $this->smtpPassword = $settings->smtp_password; + $this->smtpTimeout = $settings->smtp_timeout; + $this->resendEnabled = false; + $this->saveModel(); return; } if ($settings->resend_enabled) { - $team = currentTeam(); - $team->update([ - 'resend_enabled' => $settings->resend_enabled, - 'resend_api_key' => $settings->resend_api_key, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->resendEnabled = true; + $this->resendApiKey = $settings->resend_api_key; + $this->smtpEnabled = false; + $this->saveModel(); return; } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 862b9b3ea..15ec20577 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -4,67 +4,157 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Telegram extends Component { public Team $team; - protected $rules = [ - 'team.telegram_enabled' => 'nullable|boolean', - 'team.telegram_token' => 'required|string', - 'team.telegram_chat_id' => 'required|string', - 'team.telegram_notifications_test' => 'nullable|boolean', - 'team.telegram_notifications_deployments' => 'nullable|boolean', - 'team.telegram_notifications_status_changes' => 'nullable|boolean', - 'team.telegram_notifications_database_backups' => 'nullable|boolean', - 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.telegram_notifications_test_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', - 'team.telegram_notifications_server_disk_usage' => 'nullable|boolean', - ]; + #[Validate(['boolean'])] + public bool $telegramEnabled = false; - protected $validationAttributes = [ - 'team.telegram_token' => 'Token', - 'team.telegram_chat_id' => 'Chat ID', - ]; + #[Validate(['nullable', 'string'])] + public ?string $telegramToken = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramChatId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsScheduledTasks = false; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsTestMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDeploymentsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsStatusChangesMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsScheduledTasksThreadId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->telegram_enabled = $this->telegramEnabled; + $this->team->telegram_token = $this->telegramToken; + $this->team->telegram_chat_id = $this->telegramChatId; + $this->team->telegram_notifications_test = $this->telegramNotificationsTest; + $this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments; + $this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges; + $this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups; + $this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks; + $this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId; + $this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId; + $this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId; + $this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId; + $this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId; + $this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->telegramEnabled = $this->team->telegram_enabled; + $this->telegramToken = $this->team->telegram_token; + $this->telegramChatId = $this->team->telegram_chat_id; + $this->telegramNotificationsTest = $this->team->telegram_notifications_test; + $this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments; + $this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes; + $this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups; + $this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks; + $this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id; + $this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id; + $this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id; + $this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id; + $this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id; + $this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage; + } + } public function instantSave() { try { - $this->submit(); - } catch (\Throwable) { - $this->team->telegram_enabled = false; - $this->validate(); + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveTelegramEnabled() + { + try { + $this->validate([ + 'telegramToken' => 'required', + 'telegramChatId' => 'required', + ], [ + 'telegramToken.required' => 'Telegram Token is required.', + 'telegramChatId.required' => 'Telegram Chat ID is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->telegramEnabled = false; + + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 483f68cf8..53314cd5c 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -2,6 +2,7 @@ namespace App\Livewire\Profile; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Validate; @@ -24,9 +25,9 @@ class Index extends Component public function mount() { - $this->userId = auth()->user()->id; - $this->name = auth()->user()->name; - $this->email = auth()->user()->email; + $this->userId = Auth::id(); + $this->name = Auth::user()->name; + $this->email = Auth::user()->email; } public function submit() @@ -35,7 +36,7 @@ class Index extends Component $this->validate([ 'name' => 'required', ]); - auth()->user()->update([ + Auth::user()->update([ 'name' => $this->name, ]); diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index c3353be84..fd976548a 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -3,24 +3,17 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class AddEmpty extends Component { - public string $name = ''; + #[Validate(['required', 'string', 'min:3'])] + public string $name; + #[Validate(['nullable', 'string'])] public string $description = ''; - protected $rules = [ - 'name' => 'required|string|min:3', - 'description' => 'nullable|string', - ]; - - protected $validationAttributes = [ - 'name' => 'Project Name', - 'description' => 'Project Description', - ]; - public function submit() { try { @@ -34,8 +27,6 @@ class AddEmpty extends Component return redirect()->route('project.show', $project->uuid); } catch (\Throwable $e) { return handleError($e, $this); - } finally { - $this->name = ''; } } } diff --git a/app/Livewire/Project/AddEnvironment.php b/app/Livewire/Project/AddEnvironment.php deleted file mode 100644 index 7b2767dc6..000000000 --- a/app/Livewire/Project/AddEnvironment.php +++ /dev/null @@ -1,44 +0,0 @@ - 'required|string|min:3', - ]; - - protected $validationAttributes = [ - 'name' => 'Environment Name', - ]; - - public function submit() - { - try { - $this->validate(); - $environment = Environment::create([ - 'name' => $this->name, - 'project_id' => $this->project->id, - ]); - - return redirect()->route('project.resource.index', [ - 'project_uuid' => $this->project->uuid, - 'environment_name' => $environment->name, - ]); - } catch (\Throwable $e) { - handleError($e, $this); - } finally { - $this->name = ''; - } - } -} diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index a3a688f7c..05ac25429 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -3,120 +3,200 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component { public Application $application; - public bool $is_force_https_enabled; + #[Validate(['boolean'])] + public bool $isForceHttpsEnabled = false; - public bool $is_gzip_enabled; + #[Validate(['boolean'])] + public bool $isGitSubmodulesEnabled = false; - public bool $is_stripprefix_enabled; + #[Validate(['boolean'])] + public bool $isGitLfsEnabled = false; - protected $rules = [ - 'application.settings.is_git_submodules_enabled' => 'boolean|required', - 'application.settings.is_git_lfs_enabled' => 'boolean|required', - 'application.settings.is_preview_deployments_enabled' => 'boolean|required', - 'application.settings.is_auto_deploy_enabled' => 'boolean|required', - 'is_force_https_enabled' => 'boolean|required', - 'application.settings.is_log_drain_enabled' => 'boolean|required', - 'application.settings.is_gpu_enabled' => 'boolean|required', - 'application.settings.is_build_server_enabled' => 'boolean|required', - 'application.settings.is_consistent_container_name_enabled' => 'boolean|required', - 'application.settings.custom_internal_name' => 'string|nullable', - 'application.settings.is_gzip_enabled' => 'boolean|required', - 'application.settings.is_stripprefix_enabled' => 'boolean|required', - 'application.settings.gpu_driver' => 'string|required', - 'application.settings.gpu_count' => 'string|required', - 'application.settings.gpu_device_ids' => 'string|required', - 'application.settings.gpu_options' => 'string|required', - 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', - 'application.settings.connect_to_docker_network' => 'boolean|required', - ]; + #[Validate(['boolean'])] + public bool $isPreviewDeploymentsEnabled = false; + + #[Validate(['boolean'])] + public bool $isAutoDeployEnabled = true; + + #[Validate(['boolean'])] + public bool $isLogDrainEnabled = false; + + #[Validate(['boolean'])] + public bool $isGpuEnabled = false; + + #[Validate(['string'])] + public string $gpuDriver = ''; + + #[Validate(['string', 'nullable'])] + public ?string $gpuCount = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuDeviceIds = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuOptions = null; + + #[Validate(['boolean'])] + public bool $isBuildServerEnabled = false; + + #[Validate(['boolean'])] + public bool $isConsistentContainerNameEnabled = false; + + #[Validate(['string', 'nullable'])] + public ?string $customInternalName = null; + + #[Validate(['boolean'])] + public bool $isGzipEnabled = true; + + #[Validate(['boolean'])] + public bool $isStripprefixEnabled = true; + + #[Validate(['boolean'])] + public bool $isRawComposeDeploymentEnabled = false; + + #[Validate(['boolean'])] + public bool $isConnectToDockerNetworkEnabled = false; public function mount() { - $this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); - $this->is_gzip_enabled = $this->application->isGzipEnabled(); - $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->settings->is_force_https_enabled = $this->isForceHttpsEnabled; + $this->application->settings->is_git_submodules_enabled = $this->isGitSubmodulesEnabled; + $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; + $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; + $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; + $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->settings->is_gpu_enabled = $this->isGpuEnabled; + $this->application->settings->gpu_driver = $this->gpuDriver; + $this->application->settings->gpu_count = $this->gpuCount; + $this->application->settings->gpu_device_ids = $this->gpuDeviceIds; + $this->application->settings->gpu_options = $this->gpuOptions; + $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled; + $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled; + $this->application->settings->custom_internal_name = $this->customInternalName; + $this->application->settings->is_gzip_enabled = $this->isGzipEnabled; + $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; + $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; + $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; + $this->application->settings->save(); + } else { + $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); + $this->isGzipEnabled = $this->application->isGzipEnabled(); + $this->isStripprefixEnabled = $this->application->isStripprefixEnabled(); + $this->isLogDrainEnabled = $this->application->isLogDrainEnabled(); + + $this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled; + $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; + $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; + $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; + $this->isGpuEnabled = $this->application->settings->is_gpu_enabled; + $this->gpuDriver = $this->application->settings->gpu_driver; + $this->gpuCount = $this->application->settings->gpu_count; + $this->gpuDeviceIds = $this->application->settings->gpu_device_ids; + $this->gpuOptions = $this->application->settings->gpu_options; + $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled; + $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled; + $this->customInternalName = $this->application->settings->custom_internal_name; + $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; + $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; + } } public function instantSave() { - if ($this->application->isLogDrainEnabled()) { - if (! $this->application->destination->server->isLogDrainEnabled()) { - $this->application->settings->is_log_drain_enabled = false; - $this->dispatch('error', 'Log drain is not enabled on this server.'); + try { + if ($this->isLogDrainEnabled) { + if (! $this->application->destination->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->syncData(true); + $this->dispatch('error', 'Log drain is not enabled on this server.'); - return; + return; + } } + if ($this->application->isForceHttpsEnabled() !== $this->isForceHttpsEnabled || + $this->application->isGzipEnabled() !== $this->isGzipEnabled || + $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled + ) { + $this->dispatch('resetDefaultLabels', false); + } + + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $this->application->oldRawParser(); + } else { + $this->application->parse(); + } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) { - $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) { - $this->application->settings->is_gzip_enabled = $this->is_gzip_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) { - $this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_raw_compose_deployment_enabled) { - $this->application->oldRawParser(); - } else { - $this->application->parse(); - } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); - $this->dispatch('configurationChanged'); } public function submit() { - if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { - $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); - $this->application->settings->gpu_count = null; - $this->application->settings->gpu_device_ids = null; - $this->application->settings->save(); + try { + if ($this->gpuCount && $this->gpuDeviceIds) { + $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); + $this->gpuCount = null; + $this->gpuDeviceIds = null; + $this->syncData(true); - return; + return; + } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); } public function saveCustomName() { - if (str($this->application->settings->custom_internal_name)->isNotEmpty()) { - $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value(); + if (str($this->customInternalName)->isNotEmpty()) { + $this->customInternalName = str($this->customInternalName)->slug()->value(); } else { - $this->application->settings->custom_internal_name = null; + $this->customInternalName = null; } - if (is_null($this->application->settings->custom_internal_name)) { - $this->application->settings->save(); + if (is_null($this->customInternalName)) { + $this->syncData(true); $this->dispatch('success', 'Custom name saved.'); return; } - $customInternalName = $this->application->settings->custom_internal_name; + $customInternalName = $this->customInternalName; $server = $this->application->destination->server; $allApplications = $server->applications(); $foundSameInternalName = $allApplications->filter(function ($application) { - return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->application->settings->custom_internal_name; + return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->customInternalName; }); if ($foundSameInternalName->isNotEmpty()) { $this->dispatch('error', 'This custom container name is already in use by another application on this server.'); - $this->application->settings->custom_internal_name = $customInternalName; - $this->application->settings->refresh(); + $this->customInternalName = $customInternalName; + $this->syncData(true); return; } - $this->application->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Custom name saved.'); } diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index 73b423f90..c7b2e8184 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -10,49 +10,53 @@ class Form extends Component { public Application $application; - public string $preview_url_template; - - protected $rules = [ - 'application.preview_url_template' => 'required', - ]; - - protected $validationAttributes = [ - 'application.preview_url_template' => 'preview url template', - ]; - - public function resetToDefault() - { - $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; - $this->preview_url_template = $this->application->preview_url_template; - $this->application->save(); - $this->generate_real_url(); - } - - public function generate_real_url() - { - if (data_get($this->application, 'fqdn')) { - try { - $firstFqdn = str($this->application->fqdn)->before(','); - $url = Url::fromString($firstFqdn); - $host = $url->getHost(); - $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); - } catch (\Exception) { - $this->dispatch('error', 'Invalid FQDN.'); - } - } - } + #[Validate('required')] + public string $previewUrlTemplate; public function mount() { - $this->generate_real_url(); + try { + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); - $this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template); - $this->application->save(); - $this->dispatch('success', 'Preview url template updated.'); - $this->generate_real_url(); + try { + $this->resetErrorBag(); + $this->validate(); + $this->application->preview_url_template = str_replace(' ', '', $this->previewUrlTemplate); + $this->application->save(); + $this->dispatch('success', 'Preview url template updated.'); + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function resetToDefault() + { + try { + $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->application->save(); + $this->generateRealUrl(); + $this->dispatch('success', 'Preview url template updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function generateRealUrl() + { + if (data_get($this->application, 'fqdn')) { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->previewUrlTemplate = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } } } diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index 426626e55..ade297d50 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -4,55 +4,92 @@ namespace App\Livewire\Project\Application; use App\Models\Application; use App\Models\PrivateKey; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Source extends Component { - public $applicationId; - public Application $application; - public $private_keys; + #[Locked] + public $privateKeys; - protected $rules = [ - 'application.git_repository' => 'required', - 'application.git_branch' => 'required', - 'application.git_commit_sha' => 'nullable', - ]; + #[Validate(['nullable', 'string'])] + public ?string $privateKeyName = null; - protected $validationAttributes = [ - 'application.git_repository' => 'repository', - 'application.git_branch' => 'branch', - 'application.git_commit_sha' => 'commit sha', - ]; + #[Validate(['nullable', 'integer'])] + public ?int $privateKeyId = null; + + #[Validate(['required', 'string'])] + public string $gitRepository; + + #[Validate(['required', 'string'])] + public string $gitBranch; + + #[Validate(['nullable', 'string'])] + public ?string $gitCommitSha = null; public function mount() { - $this->get_private_keys(); + try { + $this->syncData(); + $this->getPrivateKeys(); + } catch (\Throwable $e) { + handleError($e, $this); + } } - private function get_private_keys() + public function syncData(bool $toModel = false) { - $this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { - return $key->id == $this->application->private_key_id; + if ($toModel) { + $this->validate(); + $this->application->update([ + 'git_repository' => $this->gitRepository, + 'git_branch' => $this->gitBranch, + 'git_commit_sha' => $this->gitCommitSha, + 'private_key_id' => $this->privateKeyId, + ]); + } else { + $this->gitRepository = $this->application->git_repository; + $this->gitBranch = $this->application->git_branch; + $this->gitCommitSha = $this->application->git_commit_sha; + $this->privateKeyId = $this->application->private_key_id; + $this->privateKeyName = data_get($this->application, 'private_key.name'); + } + } + + private function getPrivateKeys() + { + $this->privateKeys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { + return $key->id == $this->privateKeyId; }); } - public function setPrivateKey(int $private_key_id) + public function setPrivateKey(int $privateKeyId) { - $this->application->private_key_id = $private_key_id; - $this->application->save(); - $this->application->refresh(); - $this->get_private_keys(); + try { + $this->privateKeyId = $privateKeyId; + $this->syncData(true); + $this->getPrivateKeys(); + $this->application->refresh(); + $this->privateKeyName = $this->application->private_key->name; + $this->dispatch('success', 'Private key updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); - if (! $this->application->git_commit_sha) { - $this->application->git_commit_sha = 'HEAD'; + try { + if (str($this->gitCommitSha)->isEmpty()) { + $this->gitCommitSha = 'HEAD'; + } + $this->syncData(true); + $this->dispatch('success', 'Application source updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->save(); - $this->dispatch('success', 'Application source updated!'); } } diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php index 0151b5222..197dc41ed 100644 --- a/app/Livewire/Project/Application/Swarm.php +++ b/app/Livewire/Project/Application/Swarm.php @@ -3,32 +3,55 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Swarm extends Component { public Application $application; - public string $swarm_placement_constraints = ''; + #[Validate('required')] + public int $swarmReplicas; - protected $rules = [ - 'application.swarm_replicas' => 'required', - 'application.swarm_placement_constraints' => 'nullable', - 'application.settings.is_swarm_only_worker_nodes' => 'required', - ]; + #[Validate(['nullable'])] + public ?string $swarmPlacementConstraints = null; + + #[Validate('required')] + public bool $isSwarmOnlyWorkerNodes; public function mount() { - if ($this->application->swarm_placement_constraints) { - $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->swarm_replicas = $this->swarmReplicas; + $this->application->swarm_placement_constraints = $this->swarmPlacementConstraints ? base64_encode($this->swarmPlacementConstraints) : null; + $this->application->settings->is_swarm_only_worker_nodes = $this->isSwarmOnlyWorkerNodes; + $this->application->save(); + $this->application->settings->save(); + } else { + $this->swarmReplicas = $this->application->swarm_replicas; + if ($this->application->swarm_placement_constraints) { + $this->swarmPlacementConstraints = base64_decode($this->application->swarm_placement_constraints); + } else { + $this->swarmPlacementConstraints = null; + } + $this->isSwarmOnlyWorkerNodes = $this->application->settings->is_swarm_only_worker_nodes; } } public function instantSave() { try { - $this->validate(); - $this->application->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -38,14 +61,7 @@ class Swarm extends Component public function submit() { try { - $this->validate(); - if ($this->swarm_placement_constraints) { - $this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints); - } else { - $this->application->swarm_placement_constraints = null; - } - $this->application->save(); - + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 9b82c4b11..b3a54f0ab 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -4,56 +4,87 @@ namespace App\Livewire\Project\Database; use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Exception; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; class BackupEdit extends Component { - public ?ScheduledDatabaseBackup $backup; + public ScheduledDatabaseBackup $backup; + #[Locked] public $s3s; + #[Locked] + public $parameters; + + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_locally = false; + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_s3 = false; + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_sftp = false; + #[Validate(['nullable', 'string'])] public ?string $status = null; - public array $parameters; + #[Validate(['required', 'boolean'])] + public bool $backupEnabled = false; - protected $rules = [ - 'backup.enabled' => 'required|boolean', - 'backup.frequency' => 'required|string', - 'backup.number_of_backups_locally' => 'required|integer|min:1', - 'backup.save_s3' => 'required|boolean', - 'backup.s3_storage_id' => 'nullable|integer', - 'backup.databases_to_backup' => 'nullable', - 'backup.dump_all' => 'required|boolean', - ]; + #[Validate(['required', 'string'])] + public string $frequency = ''; - protected $validationAttributes = [ - 'backup.enabled' => 'Enabled', - 'backup.frequency' => 'Frequency', - 'backup.number_of_backups_locally' => 'Number of Backups Locally', - 'backup.save_s3' => 'Save to S3', - 'backup.s3_storage_id' => 'S3 Storage', - 'backup.databases_to_backup' => 'Databases to Backup', - 'backup.dump_all' => 'Backup All Databases', - ]; + #[Validate(['required', 'integer', 'min:1'])] + public int $numberOfBackupsLocally = 1; - protected $messages = [ - 'backup.s3_storage_id' => 'Select a S3 Storage', - ]; + #[Validate(['required', 'boolean'])] + public bool $saveS3 = false; + + #[Validate(['nullable', 'integer'])] + public ?int $s3StorageId = 1; + + #[Validate(['nullable', 'string'])] + public ?string $databasesToBackup = null; + + #[Validate(['required', 'boolean'])] + public bool $dumpAll = false; public function mount() { - $this->parameters = get_route_parameters(); - if (is_null(data_get($this->backup, 's3_storage_id'))) { - data_set($this->backup, 's3_storage_id', 'default'); + try { + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->customValidate(); + $this->backup->enabled = $this->backupEnabled; + $this->backup->frequency = $this->frequency; + $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally; + $this->backup->save_s3 = $this->saveS3; + $this->backup->s3_storage_id = $this->s3StorageId; + $this->backup->databases_to_backup = $this->databasesToBackup; + $this->backup->dump_all = $this->dumpAll; + $this->backup->save(); + } else { + $this->backupEnabled = $this->backup->enabled; + $this->frequency = $this->backup->frequency; + $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally; + $this->saveS3 = $this->backup->save_s3; + $this->s3StorageId = $this->backup->s3_storage_id; + $this->databasesToBackup = $this->backup->databases_to_backup; + $this->dumpAll = $this->backup->dump_all; } } @@ -96,16 +127,14 @@ class BackupEdit extends Component public function instantSave() { try { - $this->custom_validate(); - $this->backup->save(); - $this->backup->refresh(); + $this->syncData(true); $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } - private function custom_validate() + private function customValidate() { if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; @@ -120,19 +149,14 @@ class BackupEdit extends Component public function submit() { try { - $this->custom_validate(); - if ($this->backup->databases_to_backup === '' || $this->backup->databases_to_backup === null) { - $this->backup->databases_to_backup = null; - } - $this->backup->save(); - $this->backup->refresh(); - $this->dispatch('success', 'Backup updated successfully'); + $this->syncData(true); + $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } - public function deleteAssociatedBackupsLocally() + private function deleteAssociatedBackupsLocally() { $executions = $this->backup->executions; $backupFolder = null; @@ -152,17 +176,17 @@ class BackupEdit extends Component $execution->delete(); } - if ($backupFolder) { + if (str($backupFolder)->isNotEmpty()) { $this->deleteEmptyBackupFolder($backupFolder, $server); } } - public function deleteAssociatedBackupsS3() + private function deleteAssociatedBackupsS3() { //Add function to delete backups from S3 } - public function deleteAssociatedBackupsSftp() + private function deleteAssociatedBackupsSftp() { //Add function to delete backups from SFTP } diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 7a6446815..2d39c5151 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -7,6 +7,8 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneClickhouse; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component @@ -15,54 +17,106 @@ class General extends Component public StandaloneClickhouse $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $listeners = ['refresh']; + #[Validate(['required', 'string'])] + public string $clickhouseAdminUser; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.clickhouse_admin_user' => 'required', - 'database.clickhouse_admin_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; + #[Validate(['required', 'string'])] + public string $clickhouseAdminPassword; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.clickhouse_admin_user' => 'Postgres User', - 'database.clickhouse_admin_password' => 'Postgres Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->clickhouse_admin_user = $this->clickhouseAdminUser; + $this->database->clickhouse_admin_password = $this->clickhouseAdminPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->clickhouseAdminUser = $this->database->clickhouse_admin_user; + $this->clickhouseAdminPassword = $this->database->clickhouse_admin_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -73,16 +127,16 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -92,28 +146,28 @@ class General extends Component StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } } - public function refresh(): void + public function databaseProxyStopped() { - $this->database->refresh(); + $this->syncData(); } public function submit() { try { - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 52bced44f..0903efdfd 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -4,44 +4,45 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class CreateScheduledBackup extends Component { - public $database; - + #[Validate(['required', 'string'])] public $frequency; + #[Validate(['required', 'boolean'])] + public bool $saveToS3 = false; + + #[Locked] + public $database; + public bool $enabled = true; - public bool $save_s3 = false; + #[Validate(['required', 'integer'])] + public int $s3StorageId; - public $s3_storage_id; - - public Collection $s3s; - - protected $rules = [ - 'frequency' => 'required|string', - 'save_s3' => 'required|boolean', - ]; - - protected $validationAttributes = [ - 'frequency' => 'Backup Frequency', - 'save_s3' => 'Save to S3', - ]; + public Collection $definedS3s; public function mount() { - $this->s3s = currentTeam()->s3s; - if ($this->s3s->count() > 0) { - $this->s3_storage_id = $this->s3s->first()->id; + try { + $this->definedS3s = currentTeam()->s3s; + if ($this->definedS3s->count() > 0) { + $this->s3StorageId = $this->definedS3s->first()->id; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function submit(): void + public function submit() { try { $this->validate(); + $isValid = validate_cron_expression($this->frequency); if (! $isValid) { $this->dispatch('error', 'Invalid Cron / Human expression.'); @@ -51,8 +52,8 @@ class CreateScheduledBackup extends Component $payload = [ 'enabled' => true, 'frequency' => $this->frequency, - 'save_s3' => $this->save_s3, - 's3_storage_id' => $this->s3_storage_id, + 'save_s3' => $this->saveToS3, + 's3_storage_id' => $this->s3StorageId, 'database_id' => $this->database->id, 'database_type' => $this->database->getMorphClass(), 'team_id' => currentTeam()->id, @@ -72,10 +73,10 @@ class CreateScheduledBackup extends Component $this->dispatch('refreshScheduledBackups'); } } catch (\Throwable $e) { - handleError($e, $this); + return handleError($e, $this); } finally { $this->frequency = ''; - $this->save_s3 = true; + $this->saveToS3 = true; } } } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 394ba6c9a..ea6cd46b0 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -7,60 +7,111 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneDragonfly; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneDragonfly $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.dragonfly_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; + #[Validate(['required', 'string'])] + public string $dragonflyPassword; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.dragonfly_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->dragonfly_password = $this->dragonflyPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->dragonflyPassword = $this->database->dragonfly_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -68,11 +119,50 @@ class General extends Component } } + public function instantSave() + { + try { + if ($this->isPublic && ! $this->publicPort) { + $this->dispatch('error', 'Public port is required.'); + $this->isPublic = false; + + return; + } + if ($this->isPublic) { + if (! str($this->database->status)->startsWith('running')) { + $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; + + return; + } + StartDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is no longer publicly accessible.'); + } + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); + } catch (\Throwable $e) { + $this->isPublic = ! $this->isPublic; + $this->syncData(true); + + return handleError($e, $this); + } + } + + public function databaseProxyStopped() + { + $this->syncData(); + } + public function submit() { try { - $this->validate(); - $this->database->save(); + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; + } + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -84,45 +174,4 @@ class General extends Component } } } - - public function instantSave() - { - try { - if ($this->database->is_public && ! $this->database->public_port) { - $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; - - return; - } - if ($this->database->is_public) { - if (! str($this->database->status)->startsWith('running')) { - $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; - - return; - } - StartDatabaseProxy::run($this->database); - $this->dispatch('success', 'Database is now publicly accessible.'); - } else { - StopDatabaseProxy::run($this->database); - $this->dispatch('success', 'Database is no longer publicly accessible.'); - } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); - } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; - - return handleError($e, $this); - } - } - - public function refresh(): void - { - $this->database->refresh(); - } - - public function render() - { - return view('livewire.project.database.dragonfly.general'); - } } diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 49884ff9a..fc0febd02 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -6,6 +6,7 @@ use App\Actions\Database\RestartDatabase; use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Heading extends Component @@ -18,7 +19,7 @@ class Heading extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished', diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 663e7a6d7..062f454b1 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Database; use App\Models\Server; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; use Livewire\Component; @@ -46,7 +47,7 @@ class Import extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php index 336762981..e3baa1c8e 100644 --- a/app/Livewire/Project/Database/InitScript.php +++ b/app/Livewire/Project/Database/InitScript.php @@ -3,39 +3,39 @@ namespace App\Livewire\Project\Database; use Exception; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class InitScript extends Component { + #[Locked] public array $script; + #[Locked] public int $index; - public ?string $filename; + #[Validate(['nullable', 'string'])] + public ?string $filename = null; - public ?string $content; - - protected $rules = [ - 'filename' => 'required|string', - 'content' => 'required|string', - ]; - - protected $validationAttributes = [ - 'filename' => 'Filename', - 'content' => 'Content', - ]; + #[Validate(['nullable', 'string'])] + public ?string $content = null; public function mount() { - $this->index = data_get($this->script, 'index'); - $this->filename = data_get($this->script, 'filename'); - $this->content = data_get($this->script, 'content'); + try { + $this->index = data_get($this->script, 'index'); + $this->filename = data_get($this->script, 'filename'); + $this->content = data_get($this->script, 'content'); + } catch (Exception $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); try { + $this->validate(); $this->script['index'] = $this->index; $this->script['content'] = $this->content; $this->script['filename'] = $this->filename; @@ -47,6 +47,10 @@ class InitScript extends Component public function delete() { - $this->dispatch('delete_init_script', $this->script); + try { + $this->dispatch('delete_init_script', $this->script); + } catch (Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index df04f70d7..e768495eb 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -7,62 +7,116 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneKeydb; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneKeydb $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.keydb_conf' => 'nullable', - 'database.keydb_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; + #[Validate(['nullable', 'string'])] + public ?string $keydbConf = null; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.keydb_conf' => 'Redis Configuration', - 'database.keydb_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $keydbPassword; + + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->keydb_conf = $this->keydbConf; + $this->database->keydb_password = $this->keydbPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->keydbConf = $this->database->keydb_conf; + $this->keydbPassword = $this->database->keydb_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -70,14 +124,50 @@ class General extends Component } } + public function instantSave() + { + try { + if ($this->isPublic && ! $this->publicPort) { + $this->dispatch('error', 'Public port is required.'); + $this->isPublic = false; + + return; + } + if ($this->isPublic) { + if (! str($this->database->status)->startsWith('running')) { + $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; + + return; + } + StartDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is no longer publicly accessible.'); + } + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); + } catch (\Throwable $e) { + $this->isPublic = ! $this->isPublic; + $this->syncData(true); + + return handleError($e, $this); + } + } + + public function databaseProxyStopped() + { + $this->syncData(); + } + public function submit() { try { - $this->validate(); - if ($this->database->keydb_conf === '') { - $this->database->keydb_conf = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -89,45 +179,4 @@ class General extends Component } } } - - public function instantSave() - { - try { - if ($this->database->is_public && ! $this->database->public_port) { - $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; - - return; - } - if ($this->database->is_public) { - if (! str($this->database->status)->startsWith('running')) { - $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; - - return; - } - StartDatabaseProxy::run($this->database); - $this->dispatch('success', 'Database is now publicly accessible.'); - } else { - StopDatabaseProxy::run($this->database); - $this->dispatch('success', 'Database is no longer publicly accessible.'); - } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); - } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; - - return handleError($e, $this); - } - } - - public function refresh(): void - { - $this->database->refresh(); - } - - public function render() - { - return view('livewire.project.database.keydb.general'); - } } diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index 6d8c3aff7..1ee5de269 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -37,6 +37,6 @@ class DeleteEnvironment extends Component return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]); } - return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); + return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first."); } } diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index 360fad10a..f320a19b0 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -27,11 +27,12 @@ class DeleteProject extends Component 'project_id' => 'required|int', ]); $project = Project::findOrFail($this->project_id); - if ($project->applications->count() > 0) { - return $this->dispatch('error', 'Project has resources defined, please delete them first.'); - } - $project->delete(); + if ($project->isEmpty()) { + $project->delete(); - return redirect()->route('project.index'); + return redirect()->route('project.index'); + } + + return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first."); } } diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php index bebec4752..463febb10 100644 --- a/app/Livewire/Project/Edit.php +++ b/app/Livewire/Project/Edit.php @@ -3,34 +3,47 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Edit extends Component { public Project $project; - protected $rules = [ - 'project.name' => 'required|min:3|max:255', - 'project.description' => 'nullable|string|max:255', - ]; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - public function mount() + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; + + public function mount(string $project_uuid) { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->project->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->project->name; + $this->description = $this->project->description; } - $this->project = $project; } public function submit() { try { - $this->validate(); - $this->project->save(); - $this->dispatch('saved'); + $this->syncData(true); $this->dispatch('success', 'Project updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php index 16fc7bc36..f48220b3d 100644 --- a/app/Livewire/Project/EnvironmentEdit.php +++ b/app/Livewire/Project/EnvironmentEdit.php @@ -4,6 +4,8 @@ namespace App\Livewire\Project; use App\Models\Application; use App\Models\Project; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class EnvironmentEdit extends Component @@ -12,29 +14,45 @@ class EnvironmentEdit extends Component public Application $application; + #[Locked] public $environment; - public array $parameters; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - protected $rules = [ - 'environment.name' => 'required|min:3|max:255', - 'environment.description' => 'nullable|min:3|max:255', - ]; + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; - public function mount() + public function mount(string $project_uuid, string $environment_name) { - $this->parameters = get_route_parameters(); - $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first(); - $this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first(); + try { + $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail(); + $this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->environment->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->environment->name; + $this->description = $this->environment->description; + } } public function submit() { - $this->validate(); try { - $this->environment->save(); - - return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]); + $this->syncData(true); + $this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 7f8247597..2dc9abbf1 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -158,7 +158,7 @@ class Select extends Component [ 'id' => 'mariadb', 'name' => 'MariaDB', - 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source software under the GNU General Public License.', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.', 'logo' => '', ], [ @@ -326,7 +326,7 @@ class Select extends Component public function loadServers() { - $this->servers = Server::isUsable()->get(); + $this->servers = Server::isUsable()->get()->sortBy('name'); $this->allServers = $this->servers; } } diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index a2e48fee7..319ead361 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Actions\Docker\GetContainersStatus; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Configuration extends Component @@ -20,7 +21,7 @@ class Configuration extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 67b575599..ee43dc911 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -7,6 +7,7 @@ use App\Actions\Service\StopService; use App\Actions\Shared\PullImage; use App\Events\ServiceStatusChanged; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -34,7 +35,7 @@ class Navbar extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 05d9a7a13..0710e37ff 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -2,23 +2,60 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Models\ScheduledTask; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Locked; use Livewire\Component; class Executions extends Component { - public $executions = []; + public ScheduledTask $task; - public $selectedKey; + #[Locked] + public int $taskId; - public $task; + #[Locked] + public Collection $executions; + + #[Locked] + public ?int $selectedKey = null; + + #[Locked] + public ?string $serverTimezone = null; public function getListeners() { + $teamId = Auth::user()->currentTeam()->id; + return [ - 'selectTask', + "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions', ]; } + public function mount($taskId) + { + try { + $this->taskId = $taskId; + $this->task = ScheduledTask::findOrFail($taskId); + $this->executions = $this->task->executions()->take(20)->get(); + $this->serverTimezone = data_get($this->task, 'application.destination.server.settings.server_timezone'); + if (! $this->serverTimezone) { + $this->serverTimezone = data_get($this->task, 'service.destination.server.settings.server_timezone'); + } + if (! $this->serverTimezone) { + $this->serverTimezone = 'UTC'; + } + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshExecutions(): void + { + $this->executions = $this->task->executions()->take(20)->get(); + } + public function selectTask($key): void { if ($key == $this->selectedKey) { @@ -29,38 +66,9 @@ class Executions extends Component $this->selectedKey = $key; } - public function server() - { - if (! $this->task) { - return null; - } - - if ($this->task->application) { - if ($this->task->application->destination && $this->task->application->destination->server) { - return $this->task->application->destination->server; - } - } elseif ($this->task->service) { - if ($this->task->service->destination && $this->task->service->destination->server) { - return $this->task->service->destination->server; - } - } - - return null; - } - - public function getServerTimezone() - { - $server = $this->server(); - if (! $server) { - return 'UTC'; - } - - return $server->settings->server_timezone; - } - public function formatDateInServerTimezone($date) { - $serverTimezone = $this->getServerTimezone(); + $serverTimezone = $this->serverTimezone; $dateObj = new \DateTime($date); try { $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 36194edb7..0900a1d70 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -2,74 +2,124 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Jobs\ScheduledTaskJob; use App\Models\Application; -use App\Models\ScheduledTask as ModelsScheduledTask; +use App\Models\ScheduledTask; use App\Models\Service; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { - public $parameters; - public Application|Service $resource; - public ModelsScheduledTask $task; + public ScheduledTask $task; - public ?string $modalId = null; + #[Locked] + public array $parameters; + #[Locked] public string $type; - public string $scheduledTaskName; + #[Validate(['boolean'])] + public bool $isEnabled = false; - protected $rules = [ - 'task.enabled' => 'required|boolean', - 'task.name' => 'required|string', - 'task.command' => 'required|string', - 'task.frequency' => 'required|string', - 'task.container' => 'nullable|string', - ]; + #[Validate(['string', 'required'])] + public string $name; - protected $validationAttributes = [ - 'name' => 'name', - 'command' => 'command', - 'frequency' => 'frequency', - 'container' => 'container', - ]; + #[Validate(['string', 'required'])] + public string $command; - public function mount() + #[Validate(['string', 'required'])] + public string $frequency; + + #[Validate(['string', 'nullable'])] + public ?string $container = null; + + #[Locked] + public ?string $application_uuid; + + #[Locked] + public ?string $service_uuid; + + #[Locked] + public string $task_uuid; + + public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null) { - $this->parameters = get_route_parameters(); + try { + $this->task_uuid = $task_uuid; + if ($application_uuid) { + $this->type = 'application'; + $this->application_uuid = $application_uuid; + $this->resource = Application::ownedByCurrentTeam()->where('uuid', $application_uuid)->firstOrFail(); + } elseif ($service_uuid) { + $this->type = 'service'; + $this->service_uuid = $service_uuid; + $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail(); + } + $this->parameters = [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'application_uuid' => $application_uuid, + 'service_uuid' => $service_uuid, + ]; - if (data_get($this->parameters, 'application_uuid')) { - $this->type = 'application'; - $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->type = 'service'; - $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); + $this->task = $this->resource->scheduled_tasks()->where('uuid', $task_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Exception $e) { + return handleError($e); } + } - $this->modalId = new Cuid2; - $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); - $this->scheduledTaskName = $this->task->name; + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->task->enabled = $this->isEnabled; + $this->task->name = str($this->name)->trim()->value(); + $this->task->command = str($this->command)->trim()->value(); + $this->task->frequency = str($this->frequency)->trim()->value(); + $this->task->container = str($this->container)->trim()->value(); + $this->task->save(); + } else { + $this->isEnabled = $this->task->enabled; + $this->name = $this->task->name; + $this->command = $this->task->command; + $this->frequency = $this->task->frequency; + $this->container = $this->task->container; + } } public function instantSave() { - $this->validateOnly('task.enabled'); - $this->task->save(['enabled' => $this->task->enabled]); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + $this->refreshTasks(); + } catch (\Exception $e) { + return handleError($e); + } } public function submit() { - $this->validate(); - $this->task->name = str($this->task->name)->trim()->value(); - $this->task->container = str($this->task->container)->trim()->value(); - $this->task->save(); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshTasks() + { + try { + $this->task->refresh(); + } catch (\Exception $e) { + return handleError($e); + } } public function delete() @@ -78,12 +128,22 @@ class Show extends Component $this->task->delete(); if ($this->type === 'application') { - return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName); + return redirect()->route('project.application.configuration', $this->parameters, $this->task->name); } else { - return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName); + return redirect()->route('project.service.configuration', $this->parameters, $this->task->name); } } catch (\Exception $e) { return handleError($e); } } + + public function executeNow() + { + try { + ScheduledTaskJob::dispatch($this->task); + $this->dispatch('success', 'Scheduled task executed.'); + } catch (\Exception $e) { + return handleError($e); + } + } } diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index 1082f078c..2335519c7 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -2,27 +2,46 @@ namespace App\Livewire\Project; +use App\Models\Environment; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Project $project; - public $environments; + #[Validate(['required', 'string', 'min:3'])] + public string $name; - public function mount() + #[Validate(['nullable', 'string'])] + public ?string $description = null; + + public function mount(string $project_uuid) { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; - - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); } + } - $this->environments = $project->environments->sortBy('created_at'); - $this->project = $project; + public function submit() + { + try { + $this->validate(); + $environment = Environment::create([ + 'name' => $this->name, + 'project_id' => $this->project->id, + ]); + + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project->uuid, + 'environment_name' => $environment->name, + ]); + } catch (\Throwable $e) { + handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index becae9f04..0852abebf 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -4,7 +4,7 @@ namespace App\Livewire\Server; use App\Jobs\DockerCleanupJob; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component @@ -13,28 +13,28 @@ class Advanced extends Component public array $parameters = []; - #[Rule(['integer', 'min:1'])] + #[Validate(['integer', 'min:1'])] public int $concurrentBuilds = 1; - #[Rule(['integer', 'min:1'])] + #[Validate(['integer', 'min:1'])] public int $dynamicTimeout = 1; - #[Rule('boolean')] + #[Validate('boolean')] public bool $forceDockerCleanup = false; - #[Rule('string')] + #[Validate('string')] public string $dockerCleanupFrequency = '*/10 * * * *'; - #[Rule(['integer', 'min:1', 'max:99'])] + #[Validate(['integer', 'min:1', 'max:99'])] public int $dockerCleanupThreshold = 10; - #[Rule(['integer', 'min:1', 'max:99'])] + #[Validate(['integer', 'min:1', 'max:99'])] public int $serverDiskUsageNotificationThreshold = 50; - #[Rule('boolean')] + #[Validate('boolean')] public bool $deleteUnusedVolumes = false; - #[Rule('boolean')] + #[Validate('boolean')] public bool $deleteUnusedNetworks = false; public function mount(string $server_uuid) diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php index f16962bca..311196473 100644 --- a/app/Livewire/Server/CloudflareTunnels.php +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -3,14 +3,14 @@ namespace App\Livewire\Server; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class CloudflareTunnels extends Component { public Server $server; - #[Rule(['required', 'boolean'])] + #[Validate(['required', 'boolean'])] public bool $isCloudflareTunnelsEnabled; public function mount(string $server_uuid) diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destinations.php similarity index 94% rename from app/Livewire/Server/Destination/Show.php rename to app/Livewire/Server/Destinations.php index 71c051a74..dbab6e03f 100644 --- a/app/Livewire/Server/Destination/Show.php +++ b/app/Livewire/Server/Destinations.php @@ -1,6 +1,6 @@ networks = collect(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); - loggy($this->server); } catch (\Throwable $e) { return handleError($e, $this); } @@ -86,6 +85,6 @@ class Show extends Component public function render() { - return view('livewire.server.destination.show'); + return view('livewire.server.destinations'); } } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php deleted file mode 100644 index 740421373..000000000 --- a/app/Livewire/Server/Form.php +++ /dev/null @@ -1,281 +0,0 @@ -user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured', - 'refreshServerShow' => 'serverInstalled', - 'revalidate' => '$refresh', - ]; - } - - protected $rules = [ - 'server.name' => 'required', - 'server.description' => 'nullable', - 'server.ip' => 'required', - 'server.user' => 'required', - 'server.port' => 'required', - 'wildcard_domain' => 'nullable|url', - 'server.settings.is_reachable' => 'required', - 'server.settings.is_swarm_manager' => 'required|boolean', - 'server.settings.is_swarm_worker' => 'required|boolean', - 'server.settings.is_build_server' => 'required|boolean', - 'server.settings.is_metrics_enabled' => 'required|boolean', - 'server.settings.sentinel_token' => 'required', - 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1', - 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1', - 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10', - 'server.settings.sentinel_custom_url' => 'nullable|url', - 'server.settings.is_sentinel_enabled' => 'required|boolean', - 'server.settings.is_sentinel_debug_enabled' => 'required|boolean', - 'server.settings.server_timezone' => 'required|string|timezone', - ]; - - protected $validationAttributes = [ - 'server.name' => 'Name', - 'server.description' => 'Description', - 'server.ip' => 'IP address/Domain', - 'server.user' => 'User', - 'server.port' => 'Port', - 'server.settings.is_reachable' => 'Is reachable', - 'server.settings.is_swarm_manager' => 'Swarm Manager', - 'server.settings.is_swarm_worker' => 'Swarm Worker', - 'server.settings.is_build_server' => 'Build Server', - 'server.settings.is_metrics_enabled' => 'Metrics', - 'server.settings.sentinel_token' => 'Metrics Token', - 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval', - 'server.settings.sentinel_metrics_history_days' => 'Metrics History', - 'server.settings.sentinel_push_interval_seconds' => 'Push Interval', - 'server.settings.is_sentinel_enabled' => 'Server API', - 'server.settings.is_sentinel_debug_enabled' => 'Debug', - 'server.settings.sentinel_custom_url' => 'Coolify URL', - 'server.settings.server_timezone' => 'Server Timezone', - ]; - - public function mount(Server $server) - { - $this->server = $server; - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); - $this->wildcard_domain = $this->server->settings->wildcard_domain; - } - - public function checkSyncStatus() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function regenerateSentinelToken() - { - try { - $this->server->settings->generateSentinelToken(); - $this->server->settings->refresh(); - // $this->restartSentinel(notification: false); - $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function updated($field) - { - if ($field === 'server.settings.docker_cleanup_frequency') { - $frequency = $this->server->settings->docker_cleanup_frequency; - if (! validate_cron_expression($frequency)) { - $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); - $this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; - } - } - } - - public function cloudflareTunnelConfigured() - { - $this->serverInstalled(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - } - - public function serverInstalled() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function updatedServerSettingsIsBuildServer() - { - $this->dispatch('refreshServerShow'); - $this->dispatch('serverRefresh'); - $this->dispatch('proxyStatusUpdated'); - } - - public function updatedServerSettingsIsSentinelEnabled($value) - { - $this->validate([ - 'server.settings.sentinel_custom_url' => 'required|url', - ]); - if ($value === false) { - StopSentinel::dispatch($this->server); - $this->server->settings->is_metrics_enabled = false; - $this->server->settings->save(); - $this->server->sentinelHeartbeat(isReset: true); - } else { - try { - StartSentinel::run($this->server); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - } - - public function updatedServerSettingsIsMetricsEnabled() - { - $this->restartSentinel(); - } - - public function updatedServerSettingsIsSentinelDebugEnabled() - { - $this->restartSentinel(); - } - - public function instantSave() - { - try { - $this->validate(); - refresh_server_connection($this->server->privateKey); - $this->validateServer(false); - - $this->server->settings->save(); - $this->server->save(); - $this->dispatch('success', 'Server updated.'); - $this->dispatch('refreshServerShow'); - } catch (\Throwable $e) { - $this->server->settings->refresh(); - - return handleError($e, $this); - } finally { - } - } - - public function saveSentinel() - { - try { - $this->validate(); - $this->server->settings->save(); - $this->dispatch('success', 'Sentinel updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->checkSyncStatus(); - } - } - - public function restartSentinel($notification = true) - { - try { - $this->validate(); - $this->validate([ - 'server.settings.sentinel_custom_url' => 'required|url', - ]); - $this->server->settings->save(); - $this->server->restartSentinel(async: false); - if ($notification) { - $this->dispatch('success', 'Sentinel restarted.'); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function revalidate() - { - $this->revalidate = true; - } - - public function checkLocalhostConnection() - { - $this->submit(); - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - $this->server->settings->is_reachable = true; - $this->server->settings->is_usable = true; - $this->server->settings->save(); - $this->dispatch('proxyStatusUpdated'); - } else { - $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); - - return; - } - } - - public function validateServer($install = true) - { - $this->server->update([ - 'validation_logs' => null, - ]); - $this->dispatch('init', $install); - } - - public function submit() - { - try { - if (isCloud() && ! isDev()) { - $this->validate(); - $this->validate([ - 'server.ip' => 'required', - ]); - } else { - $this->validate(); - } - $uniqueIPs = Server::all()->reject(function (Server $server) { - return $server->id === $this->server->id; - })->pluck('ip')->toArray(); - if (in_array($this->server->ip, $uniqueIPs)) { - $this->dispatch('error', 'IP address is already in use by another team.'); - - return; - } - refresh_server_connection($this->server->privateKey); - $this->server->settings->wildcard_domain = $this->wildcard_domain; - $currentTimezone = $this->server->settings->getOriginal('server_timezone'); - $newTimezone = $this->server->settings->server_timezone; - if ($currentTimezone !== $newTimezone || $currentTimezone === '') { - $this->server->settings->server_timezone = $newTimezone; - } - $this->server->settings->save(); - $this->server->save(); - - $this->dispatch('success', 'Server updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index fb8ef329f..6599149c4 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -5,38 +5,38 @@ namespace App\Livewire\Server; use App\Actions\Server\StartLogDrain; use App\Actions\Server\StopLogDrain; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class LogDrains extends Component { public Server $server; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainNewRelicEnabled = false; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainCustomEnabled = false; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainAxiomEnabled = false; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainNewRelicLicenseKey = null; - #[Rule(['url', 'nullable'])] + #[Validate(['url', 'nullable'])] public ?string $logDrainNewRelicBaseUri = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainAxiomDatasetName = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainAxiomApiKey = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainCustomConfig = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainCustomConfigParser = null; public function mount(string $server_uuid) @@ -52,7 +52,7 @@ class LogDrains extends Component public function syncData(bool $toModel = false) { if ($toModel) { - $this->validate(); + $this->customValidation(); $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; @@ -79,6 +79,44 @@ class LogDrains extends Component } } + public function customValidation() + { + if ($this->isLogDrainNewRelicEnabled) { + try { + $this->validate([ + 'logDrainNewRelicLicenseKey' => ['required'], + 'logDrainNewRelicBaseUri' => ['required', 'url'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainNewRelicEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainAxiomEnabled) { + try { + $this->validate([ + 'logDrainAxiomDatasetName' => ['required'], + 'logDrainAxiomApiKey' => ['required'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainAxiomEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainCustomEnabled) { + try { + $this->validate([ + 'logDrainCustomConfig' => ['required'], + 'logDrainCustomConfigParser' => ['string', 'nullable'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainCustomEnabled = false; + + throw $e; + } + } + } + public function instantSave() { try { diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 2e4b67cc6..aea07efe0 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -5,79 +5,81 @@ namespace App\Livewire\Server; use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Server $server; - #[Rule(['required'])] + #[Validate(['required'])] public string $name; - #[Rule(['nullable'])] - public ?string $description; + #[Validate(['nullable'])] + public ?string $description = null; - #[Rule(['required'])] + #[Validate(['required'])] public string $ip; - #[Rule(['required'])] + #[Validate(['required'])] public string $user; - #[Rule(['required'])] + #[Validate(['required'])] public string $port; - #[Rule(['nullable'])] + #[Validate(['nullable'])] public ?string $validationLogs = null; - #[Rule(['nullable', 'url'])] - public ?string $wildcardDomain; + #[Validate(['nullable', 'url'])] + public ?string $wildcardDomain = null; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isReachable; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isUsable; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSwarmManager; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSwarmWorker; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isBuildServer; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isMetricsEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public string $sentinelToken; - #[Rule(['nullable'])] - public ?string $sentinelUpdatedAt; + #[Validate(['nullable'])] + public ?string $sentinelUpdatedAt = null; - #[Rule(['required', 'integer', 'min:1'])] + #[Validate(['required', 'integer', 'min:1'])] public int $sentinelMetricsRefreshRateSeconds; - #[Rule(['required', 'integer', 'min:1'])] + #[Validate(['required', 'integer', 'min:1'])] public int $sentinelMetricsHistoryDays; - #[Rule(['required', 'integer', 'min:10'])] + #[Validate(['required', 'integer', 'min:10'])] public int $sentinelPushIntervalSeconds; - #[Rule(['nullable', 'url'])] - public ?string $sentinelCustomUrl; + #[Validate(['nullable', 'url'])] + public ?string $sentinelCustomUrl = null; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSentinelEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSentinelDebugEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public string $serverTimezone; + #[Locked] public array $timezones; public function getListeners() @@ -85,8 +87,8 @@ class Show extends Component $teamId = auth()->user()->currentTeam()->id; return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => '$refresh', - 'refreshServerShow' => '$refresh', + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', + 'refreshServerShow' => 'refresh', ]; } @@ -151,6 +153,12 @@ class Show extends Component } } + public function refresh() + { + $this->syncData(); + $this->dispatch('$refresh'); + } + public function validateServer($install = true) { try { diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 0a6c5bae6..2991b8ae8 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -8,7 +8,7 @@ use App\Models\Server; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Locked; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class Index extends Component @@ -20,58 +20,58 @@ class Index extends Component #[Locked] public $timezones; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_auto_update_enabled; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $fqdn = null; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $resale_license = null; - #[Rule('required|integer|min:1025|max:65535')] + #[Validate('required|integer|min:1025|max:65535')] public int $public_port_min; - #[Rule('required|integer|min:1025|max:65535')] + #[Validate('required|integer|min:1025|max:65535')] public int $public_port_max; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $custom_dns_servers = null; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $instance_name = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $allowed_ips = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $public_ipv4 = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $public_ipv6 = null; - #[Rule('string')] + #[Validate('string')] public string $auto_update_frequency; - #[Rule('string')] + #[Validate('string')] public string $update_check_frequency; - #[Rule('required|string|timezone')] + #[Validate('required|string|timezone')] public string $instance_timezone; - #[Rule('boolean')] + #[Validate('boolean')] public bool $do_not_track; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_registration_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_dns_validation_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_api_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $disable_two_step_confirmation; public function render() diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 38f7e548a..6dc5d6ab3 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -8,7 +8,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; use Livewire\Attributes\Locked; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsBackup extends Component @@ -25,19 +25,19 @@ class SettingsBackup extends Component #[Locked] public $executions = []; - #[Rule(['required'])] + #[Validate(['required'])] public string $uuid; - #[Rule(['required'])] + #[Validate(['required'])] public string $name; - #[Rule(['nullable'])] + #[Validate(['nullable'])] public ?string $description = null; - #[Rule(['required'])] + #[Validate(['required'])] public string $postgres_user; - #[Rule(['required'])] + #[Validate(['required'])] public string $postgres_password; public function mount() diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index fe329f270..0ab5754f2 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -3,102 +3,83 @@ namespace App\Livewire; use App\Models\InstanceSettings; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsEmail extends Component { public InstanceSettings $settings; - public string $emails; + #[Validate(['boolean'])] + public bool $smtpEnabled = false; - protected $rules = [ - 'settings.smtp_enabled' => 'nullable|boolean', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_enabled' => 'nullable|boolean', - 'settings.resend_api_key' => 'nullable', + #[Validate(['nullable', 'string'])] + public ?string $smtpHost = null; - ]; + #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] + public ?int $smtpPort = null; - protected $validationAttributes = [ - 'settings.smtp_from_address' => 'From Address', - 'settings.smtp_from_name' => 'From Name', - 'settings.smtp_recipients' => 'Recipients', - 'settings.smtp_host' => 'Host', - 'settings.smtp_port' => 'Port', - 'settings.smtp_encryption' => 'Encryption', - 'settings.smtp_username' => 'Username', - 'settings.smtp_password' => 'Password', - 'settings.smtp_timeout' => 'Timeout', - 'settings.resend_api_key' => 'Resend API Key', - ]; + #[Validate(['nullable', 'string'])] + public ?string $smtpEncryption = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpUsername = null; + + #[Validate(['nullable'])] + public ?string $smtpPassword = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpTimeout = null; + + #[Validate(['nullable', 'email'])] + public ?string $smtpFromAddress = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpFromName = null; + + #[Validate(['boolean'])] + public bool $resendEnabled = false; + + #[Validate(['nullable', 'string'])] + public ?string $resendApiKey = null; public function mount() { - if (isInstanceAdmin()) { - $this->settings = instanceSettings(); - $this->emails = auth()->user()->email; - } else { + if (isInstanceAdmin() === false) { return redirect()->route('dashboard'); } + $this->settings = instanceSettings(); + $this->syncData(); } - public function submitFromFields() + public function syncData(bool $toModel = false) { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - ]); + if ($toModel) { + $this->validate(); + $this->settings->smtp_enabled = $this->smtpEnabled; + $this->settings->smtp_host = $this->smtpHost; + $this->settings->smtp_port = $this->smtpPort; + $this->settings->smtp_encryption = $this->smtpEncryption; + $this->settings->smtp_username = $this->smtpUsername; + $this->settings->smtp_password = $this->smtpPassword; + $this->settings->smtp_timeout = $this->smtpTimeout; + + $this->settings->resend_enabled = $this->resendEnabled; + $this->settings->resend_api_key = $this->resendApiKey; $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } + } else { + $this->smtpEnabled = $this->settings->smtp_enabled; + $this->smtpHost = $this->settings->smtp_host; + $this->smtpPort = $this->settings->smtp_port; + $this->smtpEncryption = $this->settings->smtp_encryption; + $this->smtpUsername = $this->settings->smtp_username; + $this->smtpPassword = $this->settings->smtp_password; + $this->smtpTimeout = $this->settings->smtp_timeout; + $this->smtpFromAddress = $this->settings->smtp_from_address; + $this->smtpFromName = $this->settings->smtp_from_name; - public function submitResend() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_api_key' => 'required', - ]); - $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->settings->resend_enabled = false; - - return handleError($e, $this); - } - } - - public function instantSaveResend() - { - try { - $this->settings->smtp_enabled = false; - $this->submitResend(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->settings->resend_enabled = false; - $this->submit(); - } catch (\Throwable $e) { - return handleError($e, $this); + $this->resendEnabled = $this->settings->resend_enabled; + $this->resendApiKey = $this->settings->resend_api_key; } } @@ -106,20 +87,29 @@ class SettingsEmail extends Component { try { $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - ]); - $this->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { return handleError($e, $this); } } + + public function instantSave(string $type) + { + try { + if ($type === 'SMTP') { + $this->resendEnabled = false; + } else { + $this->smtpEnabled = false; + } + $this->syncData(true); + if ($this->smtpEnabled || $this->resendEnabled) { + $this->dispatch('success', "{$type} enabled."); + } else { + $this->dispatch('success', "{$type} disabled."); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } } diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php index 9186cc978..6b2d3fb36 100644 --- a/app/Livewire/Subscription/PricingPlans.php +++ b/app/Livewire/Subscription/PricingPlans.php @@ -2,6 +2,7 @@ namespace App\Livewire\Subscription; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Stripe\Checkout\Session; use Stripe\Stripe; @@ -26,7 +27,7 @@ class PricingPlans extends Component $payload = [ 'allow_promotion_codes' => true, 'billing_address_collection' => 'required', - 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id, + 'client_reference_id' => Auth::id().':'.currentTeam()->id, 'line_items' => [[ 'price' => $priceId, 'adjustable_quantity' => [ @@ -43,7 +44,7 @@ class PricingPlans extends Component ], 'subscription_data' => [ 'metadata' => [ - 'user_id' => auth()->user()->id, + 'user_id' => Auth::id(), 'team_id' => currentTeam()->id, ], ], @@ -60,7 +61,7 @@ class PricingPlans extends Component 'name' => 'auto', ]; } else { - $payload['customer_email'] = auth()->user()->email; + $payload['customer_email'] = Auth::user()->email; } $session = Session::create($payload); diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php deleted file mode 100644 index 116f19e4e..000000000 --- a/app/Livewire/Tags/Index.php +++ /dev/null @@ -1,82 +0,0 @@ - 'updateDeployments']; - - public function render() - { - return view('livewire.tags.index'); - } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tagUpdated(); - } - } - - public function updateDeployments($deployments) - { - $this->deploymentsPerTagPerServer = $deployments; - } - - public function tagUpdated() - { - if ($this->tag === '') { - return; - } - $sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8'); - $tag = $this->tags->where('name', $sanitizedTag)->first(); - if (! $tag) { - $this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.'); - $this->tag = ''; - - return; - } - $this->webhook = generateTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - } - - public function redeployAll() - { - try { - $this->applications->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->services->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->dispatch('success', 'Mass deployment started.'); - } catch (\Exception $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 0dffcce57..fc5b13374 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,43 +5,57 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; use Livewire\Attributes\Title; use Livewire\Component; #[Title('Tags | Coolify')] class Show extends Component { - public $tags; + #[Locked] + public ?string $tagName = null; - public Tag $tag; + #[Locked] + public ?Collection $tags = null; - public $applications; + #[Locked] + public ?Tag $tag = null; - public $services; + #[Locked] + public ?Collection $applications = null; - public $webhook = null; + #[Locked] + public ?Collection $services = null; - public $deployments_per_tag_per_server = []; + #[Locked] + public ?string $webhook = null; + + #[Locked] + public ?array $deploymentsPerTagPerServer = null; public function mount() { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - $tag = $this->tags->where('name', request()->tag_name)->first(); - if (! $tag) { - return redirect()->route('tags.index'); + try { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if (str($this->tagName)->isNotEmpty()) { + $tag = $this->tags->where('name', $this->tagName)->first(); + $this->webhook = generateTagDeployWebhook($tag->name); + $this->applications = $tag->applications()->get(); + $this->services = $tag->services()->get(); + $this->tag = $tag; + $this->getDeployments(); + } + } catch (\Exception $e) { + return handleError($e, $this); } - $this->webhook = generateTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - $this->tag = $tag; - $this->get_deployments(); } - public function get_deployments() + public function getDeployments() { try { $resource_ids = $this->applications->pluck('id'); - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ 'id', 'application_id', 'application_name', @@ -56,7 +70,7 @@ class Show extends Component } } - public function redeploy_all() + public function redeployAll() { try { $message = collect([]); diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index c9dabcb5c..cfb47d9d8 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -74,6 +74,9 @@ class AdminView extends Component public function delete($id, $password) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php index 992833da5..f805d6122 100644 --- a/app/Livewire/Team/Create.php +++ b/app/Livewire/Team/Create.php @@ -3,28 +3,21 @@ namespace App\Livewire\Team; use App\Models\Team; +use Livewire\Attributes\Validate; use Livewire\Component; class Create extends Component { + #[Validate(['required', 'min:3', 'max:255'])] public string $name = ''; + #[Validate(['nullable', 'min:3', 'max:255'])] public ?string $description = null; - protected $rules = [ - 'name' => 'required|min:3|max:255', - 'description' => 'nullable|min:3|max:255', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'description' => 'description', - ]; - public function submit() { - $this->validate(); try { + $this->validate(); $team = Team::create([ 'name' => $this->name, 'description' => $this->description, diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 45600dbfe..0972e7364 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -4,6 +4,7 @@ namespace App\Livewire\Team; use App\Models\Team; use App\Models\TeamInvitation; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -55,7 +56,7 @@ class Index extends Component $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === auth()->user()->id) { + if ($user->id === Auth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Models/Application.php b/app/Models/Application.php index cd7bb533e..0ef787b2e 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -117,14 +117,31 @@ class Application extends BaseModel if ($application->fqdn === '') { $application->fqdn = null; } - $application->forceFill([ - 'fqdn' => $application->fqdn, - 'install_command' => str($application->install_command)->trim(), - 'build_command' => str($application->build_command)->trim(), - 'start_command' => str($application->start_command)->trim(), - 'base_directory' => str($application->base_directory)->trim(), - 'publish_directory' => str($application->publish_directory)->trim(), - ]); + $payload = []; + if ($application->isDirty('fqdn')) { + $payload['fqdn'] = $application->fqdn; + } + if ($application->isDirty('install_command')) { + $payload['install_command'] = str($application->install_command)->trim(); + } + if ($application->isDirty('build_command')) { + $payload['build_command'] = str($application->build_command)->trim(); + } + if ($application->isDirty('start_command')) { + $payload['start_command'] = str($application->start_command)->trim(); + } + if ($application->isDirty('base_directory')) { + $payload['base_directory'] = str($application->base_directory)->trim(); + } + if ($application->isDirty('publish_directory')) { + $payload['publish_directory'] = str($application->publish_directory)->trim(); + } + if ($application->isDirty('status')) { + $payload['last_online_at'] = now(); + } + if (count($payload) > 0) { + $application->forceFill($payload); + } }); static::created(function ($application) { ApplicationSetting::create([ @@ -155,6 +172,11 @@ class Application extends BaseModel return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(bool $previewDeployments = false): array { $containers = $previewDeployments diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 04a0ab27e..bf2bf05bf 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel }); } }); + static::saving(function ($preview) { + if ($preview->isDirty('status')) { + $preview->forceFill(['last_online_at' => now()]); + } + }); } public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) diff --git a/app/Models/Project.php b/app/Models/Project.php index 3a09b0b8f..f27e6c208 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -122,9 +122,18 @@ class Project extends BaseModel return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } - public function resource_count() + public function isEmpty() { - return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count(); + return $this->applications()->count() == 0 && + $this->redis()->count() == 0 && + $this->postgresqls()->count() == 0 && + $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && + $this->mariadbs()->count() == 0 && + $this->mongodbs()->count() == 0 && + $this->services()->count() == 0; } public function databases() diff --git a/app/Models/Server.php b/app/Models/Server.php index 9527e8820..3076308ad 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -507,20 +507,6 @@ $schema://$host { return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); } - public function skipServer() - { - if ($this->ip === '1.2.3.4') { - // ray('skipping 1.2.3.4'); - return true; - } - if ($this->settings->force_disabled === true) { - // ray('force_disabled'); - return true; - } - - return false; - } - public function isForceDisabled() { return $this->settings->force_disabled; @@ -691,7 +677,7 @@ $schema://$host { } } } else { - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); + $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false); $containers = format_docker_command_output_to_json($containers); $containerReplicates = collect([]); } @@ -917,11 +903,23 @@ $schema://$host { return true; } + public function skipServer() + { + if ($this->ip === '1.2.3.4') { + return true; + } + if ($this->settings->force_disabled === true) { + return true; + } + + return false; + } + public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; + $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4'; - if (! $isFunctional) { + if ($isFunctional === false) { Storage::disk('ssh-mux')->delete($this->muxFilename()); } @@ -988,9 +986,6 @@ $schema://$host { public function status(): bool { - if ($this->skipServer()) { - return false; - } ['uptime' => $uptime] = $this->validateConnection(false); if ($uptime === false) { foreach ($this->applications() as $application) { @@ -1234,7 +1229,7 @@ $schema://$host { return str($this->ip)->contains(':'); } - public function restartSentinel(bool $async = true): void + public function restartSentinel(bool $async = true) { try { if ($async) { @@ -1243,7 +1238,7 @@ $schema://$host { StartSentinel::run($this, true); } } catch (\Throwable $e) { - loggy('Error restarting Sentinel: '.$e->getMessage()); + return handleError($e); } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index bca16536e..fc2c5a0f4 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; use OpenApi\Attributes as OA; #[OA\Schema( @@ -63,13 +64,13 @@ class ServerSetting extends Model static::creating(function ($setting) { try { if (str($setting->sentinel_token)->isEmpty()) { - $setting->generateSentinelToken(save: false); + $setting->generateSentinelToken(save: false, ignoreEvent: true); } if (str($setting->sentinel_custom_url)->isEmpty()) { - $setting->generateSentinelUrl(save: false); + $setting->generateSentinelUrl(save: false, ignoreEvent: true); } } catch (\Throwable $e) { - loggy('Error creating server setting: '.$e->getMessage()); + Log::error('Error creating server setting: '.$e->getMessage()); } }); static::updated(function ($settings) { @@ -88,7 +89,7 @@ class ServerSetting extends Model }); } - public function generateSentinelToken(bool $save = true) + public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false) { $data = [ 'server_uuid' => $this->server->uuid, @@ -97,13 +98,17 @@ class ServerSetting extends Model $encrypted = encrypt($token); $this->sentinel_token = $encrypted; if ($save) { - $this->save(); + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } } return $token; } - public function generateSentinelUrl(bool $save = true) + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) { $domain = null; $settings = InstanceSettings::get(); @@ -118,7 +123,11 @@ class ServerSetting extends Model } $this->sentinel_custom_url = $domain; if ($save) { - $this->save(); + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } } return $domain; diff --git a/app/Models/Service.php b/app/Models/Service.php index f88a23641..0c9e081a1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -133,6 +133,11 @@ class Service extends BaseModel return $this->morphToMany(Tag::class, 'taggable'); } + public static function ownedByCurrentTeam() + { + return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(): array { $containersToStop = []; diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 0e79e1e2e..5cafc9042 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); } public function restart() @@ -32,6 +37,11 @@ class ServiceApplication extends BaseModel return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function isRunning() { return str($this->status)->contains('running'); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 927527118..5fdd52637 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -17,6 +17,21 @@ class ServiceDatabase extends BaseModel $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); + } + + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } public function restart() diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index fa27d50d8..6d66c6854 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -38,6 +38,11 @@ class StandaloneClickhouse extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index fa0bd4b44..f7d83f0a3 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -38,6 +38,11 @@ class StandaloneDragonfly extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 56ee4d4a2..083c743d9 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -38,6 +38,11 @@ class StandaloneKeydb extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index c55a76af7..833dad6c4 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -38,6 +38,11 @@ class StandaloneMariadb extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 3f12a8557..dd8893180 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -42,6 +42,11 @@ class StandaloneMongodb extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 0d8359f88..710fea1bc 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -39,6 +39,11 @@ class StandaloneMysql extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index f31582c35..4a457a6cf 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -39,6 +39,11 @@ class StandalonePostgresql extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } public function workdir() diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 119978530..826bb951c 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -34,6 +34,11 @@ class StandaloneRedis extends BaseModel $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute diff --git a/app/Models/Team.php b/app/Models/Team.php index 5b4a80cb1..db485054b 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -165,7 +165,7 @@ class Team extends Model implements SendsDiscord, SendsEmail return 0; } - return data_get($team, 'limits.serverLimit', 0); + return data_get($team, 'limits', 0); } public function limits(): Attribute @@ -187,9 +187,8 @@ class Team extends Model implements SendsDiscord, SendsEmail } else { $serverLimit = config('constants.limits.server')[strtolower($subscription)]; } - $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; - return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; + return $serverLimit ?? 2; } ); diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index 0f298a829..bc1a90d58 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -28,8 +28,8 @@ class TeamInvitation extends Model public function isValid() { $createdAt = $this->created_at; - $diff = $createdAt->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { + $diff = $createdAt->diffInDays(now()); + if ($diff <= config('constants.invitation.link.expiration_days')) { return true; } else { $this->delete(); diff --git a/app/Models/User.php b/app/Models/User.php index ecc4ef6b6..25fb33d66 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; @@ -158,7 +159,7 @@ class User extends Authenticatable implements SendsEmail public function isAdminFromSession() { - if (auth()->user()->id === 0) { + if (Auth::id() === 0) { return true; } $teams = $this->teams()->get(); @@ -178,9 +179,9 @@ class User extends Authenticatable implements SendsEmail public function isInstanceAdmin() { - $found_root_team = auth()->user()->teams->filter(function ($team) { + $found_root_team = Auth::user()->teams->filter(function ($team) { if ($team->id == 0) { - if (! auth()->user()->isAdmin()) { + if (! Auth::user()->isAdmin()) { return false; } @@ -195,9 +196,9 @@ class User extends Authenticatable implements SendsEmail public function currentTeam() { - return Cache::remember('team:'.auth()->user()->id, 3600, function () { - if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) { - return auth()->user()->teams[0]; + return Cache::remember('team:'.Auth::id(), 3600, function () { + if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) { + return Auth::user()->teams[0]; } return Team::find(session('currentTeam')->id); @@ -206,7 +207,7 @@ class User extends Authenticatable implements SendsEmail public function otherTeams() { - return auth()->user()->teams->filter(function ($team) { + return Auth::user()->teams->filter(function ($team) { return $team->id != currentTeam()->id; }); } @@ -216,7 +217,7 @@ class User extends Authenticatable implements SendsEmail if (data_get($this, 'pivot')) { return $this->pivot->role; } - $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + $user = Auth::user()->teams->where('id', currentTeam()->id)->first(); return data_get($user, 'pivot.role'); } diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index 3a33d8902..86276fec9 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -17,6 +17,6 @@ class DiscordChannel if (! $webhookUrl) { return; } - dispatch(new SendMessageToDiscordJob($message, $webhookUrl)); + dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high'); } } diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index 4b1fa49dd..b3d4e384b 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -41,6 +41,6 @@ class TelegramChannel if (! $telegramToken || ! $chatId || ! $message) { return; } - dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId)); + dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high'); } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index b916b6234..e8784bab3 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -75,7 +75,8 @@ class FortifyServiceProvider extends ServiceProvider }); Fortify::authenticateUsing(function (Request $request) { - $user = User::where('email', $request->email)->with('teams')->first(); + $email = strtolower($request->email); + $user = User::where('email', $email)->with('teams')->first(); if ( $user && Hash::check($request->password, $user->password) diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index fbd7b0b15..7283ef20f 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -23,6 +23,8 @@ class Input extends Component public bool $isMultiline = false, public string $defaultClass = 'input', public string $autocomplete = 'off', + public ?int $minlength = null, + public ?int $maxlength = null, ) {} public function render(): View|Closure|string diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 3f887877c..6081c2a8a 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -30,7 +30,9 @@ class Textarea extends Component public bool $realtimeValidation = false, public bool $allowToPeak = true, public string $defaultClass = 'input scrollbar font-mono', - public string $defaultClassInput = 'input' + public string $defaultClassInput = 'input', + public ?int $minlength = null, + public ?int $maxlength = null, ) { // } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index b3e8011b9..eb331f8c2 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; - ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); + // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); if ($deployments->count() > $concurrent_builds) { return false; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 52435703c..2e583b94d 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -654,7 +654,7 @@ function isDatabaseImage(?string $image = null) return false; } -function convert_docker_run_to_compose(?string $custom_docker_run_options = null) +function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; $compose_options = collect([]); @@ -679,9 +679,17 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null '--privileged' => 'privileged', '--ip' => 'ip', '--shm-size' => 'shm_size', + '--gpus' => 'gpus', ]); foreach ($matches as $match) { $option = $match[1]; + if ($option === '--gpus') { + $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/'; + preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches); + $value = $device_matches[1] ?? 'all'; + $options[$option][] = $value; + $options[$option] = array_unique($options[$option]); + } if (isset($match[2]) && $match[2] !== '') { $value = $match[2]; $options[$option][] = $value; @@ -694,7 +702,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null $options = collect($options); // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js foreach ($options as $option => $value) { - // ray($option,$value); if (! data_get($mapping, $option)) { continue; } @@ -723,6 +730,28 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null if (! is_null($value) && is_array($value) && count($value) > 0) { $compose_options->put($mapping[$option], $value[0]); } + } elseif ($option === '--gpus') { + $payload = [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + ]; + if (! is_null($value) && is_array($value) && count($value) > 0) { + if (str($value[0]) != 'all') { + if (str($value[0])->contains(',')) { + $payload['device_ids'] = str($value[0])->explode(',')->toArray(); + } else { + $payload['device_ids'] = [$value[0]]; + } + } + } + ray($payload); + $compose_options->put('deploy', [ + 'resources' => [ + 'reservations' => [ + 'devices' => [$payload], + ], + ], + ]); } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { @@ -744,7 +773,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null return $compose_options->toArray(); } -function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network) +function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network) { $ipv4 = data_get($docker_run_options, 'ip.0'); $ipv6 = data_get($docker_run_options, 'ip6.0'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index fb4ae3699..f6875cc81 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -35,6 +35,7 @@ use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Process\Pool; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; @@ -100,12 +101,12 @@ function isInstanceAdmin() function currentTeam() { - return auth()?->user()?->currentTeam() ?? null; + return Auth::user()?->currentTeam() ?? null; } function showBoarding(): bool { - if (auth()->user()?->isMember()) { + if (Auth::user()?->isMember()) { return false; } @@ -114,14 +115,14 @@ function showBoarding(): bool function refreshSession(?Team $team = null): void { if (! $team) { - if (auth()->user()?->currentTeam()) { - $team = Team::find(auth()->user()->currentTeam()->id); + if (Auth::user()->currentTeam()) { + $team = Team::find(Auth::user()->currentTeam()->id); } else { - $team = User::find(auth()->user()->id)->teams->first(); + $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.auth()->user()->id); - Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) { + Cache::forget('team:'.Auth::id()); + Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); diff --git a/composer.lock b/composer.lock index fb0dcd018..5eb03b5fc 100644 --- a/composer.lock +++ b/composer.lock @@ -9113,16 +9113,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.1.5", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" + "reference": "5183b61657807099d98f3367bcccb850238b17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9", + "reference": "5183b61657807099d98f3367bcccb850238b17a9", "shasum": "" }, "require": { @@ -9170,7 +9170,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.7" }, "funding": [ { @@ -9186,7 +9186,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-11-06T09:02:46+00:00" }, { "name": "symfony/http-kernel", @@ -9384,16 +9384,16 @@ }, { "name": "symfony/mime", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" + "reference": "caa1e521edb2650b8470918dfe51708c237f0598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", + "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", + "reference": "caa1e521edb2650b8470918dfe51708c237f0598", "shasum": "" }, "require": { @@ -9448,7 +9448,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.5" + "source": "https://github.com/symfony/mime/tree/v7.1.6" }, "funding": [ { @@ -9464,7 +9464,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/options-resolver", @@ -10251,16 +10251,16 @@ }, { "name": "symfony/process", - "version": "v7.1.5", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5c03ee6369281177f07f7c68252a280beccba847" + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", - "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585", + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585", "shasum": "" }, "require": { @@ -10292,7 +10292,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.5" + "source": "https://github.com/symfony/process/tree/v7.1.7" }, "funding": [ { @@ -10308,7 +10308,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-11-06T09:25:12+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -14974,16 +14974,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.12", + "version": "v6.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826", + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826", "shasum": "" }, "require": { @@ -15047,7 +15047,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.12" + "source": "https://github.com/symfony/http-client/tree/v6.4.14" }, "funding": [ { @@ -15063,7 +15063,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:21:33+00:00" + "time": "2024-11-05T16:39:55+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/constants.php b/config/constants.php index 5792b358c..1bec2e3bf 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,6 +1,7 @@ '26.0', 'docs' => [ 'base_url' => 'https://coolify.io/docs', 'contact' => 'https://coolify.io/docs/contact', @@ -18,7 +19,7 @@ return [ 'invitation' => [ 'link' => [ 'base_url' => '/invitations/', - 'expiration' => 10, + 'expiration_days' => 3, ], ], 'services' => [ diff --git a/config/coolify.php b/config/coolify.php index f9878fff7..225dfe6fa 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -1,6 +1,7 @@ env('SENTRY_DSN'), 'docs' => 'https://coolify.io/docs/', 'contact' => 'https://coolify.io/docs/contact', 'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'), diff --git a/config/horizon.php b/config/horizon.php index 939d74883..6086b30da 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -197,6 +197,7 @@ return [ 'production' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), @@ -206,6 +207,7 @@ return [ 'local' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), diff --git a/config/sentry.php b/config/sentry.php index e8b6ab098..f0717020c 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568', + 'dsn' => config('coolify.sentry_dsn'), // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.361', + 'release' => '4.0.0-beta.362', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/telescope.php b/config/telescope.php index 2444ee8cf..c940bec8a 100644 --- a/config/telescope.php +++ b/config/telescope.php @@ -76,8 +76,8 @@ return [ */ 'queue' => [ - 'connection' => env('TELESCOPE_QUEUE_CONNECTION', null), - 'queue' => env('TELESCOPE_QUEUE', null), + 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'), + 'queue' => env('TELESCOPE_QUEUE', 'default'), ], /* diff --git a/config/version.php b/config/version.php index 0e83ff40e..ba46a4727 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ update(['metrics_history_days' => 7]); } catch (\Exception $e) { - loggy($e); + Log::error('Error updating db: '.$e->getMessage()); } } diff --git a/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php new file mode 100644 index 000000000..51b8fb3ba --- /dev/null +++ b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php @@ -0,0 +1,96 @@ +timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + + } +}; diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php index 117ba6782..3cf913933 100644 --- a/database/seeders/SentinelSeeder.php +++ b/database/seeders/SentinelSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; use App\Models\Server; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Log; class SentinelSeeder extends Seeder { @@ -13,17 +14,17 @@ class SentinelSeeder extends Seeder foreach ($servers as $server) { try { if (str($server->settings->sentinel_token)->isEmpty()) { - $server->settings->generateSentinelToken(); + $server->settings->generateSentinelToken(ignoreEvent: true); } if (str($server->settings->sentinel_custom_url)->isEmpty()) { - $url = $server->settings->generateSentinelUrl(); + $url = $server->settings->generateSentinelUrl(ignoreEvent: true); if (str($url)->isEmpty()) { $server->settings->is_sentinel_enabled = false; $server->settings->save(); } } } catch (\Throwable $e) { - loggy("Error: {$e->getMessage()}\n"); + Log::error('Error seeding sentinel: '.$e->getMessage()); } } }); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b15a109c3..80555e377 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -113,7 +113,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index ef2de82e9..d92dc6332 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -103,7 +103,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' pull_policy: always container_name: coolify-realtime restart: always diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 7aa9d8722..48f401da4 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.14.1 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.35.1 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.28.0 +ARG NIXPACKS_VERSION=1.29.0 USER root WORKDIR /artifacts diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index f0d6db906..9f94518f5 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -22,6 +22,4 @@ RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" - - ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json new file mode 100644 index 000000000..f5fd1ba18 --- /dev/null +++ b/docker/coolify-realtime/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "coolify-realtime", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "axios": "1.7.5", + "cookie": "1.0.1", + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 146e6b90a..faeb80f54 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -2,12 +2,12 @@ "private": true, "type": "module", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "cookie": "^0.7.0", + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "cookie": "1.0.1", "axios": "1.7.5", - "dotenv": "^16.4.5", - "node-pty": "^1.0.0", - "ws": "^8.17.0" + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" } } diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 22a5b143a..37e0481bb 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist -FROM node:20 as static-assets +FROM node:20 AS static-assets WORKDIR /app COPY . . COPY --from=base --chown=9999:9999 /var/www/html . @@ -45,6 +45,8 @@ RUN composer dump-autoload COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/ +RUN php artisan route:clear +RUN php artisan view:clear RUN php artisan route:cache RUN php artisan view:cache diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 04faf50ea..29ce52b77 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -5,7 +5,7 @@ set -e # Exit immediately if a command exits with a non-zero status ## $1 could be empty, so we need to disable this check #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -CDN="https://cdn.coollabs.io/coolify-nightly" +CDN="https://cdn.coollabs.io/coolify" DATE=$(date +"%Y%m%d-%H%M%S") VERSION="1.6" @@ -13,7 +13,7 @@ DOCKER_VERSION="26.0" # TODO: Ask for a user CURRENT_USER=$USER -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic @@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed) esac - echo -e "2. Check OpenSSH server configuration. " # Detect OpenSSH server @@ -262,9 +261,14 @@ if ! [ -x "$(command -v docker)" ]; then fi ;; *) - curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 + if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then + echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." + echo "Please install Docker manually." + exit 1 + fi + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 if ! [ -x "$(command -v docker)" ]; then - curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1 if ! [ -x "$(command -v docker)" ]; then echo " - Docker installation failed." echo " Maybe your OS is not supported?" @@ -287,7 +291,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon "log-opts": { "max-size": "10m", "max-file": "3" - } + }, + "default-address-pools": [ + {"base":"10.0.0.0/8","size":24} + ] } EOL cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify </dev/null 2>&1 +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" echo " - Coolify installed successfully." rm -f $ENV_FILE-$DATE diff --git a/other/nightly/versions.json b/other/nightly/versions.json index c04a3dee6..3ae51e8cb 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,19 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.354" + "version": "4.0.0-beta.362" }, "nightly": { - "version": "4.0.0-beta.355" + "version": "4.0.0-beta.363" }, "helper": { - "version": "1.0.2" + "version": "1.0.3" }, "realtime": { - "version": "1.0.3" + "version": "1.0.4" + }, + "sentinel": { + "version": "0.0.15" } } } diff --git a/public/svgs/firefox.svg b/public/svgs/firefox.svg new file mode 100644 index 000000000..9a6371d47 --- /dev/null +++ b/public/svgs/firefox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/hoarder.svg b/public/svgs/hoarder.svg new file mode 100644 index 000000000..6215461d2 --- /dev/null +++ b/public/svgs/hoarder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/postiz.svg b/public/svgs/postiz.svg new file mode 100644 index 000000000..6e3baa813 --- /dev/null +++ b/public/svgs/postiz.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/svgs/readeck.svg b/public/svgs/readeck.svg new file mode 100644 index 000000000..07f6e6157 --- /dev/null +++ b/public/svgs/readeck.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/svgs/wikijs.svg b/public/svgs/wikijs.svg new file mode 100644 index 000000000..52c4a790b --- /dev/null +++ b/public/svgs/wikijs.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index f5eb5778a..c8c4a499f 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -10,7 +10,7 @@ ])
$fullWidth, ])> @if (!$hideLabel) diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index fb206fac4..d832cb30d 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -41,8 +41,9 @@ @if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" - type="{{ $type }}" @disabled($disabled) - min="{{ $attributes->get('min') }}" max="{{ $attributes->get('max') }}" + type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}" + max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}" + maxlength="{{ $attributes->get('maxlength') }}" @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"> @endif diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index 24226ecdb..b3669e43d 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -51,8 +51,8 @@ type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}"> -