Merge pull request #1193 from coollabsio/next

v4.0.0-beta.21
This commit is contained in:
Andras Bacsai 2023-09-01 10:27:49 +02:00 committed by GitHub
commit 7a180c7310
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 2062 additions and 1384 deletions

View File

@ -9,6 +9,7 @@ class SaveConfigurationSync
{ {
public function __invoke(Server $server, string $configuration) public function __invoke(Server $server, string $configuration)
{ {
try {
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
@ -19,5 +20,9 @@ public function __invoke(Server $server, string $configuration)
"mkdir -p $proxy_path", "mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server); ], $server);
} catch (\Throwable $th) {
ray($th);
}
} }
} }

View File

@ -12,12 +12,6 @@ class StartProxy
{ {
public function __invoke(Server $server): Activity public function __invoke(Server $server): Activity
{ {
// TODO: check for other proxies
if (is_null(data_get($server, 'proxy.type'))) {
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$server->proxy->status = ProxyStatus::EXITED->value;
$server->save();
}
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];

View File

@ -37,12 +37,15 @@ public function __invoke(Server $server, Team $team)
"docker network create --attachable coolify", "docker network create --attachable coolify",
"echo ####### Done!" "echo ####### Done!"
], $server); ], $server);
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) {
StandaloneDocker::create([ StandaloneDocker::create([
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} }
}
return $activity; return $activity;

View File

@ -7,9 +7,9 @@
class UpdateCoolify class UpdateCoolify
{ {
public Server $server; public ?Server $server = null;
public string $latest_version; public ?string $latestVersion = null;
public string $current_version; public ?string $currentVersion = null;
public function __invoke(bool $force) public function __invoke(bool $force)
{ {
@ -17,13 +17,16 @@ public function __invoke(bool $force)
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob'); ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost'; $localhost_name = 'localhost';
$this->server = Server::where('name', $localhost_name)->firstOrFail(); $this->server = Server::where('name', $localhost_name)->first();
$this->latest_version = get_latest_version_of_coolify(); if (!$this->server) {
$this->current_version = config('version'); return;
ray('latest version:' . $this->latest_version . " current version: " . $this->current_version . ' force: ' . $force); }
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);
if ($settings->next_channel) { if ($settings->next_channel) {
ray('next channel enabled'); ray('next channel enabled');
$this->latest_version = 'next'; $this->latestVersion = 'next';
} }
if ($force) { if ($force) {
$this->update(); $this->update();
@ -31,15 +34,15 @@ public function __invoke(bool $force)
if (!$settings->is_auto_update_enabled) { if (!$settings->is_auto_update_enabled) {
return 'Auto update is disabled'; return 'Auto update is disabled';
} }
if ($this->latest_version === $this->current_version) { if ($this->latestVersion === $this->currentVersion) {
return 'Already on latest version'; return 'Already on latest version';
} }
if (version_compare($this->latest_version, $this->current_version, '<')) { if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
return 'Latest version is lower than current version?!'; return 'Latest version is lower than current version?!';
} }
$this->update(); $this->update();
} }
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latest_version . ' from version: ' . $this->current_version); send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) { } catch (\Exception $th) {
ray('InstanceAutoUpdateJob failed'); ray('InstanceAutoUpdateJob failed');
ray($th->getMessage()); ray($th->getMessage());
@ -51,7 +54,7 @@ public function __invoke(bool $force)
private function update() private function update()
{ {
if (isDev()) { if (isDev()) {
ray("Running update on local docker container. Updating to $this->latest_version"); ray("Running update on local docker container. Updating to $this->latestVersion");
remote_process([ remote_process([
"sleep 10" "sleep 10"
], $this->server); ], $this->server);
@ -61,7 +64,7 @@ private function update()
ray('Running update on production server'); ray('Running update on production server');
remote_process([ remote_process([
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh", "curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh",
"bash /data/coolify/source/upgrade.sh $this->latest_version" "bash /data/coolify/source/upgrade.sh $this->latestVersion"
], $this->server); ], $this->server);
return; return;
} }

View File

@ -25,15 +25,15 @@ protected function schedule(Schedule $schedule): void
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly(); // $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour(); $schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute(); // $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute(); $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes(); $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes(); $schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
} }
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);

View File

@ -5,11 +5,9 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project; use App\Models\Project;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
@ -19,25 +17,19 @@ class Controller extends BaseController
{ {
use AuthorizesRequests, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
public function waitlist() {
$waiting_in_line = Waitlist::whereVerified(true)->count();
return view('auth.waitlist', [
'waiting_in_line' => $waiting_in_line,
]);
}
public function subscription() public function subscription()
{ {
if (!is_cloud()) { if (!isCloud()) {
abort(404); abort(404);
} }
return view('subscription.show', [ return view('subscription.index', [
'settings' => InstanceSettings::get(), 'settings' => InstanceSettings::get(),
]); ]);
} }
public function license() public function license()
{ {
if (!is_cloud()) { if (!isCloud()) {
abort(404); abort(404);
} }
return view('settings.license', [ return view('settings.license', [
@ -48,23 +40,6 @@ public function license()
public function force_passoword_reset() { public function force_passoword_reset() {
return view('auth.force-password-reset'); return view('auth.force-password-reset');
} }
public function dashboard()
{
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
$resources += $project->postgresqls->count();
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function boarding() { public function boarding() {
if (currentTeam()->boarding || isDev()) { if (currentTeam()->boarding || isDev()) {
return view('boarding'); return view('boarding');
@ -97,7 +72,7 @@ public function team()
if (auth()->user()->isAdminFromSession()) { if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); $invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
} }
return view('team.show', [ return view('team.index', [
'invitations' => $invitations, 'invitations' => $invitations,
]); ]);
} }
@ -146,7 +121,7 @@ public function acceptInvitation()
if ($diff <= config('constants.invitation.link.expiration')) { if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete(); $invitation->delete();
return redirect()->route('team.show'); return redirect()->route('team.index');
} else { } else {
$invitation->delete(); $invitation->delete();
abort(401); abort(401);
@ -168,7 +143,7 @@ public function revokeInvitation()
abort(401); abort(401);
} }
$invitation->delete(); $invitation->delete();
return redirect()->route('team.show'); return redirect()->route('team.index');
} catch (Throwable $th) { } catch (Throwable $th) {
throw $th; throw $th;
} }

View File

@ -43,6 +43,7 @@ public function new()
{ {
$type = request()->query('type'); $type = request()->query('type');
$destination_uuid = request()->query('destination'); $destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
@ -59,6 +60,9 @@ public function new()
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid, 'database_uuid' => $standalone_postgresql->uuid,
]); ]);
}
if ($server) {
} }
return view('project.new', [ return view('project.new', [
'type' => $type 'type' => $type

View File

@ -12,20 +12,21 @@ class ServerController extends Controller
public function new_server() public function new_server()
{ {
if (!is_cloud() || isInstanceAdmin()) { $privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [ return view('server.create', [
'limit_reached' => false, 'limit_reached' => false,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(), 'private_keys' => $privateKeys,
]); ]);
} }
$servers = currentTeam()->servers->count(); $team = currentTeam();
$subscription = currentTeam()?->subscription->type(); $servers = $team->servers->count();
$your_limit = config('constants.limits.server')[strtolower($subscription)]; ['serverLimit' => $serverLimit] = $team->limits;
$limit_reached = $servers >= $your_limit; $limit_reached = $servers >= $serverLimit;
return view('server.create', [ return view('server.create', [
'limit_reached' => $limit_reached, 'limit_reached' => $limit_reached,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(), 'private_keys' => $privateKeys,
]); ]);
} }
} }

View File

@ -38,7 +38,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class, \App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class, \App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class, \App\Http\Middleware\IsBoardingFlow::class,
], ],

View File

@ -1,17 +1,20 @@
<?php <?php
namespace App\Http\Livewire; namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Boarding extends Component class Index extends Component
{ {
public string $currentState = 'welcome'; public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
public ?string $privateKey = null; public ?string $privateKey = null;
public ?string $publicKey = null; public ?string $publicKey = null;
@ -19,6 +22,8 @@ class Boarding extends Component
public ?string $privateKeyDescription = null; public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null; public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null; public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null; public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null; public ?string $remoteServerHost = null;
@ -26,6 +31,8 @@ class Boarding extends Component
public ?string $remoteServerUser = 'root'; public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null; public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
public function mount() public function mount()
@ -45,6 +52,12 @@ public function mount()
$this->remoteServerHost = 'coolify-testing-host'; $this->remoteServerHost = 'coolify-testing-host';
} }
} }
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding() public function restartBoarding()
{ {
if ($this->createdServer) { if ($this->createdServer) {
@ -63,20 +76,62 @@ public function skipBoarding()
refreshSession(); refreshSession();
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
public function setServer(string $type)
public function setServerType(string $type)
{ {
if ($type === 'localhost') { if ($type === 'localhost') {
$this->createdServer = Server::find(0); $this->createdServer = Server::find(0);
if (!$this->createdServer) { if (!$this->createdServer) {
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
} }
$this->currentState = 'select-proxy'; return $this->validateServer();
} elseif ($type === 'remote') { } elseif ($type === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->currentState = 'select-existing-server';
return;
}
$this->currentState = 'private-key'; $this->currentState = 'private-key';
} }
} }
public function selectExistingServer()
{
$this->createdServer = Server::find($this->selectedExistingServer);
if (!$this->createdServer) {
$this->emit('error', 'Server is not found.');
$this->currentState = 'private-key';
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->validateServer();
$this->getProxyType();
$this->getProjects();
}
public function getProxyType() {
$proxyTypeSet = $this->createdServer->proxy->type;
if (!$proxyTypeSet) {
$this->currentState = 'select-proxy';
return;
}
$this->getProjects();
}
public function selectExistingPrivateKey()
{
$this->currentState = 'create-server';
}
public function createNewServer()
{
$this->selectedExistingServer = null;
$this->currentState = 'private-key';
}
public function setPrivateKey(string $type) public function setPrivateKey(string $type)
{ {
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type; $this->privateKeyType = $type;
if ($type === 'create' && !isDev()) { if ($type === 'create' && !isDev()) {
$this->createNewPrivateKey(); $this->createNewPrivateKey();
@ -115,11 +170,12 @@ public function saveServer()
'private_key_id' => $this->createdPrivateKey->id, 'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id 'team_id' => currentTeam()->id
]); ]);
$this->validateServer();
}
public function validateServer() {
try { try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer); ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
if (!$uptime) { if (!$uptime) {
$this->createdServer->delete();
$this->createdPrivateKey->delete();
throw new \Exception('Server is not reachable.'); throw new \Exception('Server is not reachable.');
} else { } else {
$this->createdServer->settings->update([ $this->createdServer->settings->update([
@ -127,11 +183,14 @@ public function saveServer()
]); ]);
$this->emit('success', 'Server is reachable.'); $this->emit('success', 'Server is reachable.');
} }
if ($dockerVersion) { ray($dockerVersion, $uptime);
if (!$dockerVersion) {
$this->emit('error', 'Docker is not installed on the server.'); $this->emit('error', 'Docker is not installed on the server.');
$this->currentState = 'install-docker'; $this->currentState = 'install-docker';
return; return;
} }
$this->getProxyType();
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this); return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} }
@ -145,13 +204,25 @@ public function installDocker()
public function selectProxy(string|null $proxyType = null) public function selectProxy(string|null $proxyType = null)
{ {
if (!$proxyType) { if (!$proxyType) {
return $this->currentState = 'create-project'; return $this->getProjects();
} }
$this->createdServer->proxy->type = $proxyType; $this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited'; $this->createdServer->proxy->status = 'exited';
$this->createdServer->save(); $this->createdServer->save();
$this->getProjects();
}
public function getProjects() {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
if ($this->projects->count() > 0) {
$this->selectedExistingProject = $this->projects->first()->id;
}
$this->currentState = 'create-project'; $this->currentState = 'create-project';
} }
public function selectExistingProject() {
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
public function createNewProject() public function createNewProject()
{ {
$this->createdProject = Project::create([ $this->createdProject = Project::create([
@ -168,7 +239,7 @@ public function showNewResource()
[ [
'project_uuid' => $this->createdProject->uuid, 'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production', 'environment_name' => 'production',
'server'=> $this->createdServer->id,
] ]
); );
} }
@ -178,4 +249,8 @@ private function createNewPrivateKey()
$this->privateKeyDescription = 'Created by Coolify'; $this->privateKeyDescription = 'Created by Coolify';
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey(); ['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
} }
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');
}
} }

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
}
public function render()
{
return view('livewire.dashboard');
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Livewire\Dev;
use App\Models\S3Storage;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
class S3Test extends Component
{
use WithFileUploads;
public $s3;
public $file;
public function mount()
{
$this->s3 = S3Storage::first();
}
public function save()
{
try {
$this->validate([
'file' => 'required|max:150', // 1MB Max
]);
set_s3_target($this->s3);
$this->file->storeAs('files', $this->file->getClientOriginalName(), 'custom-s3');
$this->emit('success', 'File uploaded successfully.');
} catch (\Throwable $th) {
return general_error_handler($th, $this, false);
}
}
public function get_files()
{
set_s3_target($this->s3);
dd(Storage::disk('custom-s3')->files('files'));
}
}

View File

@ -6,55 +6,143 @@
use App\Models\Team; use App\Models\Team;
use App\Notifications\Test; use App\Notifications\Test;
use Livewire\Component; use Livewire\Component;
use Log;
class EmailSettings extends Component class EmailSettings extends Component
{ {
public Team $model; public Team $team;
public string $emails; public string $emails;
public bool $sharedEmailEnabled = false;
protected $rules = [ protected $rules = [
'model.smtp_enabled' => 'nullable|boolean', 'team.smtp_enabled' => 'nullable|boolean',
'model.smtp_from_address' => 'required|email', 'team.smtp_from_address' => 'required|email',
'model.smtp_from_name' => 'required', 'team.smtp_from_name' => 'required',
'model.smtp_recipients' => 'nullable', 'team.smtp_recipients' => 'nullable',
'model.smtp_host' => 'required', 'team.smtp_host' => 'required',
'model.smtp_port' => 'required', 'team.smtp_port' => 'required',
'model.smtp_encryption' => 'nullable', 'team.smtp_encryption' => 'nullable',
'model.smtp_username' => 'nullable', 'team.smtp_username' => 'nullable',
'model.smtp_password' => 'nullable', 'team.smtp_password' => 'nullable',
'model.smtp_timeout' => 'nullable', 'team.smtp_timeout' => 'nullable',
'model.smtp_notifications_test' => 'nullable|boolean', 'team.smtp_notifications_test' => 'nullable|boolean',
'model.smtp_notifications_deployments' => 'nullable|boolean', 'team.smtp_notifications_deployments' => 'nullable|boolean',
'model.smtp_notifications_status_changes' => 'nullable|boolean', 'team.smtp_notifications_status_changes' => 'nullable|boolean',
'model.smtp_notifications_database_backups' => 'nullable|boolean', 'team.smtp_notifications_database_backups' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'model.smtp_from_address' => 'From Address', 'team.smtp_from_address' => 'From Address',
'model.smtp_from_name' => 'From Name', 'team.smtp_from_name' => 'From Name',
'model.smtp_recipients' => 'Recipients', 'team.smtp_recipients' => 'Recipients',
'model.smtp_host' => 'Host', 'team.smtp_host' => 'Host',
'model.smtp_port' => 'Port', 'team.smtp_port' => 'Port',
'model.smtp_encryption' => 'Encryption', 'team.smtp_encryption' => 'Encryption',
'model.smtp_username' => 'Username', 'team.smtp_username' => 'Username',
'model.smtp_password' => 'Password', 'team.smtp_password' => 'Password',
'team.smtp_timeout' => 'Timeout',
'team.resend_enabled' => 'Resend Enabled',
'team.resend_api_key' => 'Resend API Key',
]; ];
public function mount() public function mount()
{ {
$this->decrypt(); ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} }
public function submitFromFields()
private function decrypt()
{ {
if (data_get($this->model, 'smtp_password')) {
try { try {
$this->model->smtp_password = decrypt($this->model->smtp_password); $this->resetErrorBag();
$this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler($e, $this);
} }
} }
public function sendTestNotification()
{
$this->team->notify(new Test($this->emails));
$this->emit('success', 'Test Email sent successfully.');
}
public function instantSaveInstance()
{
try {
if (!$this->sharedEmailEnabled) {
throw new \Exception('Not allowed to change settings. Please upgrade your subscription.');
}
$this->team->smtp_enabled = false;
$this->team->resend_enabled = false;
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
} }
public function instantSaveResend()
{
try {
$this->team->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->team->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.smtp_host' => 'required',
'team.smtp_port' => 'required|numeric',
'team.smtp_encryption' => 'nullable',
'team.smtp_username' => 'nullable',
'team.smtp_password' => 'nullable',
'team.smtp_timeout' => 'nullable',
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function submitResend()
{
try {
$this->resetErrorBag();
$this->validate([
'team.resend_api_key' => 'required'
]);
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->team->resend_enabled = false;
return general_error_handler($e, $this);
}
}
public function copyFromInstanceSettings() public function copyFromInstanceSettings()
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
@ -72,55 +160,22 @@ public function copyFromInstanceSettings()
'smtp_password' => $settings->smtp_password, 'smtp_password' => $settings->smtp_password,
'smtp_timeout' => $settings->smtp_timeout, 'smtp_timeout' => $settings->smtp_timeout,
]); ]);
$this->decrypt();
if (is_a($team, Team::class)) {
refreshSession(); refreshSession();
} $this->team = $team;
$this->model = $team;
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
} else { return;
$this->emit('error', 'Instance SMTP settings are not enabled.');
} }
} if ($settings->resend_enabled) {
$team = currentTeam();
public function sendTestNotification() $team->update([
{ 'resend_enabled' => $settings->resend_enabled,
$this->model->notify(new Test($this->emails)); 'resend_api_key' => $settings->resend_api_key,
$this->emit('success', 'Test Email sent successfully.'); ]);
}
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
$this->model->smtp_enabled = false;
$this->validate();
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
if ($this->model->smtp_password) {
$this->model->smtp_password = encrypt($this->model->smtp_password);
} else {
$this->model->smtp_password = null;
}
$this->model->smtp_recipients = str_replace(' ', '', $this->model->smtp_recipients);
$this->saveModel();
}
public function saveModel()
{
$this->model->save();
$this->decrypt();
if (is_a($this->model, Team::class)) {
refreshSession(); refreshSession();
} $this->team = $team;
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
return;
}
$this->emit('error', 'Instance SMTP/Resend settings are not enabled.');
} }
} }

View File

@ -7,15 +7,19 @@
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {
use SaveFromRedirect;
public $current_step = 'github_apps'; public $current_step = 'github_apps';
public $github_apps; public $github_apps;
public GithubApp $github_app; public GithubApp $github_app;
public $parameters; public $parameters;
public $currentRoute;
public $query; public $query;
public $type; public $type;
@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component
public string|null $publish_directory = null; public string|null $publish_directory = null;
protected int $page = 1; protected int $page = 1;
// public function saveFromRedirect(string $route, ?Collection $parameters = null){
// session()->forget('from');
// if (!$parameters || $parameters->count() === 0) {
// $parameters = $this->parameters;
// }
// $parameters = collect($parameters) ?? collect([]);
// $queries = collect($this->query) ?? collect([]);
// $parameters = $parameters->merge($queries);
// session(['from'=> [
// 'back'=> $this->currentRoute,
// 'route' => $route,
// 'parameters' => $parameters
// ]]);
// return redirect()->route($route);
// }
public function mount() public function mount()
{ {
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->repositories = $this->branches = collect(); $this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private(); $this->github_apps = GithubApp::private();
} }
public function loadRepositories($github_app_id) public function loadRepositories($github_app_id)
{ {
$this->repositories = collect(); $this->repositories = collect();

View File

@ -3,19 +3,28 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable; use Countable;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Route;
class Select extends Component class Select extends Component
{ {
public $current_step = 'type'; public $current_step = 'type';
public ?int $server = null;
public string $type; public string $type;
public string $server_id; public string $server_id;
public string $destination_uuid; public string $destination_uuid;
public Countable|array|Server $servers; public Countable|array|Server $servers;
public $destinations = []; public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
protected $queryString = [
'server',
];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
@ -31,13 +40,20 @@ public function set_type(string $type)
$this->set_destination($server->destinations()->first()->uuid); $this->set_destination($server->destinations()->first()->uuid);
} }
} }
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) {
return $this->set_server($foundServer);
}
}
$this->current_step = 'servers'; $this->current_step = 'servers';
} }
public function set_server(Server $server) public function set_server(Server $server)
{ {
$this->server_id = $server->id; $this->server_id = $server->id;
$this->destinations = $server->destinations(); $this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations'; $this->current_step = 'destinations';
} }

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class All extends Component
{
public ?Collection $servers = null;
public function mount () {
$this->servers = Server::ownedByCurrentTeam()->get();
}
public function render()
{
return view('livewire.server.all');
}
}

View File

@ -4,10 +4,12 @@
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public $uptime; public $uptime;
public $dockerVersion; public $dockerVersion;
@ -64,14 +66,20 @@ public function validateServer()
public function delete() public function delete()
{ {
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) { if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.'); $this->emit('error', 'Server has defined resources. Please delete them first.');
return; return;
} }
$this->server->delete(); $this->server->delete();
redirect()->route('server.all'); return redirect()->route('server.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} }
}
public function submit() public function submit()
{ {
$this->validate(); $this->validate();

View File

@ -12,6 +12,7 @@ class Status extends Component
public function get_status() public function get_status()
{ {
if (data_get($this->server,'settings.is_usable')) {
dispatch_sync(new ProxyContainerStatusJob( dispatch_sync(new ProxyContainerStatusJob(
server: $this->server server: $this->server
)); ));
@ -19,3 +20,4 @@ public function get_status()
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
} }
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public function mount()
{
try {
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function render()
{
return view('livewire.server.show');
}
}

View File

@ -6,7 +6,7 @@
use Livewire\Component; use Livewire\Component;
use Masmerise\Toaster\Toaster; use Masmerise\Toaster\Toaster;
class PrivateKey extends Component class ShowPrivateKey extends Component
{ {
public Server $server; public Server $server;
public $privateKeys; public $privateKeys;

View File

@ -20,6 +20,9 @@ class Email extends Component
'settings.smtp_timeout' => 'nullable', 'settings.smtp_timeout' => 'nullable',
'settings.smtp_from_address' => 'required|email', 'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required', 'settings.smtp_from_name' => 'required',
'settings.resend_enabled' => 'nullable|boolean',
'settings.resend_api_key' => 'nullable'
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'settings.smtp_from_address' => 'From Address', 'settings.smtp_from_address' => 'From Address',
@ -30,48 +33,68 @@ class Email extends Component
'settings.smtp_encryption' => 'Encryption', 'settings.smtp_encryption' => 'Encryption',
'settings.smtp_username' => 'Username', 'settings.smtp_username' => 'Username',
'settings.smtp_password' => 'Password', 'settings.smtp_password' => 'Password',
'settings.smtp_timeout' => 'Timeout',
'settings.resend_api_key' => 'Resend API Key'
]; ];
public function mount() public function mount()
{ {
$this->decrypt();
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} }
private function decrypt() public function submitFromFields() {
{
if (data_get($this->settings, 'smtp_password')) {
try { try {
$this->settings->smtp_password = decrypt($this->settings->smtp_password); $this->resetErrorBag();
$this->validate([
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
]);
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler($e, $this);
} }
} }
public function submitResend() {
try {
$this->resetErrorBag();
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->settings->resend_enabled = false;
return general_error_handler($e, $this);
}
} }
public function instantSave() public function instantSave()
{ {
try { try {
$this->submit(); $this->submit();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->settings->smtp_enabled = false; return general_error_handler($e, $this);
$this->validate();
} }
} }
public function submit() public function submit()
{ {
try {
$this->resetErrorBag(); $this->resetErrorBag();
$this->validate(); $this->validate([
if ($this->settings->smtp_password) { 'settings.smtp_host' => 'required',
$this->settings->smtp_password = encrypt($this->settings->smtp_password); 'settings.smtp_port' => 'required|numeric',
} else { 'settings.smtp_encryption' => 'nullable',
$this->settings->smtp_password = null; 'settings.smtp_username' => 'nullable',
} 'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->resend_enabled = false;
$this->settings->save(); $this->settings->save();
$this->emit('success', 'Transaction email settings updated successfully.'); $this->emit('success', 'Settings saved successfully.');
$this->decrypt(); } catch (\Exception $e) {
return general_error_handler($e, $this);
}
} }
public function sendTestNotification() public function sendTestNotification()

View File

@ -37,10 +37,14 @@ class Change extends Component
public function mount() public function mount()
{ {
if (isCloud() && !isDev()) {
$this->webhook_endpoint = config('app.url');
} else {
$this->webhook_endpoint = $this->ipv4; $this->webhook_endpoint = $this->ipv4;
$this->parameters = get_route_parameters();
$this->is_system_wide = $this->github_app->is_system_wide; $this->is_system_wide = $this->github_app->is_system_wide;
} }
$this->parameters = get_route_parameters();
}
public function submit() public function submit()
{ {

View File

@ -42,6 +42,9 @@ public function createGitHubApp()
'is_system_wide' => $this->is_system_wide, 'is_system_wide' => $this->is_system_wide,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]);
}
redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(err: $e, that: $this); return general_error_handler(err: $e, that: $this);

View File

@ -48,8 +48,8 @@ public function subscribeStripe($type)
'enabled' => true, 'enabled' => true,
], ],
'mode' => 'subscription', 'mode' => 'subscription',
'success_url' => route('subscription.success'), 'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.show',['cancelled' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]),
]; ];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null; $customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) { if ($customer) {

View File

@ -30,7 +30,7 @@ public function submit()
]); ]);
auth()->user()->teams()->attach($team, ['role' => 'admin']); auth()->user()->teams()->attach($team, ['role' => 'admin']);
refreshSession(); refreshSession();
return redirect()->route('team.show'); return redirect()->route('team.index');
} catch (\Throwable $th) { } catch (\Throwable $th) {
return general_error_handler($th, $this); return general_error_handler($th, $this);
} }

View File

@ -25,6 +25,6 @@ public function delete()
}); });
refreshSession(); refreshSession();
return redirect()->route('team.show'); return redirect()->route('team.index');
} }
} }

View File

@ -28,7 +28,6 @@ public function submit()
try { try {
$this->team->save(); $this->team->save();
refreshSession(); refreshSession();
$this->emit('reloadWindow');
} catch (\Throwable $th) { } catch (\Throwable $th) {
return general_error_handler($th, $this); return general_error_handler($th, $this);
} }

View File

@ -14,6 +14,7 @@ public function deleteInvitation(int $invitation_id)
{ {
TeamInvitation::find($invitation_id)->delete(); TeamInvitation::find($invitation_id)->delete();
$this->refreshInvitations(); $this->refreshInvitations();
$this->emit('success', 'Invitation revoked.');
} }
public function refreshInvitations() public function refreshInvitations()

View File

@ -1,22 +1,27 @@
<?php <?php
namespace App\Http\Livewire; namespace App\Http\Livewire\Waitlist;
use App\Jobs\SendConfirmationForWaitlistJob; use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist as ModelsWaitlist; use App\Models\Waitlist;
use Livewire\Component; use Livewire\Component;
class Waitlist extends Component class Index extends Component
{ {
public string $email; public string $email;
public int $waiting_in_line = 0; public int $waitingInLine = 0;
protected $rules = [ protected $rules = [
'email' => 'required|email', 'email' => 'required|email',
]; ];
public function render()
{
return view('livewire.waitlist.index')->layout('layouts.simple');
}
public function mount() public function mount()
{ {
$this->waitingInLine = Waitlist::whereVerified(true)->count();
if (isDev()) { if (isDev()) {
$this->email = 'waitlist@example.com'; $this->email = 'waitlist@example.com';
} }
@ -29,7 +34,7 @@ public function submit()
if ($already_registered) { if ($already_registered) {
throw new \Exception('You are already on the waitlist or registered. <br>Please check your email to verify your email address or contact support.'); throw new \Exception('You are already on the waitlist or registered. <br>Please check your email to verify your email address or contact support.');
} }
$found = ModelsWaitlist::where('email', $this->email)->first(); $found = Waitlist::where('email', $this->email)->first();
if ($found) { if ($found) {
if (!$found->verified) { if (!$found->verified) {
$this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.'); $this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.');
@ -38,7 +43,7 @@ public function submit()
$this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.'); $this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.');
return; return;
} }
$waitlist = ModelsWaitlist::create([ $waitlist = Waitlist::create([
'email' => $this->email, 'email' => $this->email,
'type' => 'registration', 'type' => 'registration',
]); ]);

View File

@ -15,7 +15,7 @@ class IsBoardingFlow
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
// ray('IsBoardingFlow Middleware'); ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding'); return redirect('boarding');
} }

View File

@ -6,14 +6,14 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class SubscriptionValid class IsSubscriptionValid
{ {
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
return $next($request); return $next($request);
} }
if (!auth()->user() || !is_cloud()) { if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') { if ($request->path() === 'subscription') {
return redirect('/'); return redirect('/');
} else { } else {

View File

@ -20,6 +20,7 @@
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -65,6 +66,12 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
public function middleware(): array
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); ray()->clearScreen();

View File

@ -37,7 +37,7 @@ public function handle(): void
private function cleanup_waitlist() private function cleanup_waitlist()
{ {
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get(); $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
foreach ($waitlist as $item) { foreach ($waitlist as $item) {
$item->delete(); $item->delete();
} }

View File

@ -7,6 +7,7 @@
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -15,51 +16,69 @@ class DockerCleanupJob implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500; public $timeout = 500;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
/** public function middleware(): array
* Create a new job instance. {
*/ return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct() public function __construct()
{ {
//
} }
/**
* Execute the job.
*/
public function handle(): void public function handle(): void
{ {
try { try {
ray()->showQueries()->color('orange');
$servers = Server::all(); $servers = Server::all();
foreach ($servers as $server) { foreach ($servers as $server) {
if (isDev()) { if (
$docker_root_filesystem = "/"; !$server->settings->is_reachable && !$server->settings->is_usable
} else { ) {
$docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server); continue;
} }
$disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem); if (isDev()) {
if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) { $this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$server,
false
);
}
if (!$this->dockerRootFilesystem) {
continue;
}
$this->usageBefore = $this->getFilesystemUsage($server);
if ($this->usageBefore >= $server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $server); instant_remote_process(['docker image prune -af'], $server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server); instant_remote_process(['docker builder prune -af'], $server);
$disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem); $usageAfter = $this->getFilesystemUsage($server);
if ($disk_percentage_after < $disk_percentage_before) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name); ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
} else {
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
} }
} else {
ray('No need to clean up ' . $server->name)->color('orange');
} }
} }
} catch (\Exception $e) { } catch (\Exception $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage())->color('orange');
throw $e; throw $e;
} }
} }
private function get_disk_usage(Server $server, string $docker_root_filesystem) private function getFilesystemUsage(Server $server)
{ {
$disk_usage = json_decode(instant_remote_process(['df -hP | awk \'BEGIN {printf"{\\"disks\\":["}{if($1=="Filesystem")next;if(a)printf",";printf"{\\"mount\\":\\""$6"\\",\\"size\\":\\""$2"\\",\\"used\\":\\""$3"\\",\\"avail\\":\\""$4"\\",\\"use%\\":\\""$5"\\"}";a++;}END{print"]}";}\''], $server), true); return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
$mount_point = collect(data_get($disk_usage, 'disks'))->where('mount', $docker_root_filesystem)->first();
ray($mount_point);
return Str::of(data_get($mount_point, 'use%'))->trim()->replace('%', '')->value();
} }
} }

View File

@ -33,9 +33,10 @@ public function handle()
if ($status === 'running') { if ($status === 'running') {
continue; continue;
} }
// $server->team->notify(new ProxyStoppedNotification($server)); if (data_get($server, 'proxy.type')) {
resolve(StartProxy::class)($server); resolve(StartProxy::class)($server);
} }
}
} catch (\Throwable $th) { } catch (\Throwable $th) {
ray($th->getMessage()); ray($th->getMessage());
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage()); send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());

View File

@ -39,9 +39,9 @@ public function uniqueId(): int
public function handle(): void public function handle(): void
{ {
try { try {
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true); $container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$status = data_get($container, 'State.Status'); $status = data_get($container, 'State.Status');
if (data_get($this->server,'proxy.status') !== $status) { if ($status && data_get($this->server, 'proxy.status') !== $status) {
$this->server->proxy->status = $status; $this->server->proxy->status = $status;
if ($this->server->proxy->status === 'running') { if ($this->server->proxy->status === 'running') {
$traefik = $container['Config']['Labels']['org.opencontainers.image.title']; $traefik = $container['Config']['Labels']['org.opencontainers.image.title'];

View File

@ -3,6 +3,8 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -27,6 +29,11 @@ public function handle()
if ($status === 'running') { if ($status === 'running') {
return; return;
} }
if (is_null(data_get($this->server, 'proxy.type'))) {
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->server->proxy->status = ProxyStatus::EXITED->value;
$this->server->save();
}
resolve(StartProxy::class)($this->server); resolve(StartProxy::class)($this->server);
} catch (\Throwable $th) { } catch (\Throwable $th) {
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage()); send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());

View File

@ -37,7 +37,7 @@ public function handle()
$mail->subject('You are on the waitlist!'); $mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email); send_user_an_email($mail, $this->email);
} catch (\Throwable $th) { } catch (\Throwable $th) {
send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage()); send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage());
ray($th->getMessage()); ray($th->getMessage());
throw $th; throw $th;
} }

View File

@ -9,7 +9,6 @@
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stripe\Stripe;
class SubscriptionInvoiceFailedJob implements ShouldQueue class SubscriptionInvoiceFailedJob implements ShouldQueue
{ {

View File

@ -13,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail
protected $guarded = []; protected $guarded = [];
protected $casts = [ protected $casts = [
'resale_license' => 'encrypted', 'resale_license' => 'encrypted',
'smtp_password' => 'encrypted',
]; ];
public static function get() public static function get()

View File

@ -33,6 +33,9 @@ protected static function booted()
}); });
static::deleting(function ($server) { static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
$server->settings()->delete(); $server->settings()->delete();
}); });
} }
@ -70,8 +73,6 @@ static public function destinationsByServer(string $server_id)
return $standaloneDocker->concat($swarmDocker); return $standaloneDocker->concat($swarmDocker);
} }
public function settings() public function settings()
{ {
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
@ -84,12 +85,20 @@ public function scopeWithProxy(): Builder
public function isEmpty() public function isEmpty()
{ {
if ($this->applications()->count() === 0) { $applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
return true; return true;
} }
return false; return false;
} }
public function databases() {
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]);
})->flatten();
}
public function applications() public function applications()
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model class Subscription extends Model
{ {
@ -14,6 +15,7 @@ public function team()
} }
public function type() public function type()
{ {
if (isLemon()) {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
@ -28,6 +30,30 @@ public function type()
if (in_array($subscription, $ultimate)) { if (in_array($subscription, $ultimate)) {
return 'ultimate'; return 'ultimate';
} }
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
return null;
}
$subscriptionPlanId = data_get($subscription,'stripe_plan_id');
if (!$subscriptionPlanId) {
return null;
}
$subscriptionConfigs = collect(config('subscription'));
$stripePlanId = null;
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
if ($value === $subscriptionPlanId){
$stripePlanId = $key;
};
})->first();
if ($stripePlanId) {
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown'; return 'unknown';
} }
} }

View File

@ -4,6 +4,7 @@
use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
protected $guarded = []; protected $guarded = [];
protected $casts = [ protected $casts = [
'personal_team' => 'boolean', 'personal_team' => 'boolean',
'smtp_password' => 'encrypted',
'resend_api_key' => 'encrypted',
]; ];
public function routeNotificationForDiscord() public function routeNotificationForDiscord()
@ -30,6 +33,27 @@ public function getRecepients($notification)
} }
return explode(',', $recipients); return explode(',', $recipients);
} }
public function limits(): Attribute
{
return Attribute::make(
get: function () {
if (config('coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted';
} else {
$subscription = data_get($this, 'subscription');
if (is_null($subscription)) {
$subscription = 'zero';
} else {
$subscription = $subscription->type();
}
}
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
}
);
}
public function members() public function members()
{ {

View File

@ -32,6 +32,7 @@ protected static function boot()
$team = [ $team = [
'name' => $user->name . "'s Team", 'name' => $user->name . "'s Team",
'personal_team' => true, 'personal_team' => true,
'show_boarding' => true
]; ];
if ($user->id === 0) { if ($user->id === 0) {
$team['id'] = 0; $team['id'] = 0;
@ -91,29 +92,20 @@ public function isInstanceAdmin()
return $found_root_team->count() > 0; return $found_root_team->count() > 0;
} }
public function personalTeam()
{
return $this->teams()->where('personal_team', true)->first();
}
public function currentTeam() public function currentTeam()
{ {
return $this->teams()->where('team_id', session('currentTeam')->id)->first(); return Team::find(session('currentTeam')->id);
} }
public function otherTeams() public function otherTeams()
{ {
$team_id = currentTeam()->id; return auth()->user()->teams->filter(function ($team) {
return auth()->user()->teams->filter(function ($team) use ($team_id) { return $team->id != currentTeam()->id;
return $team->id != $team_id;
}); });
} }
public function role() public function role()
{ {
if ($this->teams()->where('team_id', 0)->first()) { return session('currentTeam')->pivot->role;
return 'admin';
}
return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role;
} }
} }

View File

@ -44,7 +44,7 @@ public function __construct(Application $application, string $deployment_uuid, A
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled'); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments');

View File

@ -6,10 +6,10 @@
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class EmailChannel class EmailChannel
{ {
private bool $isResend = false;
public function send(SendsEmail $notifiable, Notification $notification): void public function send(SendsEmail $notifiable, Notification $notification): void
{ {
$this->bootConfigs($notifiable); $this->bootConfigs($notifiable);
@ -20,6 +20,22 @@ public function send(SendsEmail $notifiable, Notification $notification): void
} }
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) {
foreach ($recepients as $receipient) {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->to($receipient)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
} else {
Mail::send( Mail::send(
[], [],
[], [],
@ -33,16 +49,26 @@ public function send(SendsEmail $notifiable, Notification $notification): void
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} }
}
private function bootConfigs($notifiable): void private function bootConfigs($notifiable): void
{ {
$password = data_get($notifiable, 'smtp_password'); if (data_get($notifiable, 'use_instance_email_settings')) {
if ($password) $password = decrypt($password); $type = set_transanctional_email_settings();
if (!$type) {
if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) { throw new Exception('No email settings found.');
}
if ($type === 'resend') {
$this->isResend = true;
}
return;
}
if (data_get($notifiable, 'resend_enabled')) {
$this->isResend = true;
config()->set('mail.default', 'resend'); config()->set('mail.default', 'resend');
config()->set('resend.api_key', $password); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
} else { }
if (data_get($notifiable, 'smtp_enabled')) {
config()->set('mail.default', 'smtp'); config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [ config()->set('mail.mailers.smtp', [
"transport" => "smtp", "transport" => "smtp",
@ -50,7 +76,7 @@ private function bootConfigs($notifiable): void
"port" => data_get($notifiable, 'smtp_port'), "port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'), "encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'), "username" => data_get($notifiable, 'smtp_username'),
"password" => $password, "password" => data_get($notifiable, 'smtp_password'),
"timeout" => data_get($notifiable, 'smtp_timeout'), "timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null, "local_domain" => null,
]); ]);

View File

@ -4,16 +4,20 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Exception;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Log;
class TransactionalEmailChannel class TransactionalEmailChannel
{ {
private bool $isResend = false;
public function send(User $notifiable, Notification $notification): void public function send(User $notifiable, Notification $notification): void
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
if (data_get($settings, 'smtp_enabled') !== true) { if (!data_get($settings, 'smtp_enabled') && !data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');
return; return;
} }
$email = $notifiable->email; $email = $notifiable->email;
@ -22,22 +26,43 @@ public function send(User $notifiable, Notification $notification): void
} }
$this->bootConfigs(); $this->bootConfigs();
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) {
Mail::send( Mail::send(
[], [],
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->from( ->from(
data_get($settings, 'smtp_from_address'), data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name') data_get($settings, 'smtp_from_name'),
) )
->to($email) ->to($email)
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} else {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name'),
)
->bcc($email)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
} }
private function bootConfigs(): void private function bootConfigs(): void
{ {
set_transanctional_email_settings(); $type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
if ($type === 'resend') {
$this->isResend = true;
}
} }
} }

View File

@ -25,7 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database, p
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled'); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');

View File

@ -25,7 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database)
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled'); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');

View File

@ -23,7 +23,7 @@ public function __construct(public Server $server)
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled'); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');

View File

@ -20,7 +20,7 @@ public function __construct(public string|null $emails = null)
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; $channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled'); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
if ($isDiscordEnabled && empty($this->emails)) { if ($isDiscordEnabled && empty($this->emails)) {

View File

@ -31,24 +31,11 @@ public static function toMailUsing($callback)
public function via($notifiable) public function via($notifiable)
{ {
if ($this->settings->smtp_enabled) { $type = set_transanctional_email_settings();
$password = data_get($this->settings, 'smtp_password'); if (!$type) {
if ($password) $password = decrypt($password); throw new \Exception('No email settings found.');
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($this->settings, 'smtp_host'),
"port" => data_get($this->settings, 'smtp_port'),
"encryption" => data_get($this->settings, 'smtp_encryption'),
"username" => data_get($this->settings, 'smtp_username'),
"password" => $password,
"timeout" => data_get($this->settings, 'smtp_timeout'),
"local_domain" => null,
]);
return ['mail'];
} }
throw new \Exception('SMTP is not enabled'); return ['mail'];
} }
public function toMail($notifiable) public function toMail($notifiable)

View File

@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ServerPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Server $server): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Server $server): bool
{
return false;
}
}

View File

@ -43,22 +43,16 @@ public function toResponse($request)
*/ */
public function boot(): void public function boot(): void
{ {
Fortify::createUsersUsing(CreateNewUser::class); Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () { Fortify::registerView(function () {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$waiting_in_line = Waitlist::whereVerified(true)->count();
if (!$settings->is_registration_enabled) { if (!$settings->is_registration_enabled) {
return redirect()->route('login'); return redirect()->route('login');
} }
if (config('coolify.waitlist')) { if (config('coolify.waitlist')) {
return view('auth.waitlist',[ return redirect()->route('waitlist.index');
'waiting_in_line' => $waiting_in_line,
]);
} else { } else {
return view('auth.register',[ return view('auth.register');
'waiting_in_line' => $waiting_in_line,
]);
} }
}); });

View File

@ -41,6 +41,7 @@ public function execute_remote_command(...$commands)
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$output = Str::of($output)->trim();
$new_log_entry = [ $new_log_entry = [
'command' => $command, 'command' => $command,
'output' => $output, 'output' => $output,

View File

@ -0,0 +1,25 @@
<?php
namespace App\Traits;
use Illuminate\Support\Collection;
trait SaveFromRedirect
{
public function saveFromRedirect(string $route, ?Collection $parameters = null)
{
session()->forget('from');
if (!$parameters || $parameters->count() === 0) {
$parameters = $this->parameters;
}
$parameters = collect($parameters) ?? collect([]);
$queries = collect($this->query) ?? collect([]);
$parameters = $parameters->merge($queries);
session(['from' => [
'back' => $this->currentRoute,
'route' => $route,
'parameters' => $parameters
]]);
return redirect()->route($route);
}
}

View File

@ -15,7 +15,7 @@ public function __construct(
public bool $disabled = false, public bool $disabled = false,
public bool $isModal = false, public bool $isModal = false,
public bool $noStyle = false, public bool $noStyle = false,
public string|null $modalId = null, public ?string $modalId = null,
public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none" public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none"
) { ) {
if ($this->noStyle) { if ($this->noStyle) {
@ -23,9 +23,6 @@ public function __construct(
} }
} }
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string public function render(): View|Closure|string
{ {
return view('components.forms.button'); return view('components.forms.button');

View File

@ -17,7 +17,7 @@ public function __construct(
public string|null $value = null, public string|null $value = null,
public string|null $label = null, public string|null $label = null,
public string|null $helper = null, public string|null $helper = null,
public bool $instantSave = false, public string|bool $instantSave = false,
public bool $disabled = false, public bool $disabled = false,
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700" public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700"
) { ) {

View File

@ -58,9 +58,11 @@ function format_docker_envs_to_json($rawOutput)
} }
function getApplicationContainerStatus(Application $application) { function getApplicationContainerStatus(Application $application) {
$server = $application->destination->server; $server = data_get($application,'destination.server');
$id = $application->id; $id = $application->id;
if (!$server) {
return 'exited';
}
$containers = getCurrentApplicationContainerStatus($server, $id); $containers = getCurrentApplicationContainerStatus($server, $id);
if ($containers->count() > 0) { if ($containers->count() > 0) {
$status = data_get($containers[0], 'State', 'exited'); $status = data_get($containers[0], 'State', 'exited');

View File

@ -16,11 +16,6 @@
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
/**
* Run a Remote Process, which SSH's asynchronously into a machine to run the command(s).
* @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo
*
*/
function remote_process( function remote_process(
array $command, array $command,
Server $server, Server $server,
@ -167,17 +162,23 @@ function validateServer(Server $server)
{ {
try { try {
refresh_server_connection($server->privateKey); refresh_server_connection($server->privateKey);
$uptime = instant_remote_process(['uptime'], $server); $uptime = instant_remote_process(['uptime'], $server, false);
if (!$uptime) { if (!$uptime) {
$uptime = 'Server not reachable.'; $server->settings->is_reachable = false;
throw new \Exception('Server not reachable.'); return [
"uptime" => null,
"dockerVersion" => null,
];
} }
$server->settings->is_reachable = true; $server->settings->is_reachable = true;
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false); $dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
if (!$dockerVersion) { if (!$dockerVersion) {
$dockerVersion = 'Not installed.'; $dockerVersion = null;
throw new \Exception('Docker not installed.'); return [
"uptime" => $uptime,
"dockerVersion" => null,
];
} }
$server->settings->is_usable = true; $server->settings->is_usable = true;
return [ return [

View File

@ -7,6 +7,7 @@
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -52,12 +53,13 @@ function showBoarding(): bool
} }
function refreshSession(): void function refreshSession(): void
{ {
$team = currentTeam(); $team = Team::find(currentTeam()->id);
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
} }
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
{ {
try { try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage()); ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) { if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') { if ($err->errorInfo[0] === '23505') {
@ -70,6 +72,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs
} elseif ($err instanceof TooManyRequestsException) { } elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds."); throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else { } else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage()); throw new Exception($customErrorMessage ?? $err->getMessage());
} }
} catch (Throwable $error) { } catch (Throwable $error) {
@ -120,7 +125,8 @@ function generateSSHKey()
'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']) 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
]; ];
} }
function formatPrivateKey(string $privateKey) { function formatPrivateKey(string $privateKey)
{
$privateKey = trim($privateKey); $privateKey = trim($privateKey);
if (!str_ends_with($privateKey, "\n")) { if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n"; $privateKey .= "\n";
@ -135,19 +141,20 @@ function generate_application_name(string $git_repository, string $git_branch):
function is_transactional_emails_active(): bool function is_transactional_emails_active(): bool
{ {
return data_get(InstanceSettings::get(), 'smtp_enabled'); return isEmailEnabled(InstanceSettings::get());
} }
function set_transanctional_email_settings(InstanceSettings | null $settings = null): void function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null
{ {
if (!$settings) { if (!$settings) {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
} }
$password = data_get($settings, 'smtp_password'); if (data_get($settings, 'resend_enabled')) {
if (isset($password)) { config()->set('mail.default', 'resend');
$password = decrypt($password); config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend';
} }
if (data_get($settings, 'smtp_enabled')) {
config()->set('mail.default', 'smtp'); config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [ config()->set('mail.mailers.smtp', [
"transport" => "smtp", "transport" => "smtp",
@ -155,10 +162,13 @@ function set_transanctional_email_settings(InstanceSettings | null $settings = n
"port" => data_get($settings, 'smtp_port'), "port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'), "encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'), "username" => data_get($settings, 'smtp_username'),
"password" => $password, "password" => data_get($settings, 'smtp_password'),
"timeout" => data_get($settings, 'smtp_timeout'), "timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null, "local_domain" => null,
]); ]);
return 'smtp';
}
return null;
} }
function base_ip(): string function base_ip(): string
@ -212,7 +222,7 @@ function isDev(): bool
return config('app.env') === 'local'; return config('app.env') === 'local';
} }
function is_cloud(): bool function isCloud(): bool
{ {
return !config('coolify.self_hosted'); return !config('coolify.self_hosted');
} }
@ -241,7 +251,10 @@ function send_internal_notification(string $message): void
function send_user_an_email(MailMessage $mail, string $email): void function send_user_an_email(MailMessage $mail, string $email): void
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (!$type) {
throw new Exception('No email settings found.');
}
Mail::send( Mail::send(
[], [],
[], [],
@ -254,4 +267,9 @@ function send_user_an_email(MailMessage $mail, string $email): void
->subject($mail->subject) ->subject($mail->subject)
->html((string) $mail->render()) ->html((string) $mail->render())
); );
}
function isEmailEnabled($notifiable)
{
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings');
} }

View File

@ -56,21 +56,19 @@ function isSubscriptionActive()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
return $subscription->lemon_status === 'active'; return $subscription->lemon_status === 'active';
} }
if (config('subscription.provider') === 'stripe') { // if (isPaddle()) {
// return $subscription->paddle_status === 'active';
// }
if (isStripe()) {
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
} }
return false; return false;
// if (config('subscription.provider') === 'paddle') {
// return $subscription->paddle_status === 'active';
// }
} }
function isSubscriptionOnGracePeriod() function isSubscriptionOnGracePeriod()
{ {
$team = currentTeam(); $team = currentTeam();
if (!$team) { if (!$team) {
return false; return false;
@ -79,12 +77,12 @@ function isSubscriptionOnGracePeriod()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at && $is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period; return $is_still_grace_period;
} }
if (config('subscription.provider') === 'stripe') { if (isStripe()) {
return $subscription->stripe_cancel_at_period_end; return $subscription->stripe_cancel_at_period_end;
} }
return false; return false;
@ -93,10 +91,22 @@ function subscriptionProvider()
{ {
return config('subscription.provider'); return config('subscription.provider');
} }
function isLemon()
{
return config('subscription.provider') === 'lemon';
}
function isStripe()
{
return config('subscription.provider') === 'stripe';
}
function isPaddle()
{
return config('subscription.provider') === 'paddle';
}
function getStripeCustomerPortalSession(Team $team) function getStripeCustomerPortalSession(Team $team)
{ {
Stripe::setApiKey(config('subscription.stripe_api_key')); Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.show'); $return_url = route('team.index');
$stripe_customer_id = $team->subscription->stripe_customer_id; $stripe_customer_id = $team->subscription->stripe_customer_id;
$session = \Stripe\BillingPortal\Session::create([ $session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id, 'customer' => $stripe_customer_id,
@ -124,6 +134,6 @@ function allowedPathsForBoardingAccounts()
return [ return [
...allowedPathsForUnsubscribedAccounts(), ...allowedPathsForUnsubscribedAccounts(),
'boarding', 'boarding',
'livewire/message/boarding', 'livewire/message/boarding.index',
]; ];
} }

View File

@ -42,7 +42,7 @@
"laravel/pint": "^v1.8.0", "laravel/pint": "^v1.8.0",
"mockery/mockery": "^1.5.1", "mockery/mockery": "^1.5.1",
"nunomaduro/collision": "^v7.4.0", "nunomaduro/collision": "^v7.4.0",
"pestphp/pest": "^v2.4.0", "pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19", "phpunit/phpunit": "^10.0.19",
"serversideup/spin": "^v1.1.0", "serversideup/spin": "^v1.1.0",

142
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "da14dce99d76abcaaa6393166eda049a", "content-hash": "dbb08df7a80c46ce2b9b9fa397ed71c1",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -6654,16 +6654,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v6.3.2", "version": "v6.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898" "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898", "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898", "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6724,7 +6724,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v6.3.2" "source": "https://github.com/symfony/console/tree/v6.3.4"
}, },
"funding": [ "funding": [
{ {
@ -6740,7 +6740,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-19T20:17:28+00:00" "time": "2023-08-16T10:10:12+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
@ -7761,16 +7761,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a" "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7785,7 +7785,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -7823,7 +7823,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@ -7839,7 +7839,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-iconv", "name": "symfony/polyfill-iconv",
@ -7926,16 +7926,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354" "reference": "875e90aeea2777b6f135677f618529449334a612"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354", "reference": "875e90aeea2777b6f135677f618529449334a612",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7947,7 +7947,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -7987,7 +7987,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@ -8003,7 +8003,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
@ -8094,16 +8094,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8115,7 +8115,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -8158,7 +8158,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@ -8174,20 +8174,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" "reference": "42292d99c55abe617799667f454222c54c60e229"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "reference": "42292d99c55abe617799667f454222c54c60e229",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8202,7 +8202,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -8241,7 +8241,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@ -8257,7 +8257,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-07-28T09:04:16+00:00"
}, },
{ {
"name": "symfony/polyfill-php72", "name": "symfony/polyfill-php72",
@ -8337,16 +8337,16 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8355,7 +8355,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -8400,7 +8400,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@ -8416,7 +8416,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-php83", "name": "symfony/polyfill-php83",
@ -8579,16 +8579,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v6.3.2", "version": "v6.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d" "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8620,7 +8620,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v6.3.2" "source": "https://github.com/symfony/process/tree/v6.3.4"
}, },
"funding": [ "funding": [
{ {
@ -8636,7 +8636,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-12T16:00:22+00:00" "time": "2023-08-07T10:39:22+00:00"
}, },
{ {
"name": "symfony/psr-http-message-bridge", "name": "symfony/psr-http-message-bridge",
@ -9982,16 +9982,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "brianium/paratest", "name": "brianium/paratest",
"version": "v7.2.5", "version": "v7.2.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paratestphp/paratest.git", "url": "https://github.com/paratestphp/paratest.git",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942" "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/4d7ad5b6564f63baa1b948ecad05439f22880942", "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942", "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10005,19 +10005,19 @@
"phpunit/php-code-coverage": "^10.1.3", "phpunit/php-code-coverage": "^10.1.3",
"phpunit/php-file-iterator": "^4.0.2", "phpunit/php-file-iterator": "^4.0.2",
"phpunit/php-timer": "^6.0", "phpunit/php-timer": "^6.0",
"phpunit/phpunit": "^10.3.1", "phpunit/phpunit": "^10.3.2",
"sebastian/environment": "^6.0.1", "sebastian/environment": "^6.0.1",
"symfony/console": "^6.3.2", "symfony/console": "^6.3.4",
"symfony/process": "^6.3.2" "symfony/process": "^6.3.4"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^12.0.0", "doctrine/coding-standard": "^12.0.0",
"ext-pcov": "*", "ext-pcov": "*",
"ext-posix": "*", "ext-posix": "*",
"infection/infection": "^0.27.0", "infection/infection": "^0.27.0",
"phpstan/phpstan": "^1.10.26", "phpstan/phpstan": "^1.10.32",
"phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-deprecation-rules": "^1.1.4",
"phpstan/phpstan-phpunit": "^1.3.13", "phpstan/phpstan-phpunit": "^1.3.14",
"phpstan/phpstan-strict-rules": "^1.5.1", "phpstan/phpstan-strict-rules": "^1.5.1",
"squizlabs/php_codesniffer": "^3.7.2", "squizlabs/php_codesniffer": "^3.7.2",
"symfony/filesystem": "^6.3.1" "symfony/filesystem": "^6.3.1"
@ -10061,7 +10061,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/paratestphp/paratest/issues", "issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.2.5" "source": "https://github.com/paratestphp/paratest/tree/v7.2.6"
}, },
"funding": [ "funding": [
{ {
@ -10073,7 +10073,7 @@
"type": "paypal" "type": "paypal"
} }
], ],
"time": "2023-08-08T13:23:59+00:00" "time": "2023-08-29T07:47:39+00:00"
}, },
{ {
"name": "fakerphp/faker", "name": "fakerphp/faker",
@ -10707,24 +10707,24 @@
}, },
{ {
"name": "pestphp/pest", "name": "pestphp/pest",
"version": "v2.16.0", "version": "v2.16.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pestphp/pest.git", "url": "https://github.com/pestphp/pest.git",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d" "reference": "55b92666482b7d4320b7869c4eea7333d35c5631"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pestphp/pest/zipball/cbd6a650576714c673dbb0575989663f7f5c8b6d", "url": "https://api.github.com/repos/pestphp/pest/zipball/55b92666482b7d4320b7869c4eea7333d35c5631",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d", "reference": "55b92666482b7d4320b7869c4eea7333d35c5631",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"brianium/paratest": "^7.2.5", "brianium/paratest": "^7.2.6",
"nunomaduro/collision": "^7.8.1", "nunomaduro/collision": "^7.8.1",
"nunomaduro/termwind": "^1.15.1", "nunomaduro/termwind": "^1.15.1",
"pestphp/pest-plugin": "^2.0.1", "pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.3.1", "pestphp/pest-plugin-arch": "^2.3.3",
"php": "^8.1.0", "php": "^8.1.0",
"phpunit/phpunit": "^10.3.2" "phpunit/phpunit": "^10.3.2"
}, },
@ -10734,8 +10734,8 @@
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.16.0", "pestphp/pest-dev-tools": "^2.16.0",
"pestphp/pest-plugin-type-coverage": "^2.0.0", "pestphp/pest-plugin-type-coverage": "^2.2.0",
"symfony/process": "^6.3.2" "symfony/process": "^6.3.4"
}, },
"bin": [ "bin": [
"bin/pest" "bin/pest"
@ -10793,7 +10793,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/pestphp/pest/issues", "issues": "https://github.com/pestphp/pest/issues",
"source": "https://github.com/pestphp/pest/tree/v2.16.0" "source": "https://github.com/pestphp/pest/tree/v2.16.1"
}, },
"funding": [ "funding": [
{ {
@ -10805,7 +10805,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-08-21T08:42:07+00:00" "time": "2023-08-29T09:30:36+00:00"
}, },
{ {
"name": "pestphp/pest-plugin", "name": "pestphp/pest-plugin",

View File

@ -1,7 +1,7 @@
<?php <?php
return [ return [
'waitlist' => [ 'waitlist' => [
'confirmation_valid_for_minutes' => 10, 'expiration' => 10,
], ],
'invitation' => [ 'invitation' => [
'link' => [ 'link' => [
@ -11,9 +11,18 @@
], ],
'limits' => [ 'limits' => [
'server' => [ 'server' => [
'zero' => 0,
'self-hosted' => 999999999999,
'basic' => 1, 'basic' => 1,
'pro' => 3, 'pro' => 10,
'ultimate' => 9999999999999999999, 'ultimate' => 25,
],
'email' => [
'zero' => false,
'self-hosted' => true,
'basic' => false,
'pro' => true,
'ultimate' => true,
], ],
], ],
]; ];

View File

@ -3,7 +3,7 @@
return [ return [
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false), 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://license.coolify.io', 'license_url' => 'https://licenses.coollabs.io',
'mux_enabled' => env('MUX_ENABLED', true), 'mux_enabled' => env('MUX_ENABLED', true),
'dev_webhook' => env('SERVEO_URL'), 'dev_webhook' => env('SERVEO_URL'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View File

@ -7,7 +7,7 @@
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => trim(exec('jq -r .coolify.v4.version versions.json 2>/dev/null')) ?? 'unknown', 'release' => '4.0.0-beta.21',
'server_name' => env('APP_ID', 'coolify'), 'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -18,7 +18,7 @@
| |
*/ */
'driver' => env('SESSION_DRIVER', 'database'), 'driver' => env('SESSION_DRIVER', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.21'; return '4.0.0-beta.22';

View File

@ -0,0 +1,29 @@
<?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('subscriptions', function (Blueprint $table) {
$table->string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_plan_id');
});
}
};

View File

@ -0,0 +1,30 @@
<?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('instance_settings', function (Blueprint $table) {
$table->boolean('resend_enabled')->default(false);
$table->text('resend_api_key')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('resend_enabled');
$table->dropColumn('resend_api_key');
});
}
};

View File

@ -0,0 +1,32 @@
<?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('teams', function (Blueprint $table) {
$table->boolean('resend_enabled')->default(false);
$table->text('resend_api_key')->nullable();
$table->boolean('use_instance_email_settings')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('resend_enabled');
$table->dropColumn('resend_api_key');
$table->dropColumn('use_instance_email_settings');
});
}
};

View File

@ -45,6 +45,7 @@ public function run(): void
]); ]);
} }
if (config('app.name') !== 'coolify-cloud') {
// Save SSH Keys for the Coolify Host // Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal"; $coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
@ -65,7 +66,6 @@ public function run(): void
echo "Then try to install again.\n"; echo "Then try to install again.\n";
exit(1); exit(1);
} }
// Add Coolify host (localhost) as Server if it doesn't exist // Add Coolify host (localhost) as Server if it doesn't exist
if (Server::find(0) == null) { if (Server::find(0) == null) {
$server_details = [ $server_details = [
@ -99,6 +99,8 @@ public function run(): void
'server_id' => 0, 'server_id' => 0,
]); ]);
} }
}
try { try {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
if (is_null($settings->public_ipv4)) { if (is_null($settings->public_ipv4)) {

View File

@ -15,10 +15,12 @@ public function run(): void
'email' => 'test@example.com', 'email' => 'test@example.com',
]); ]);
User::factory()->create([ User::factory()->create([
'id' => 1,
'name' => 'Normal User (but in root team)', 'name' => 'Normal User (but in root team)',
'email' => 'test2@example.com', 'email' => 'test2@example.com',
]); ]);
User::factory()->create([ User::factory()->create([
'id' => 2,
'name' => 'Normal User (not in root team)', 'name' => 'Normal User (not in root team)',
'email' => 'test3@example.com', 'email' => 'test3@example.com',
]); ]);

View File

@ -1,7 +1,7 @@
version: '3.8' version: '3.8'
services: services:
coolify: coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-nightly.0}" image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.20}"
volumes: volumes:
- type: bind - type: bind
source: /data/coolify/source/.env source: /data/coolify/source/.env

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites> <testsuites>
<testsuite name="Unit"> <testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory> <directory suffix="Test.php">./tests/Unit</directory>
@ -12,11 +8,7 @@
<directory suffix="Test.php">./tests/Feature</directory> <directory suffix="Test.php">./tests/Feature</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<coverage> <coverage/>
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
@ -28,4 +20,9 @@
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/> <env name="TELESCOPE_ENABLED" value="false"/>
</php> </php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit> </phpunit>

View File

@ -408,6 +408,12 @@ const magicActions = [{
name: 'Goto: Switch Teams', name: 'Goto: Switch Teams',
icon: 'goto', icon: 'goto',
sequence: ['main', 'redirect'] sequence: ['main', 'redirect']
},
{
id: 23,
name: 'Goto: Boarding process',
icon: 'goto',
sequence: ['main', 'redirect']
} }
] ]
const initialState = { const initialState = {
@ -635,6 +641,9 @@ async function redirect() {
case 22: case 22:
targetUrl.pathname = `/team` targetUrl.pathname = `/team`
break; break;
case 23:
targetUrl.pathname = `/boarding`
break;
} }
window.location.href = targetUrl; window.location.href = targetUrl;
} }

View File

@ -1,3 +0,0 @@
<x-layout-simple>
<livewire:waitlist :waiting_in_line="$waiting_in_line" />
</x-layout-simple>

View File

@ -1,13 +0,0 @@
<x-layout-simple>
<livewire:boarding />
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Installing Docker Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
</x-layout-simple>

View File

@ -12,7 +12,9 @@
@if ($attributes->get('type') === 'submit') @if ($attributes->get('type') === 'submit')
<span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span> <span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span>
@else @else
<span wire:target="{{ explode('(', $attributes->whereStartsWith('wire:click')->first())[0] }}" wire:loading.delay @if ($attributes->has('wire:click'))
<span wire:target="{{ $attributes->get('wire:click') }}" wire:loading.delay
class="loading loading-xs loading-spinner"></span> class="loading loading-xs loading-spinner"></span>
@endif @endif
@endif
</button> </button>

View File

@ -1,68 +1 @@
<!DOCTYPE html> @extends('layouts.simple')
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
@env('local')
<title>Coolify - localhost</title>
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
@else
<title>{{ $title ?? 'Coolify' }}</title>
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
@endenv
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
<style>
[x-cloak] {
display: none !important;
}
</style>
@livewireStyles
</head>
<body>
@livewireScripts
<x-toaster-hub />
<main>
{{ $slot }}
</main>
<x-version class="fixed left-2 bottom-1" />
<script>
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
if (message) Toaster.error(message)
})
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
function changePasswordFieldType(event) {
let element = event.target
for (let i = 0; i < 10; i++) {
if (element.className === "relative") {
break;
}
element = element.parentElement;
}
element = element.children[1];
if (element.nodeName === 'INPUT') {
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
}
}
</script>
</body>
</html>

View File

@ -1,86 +1 @@
<!DOCTYPE html> @extends('layouts.subscription')
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
@env('local')
<title>Coolify - localhost</title>
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
@else
<title>{{ $title ?? 'Coolify' }}</title>
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
@endenv
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
<style>
[x-cloak] {
display: none !important;
}
</style>
@livewireStyles
</head>
<body>
@livewireScripts
<x-toaster-hub />
@if (isSubscriptionOnGracePeriod())
<div class="fixed top-3 left-4" id="vue">
<magic-bar></magic-bar>
</div>
<x-navbar />
@else
<x-navbar-subscription />
@endif
<main class="main max-w-screen-2xl">
{{ $slot }}
</main>
<x-version class="fixed left-2 bottom-1" />
<script>
function changePasswordFieldType(event) {
let element = event.target
for (let i = 0; i < 10; i++) {
if (element.className === "relative") {
break;
}
element = element.parentElement;
}
element = element.children[1];
if (element.nodeName === 'INPUT') {
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
}
}
Livewire.on('reloadWindow', (timeout) => {
if (timeout) {
setTimeout(() => {
window.location.reload();
}, timeout);
return;
} else {
window.location.reload();
}
})
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
if (message) Toaster.error(message)
})
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
</script>
</body>
</html>

View File

@ -1,133 +1 @@
<!DOCTYPE html> @extends('layouts.app')
<html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
@env('local')
<title>Coolify - localhost</title>
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
@else
<title>{{ $title ?? 'Coolify' }}</title>
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
@endenv
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
<style>
[x-cloak] {
display: none !important;
}
</style>
@livewireStyles
</head>
<body>
@livewireScripts
@auth
<x-toaster-hub />
<x-navbar />
<div class="fixed top-3 left-4" id="vue">
<magic-bar></magic-bar>
</div>
<main class="main max-w-screen-2xl">
{{ $slot }}
</main>
<x-version class="fixed left-2 bottom-1" />
<script>
let checkHealthInterval = null;
let checkIfIamDeadInterval = null;
function changePasswordFieldType(event) {
let element = event.target
for (let i = 0; i < 10; i++) {
if (element.className === "relative") {
break;
}
element = element.parentElement;
}
element = element.children[1];
if (element.nodeName === 'INPUT') {
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
}
}
function revive() {
if (checkHealthInterval) return true;
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
Toaster.success('Coolify is back online. Reloading...')
if (checkHealthInterval) clearInterval(checkHealthInterval);
setTimeout(() => {
window.location.reload();
}, 5000)
} else {
console.log('Waiting for server to come back from dead...');
}
})
}, 2000);
}
function upgrade() {
if (checkIfIamDeadInterval) return true;
console.log('Update initiated.')
checkIfIamDeadInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
console.log('It\'s alive. Waiting for server to be dead...');
} else {
Toaster.success('Update done, restarting Coolify!')
console.log('It\'s dead. Reviving... Standby... Bzz... Bzz...')
if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval);
revive();
}
})
}, 2000);
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
Livewire.emit('message', 'Copied to clipboard.');
}
Livewire.on('reloadWindow', (timeout) => {
if (timeout) {
setTimeout(() => {
window.location.reload();
}, timeout);
return;
} else {
window.location.reload();
}
})
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
if (message) Toaster.error(message)
})
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
</script>
@endauth
@guest
{{ $slot }}
@endguest
</body>
</html>

View File

@ -1,6 +1,6 @@
<div class="flex flex-col items-center justify-center h-screen"> <div class="flex flex-col items-center justify-center h-screen">
<span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span> <span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span>
<span>Please <a class="text-white underline "href="{{ route('team.show') }}">upgrade your <span>Please <a class="text-white underline "href="{{ route('team.index') }}">upgrade your
subscription<a /> to create more subscription<a /> to create more
{{ $name }}.</span> {{ $name }}.</span>
</div> </div>

View File

@ -1,6 +1,15 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar">
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</a>
</li>
<li class="pb-6" title="Logout"> <li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent"> <form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf @csrf

View File

@ -51,7 +51,7 @@ class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" vie
</a> </a>
</li> </li>
<li title="Teams"> <li title="Teams">
<a class="hover:bg-transparent" href="{{ route('team.show') }}"> <a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />

View File

@ -3,9 +3,9 @@
]) ])
<div x-data="{ selected: 'yearly' }" class="w-full pb-20"> <div x-data="{ selected: 'yearly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8"> <div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center mt-5"> <div class="flex justify-center">
<fieldset <fieldset
class="grid grid-cols-2 p-1 text-xs font-semibold leading-5 text-center rounded-full gap-x-1 "> class="grid grid-cols-2 p-1 text-xs font-semibold leading-5 text-center text-white rounded gap-x-1 bg-white/5">
<legend class="sr-only">Payment frequency</legend> <legend class="sr-only">Payment frequency</legend>
<label class="cursor-pointer rounded px-2.5 py-1" <label class="cursor-pointer rounded px-2.5 py-1"
:class="selected === 'monthly' ? 'bg-coollabs-100 text-white' : ''"> :class="selected === 'monthly' ? 'bg-coollabs-100 text-white' : ''">
@ -17,7 +17,7 @@ class="sr-only">
:class="selected === 'yearly' ? 'bg-coollabs-100 text-white' : ''"> :class="selected === 'yearly' ? 'bg-coollabs-100 text-white' : ''">
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually" <input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
class="sr-only"> class="sr-only">
<span>Annually <span class="text-xs text-warning">(save ~1 month)<span></span> <span>Annually</span>
</label> </label>
</fieldset> </fieldset>
</div> </div>
@ -181,7 +181,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
5 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." /> 10 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
</li> </li>
<li class="flex gap-x-3"> <li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
@ -192,6 +192,15 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
</svg> </svg>
Basic Support Basic Support
</li> </li>
<li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Included Email System
</li>
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600" <svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -229,7 +238,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
{{ $ultimate }} {{ $ultimate }}
@endisset @endisset
@endif @endif
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and <p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastructures and
manage them easily in one place.</p> manage them easily in one place.</p>
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 "> <ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
<li class="flex gap-x-3"> <li class="flex gap-x-3">
@ -239,7 +248,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
15 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." /> 25 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
</li> </li>
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
@ -250,6 +259,15 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
</svg> </svg>
Priority Support Priority Support
</li> </li>
<li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Included Email System
</li>
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600" <svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@ -6,7 +6,7 @@
href="{{ route('settings.configuration') }}"> href="{{ route('settings.configuration') }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@if (is_cloud()) @if (isCloud())
<a class="{{ request()->routeIs('settings.license') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('settings.license') ? 'text-white' : '' }}"
href="{{ route('settings.license') }}"> href="{{ route('settings.license') }}">
<button>Resale License</button> <button>Resale License</button>

View File

@ -4,24 +4,14 @@
<ol class="inline-flex items-center"> <ol class="inline-flex items-center">
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<span>Currently active team: {{ session('currentTeam.name') }}</span> <span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div> </div>
</li> </li>
@if (session('currentTeam.description'))
<li class="inline-flex items-center">
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<span class="truncate">{{ Str::limit(session('currentTeam.description'), 52) }}</span>
</li>
@endif
</ol> </ol>
</nav> </nav>
<nav class="navbar-main"> <nav class="navbar-main">
<a class="{{ request()->routeIs('team.show') ? 'text-white' : '' }}" href="{{ route('team.show') }}"> <a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button> <button>General</button>
</a> </a>
<a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}"> <a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">

View File

@ -1,4 +1,4 @@
Someone added this email to the Coolify Cloud's waitlist. Someone added this email to the Coolify Cloud's waitlist.
<br> <br>
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.confirmation_valid_for_minutes')}} minutes.<br><br> <a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.expiration')}} minutes.<br><br>
You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist. You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist.

View File

@ -1,14 +1,13 @@
Congratulations!<br>
Congratulations!<br>
<br>
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a> You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a>
<br> <br>
<br> <br>
Credentials: Here is your initial login information.
<br>
Email: {{ $email }}
<br>
Password: {{ $password }}
<br> <br>
Email: <br>
{{ $email }}
<br><br>
Password:<br>
{{ $password }}
<br><br>
(You will forced to change it on first login.) (You will forced to change it on first login.)

View File

@ -0,0 +1,11 @@
@extends('layouts.base')
@section('body')
@parent
<x-navbar />
<div class="fixed top-3 left-4" id="vue">
<magic-bar></magic-bar>
</div>
<main class="main max-w-screen-2xl">
{{ $slot }}
</main>
@endsection

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
<title>Coolify</title>
@env('local')
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
@else
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
@endenv
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/js/app.js', 'resources/css/app.css'])
<style>
[x-cloak] {
display: none !important;
}
</style>
@livewireStyles
</head>
@section('body')
<body>
@livewireScripts
<x-toaster-hub />
<x-version class="fixed left-2 bottom-1" />
<script>
let checkHealthInterval = null;
let checkIfIamDeadInterval = null;
function changePasswordFieldType(event) {
let element = event.target
for (let i = 0; i < 10; i++) {
if (element.className === "relative") {
break;
}
element = element.parentElement;
}
element = element.children[1];
if (element.nodeName === 'INPUT') {
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
Livewire.emit('success', 'Copied to clipboard.');
}
Livewire.on('reloadWindow', (timeout) => {
if (timeout) {
setTimeout(() => {
window.location.reload();
}, timeout);
return;
} else {
window.location.reload();
}
})
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
if (message) Toaster.error(message)
})
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
</script>
</body>
@show
</html>

View File

@ -0,0 +1,9 @@
@extends('layouts.base')
@section('body')
<main class="min-h-screen hero">
<div class="hero-content">
{{ $slot }}
</div>
</main>
@parent
@endsection

View File

@ -0,0 +1,7 @@
@extends('layouts.base')
@section('body')
@parent
<main>
{{ $slot }}
</main>
@endsection

View File

@ -0,0 +1,16 @@
@extends('layouts.base')
@section('body')
@parent
@if (isSubscriptionOnGracePeriod())
<div class="fixed top-3 left-4" id="vue">
<magic-bar></magic-bar>
</div>
<x-navbar />
@else
<x-navbar-subscription />
@endif
<main class="main max-w-screen-2xl">
{{ $slot }}
</main>
@endsection

View File

@ -1,195 +0,0 @@
@php use App\Enums\ProxyTypes; @endphp
<div class="min-h-screen hero">
<div class="hero-content">
<div>
@if ($currentState === 'welcome')
<h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center ">
<div class="justify-center box" wire:click="$set('currentState', 'select-server')">Get Started
</div>
</div>
@endif
@if ($currentState === 'select-server')
<x-boarding-step title="Server">
<x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" />
or on a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="setServer('localhost')">Localhost
</div>
<div class="justify-center box" wire:click="setServer('remote')">Remote Server
</div>
</x-slot:actions>
<x-slot:explanation>
<p>Servers are the main building blocks, as they will host your applications, databases,
services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p>
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server
for everyting.</p>
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
provider.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'private-key')
<x-boarding-step title="SSH Key">
<x-slot:question>
Do you have your own SSH Private Key?
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="setPrivateKey('own')">Yes
</div>
<div class="justify-center box" wire:click="setPrivateKey('create')">No (create one for me)
</div>
</x-slot:actions>
<x-slot:explanation>
<p>SSH Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own ssh private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your ssh private key to the remote
server's
<code class="text-warning">~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'create-private-key')
<x-boarding-step title="Create Private Key">
<x-slot:question>
Please let me know your key details.
</x-slot:question>
<x-slot:actions>
<form wire:submit.prevent='savePrivateKey' class="flex flex-col w-full gap-4 pr-10">
<x-forms.input required placeholder="Choose a name for your Private Key. Could be anything."
label="Name" id="privateKeyName" />
<x-forms.input placeholder="Description, so others will know more about this."
label="Description" id="privateKeyDescription" />
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
label="Private Key" id="privateKey" />
@if ($privateKeyType === 'create' && !isDev())
<span class="font-bold text-warning">Copy this to your server's ~/.ssh/authorized_keys file.</span>
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
@endif
<x-forms.button type="submit">Save</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your private key to the remote server's
<code>~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'create-server')
<x-boarding-step title="Create Server">
<x-slot:question>
Please let me know your server details.
</x-slot:question>
<x-slot:actions>
<form wire:submit.prevent='saveServer' class="flex flex-col w-full gap-4 pr-10">
<div class="flex gap-2">
<x-forms.input required placeholder="Choose a name for your Server. Could be anything."
label="Name" id="remoteServerName" />
<x-forms.input placeholder="Description, so others will know more about this."
label="Description" id="remoteServerDescription" />
</div>
<div class="flex gap-2">
<x-forms.input required placeholder="Hostname or IP address"
label="Hostname or IP Address" id="remoteServerHost" />
<x-forms.input required placeholder="Port number of your server. Default is 22."
label="Port" id="remoteServerPort" />
<x-forms.input required readonly
placeholder="Username to connect to your server. Default is root." label="Username"
id="remoteServerUser" />
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Username should be <x-highlighted text="root" /> for now. We are working on to use
non-root users.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'install-docker')
<x-boarding-step title="Install Docker">
<x-slot:question>
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="installDocker" onclick="installDocker.showModal()">
Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'select-proxy')
<x-boarding-step title="Select a Proxy">
<x-slot:question>
If you would like to attach any kind of domain to your resources, you need a proxy.
</x-slot:question>
<x-slot:actions>
<x-forms.button wire:click="selectProxy" class="w-64 box">
Decide later
</x-forms.button>
<x-forms.button class="w-32 box" wire:click="selectProxy('{{ ProxyTypes::TRAEFIK_V2 }}')">
Traefik
v2
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Nginx
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Caddy
</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'create-project')
<x-boarding-step title="Project">
<x-slot:question>
I will create an initial project for you. You can change all the details later on.
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="createNewProject">Let's do it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>Projects are bound together several resources into one virtual group. There are no
limitations on the number of projects you could have.</p>
<p>Each project should have at least one environment. This helps you to create a production &
staging version of the same application, but grouped separately.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'create-resource')
<x-boarding-step title="Resources">
<x-slot:question>
I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>A resource could be an application, a database or a service (like WordPress).</p>
</x-slot:explanation>
</x-boarding-step>
@endif
<div class="flex justify-center gap-2 pt-4">
<a wire:click='skipBoarding'>Skip boarding process</a>
<a wire:click='restartBoarding'>Restart boarding process</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,281 @@
@php use App\Enums\ProxyTypes; @endphp
<div>
<div>
@if ($currentState === 'welcome')
<h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="welcome">Get Started
</x-forms.button>
</div>
@endif
</div>
<div>
@if ($currentState === 'select-server-type')
<x-boarding-step title="Server">
<x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" />
or on a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">Localhost
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')" wire:click="setServerType('remote')">Remote Server
</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
<p>Servers are the main building blocks, as they will host your applications, databases,
services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p>
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server
for everyting.</p>
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
provider.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'private-key')
<x-boarding-step title="SSH Key">
<x-slot:question>
Do you have your own SSH Private Key?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">Yes
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button>
@if (count($privateKeys) > 0)
<form wire:submit.prevent='selectExistingPrivateKey' class="flex flex-col w-full gap-4 pr-10">
<x-forms.select label="Existing SSH Keys" id='selectedExistingPrivateKey'>
@foreach ($privateKeys as $privateKey)
<option wire:key="{{ $loop->index }}" value="{{ $privateKey->id }}">
{{ $privateKey->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this SSH Key</x-forms.button>
</form>
@endif
</x-slot:actions>
<x-slot:explanation>
<p>SSH Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own ssh private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your ssh private key to the remote
server's
<code class="text-warning">~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'select-existing-server')
<x-boarding-step title="Select a server">
<x-slot:question>
There are already servers available for your Team. Do you want to use one of them?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewServer">No (create one for me)
</x-forms.button>
<div>
<form wire:submit.prevent='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing servers" class="w-96" id='selectedExistingServer'>
@foreach ($servers as $server)
<option wire:key="{{ $loop->index }}" value="{{ $server->id }}">
{{ $server->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this Server</x-forms.button>
</form>
</div>
</x-slot:actions>
<x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your private key to the remote server's
<code>~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'create-private-key')
<x-boarding-step title="Create Private Key">
<x-slot:question>
Please let me know your key details.
</x-slot:question>
<x-slot:actions>
<form wire:submit.prevent='savePrivateKey' class="flex flex-col w-full gap-4 pr-10">
<x-forms.input required placeholder="Choose a name for your Private Key. Could be anything."
label="Name" id="privateKeyName" />
<x-forms.input placeholder="Description, so others will know more about this."
label="Description" id="privateKeyDescription" />
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key"
id="privateKey" />
@if ($privateKeyType === 'create' && !isDev())
<span class="font-bold text-warning">Copy this to your server's ~/.ssh/authorized_keys
file.</span>
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
@endif
<x-forms.button type="submit">Save</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your private key to the remote server's
<code>~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'create-server')
<x-boarding-step title="Create Server">
<x-slot:question>
Please let me know your server details.
</x-slot:question>
<x-slot:actions>
<form wire:submit.prevent='saveServer' class="flex flex-col w-full gap-4 pr-10">
<div class="flex gap-2">
<x-forms.input required placeholder="Choose a name for your Server. Could be anything."
label="Name" id="remoteServerName" />
<x-forms.input placeholder="Description, so others will know more about this."
label="Description" id="remoteServerDescription" />
</div>
<div class="flex gap-2">
<x-forms.input required placeholder="Hostname or IP address" label="Hostname or IP Address"
id="remoteServerHost" />
<x-forms.input required placeholder="Port number of your server. Default is 22."
label="Port" id="remoteServerPort" />
<x-forms.input required readonly
placeholder="Username to connect to your server. Default is root." label="Username"
id="remoteServerUser" />
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Username should be <x-highlighted text="root" /> for now. We are working on to use
non-root users.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'install-docker')
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<x-boarding-step title="Install Docker">
<x-slot:question>
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker" onclick="installDocker.showModal()">
Let's do
it!</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'select-proxy')
<x-boarding-step title="Select a Proxy">
<x-slot:question>
If you would like to attach any kind of domain to your resources, you need a proxy.
</x-slot:question>
<x-slot:actions>
<x-forms.button wire:click="selectProxy" class="w-64 box">
Decide later
</x-forms.button>
<x-forms.button class="w-32 box" wire:click="selectProxy('{{ ProxyTypes::TRAEFIK_V2 }}')">
Traefik
v2
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Nginx
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Caddy
</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'create-project')
<x-boarding-step title="Project">
<x-slot:question>
@if (count($projects) > 0)
You already have some projects. Do you want to use one of them or should I create a new one for
you?
@else
I will create an initial project for you. You can change all the details later on.
@endif
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new one!</x-forms.button>
<div>
@if (count($projects) > 0)
<form wire:submit.prevent='selectExistingProject'
class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing projects" class="w-96" id='selectedExistingProject'>
@foreach ($projects as $project)
<option wire:key="{{ $loop->index }}" value="{{ $project->id }}">
{{ $project->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this Project</x-forms.button>
</form>
@endif
</div>
</x-slot:actions>
<x-slot:explanation>
<p>Projects are bound together several resources into one virtual group. There are no
limitations on the number of projects you could have.</p>
<p>Each project should have at least one environment. This helps you to create a production &
staging version of the same application, but grouped separately.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@if ($currentState === 'create-resource')
<x-boarding-step title="Resources">
<x-slot:question>
I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>A resource could be an application, a database or a service (like WordPress).</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div class="flex justify-center gap-2 pt-4">
<a wire:click='skipBoarding'>Skip boarding process</a>
<a wire:click='restartBoarding'>Restart boarding process</a>
</div>
</div>

View File

@ -1,6 +1,19 @@
<x-layout> <div>
@if (session('error'))
<span x-data x-init="$wire.emit('error', '{{ session('error') }}')" />
@endif
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div class="subtitle">Something <x-highlighted text="(more)" /> useful will be here.</div> <div class="subtitle">Something <x-highlighted text="(more)" /> useful will be here.</div>
@if (request()->query->get('success'))
<div class="rounded alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Your subscription has been activated! Welcome onboard!</span>
</div>
@endif
<div class="w-full rounded stats stats-vertical lg:stats-horizontal"> <div class="w-full rounded stats stats-vertical lg:stats-horizontal">
<div class="stat"> <div class="stat">
<div class="stat-title">Servers</div> <div class="stat-title">Servers</div>
@ -19,10 +32,8 @@
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">S3 Storages</div> <div class="stat-title">S3 Storages</div>
<div class="stat-value">{{ $s3s->count() }}</div> <div class="stat-value">{{ $s3s }}</div>
</div> </div>
</div> </div>
@if (isDev())
{{-- <livewire:dev.s3-test /> --}} </div>
@endif
</x-layout>

View File

@ -16,59 +16,106 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if (isInstanceAdmin()) @if (isInstanceAdmin() && !$team->use_instance_email_settings)
<x-forms.button wire:click='copyFromInstanceSettings'> <x-forms.button wire:click='copyFromInstanceSettings'>
Copy from Instance Settings Copy from Instance Settings
</x-forms.button> </x-forms.button>
@endif @endif
@if ($model->smtp_enabled) @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
<x-forms.button onclick="sendTestEmail.showModal()" <x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary"> class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email Send Test Email
</x-forms.button> </x-forms.button>
@endif @endif
</div>
<div class="w-48">
<x-forms.checkbox instantSave id="model.smtp_enabled" label="Notification Enabled" />
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="model.smtp_recipients"
placeholder="If empty, all users will be notified in the team."
helper="Email list to send the all notifications to, separated by comma." label="Recipients" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input required id="model.smtp_host" helper="SMTP Hostname" placeholder="smtp.mailgun.org"
label="Host" />
<x-forms.input required id="model.smtp_port" helper="SMTP Port" placeholder="587" label="Port" />
<x-forms.input helper="If SMTP through SSL, set it to 'tls'." placeholder="tls"
id="model.smtp_encryption" label="Encryption" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="model.smtp_username" label="SMTP Username" />
<x-forms.input type="password" id="model.smtp_password" label="SMTP Password" />
<x-forms.input id="model.smtp_timeout" helper="Timeout value for sending emails." label="Timeout" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input required id="model.smtp_from_name" helper="Name used in emails." label="From Name" />
<x-forms.input required id="model.smtp_from_address" helper="Email address used in emails."
label="From Address" />
</div>
</div> </div>
</form> </form>
@if (data_get($model, 'smtp_enabled')) @if ($this->sharedEmailEnabled)
<h4 class="mt-4">Subscribe to events</h4> <div class="w-64 pb-4">
<x-forms.checkbox instantSave="instantSaveInstance" id="team.use_instance_email_settings"
label="Use hosted email service" />
</div>
@endif
@if (!$team->use_instance_email_settings)
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
<x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" />
<x-forms.input required id="team.smtp_from_address" helper="Email address used in emails."
label="From Address" />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
<div class="flex flex-col gap-4">
<details class="border rounded collapse border-coolgray-500 collapse-arrow ">
<summary class="text-xl collapse-title">
<div>SMTP Server</div>
<div class="w-32">
<x-forms.checkbox instantSave id="team.smtp_enabled" label="Enabled" />
</div>
</summary>
<div class="collapse-content">
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="team.smtp_host" placeholder="smtp.mailgun.org"
label="Host" />
<x-forms.input required id="team.smtp_port" placeholder="587" label="Port" />
<x-forms.input id="team.smtp_encryption" 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="team.smtp_username" label="SMTP Username" />
<x-forms.input id="team.smtp_password" type="password" label="SMTP Password" />
<x-forms.input id="team.smtp_timeout" helper="Timeout value for sending emails."
label="Timeout" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div>
</details>
<details class="border rounded collapse border-coolgray-500 collapse-arrow">
<summary class="text-xl collapse-title">
<div>Resend</div>
<div class="w-32">
<x-forms.checkbox instantSave='instantSaveResend' id="team.resend_enabled" label="Enabled" />
</div>
</summary>
<div class="collapse-content">
<form wire:submit.prevent='submitResend' class="flex flex-col">
<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="team.resend_api_key" placeholder="API key"
label="Host" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div>
</details>
</div>
@endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64"> <div class="w-64">
@if (isDev()) @if (isDev())
<x-forms.checkbox instantSave="saveModel" id="model.smtp_notifications_test" label="Test" /> <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" />
@endif @endif
<h4 class="mt-4">General</h4> <h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="model.smtp_notifications_status_changes" <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes"
label="Container Status Changes" /> label="Container Status Changes" />
<h4 class="mt-4">Applications</h4> <h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="model.smtp_notifications_deployments" label="Deployments" /> <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Deployments" />
<h4 class="mt-4">Databases</h4> <h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="model.smtp_notifications_database_backups" <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Backup Statuses" /> label="Backup Statuses" />
</div> </div>
@endif @endif

Some files were not shown because too many files have changed in this diff Show More