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,15 +9,20 @@ class SaveConfigurationSync
{
public function __invoke(Server $server, string $configuration)
{
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration);
try {
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
} catch (\Throwable $th) {
ray($th);
}
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
}

View File

@ -12,12 +12,6 @@ class StartProxy
{
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();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];

View File

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

View File

@ -7,9 +7,9 @@
class UpdateCoolify
{
public Server $server;
public string $latest_version;
public string $current_version;
public ?Server $server = null;
public ?string $latestVersion = null;
public ?string $currentVersion = null;
public function __invoke(bool $force)
{
@ -17,13 +17,16 @@ public function __invoke(bool $force)
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost';
$this->server = Server::where('name', $localhost_name)->firstOrFail();
$this->latest_version = get_latest_version_of_coolify();
$this->current_version = config('version');
ray('latest version:' . $this->latest_version . " current version: " . $this->current_version . ' force: ' . $force);
$this->server = Server::where('name', $localhost_name)->first();
if (!$this->server) {
return;
}
$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) {
ray('next channel enabled');
$this->latest_version = 'next';
$this->latestVersion = 'next';
}
if ($force) {
$this->update();
@ -31,15 +34,15 @@ public function __invoke(bool $force)
if (!$settings->is_auto_update_enabled) {
return 'Auto update is disabled';
}
if ($this->latest_version === $this->current_version) {
if ($this->latestVersion === $this->currentVersion) {
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?!';
}
$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) {
ray('InstanceAutoUpdateJob failed');
ray($th->getMessage());
@ -51,7 +54,7 @@ public function __invoke(bool $force)
private function update()
{
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([
"sleep 10"
], $this->server);
@ -61,7 +64,7 @@ private function update()
ray('Running update on production server');
remote_process([
"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);
return;
}

View File

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

View File

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

View File

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

View File

@ -12,20 +12,21 @@ class ServerController extends Controller
public function new_server()
{
if (!is_cloud() || isInstanceAdmin()) {
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [
'limit_reached' => false,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
'private_keys' => $privateKeys,
]);
}
$servers = currentTeam()->servers->count();
$subscription = currentTeam()?->subscription->type();
$your_limit = config('constants.limits.server')[strtolower($subscription)];
$limit_reached = $servers >= $your_limit;
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$limit_reached = $servers >= $serverLimit;
return view('server.create', [
'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,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class,
\App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class,
],

View File

@ -1,17 +1,20 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class Boarding extends Component
class Index extends Component
{
public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null;
public ?string $privateKey = null;
public ?string $publicKey = null;
@ -19,6 +22,8 @@ class Boarding extends Component
public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null;
@ -26,6 +31,8 @@ class Boarding extends Component
public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
public function mount()
@ -45,6 +52,12 @@ public function mount()
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding()
{
if ($this->createdServer) {
@ -63,20 +76,62 @@ public function skipBoarding()
refreshSession();
return redirect()->route('dashboard');
}
public function setServer(string $type)
public function setServerType(string $type)
{
if ($type === 'localhost') {
$this->createdServer = Server::find(0);
if (!$this->createdServer) {
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') {
$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';
}
}
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)
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
$this->createNewPrivateKey();
@ -115,11 +170,12 @@ public function saveServer()
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id
]);
$this->validateServer();
}
public function validateServer() {
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
if (!$uptime) {
$this->createdServer->delete();
$this->createdPrivateKey->delete();
throw new \Exception('Server is not reachable.');
} else {
$this->createdServer->settings->update([
@ -127,11 +183,14 @@ public function saveServer()
]);
$this->emit('success', 'Server is reachable.');
}
if ($dockerVersion) {
ray($dockerVersion, $uptime);
if (!$dockerVersion) {
$this->emit('error', 'Docker is not installed on the server.');
$this->currentState = 'install-docker';
return;
}
$this->getProxyType();
} catch (\Exception $e) {
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)
{
if (!$proxyType) {
return $this->currentState = 'create-project';
return $this->getProjects();
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
$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';
}
public function selectExistingProject() {
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
public function createNewProject()
{
$this->createdProject = Project::create([
@ -168,7 +239,7 @@ public function showNewResource()
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'server'=> $this->createdServer->id,
]
);
}
@ -176,6 +247,10 @@ private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();
$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\Notifications\Test;
use Livewire\Component;
use Log;
class EmailSettings extends Component
{
public Team $model;
public Team $team;
public string $emails;
public bool $sharedEmailEnabled = false;
protected $rules = [
'model.smtp_enabled' => 'nullable|boolean',
'model.smtp_from_address' => 'required|email',
'model.smtp_from_name' => 'required',
'model.smtp_recipients' => 'nullable',
'model.smtp_host' => 'required',
'model.smtp_port' => 'required',
'model.smtp_encryption' => 'nullable',
'model.smtp_username' => 'nullable',
'model.smtp_password' => 'nullable',
'model.smtp_timeout' => 'nullable',
'model.smtp_notifications_test' => 'nullable|boolean',
'model.smtp_notifications_deployments' => 'nullable|boolean',
'model.smtp_notifications_status_changes' => 'nullable|boolean',
'model.smtp_notifications_database_backups' => 'nullable|boolean',
'team.smtp_enabled' => 'nullable|boolean',
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.smtp_recipients' => 'nullable',
'team.smtp_host' => 'required',
'team.smtp_port' => 'required',
'team.smtp_encryption' => 'nullable',
'team.smtp_username' => 'nullable',
'team.smtp_password' => 'nullable',
'team.smtp_timeout' => 'nullable',
'team.smtp_notifications_test' => 'nullable|boolean',
'team.smtp_notifications_deployments' => 'nullable|boolean',
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
];
protected $validationAttributes = [
'model.smtp_from_address' => 'From Address',
'model.smtp_from_name' => 'From Name',
'model.smtp_recipients' => 'Recipients',
'model.smtp_host' => 'Host',
'model.smtp_port' => 'Port',
'model.smtp_encryption' => 'Encryption',
'model.smtp_username' => 'Username',
'model.smtp_password' => 'Password',
'team.smtp_from_address' => 'From Address',
'team.smtp_from_name' => 'From Name',
'team.smtp_recipients' => 'Recipients',
'team.smtp_host' => 'Host',
'team.smtp_port' => 'Port',
'team.smtp_encryption' => 'Encryption',
'team.smtp_username' => 'Username',
'team.smtp_password' => 'Password',
'team.smtp_timeout' => 'Timeout',
'team.resend_enabled' => 'Resend Enabled',
'team.resend_api_key' => 'Resend API Key',
];
public function mount()
{
$this->decrypt();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email;
}
private function decrypt()
public function submitFromFields()
{
if (data_get($this->model, 'smtp_password')) {
try {
$this->model->smtp_password = decrypt($this->model->smtp_password);
} catch (\Exception $e) {
try {
$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) {
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()
{
$settings = InstanceSettings::get();
@ -72,55 +160,22 @@ public function copyFromInstanceSettings()
'smtp_password' => $settings->smtp_password,
'smtp_timeout' => $settings->smtp_timeout,
]);
$this->decrypt();
if (is_a($team, Team::class)) {
refreshSession();
}
$this->model = $team;
$this->emit('success', 'Settings saved.');
} else {
$this->emit('error', 'Instance SMTP settings are not enabled.');
}
}
public function sendTestNotification()
{
$this->model->notify(new Test($this->emails));
$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();
$this->team = $team;
$this->emit('success', 'Settings saved.');
return;
}
$this->emit('success', 'Settings saved.');
if ($settings->resend_enabled) {
$team = currentTeam();
$team->update([
'resend_enabled' => $settings->resend_enabled,
'resend_api_key' => $settings->resend_api_key,
]);
refreshSession();
$this->team = $team;
$this->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\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component
{
use SaveFromRedirect;
public $current_step = 'github_apps';
public $github_apps;
public GithubApp $github_app;
public $parameters;
public $currentRoute;
public $query;
public $type;
@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component
public string|null $publish_directory = null;
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()
{
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private();
}
public function loadRepositories($github_app_id)
{
$this->repositories = collect();

View File

@ -3,19 +3,28 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable;
use Illuminate\Support\Collection;
use Livewire\Component;
use Route;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $servers;
public $destinations = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
@ -31,13 +40,20 @@ public function set_type(string $type)
$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';
}
public function set_server(Server $server)
{
$this->server_id = $server->id;
$this->destinations = $server->destinations();
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$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\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Form extends Component
{
use AuthorizesRequests;
public Server $server;
public $uptime;
public $dockerVersion;
@ -64,14 +66,20 @@ public function validateServer()
public function delete()
{
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
$this->server->delete();
redirect()->route('server.all');
}
}
public function submit()
{
$this->validate();

View File

@ -12,10 +12,12 @@ class Status extends Component
public function get_status()
{
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$this->emit('proxyStatusUpdated');
if (data_get($this->server,'settings.is_usable')) {
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$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 Masmerise\Toaster\Toaster;
class PrivateKey extends Component
class ShowPrivateKey extends Component
{
public Server $server;
public $privateKeys;

View File

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

View File

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

View File

@ -42,6 +42,9 @@ public function createGitHubApp()
'is_system_wide' => $this->is_system_wide,
'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]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,27 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Waitlist;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User;
use App\Models\Waitlist as ModelsWaitlist;
use App\Models\Waitlist;
use Livewire\Component;
class Waitlist extends Component
class Index extends Component
{
public string $email;
public int $waiting_in_line = 0;
public int $waitingInLine = 0;
protected $rules = [
'email' => 'required|email',
];
public function render()
{
return view('livewire.waitlist.index')->layout('layouts.simple');
}
public function mount()
{
$this->waitingInLine = Waitlist::whereVerified(true)->count();
if (isDev()) {
$this->email = 'waitlist@example.com';
}
@ -29,7 +34,7 @@ public function submit()
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.');
}
$found = ModelsWaitlist::where('email', $this->email)->first();
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
if (!$found->verified) {
$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.');
return;
}
$waitlist = ModelsWaitlist::create([
$waitlist = Waitlist::create([
'email' => $this->email,
'type' => 'registration',
]);

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ public function handle(): void
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) {
$item->delete();
}

View File

@ -7,6 +7,7 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
@ -15,51 +16,69 @@ class DockerCleanupJob implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
/**
* Create a new job instance.
*/
public function middleware(): array
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
ray()->showQueries()->color('orange');
$servers = Server::all();
foreach ($servers as $server) {
if (isDev()) {
$docker_root_filesystem = "/";
} else {
$docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server);
if (
!$server->settings->is_reachable && !$server->settings->is_usable
) {
continue;
}
$disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem);
if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) {
if (isDev()) {
$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 container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server);
$disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem);
if ($disk_percentage_after < $disk_percentage_before) {
ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name);
$usageAfter = $this->getFilesystemUsage($server);
if ($usageAfter < $this->usageBefore) {
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) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage());
ray($e->getMessage())->color('orange');
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);
$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();
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
}
}

View File

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

View File

@ -39,9 +39,9 @@ public function uniqueId(): int
public function handle(): void
{
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');
if (data_get($this->server,'proxy.status') !== $status) {
if ($status && data_get($this->server, 'proxy.status') !== $status) {
$this->server->proxy->status = $status;
if ($this->server->proxy->status === 'running') {
$traefik = $container['Config']['Labels']['org.opencontainers.image.title'];

View File

@ -3,6 +3,8 @@
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -27,6 +29,11 @@ public function handle()
if ($status === 'running') {
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);
} catch (\Throwable $th) {
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());

View File

@ -37,7 +37,7 @@ public function handle()
$mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email);
} 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());
throw $th;
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model
{
@ -14,19 +15,44 @@ public function team()
}
public function type()
{
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
if (isLemon()) {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $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';
}

View File

@ -4,6 +4,7 @@
use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
protected $guarded = [];
protected $casts = [
'personal_team' => 'boolean',
'smtp_password' => 'encrypted',
'resend_api_key' => 'encrypted',
];
public function routeNotificationForDiscord()
@ -30,6 +33,27 @@ public function getRecepients($notification)
}
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()
{

View File

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

View File

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

View File

@ -6,10 +6,10 @@
use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class EmailChannel
{
private bool $isResend = false;
public function send(SendsEmail $notifiable, Notification $notification): void
{
$this->bootConfigs($notifiable);
@ -20,29 +20,55 @@ public function send(SendsEmail $notifiable, Notification $notification): void
}
$mailMessage = $notification->toMail($notifiable);
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
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(
[],
[],
fn (Message $message) => $message
->from(
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
}
private function bootConfigs($notifiable): void
{
$password = data_get($notifiable, 'smtp_password');
if ($password) $password = decrypt($password);
if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) {
if (data_get($notifiable, 'use_instance_email_settings')) {
$type = set_transanctional_email_settings();
if (!$type) {
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('resend.api_key', $password);
} else {
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
}
if (data_get($notifiable, 'smtp_enabled')) {
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
@ -50,7 +76,7 @@ private function bootConfigs($notifiable): void
"port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'),
"password" => $password,
"password" => data_get($notifiable, 'smtp_password'),
"timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null,
]);

View File

@ -4,16 +4,20 @@
use App\Models\InstanceSettings;
use App\Models\User;
use Exception;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail;
use Log;
class TransactionalEmailChannel
{
private bool $isResend = false;
public function send(User $notifiable, Notification $notification): void
{
$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;
}
$email = $notifiable->email;
@ -22,22 +26,43 @@ public function send(User $notifiable, Notification $notification): void
}
$this->bootConfigs();
$mailMessage = $notification->toMail($notifiable);
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
)
->to($email)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
if ($this->isResend) {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name'),
)
->to($email)
->subject($mailMessage->subject)
->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
{
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
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_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
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_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
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_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
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
if ($isDiscordEnabled && empty($this->emails)) {

View File

@ -31,24 +31,11 @@ public static function toMailUsing($callback)
public function via($notifiable)
{
if ($this->settings->smtp_enabled) {
$password = data_get($this->settings, 'smtp_password');
if ($password) $password = decrypt($password);
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'];
$type = set_transanctional_email_settings();
if (!$type) {
throw new \Exception('No email settings found.');
}
throw new \Exception('SMTP is not enabled');
return ['mail'];
}
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
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () {
$settings = InstanceSettings::get();
$waiting_in_line = Waitlist::whereVerified(true)->count();
if (!$settings->is_registration_enabled) {
return redirect()->route('login');
}
if (config('coolify.waitlist')) {
return view('auth.waitlist',[
'waiting_in_line' => $waiting_in_line,
]);
return redirect()->route('waitlist.index');
} else {
return view('auth.register',[
'waiting_in_line' => $waiting_in_line,
]);
return view('auth.register');
}
});

View File

@ -41,6 +41,7 @@ public function execute_remote_command(...$commands)
$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) {
$output = Str::of($output)->trim();
$new_log_entry = [
'command' => $command,
'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 $isModal = 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"
) {
if ($this->noStyle) {
@ -23,9 +23,6 @@ public function __construct(
}
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.forms.button');

View File

@ -17,7 +17,7 @@ public function __construct(
public string|null $value = null,
public string|null $label = null,
public string|null $helper = null,
public bool $instantSave = false,
public string|bool $instantSave = 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"
) {

View File

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

View File

@ -16,11 +16,6 @@
use Illuminate\Support\Sleep;
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(
array $command,
Server $server,
@ -167,17 +162,23 @@ function validateServer(Server $server)
{
try {
refresh_server_connection($server->privateKey);
$uptime = instant_remote_process(['uptime'], $server);
$uptime = instant_remote_process(['uptime'], $server, false);
if (!$uptime) {
$uptime = 'Server not reachable.';
throw new \Exception('Server not reachable.');
$server->settings->is_reachable = false;
return [
"uptime" => null,
"dockerVersion" => null,
];
}
$server->settings->is_reachable = true;
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
if (!$dockerVersion) {
$dockerVersion = 'Not installed.';
throw new \Exception('Docker not installed.');
$dockerVersion = null;
return [
"uptime" => $uptime,
"dockerVersion" => null,
];
}
$server->settings->is_usable = true;
return [

View File

@ -7,6 +7,7 @@
use Illuminate\Database\QueryException;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;
@ -52,12 +53,13 @@ function showBoarding(): bool
}
function refreshSession(): void
{
$team = currentTeam();
$team = Team::find(currentTeam()->id);
session(['currentTeam' => $team]);
}
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
{
try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') {
@ -70,6 +72,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs
} elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage());
}
} catch (Throwable $error) {
@ -117,10 +122,11 @@ function generateSSHKey()
$key = RSA::createKey();
return [
'private' => $key->toString('PKCS1'),
'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);
if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n";
@ -135,30 +141,34 @@ function generate_application_name(string $git_repository, string $git_branch):
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) {
$settings = InstanceSettings::get();
}
$password = data_get($settings, 'smtp_password');
if (isset($password)) {
$password = decrypt($password);
if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend';
}
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($settings, 'smtp_host'),
"port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'),
"password" => $password,
"timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null,
]);
if (data_get($settings, 'smtp_enabled')) {
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($settings, 'smtp_host'),
"port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'),
"password" => data_get($settings, 'smtp_password'),
"timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null,
]);
return 'smtp';
}
return null;
}
function base_ip(): string
@ -212,7 +222,7 @@ function isDev(): bool
return config('app.env') === 'local';
}
function is_cloud(): bool
function isCloud(): bool
{
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
{
$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(
[],
[],
@ -254,4 +267,9 @@ function send_user_an_email(MailMessage $mail, string $email): void
->subject($mail->subject)
->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) {
return false;
}
if (config('subscription.provider') === 'lemon') {
if (isLemon()) {
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 false;
// if (config('subscription.provider') === 'paddle') {
// return $subscription->paddle_status === 'active';
// }
}
function isSubscriptionOnGracePeriod()
{
$team = currentTeam();
if (!$team) {
return false;
@ -79,12 +77,12 @@ function isSubscriptionOnGracePeriod()
if (!$subscription) {
return false;
}
if (config('subscription.provider') === 'lemon') {
if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period;
}
if (config('subscription.provider') === 'stripe') {
if (isStripe()) {
return $subscription->stripe_cancel_at_period_end;
}
return false;
@ -93,10 +91,22 @@ function subscriptionProvider()
{
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)
{
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;
$session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
@ -124,6 +134,6 @@ function allowedPathsForBoardingAccounts()
return [
...allowedPathsForUnsubscribedAccounts(),
'boarding',
'livewire/message/boarding',
'livewire/message/boarding.index',
];
}

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => 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'),
// When left empty or `null` the Laravel environment will be used
'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
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,60 +45,62 @@ public function run(): void
]);
}
// Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
if (config('app.name') !== 'coolify-cloud') {
// Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
if ($coolify_key) {
PrivateKey::updateOrCreate(
[
if ($coolify_key) {
PrivateKey::updateOrCreate(
[
'id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'team_id' => 0,
],
['private_key' => $coolify_key]
);
} else {
echo "No SSH key found for the Coolify host machine (localhost).\n";
echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n";
echo "Then try to install again.\n";
exit(1);
}
// Add Coolify host (localhost) as Server if it doesn't exist
if (Server::find(0) == null) {
$server_details = [
'id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'name' => "localhost",
'description' => "This is the server where Coolify is running on. Don't delete this!",
'user' => 'root',
'ip' => "host.docker.internal",
'team_id' => 0,
],
['private_key' => $coolify_key]
);
} else {
echo "No SSH key found for the Coolify host machine (localhost).\n";
echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n";
echo "Then try to install again.\n";
exit(1);
'private_key_id' => 0
];
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK_V2->value,
'status' => ProxyStatus::EXITED->value
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
} else {
$server = Server::find(0);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
}
if (StandaloneDocker::find(0) == null) {
StandaloneDocker::create([
'id' => 0,
'name' => 'localhost-coolify',
'network' => 'coolify',
'server_id' => 0,
]);
}
}
// Add Coolify host (localhost) as Server if it doesn't exist
if (Server::find(0) == null) {
$server_details = [
'id' => 0,
'name' => "localhost",
'description' => "This is the server where Coolify is running on. Don't delete this!",
'user' => 'root',
'ip' => "host.docker.internal",
'team_id' => 0,
'private_key_id' => 0
];
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK_V2->value,
'status' => ProxyStatus::EXITED->value
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
} else {
$server = Server::find(0);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
}
if (StandaloneDocker::find(0) == null) {
StandaloneDocker::create([
'id' => 0,
'name' => 'localhost-coolify',
'network' => 'coolify',
'server_id' => 0,
]);
}
try {
$settings = InstanceSettings::get();
if (is_null($settings->public_ipv4)) {

View File

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

View File

@ -1,7 +1,7 @@
version: '3.8'
services:
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:
- type: bind
source: /data/coolify/source/.env

View File

@ -1,31 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
<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">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage/>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit>

View File

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

View File

@ -8,7 +8,7 @@
<div class="w-96">
<form action="/user/confirm-password" method="POST" class="flex flex-col gap-2">
@csrf
<x-forms.input required type="password" name="password " label="{{ __('input.password') }}"
<x-forms.input required type="password" name="password" label="{{ __('input.password') }}"
autofocus />
<x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button>
</form>

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

View File

@ -1,68 +1 @@
<!DOCTYPE html>
<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>
@extends('layouts.simple')

View File

@ -1,86 +1 @@
<!DOCTYPE html>
<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>
@extends('layouts.subscription')

View File

@ -1,133 +1 @@
<!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">
@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>
@extends('layouts.app')

View File

@ -1,6 +1,6 @@
<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>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
{{ $name }}.</span>
</div>

View File

@ -1,6 +1,15 @@
@auth
<nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar">
<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">
<form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf

View File

@ -51,7 +51,7 @@ class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" vie
</a>
</li>
<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"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />

View File

@ -3,21 +3,21 @@
])
<div x-data="{ selected: 'yearly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center mt-5">
<div class="flex justify-center">
<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>
<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' : ''">
<input type="radio" x-on:click="selected = 'monthly'" name="frequency" value="monthly"
class="sr-only">
<span>Monthly</span>
</label>
<label class="cursor-pointer rounded px-2.5 py-1"
<label class="cursor-pointer rounded px-2.5 py-1"
:class="selected === 'yearly' ? 'bg-coollabs-100 text-white' : ''">
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
class="sr-only">
<span>Annually <span class="text-xs text-warning">(save ~1 month)<span></span>
<span>Annually</span>
</label>
</fieldset>
</div>
@ -167,9 +167,9 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
<span>billed annually</span>
</span>
@if ($showSubscribeButtons)
@isset($pro)
{{ $pro }}
@endisset
@isset($pro)
{{ $pro }}
@endisset
@endif
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
</p>
@ -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"
clip-rule="evenodd" />
</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 class="flex gap-x-3">
<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>
Basic Support
</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">
<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">
@ -229,7 +238,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
{{ $ultimate }}
@endisset
@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>
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
<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"
clip-rule="evenodd" />
</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 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"
@ -250,6 +259,15 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
</svg>
Priority Support
</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">
<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">

View File

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

View File

@ -4,24 +4,14 @@
<ol class="inline-flex items-center">
<li>
<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>
</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>
</nav>
<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>
</a>
<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.
<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.

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>
<br>
<br>
Credentials:
<br>
Email: {{ $email }}
<br>
Password: {{ $password }}
Here is your initial login information.
<br>
Email: <br>
{{ $email }}
<br><br>
Password:<br>
{{ $password }}
<br><br>
(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>
<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="stat">
<div class="stat-title">Servers</div>
@ -19,10 +32,8 @@
</div>
<div class="stat">
<div class="stat-title">S3 Storages</div>
<div class="stat-value">{{ $s3s->count() }}</div>
<div class="stat-value">{{ $s3s }}</div>
</div>
</div>
@if (isDev())
{{-- <livewire:dev.s3-test /> --}}
@endif
</x-layout>
</div>

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