Merge branch 'next' into feat/deployment-token

This commit is contained in:
Kael
2024-11-04 23:33:26 +11:00
committed by GitHub
76 changed files with 1463 additions and 942 deletions

View File

@@ -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([]);

View File

@@ -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();

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Actions\Server;
use App\Models\Application;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class ResourcesCheck
{
use AsAction;
public function handle()
{
$seconds = 60;
try {
Application::where('last_online_at', '<', now()->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);
}
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace App\Actions\Server;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\ServerStorageCheckJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Arr;
use Lorisleiva\Actions\Concerns\AsAction;
class ServerCheck
{
use AsAction;
public Server $server;
public bool $isSentinel = false;
public $containers;
public $databases;
public function handle(Server $server, $data = null)
{
$this->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);
}
} else {
StartLogDrain::dispatch($this->server);
}
}
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));
}
}
}
}
}
}
}

View File

@@ -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;
@@ -41,6 +42,7 @@ class Init extends Command
$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')) {
@@ -92,6 +94,15 @@ class Init extends Command
}
}
private function update_user_emails()
{
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";
}
}
private function update_traefik_labels()
{
try {

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Actions\Server\ServerCheck;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Console\Command;
use Str;
class Weird extends Command
{
protected $signature = 'weird {--number=1} {--run}';
protected $description = 'Weird stuff';
public function handle()
{
try {
if (! isDev()) {
$this->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());
}
}
}

View File

@@ -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;
@@ -31,7 +32,7 @@ class Kernel extends ConsoleKernel
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();
@@ -41,13 +42,16 @@ class Kernel extends ConsoleKernel
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->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();
@@ -57,9 +61,11 @@ class Kernel extends ConsoleKernel
$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,7 +75,7 @@ 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) {
@@ -103,23 +109,33 @@ 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();
}
// $schedule->job(new \App\Jobs\ResourcesCheck)->everyMinute()->onOneServer();
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 +150,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,7 +163,7 @@ class Kernel extends ConsoleKernel
$server = $scheduled_backup->server();
if (! $server) {
if (is_null($server)) {
continue;
}
$serverTimezone = $server->settings->server_timezone;

View File

@@ -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.');

View File

@@ -39,7 +39,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()) {

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Jobs;
use App\Actions\Server\ServerCheck;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function __construct(public Server $server) {}
public function handle()
{
try {
ServerCheck::run($this->server);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -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');

View File

@@ -3,16 +3,19 @@
namespace App\Livewire\Admin;
use App\Models\User;
use Illuminate\Support\Collection;
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()
{
@@ -29,39 +32,21 @@ 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)

View File

@@ -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',

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Livewire\Destination;
use Livewire\Component;
class Form extends Component
{
public mixed $destination;
protected $rules = [
'destination.name' => '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);
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Livewire\Destination;
use App\Models\Server;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Index extends Component
{
#[Locked]
public $servers;
public function mount()
{
$this->servers = Server::isUsable()->get();
}
public function render()
{
return view('livewire.destination.index');
}
}

View File

@@ -3,111 +3,89 @@
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\Rule;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Docker extends Component
{
#[Locked]
public $servers;
#[Locked]
public Server $selectedServer;
#[Rule(['required', 'string'])]
public string $name;
#[Rule(['required', 'string'])]
public string $network;
public ?Collection $servers = null;
#[Rule(['required', 'string'])]
public string $serverId;
public Server $server;
#[Rule(['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');
} 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;
$this->servers = Server::isUsable()->get();
if ($server_id) {
$this->selectedServer = $this->servers->find($server_id);
} else {
$this->selectedServer = $this->servers->first();
}
if ($this->servers->count() > 0) {
$this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();
}
$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);
}
}

View File

@@ -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\Rule;
use Livewire\Component;
class Show extends Component
{
public Server $server;
#[Locked]
public $destination;
public Collection|array $networks = [];
#[Rule(['string', 'required'])]
public string $name;
private function createNetworkAndAttachToProxy()
#[Rule(['string', 'required'])]
public string $network;
#[Rule(['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();
}
} 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();
}
}
public function scan()
{
if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers;
} else {
$alreadyAddedNetworks = $this->server->standaloneDockers;
}
$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;
if ($ownedByTeam === false) {
return redirect()->route('destination.index');
}
$this->dispatch('success', 'Scan done.');
$this->destination = $destination;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->destination->name = $this->name;
$this->destination->network = $this->network;
$this->destination->server->ip = $this->serverIp;
$this->destination->save();
} else {
$this->name = $this->destination->name;
$this->network = $this->destination->network;
$this->serverIp = $this->destination->server->ip;
}
}
public function submit()
{
try {
$this->syncData(true);
$this->dispatch('success', 'Destination saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
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');
}
}

View File

@@ -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\Rule;
use Livewire\Component;
class Help extends Component
{
use WithRateLimiting;
#[Rule(['required', 'min:10', 'max:1000'])]
public string $description;
#[Rule(['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.'`',
]);

View File

@@ -4,47 +4,94 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Rule;
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',
];
#[Rule(['boolean'])]
public bool $discordEnabled = false;
protected $validationAttributes = [
'team.discord_webhook_url' => 'Discord Webhook',
];
#[Rule(['url', 'nullable'])]
public ?string $discordWebhookUrl = null;
#[Rule(['boolean'])]
public bool $discordNotificationsTest = false;
#[Rule(['boolean'])]
public bool $discordNotificationsDeployments = false;
#[Rule(['boolean'])]
public bool $discordNotificationsStatusChanges = false;
#[Rule(['boolean'])]
public bool $discordNotificationsDatabaseBackups = false;
#[Rule(['boolean'])]
public bool $discordNotificationsScheduledTasks = false;
#[Rule(['boolean'])]
public bool $discordNotificationsServerDiskUsage = false;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
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;
try {
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} 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 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()
{
try {
$this->resetErrorBag();
$this->validate();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
@@ -56,8 +103,12 @@ class Discord extends Component
public function sendTestNotification()
{
$this->team?->notify(new Test);
try {
$this->team->notify(new Test);
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()

View File

@@ -3,24 +3,17 @@
namespace App\Livewire\Project;
use App\Models\Project;
use Livewire\Attributes\Rule;
use Livewire\Component;
class AddEmpty extends Component
{
public string $name = '';
#[Rule(['required', 'string', 'min:3'])]
public string $name;
#[Rule(['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 = '';
}
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use Livewire\Component;
class AddEnvironment extends Component
{
public Project $project;
public string $name = '';
public string $description = '';
protected $rules = [
'name' => '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 = '';
}
}
}

View File

@@ -4,55 +4,92 @@ namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\PrivateKey;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
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',
];
#[Rule(['nullable', 'string'])]
public ?string $privateKeyName = null;
protected $validationAttributes = [
'application.git_repository' => 'repository',
'application.git_branch' => 'branch',
'application.git_commit_sha' => 'commit sha',
];
#[Rule(['nullable', 'integer'])]
public ?int $privateKeyId = null;
#[Rule(['required', 'string'])]
public string $gitRepository;
#[Rule(['required', 'string'])]
public string $gitBranch;
#[Rule(['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();
try {
$this->privateKeyId = $privateKeyId;
$this->syncData(true);
$this->getPrivateKeys();
$this->application->refresh();
$this->get_private_keys();
$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->application->save();
$this->syncData(true);
$this->dispatch('success', 'Application source updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -3,34 +3,47 @@
namespace App\Livewire\Project;
use App\Models\Project;
use Livewire\Attributes\Rule;
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',
];
#[Rule(['required', 'string', 'min:3', 'max:255'])]
public string $name;
public function mount()
#[Rule(['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);

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Project;
use App\Models\Application;
use App\Models\Project;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component;
class EnvironmentEdit extends Component
@@ -12,29 +14,45 @@ class EnvironmentEdit extends Component
public Application $application;
#[Locked]
public $environment;
public array $parameters;
#[Rule(['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',
];
#[Rule(['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);
}

View File

@@ -2,27 +2,46 @@
namespace App\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use Livewire\Attributes\Rule;
use Livewire\Component;
class Show extends Component
{
public Project $project;
public $environments;
#[Rule(['required', 'string', 'min:3'])]
public string $name;
public function mount()
#[Rule(['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()

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Livewire\Server\Destination;
namespace App\Livewire\Server;
use App\Models\Server;
use App\Models\StandaloneDocker;
@@ -8,7 +8,7 @@ use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
class Destinations extends Component
{
public Server $server;
@@ -86,6 +86,6 @@ class Show extends Component
public function render()
{
return view('livewire.server.destination.show');
return view('livewire.server.destinations');
}
}

View File

@@ -3,102 +3,83 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
use Livewire\Attributes\Rule;
use Livewire\Component;
class SettingsEmail extends Component
{
public InstanceSettings $settings;
public string $emails;
#[Rule(['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',
#[Rule(['nullable', 'string'])]
public ?string $smtpHost = null;
];
#[Rule(['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',
];
#[Rule(['nullable', 'string'])]
public ?string $smtpEncryption = null;
#[Rule(['nullable', 'string'])]
public ?string $smtpUsername = null;
#[Rule(['nullable'])]
public ?string $smtpPassword = null;
#[Rule(['nullable', 'numeric'])]
public ?int $smtpTimeout = null;
#[Rule(['nullable', 'email'])]
public ?string $smtpFromAddress = null;
#[Rule(['nullable', 'string'])]
public ?string $smtpFromName = null;
#[Rule(['boolean'])]
public bool $resendEnabled = false;
#[Rule(['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);
}
}
}

View File

@@ -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.');

View File

@@ -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([

View File

@@ -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)

View File

@@ -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());
}
@@ -976,10 +974,10 @@ $schema://$host {
public function serverStatus(): bool
{
if ($this->status() === false) {
if ($this->isFunctional() === false) {
return false;
}
if ($this->isFunctional() === false) {
if ($this->status() === false) {
return false;
}
@@ -988,7 +986,7 @@ $schema://$host {
public function status(): bool
{
if ($this->skipServer()) {
if ($this->isFunctional() === false) {
return false;
}
['uptime' => $uptime] = $this->validateConnection(false);

View File

@@ -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()

View File

@@ -17,6 +17,11 @@ 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 function restart()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
) {
//
}

View File

@@ -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),

View File

@@ -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'),
],
/*

View File

@@ -0,0 +1,96 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->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');
});
}
};

View File

@@ -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

View File

@@ -51,8 +51,8 @@
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
<textarea x-cloak x-show="type !== 'password'" placeholder="{{ $placeholder }}"
{{ $attributes->merge(['class' => $defaultClass]) }}
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else
wire:model={{ $value ?? $id }}
@@ -62,7 +62,8 @@
</div>
@else
<textarea {{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else

View File

@@ -148,7 +148,7 @@
<li>
<a title="Destinations"
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('destination.all') }}">
href="{{ route('destination.index') }}" wire:navigate>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round"

View File

@@ -1,44 +0,0 @@
<x-layout>
<x-slot:title>
Destinations | Coolify
</x-slot>
<div class="flex items-start gap-2">
<h1>Destinations</h1>
@if ($servers->count() > 0)
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker :server_id="$server_id" />
</x-modal-input>
@endif
</div>
<div class="subtitle">Network endpoints to deploy your resources.</div>
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($destinations as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@empty
<div>
@if ($servers->count() === 0)
<div>No servers found. Please add one first.</div>
@else
<div>No destinations found.</div>
@endif
</div>
@endforelse
</div>
</x-layout>

View File

@@ -1,3 +0,0 @@
<x-layout>
<livewire:destination.form :destination="$destination" />
</x-layout>

View File

@@ -2,4 +2,4 @@
{{ Illuminate\Mail\Markdown::parse('---') }}
{{ Illuminate\Mail\Markdown::parse($debug) }}
{{-- {{ Illuminate\Mail\Markdown::parse($debug) }} --}}

View File

@@ -6,26 +6,25 @@
<x-forms.input wire:model="search" placeholder="Search for a user" />
<x-forms.button type="submit">Search</x-forms.button>
</form>
<h3 class="pt-4">Active Subscribers</h3>
<div class="flex flex-wrap gap-2">
@forelse ($active_subscribers as $user)
<div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p>
<p>{{ $user->email }}</p>
<div class="pt-4">Active Subscribers : {{ $activeSubscribers }}</div>
<div>Inactive Subscribers : {{ $inactiveSubscribers }}</div>
@if ($search)
@if ($foundUsers->count() > 0)
<div class="flex flex-wrap gap-2 pt-4">
@foreach ($foundUsers as $user)
<div class="box w-64 group">
<div class="flex flex-col gap-2">
<div class="box-title">{{ $user->name }}</div>
<div class="box-description">{{ $user->email }}</div>
<div class="box-description">Active:
{{ $user->teams()->whereRelation('subscription', 'stripe_subscription_id', '!=', null)->exists() ? 'Yes' : 'No' }}
</div>
@empty
<p>No active subscribers</p>
@endforelse
</div>
<h3 class="pt-4">Inactive Subscribers</h3>
<div class="flex flex-col flex-wrap gap-2">
@forelse ($inactive_subscribers as $user)
<div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p>
<p>{{ $user->email }}</p>
</div>
@empty
<p>No inactive subscribers</p>
@endforelse
@endforeach
</div>
@else
<div>No users found with {{ $search }}</div>
@endif
@endif
</div>

View File

@@ -94,7 +94,7 @@
@endforeach
</div>
@else
@if ($private_keys->count() === 0)
@if ($privateKeys->count() === 0)
<div class="flex flex-col gap-1">
<div class='font-bold dark:text-warning'>No private keys found.</div>
<div class="flex items-center gap-1">Before you can add your server, first <x-modal-input
@@ -126,26 +126,17 @@
<section>
<div class="flex items-center gap-2">
<h3 class="pb-2">Deployments</h3>
@if (count($deployments_per_server) > 0)
@if (count($deploymentsPerServer) > 0)
<x-loading />
@endif
<x-modal-confirmation
title="Confirm Cleanup Queues?"
buttonTitle="Cleanup Queues"
isErrorButton
submitAction="cleanup_queue"
:actions="['All running Deployment Queues will be cleaned up.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Permanently Cleanup Deployment Queues"
:dispatchEvent="true"
dispatchEventType="success"
dispatchEventMessage="Deployment Queues cleanup started."
/>
<x-modal-confirmation title="Confirm Cleanup Queues?" buttonTitle="Cleanup Queues" isErrorButton
submitAction="cleanupQueue" :actions="['All running Deployment Queues will be cleaned up.']" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Permanently Cleanup Deployment Queues" :dispatchEvent="true"
dispatchEventType="success" dispatchEventMessage="Deployment Queues cleanup started." />
</div>
<div wire:poll.3000ms="get_deployments" class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments)
<h4 class="pb-2">{{ $server_name }}</h4>
<div wire:poll.3000ms="loadDeployments" class="grid grid-cols-1">
@forelse ($deploymentsPerServer as $serverName => $deployments)
<h4 class="pb-2">{{ $serverName }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
@@ -187,7 +178,4 @@
}
}
</script>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div>

View File

@@ -1,29 +0,0 @@
<div>
<form class="flex flex-col">
<div class="flex items-center gap-2">
<h1>Destination</h1>
<x-forms.button wire:click.prevent='submit' type="submit">
Save
</x-forms.button>
@if ($destination->network !== 'coolify')
<x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
@endif
</div>
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="subtitle ">A Docker network in a non-swarm environment.</div>
@else
<div class="subtitle ">Your swarm docker network. WIP</div>
@endif
<div class="flex gap-2">
<x-forms.input id="destination.name" label="Name" />
<x-forms.input id="destination.server.ip" label="Server IP" readonly />
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<x-forms.input id="destination.network" label="Docker Network" readonly />
@endif
</div>
</form>
</div>

View File

@@ -0,0 +1,44 @@
<div>
<x-slot:title>
Destinations | Coolify
</x-slot>
<div class="flex items-start gap-2">
<h1>Destinations</h1>
@if ($servers->count() > 0)
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker />
</x-modal-input>
@endif
</div>
<div class="subtitle">Network endpoints to deploy your resources.</div>
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($servers as $server)
@forelse ($server->destinations() as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"
wire:navigate>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">Server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"
wire:navigate>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@empty
<div>No destinations found.</div>
@endforelse
@empty
<div>No servers found.</div>
@endforelse
</div>
</div>

View File

@@ -5,7 +5,7 @@
<x-forms.input id="name" label="Name" required />
<x-forms.input id="network" label="Network" required />
</div>
<x-forms.select id="server_id" label="Select a server" required wire:change="generate_name">
<x-forms.select id="serverId" label="Select a server" required wire:change="generateName">
<option disabled>Select a server</option>
@foreach ($servers as $server)
<option value="{{ $server->id }}">{{ $server->name }}</option>

View File

@@ -1,42 +1,29 @@
<div>
@if ($server->isFunctional())
<div class="flex items-end gap-2">
<h2>Destinations</h2>
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker :server_id="$server->id" />
</x-modal-input>
<x-forms.button wire:click='scan'>Scan for Destinations</x-forms.button>
</div>
<div>Destinations are used to segregate resources by network.</div>
<div class="flex gap-2 pt-6">
Available for using:
@forelse ($server->standaloneDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="dark:text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@endforelse
@forelse ($server->swarmDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="dark:text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@endforelse
</div>
<div class="pt-2">
@if (count($networks) > 0)
<h3 class="pb-4">Found Destinations</h3>
<form class="flex flex-col">
<div class="flex items-center gap-2">
<h1>Destination</h1>
<x-forms.button wire:click.prevent='submit' type="submit">
Save
</x-forms.button>
@if ($network !== 'coolify')
<x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
@endif
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network)
<div class="min-w-fit">
<x-forms.button wire:click="add('{{ data_get($network, 'Name') }}')">Add
{{ data_get($network, 'Name') }}</x-forms.button>
</div>
@endforeach
</div>
</div>
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="subtitle ">A simple Docker network.</div>
@else
<div>Server is not validated. Validate first.</div>
<div class="subtitle ">A swarm Docker network. WIP</div>
@endif
<div class="flex gap-2">
<x-forms.input id="name" label="Name" />
<x-forms.input id="serverIp" label="Server IP" readonly />
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<x-forms.input id="network" label="Docker Network" readonly />
@endif
</div>
</form>
</div>

View File

@@ -1,10 +1,11 @@
<div class="flex flex-col w-full gap-2">
<div>Your feedback helps us to improve Coolify. Thank you! 💜</div>
<form wire:submit="submit" class="flex flex-col gap-4 pt-4">
<x-forms.input id="subject" label="Subject" placeholder="Summary of your problem."></x-forms.input>
<x-forms.textarea rows="10" id="description" label="Description" class="font-sans" spellcheck
placeholder="Please provide as much information as possible."></x-forms.textarea>
<x-forms.input minlength="3" required id="subject" label="Subject" placeholder="Help with..."></x-forms.input>
<x-forms.textarea minlength="10" maxlength="1000" required rows="10" id="description" label="Description"
class="font-sans" spellcheck
placeholder="Having trouble with... Please provide as much information as possible."></x-forms.textarea>
<div></div>
<x-forms.button class="w-full mt-4" type="submit" @click="modalOpen=false">Send</x-forms.button>
<x-forms.button class="w-full mt-4" type="submit">Send</x-forms.button>
</form>
</div>

View File

@@ -9,7 +9,7 @@
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($team->discord_enabled)
@if ($discordEnabled)
<x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
@@ -17,27 +17,26 @@
@endif
</div>
<div class="w-32">
<x-forms.checkbox instantSave id="team.discord_enabled" label="Enabled" />
<x-forms.checkbox instantSave id="discordEnabled" label="Enabled" />
</div>
<x-forms.input type="password"
helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required
id="team.discord_webhook_url" label="Webhook" />
id="discordWebhookUrl" label="Webhook" />
</form>
@if (data_get($team, 'discord_enabled'))
@if ($discordEnabled)
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" />
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsTest" label="Test" />
@endif
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsStatusChanges"
label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsDeployments"
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_scheduled_tasks"
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsDatabaseBackups" label="Backup Status" />
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsScheduledTasks"
label="Scheduled Tasks Status" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_server_disk_usage"
<x-forms.checkbox instantSave="saveModel" id="discordNotificationsServerDiskUsage"
label="Server Disk Usage" />
</div>
@endif

View File

@@ -1,8 +1,9 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="Your Cool Project" id="name" label="Name" required />
<x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" />
<div class="subtitle">New project will have a default production environment.</div>
<x-forms.button type="submit" @click="slideOverOpen=false">
<div class="subtitle">New project will have a default <span class="dark:text-warning font-bold">production</span>
environment.</div>
<x-forms.button type="submit">
Continue
</x-forms.button>
</form>

View File

@@ -1,6 +0,0 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit" @click="slideOverOpen=false">
Save
</x-forms.button>
</form>

View File

@@ -27,24 +27,22 @@
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository"
label="Repository" />
<x-forms.input placeholder="main" id="application.git_branch" label="Branch" />
<x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" />
<x-forms.input placeholder="main" id="gitBranch" label="Branch" />
</div>
<div class="flex items-end gap-2">
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD"
label="Commit SHA" />
<x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" />
</div>
</div>
@if (data_get($application, 'private_key_id'))
@if ($privateKeyId)
<h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attached Private Key: <span
class="dark:text-warning">{{ data_get($application, 'private_key.name') }}</span>
class="dark:text-warning">{{ $privateKeyName }}</span>
</div>
<h4 class="py-2 ">Select another Private Key</h4>
<div class="flex flex-wrap gap-2">
@foreach ($private_keys as $key)
@foreach ($privateKeys as $key)
<x-forms.button wire:click.defer="setPrivateKey('{{ $key->id }}')">{{ $key->name }}
</x-forms.button>
@endforeach

View File

@@ -11,10 +11,9 @@
</div>
</div>
<div class="pt-2 pb-10">Edit project details here.</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="project.name" />
<x-forms.input label="Description" id="project.description" />
<x-forms.input label="Name" id="name" />
<x-forms.input label="Description" id="description" />
</div>
</form>
</div>

View File

@@ -13,7 +13,7 @@
<li class="inline-flex items-center">
<div class="flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}">
{{ $project->name }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
@@ -26,7 +26,9 @@
<li>
<div class="flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a>
href="{{ route('project.resource.index', ['environment_name' => $environment->name, 'project_uuid' => $project->uuid]) }}">
{{ $environment->name }}
</a>
</div>
</li>
<li>
@@ -43,8 +45,8 @@
</ol>
</nav>
<div class="flex gap-2">
<x-forms.input label="Name" id="environment.name" />
<x-forms.input label="Description" id="environment.description" />
<x-forms.input label="Name" id="name" />
<x-forms.input label="Description" id="description" />
</div>
</form>
</div>

View File

@@ -5,13 +5,18 @@
<div class="flex items-center gap-2">
<h1>Environments</h1>
<x-modal-input buttonTitle="+ Add" title="New Environment">
<livewire:project.add-environment :project="$project" />
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
</x-modal-input>
<livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" />
</div>
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}.</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($environments as $environment)
@forelse ($project->environments->sortBy('created_at') as $environment)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')">
<div class="flex flex-1 mx-6">
@@ -28,12 +33,6 @@
</a>
</div>
</div>
{{-- <div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="mx-4 font-bold hover:underline"
href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}">
Settings
</a>
</div> --}}
</div>
@empty
<p>No environments found.</p>

View File

@@ -1,4 +1,7 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Advanced | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="advanced" />

View File

@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
@@ -8,6 +8,7 @@
<div class="w-full">
<h2>Metrics</h2>
<div class="pb-4">Basic metrics for your container.</div>
@if ($server->isMetricsEnabled())
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
@@ -248,6 +249,9 @@
</div>
</div>
@else
<div>Metrics are disabled for this server.</div>
@endif
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Log Drains | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Connection | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify
{{ data_get_str($server, 'name')->limit(10) }} > General | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,19 +1,21 @@
<div>
<x-slot:title>
Settings | Coolify
Transactional Email | Coolify
</x-slot>
<x-settings.navbar />
<form wire:submit='submit' class="flex flex-col gap-2 pb-4">
<div class="flex items-center gap-2">
<h2>Transactional Email</h2>
</div>
<div class="pb-4 ">Email settings for password resets, invitations, etc.</div>
<form wire:submit='submitFromFields' class="flex flex-col gap-2 pb-4">
<x-forms.input required id="settings.smtp_from_name" helper="Name used in emails." label="From Name" />
<x-forms.input required id="settings.smtp_from_address" helper="Email address used in emails."
label="From Address" />
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
<div class="pb-4 ">Email settings for password resets, invitations, etc.</div>
<div class="flex gap-4">
<x-forms.input required id="smtpFromName" helper="Name used in emails." label="From Name" />
<x-forms.input required id="smtpFromAddress" helper="Email address used in emails." label="From Address" />
</div>
</form>
<div class="flex flex-col gap-4">
<div class="p-4 border dark:border-coolgray-300">
@@ -25,27 +27,26 @@
</x-forms.button>
</div>
<div class="w-32">
<x-forms.checkbox instantSave id="settings.smtp_enabled" label="Enabled" />
<x-forms.checkbox instantSave='instantSave("SMTP")' id="smtpEnabled" label="Enabled" />
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="settings.smtp_host" placeholder="smtp.mailgun.org" label="Host" />
<x-forms.input required id="settings.smtp_port" placeholder="587" label="Port" />
<x-forms.input id="settings.smtp_encryption" helper="If SMTP uses SSL, set it to 'tls'."
placeholder="tls" label="Encryption" />
<x-forms.input required id="smtpHost" placeholder="smtp.mailgun.org" label="Host" />
<x-forms.input required id="smtpPort" placeholder="587" label="Port" />
<x-forms.input id="smtpEncryption" helper="If SMTP uses SSL, set it to 'tls'." placeholder="tls"
label="Encryption" />
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="settings.smtp_username" label="SMTP Username" />
<x-forms.input id="settings.smtp_password" type="password" label="SMTP Password"
<x-forms.input id="smtpUsername" label="SMTP Username" />
<x-forms.input id="smtpPassword" type="password" label="SMTP Password"
autocomplete="new-password" />
<x-forms.input id="settings.smtp_timeout" helper="Timeout value for sending emails."
label="Timeout" />
<x-forms.input id="smtpTimeout" helper="Timeout value for sending emails." label="Timeout" />
</div>
</div>
</form>
</div>
<div class="p-4 border dark:border-coolgray-300">
<form wire:submit='submitResend' class="flex flex-col">
<form wire:submit='submit' class="flex flex-col">
<div class="flex gap-2">
<h3>Resend</h3>
<x-forms.button type="submit">
@@ -53,12 +54,12 @@
</x-forms.button>
</div>
<div class="w-32">
<x-forms.checkbox instantSave='instantSaveResend' id="settings.resend_enabled" label="Enabled" />
<x-forms.checkbox instantSave='instantSave("Resend")' id="resendEnabled" label="Enabled" />
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key" required
label="Host" autocomplete="new-password" />
<x-forms.input type="password" id="resendApiKey" placeholder="API key" required label="API Key"
autocomplete="new-password" />
</div>
</div>
</form>

View File

@@ -149,6 +149,7 @@ Route::group([
}
$data = request()->all();
// \App\Jobs\ServerCheckNewJob::dispatch($server, $data);
PushServerUpdateJob::dispatch($server, $data);
return response()->json(['message' => 'ok'], 200);

View File

@@ -7,6 +7,8 @@ use App\Http\Controllers\UploadController;
use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\Dashboard;
use App\Livewire\Destination\Index as DestinationIndex;
use App\Livewire\Destination\Show as DestinationShow;
use App\Livewire\Dev\Compose as Compose;
use App\Livewire\ForcePasswordReset;
use App\Livewire\Notifications\Discord as NotificationDiscord;
@@ -38,7 +40,7 @@ use App\Livewire\Server\Advanced as ServerAdvanced;
use App\Livewire\Server\Charts as ServerCharts;
use App\Livewire\Server\CloudflareTunnels;
use App\Livewire\Server\Delete as DeleteServer;
use App\Livewire\Server\Destination\Show as DestinationShow;
use App\Livewire\Server\Destinations as ServerDestinations;
use App\Livewire\Server\Index as ServerIndex;
use App\Livewire\Server\LogDrains;
use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
@@ -72,8 +74,6 @@ use App\Livewire\Terminal\Index as TerminalIndex;
use App\Models\GitlabApp;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
@@ -98,14 +98,14 @@ Route::middleware(['throttle:login'])->group(function () {
Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect');
Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback');
Route::prefix('magic')->middleware(['auth'])->group(function () {
Route::get('/servers', [MagicController::class, 'servers']);
Route::get('/destinations', [MagicController::class, 'destinations']);
Route::get('/projects', [MagicController::class, 'projects']);
Route::get('/environments', [MagicController::class, 'environments']);
Route::get('/project/new', [MagicController::class, 'newProject']);
Route::get('/environment/new', [MagicController::class, 'newEnvironment']);
});
// Route::prefix('magic')->middleware(['auth'])->group(function () {
// Route::get('/servers', [MagicController::class, 'servers']);
// Route::get('/destinations', [MagicController::class, 'destinations']);
// Route::get('/projects', [MagicController::class, 'projects']);
// Route::get('/environments', [MagicController::class, 'environments']);
// Route::get('/project/new', [MagicController::class, 'newProject']);
// Route::get('/environment/new', [MagicController::class, 'newEnvironment']);
// });
Route::middleware(['auth', 'verified'])->group(function () {
Route::middleware(['throttle:force-password-reset'])->group(function () {
@@ -212,7 +212,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels');
Route::get('/destinations', DestinationShow::class)->name('server.destinations');
Route::get('/destinations', ServerDestinations::class)->name('server.destinations');
Route::get('/log-drains', LogDrains::class)->name('server.log-drains');
Route::get('/metrics', ServerCharts::class)->name('server.charts');
Route::get('/danger', DeleteServer::class)->name('server.delete');
@@ -221,6 +221,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command');
});
Route::get('/destinations', DestinationIndex::class)->name('destination.index');
Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
// Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', SecurityPrivateKeyIndex::class)->name('security.private-key.index');
@@ -312,52 +314,7 @@ Route::middleware(['auth'])->group(function () {
return response()->json(['message' => $e->getMessage()], 500);
}
})->name('download.backup');
Route::get('/destinations', function () {
$servers = Server::isUsable()->get();
$destinations = collect([]);
foreach ($servers as $server) {
$destinations = $destinations->merge($server->destinations());
}
$pre_selected_server_uuid = data_get(request()->query(), 'server');
if ($pre_selected_server_uuid) {
$server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
if ($server) {
$server_id = $server->id;
}
}
return view('destination.all', [
'destinations' => $destinations,
'servers' => $servers,
'server_id' => $server_id ?? null,
]);
})->name('destination.all');
// Route::get('/destination/new', function () {
// $servers = Server::isUsable()->get();
// $pre_selected_server_uuid = data_get(request()->query(), 'server');
// if ($pre_selected_server_uuid) {
// $server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
// if ($server) {
// $server_id = $server->id;
// }
// }
// return view('destination.new', [
// "servers" => $servers,
// "server_id" => $server_id ?? null,
// ]);
// })->name('destination.new');
Route::get('/destination/{destination_uuid}', function () {
$standalone_dockers = StandaloneDocker::where('uuid', request()->destination_uuid)->first();
$swarm_dockers = SwarmDocker::where('uuid', request()->destination_uuid)->first();
if (! $standalone_dockers && ! $swarm_dockers) {
abort(404);
}
$destination = $standalone_dockers ? $standalone_dockers : $swarm_dockers;
return view('destination.show', [
'destination' => $destination->load(['server']),
]);
})->name('destination.show');
});
Route::any('/{any}', function () {